[CPP Module00 / EX01: My Awesome Phonebook 과제 소개]
The user is prompted for input: you should accept the ADD
command, the SEARCH
command or the EXIT
command. Any other input is discarded
프로그램은 연락처가 없는 비어있는 상태에서 시작되며, 동적할당은 허용되지 않는다.
최대 8개의 연락처를 저장할 수 있다. 아홉 번째 번호를 저장하려고 할 때 행동을 정의해야한다.
명령이 올바르게 실행되었다면, 다시 프로그램은 ADD or SEARCH or EXIT 명령의 입력을 기다린다.
EXIT command
ADD command
SEARCH command
.
)으로 대체한다. 오른쪽 정렬한다.C의 구조체를 떠올리며 클래스를 이해해보자. CPP의 구조체는 C와는 다르게 구조체 멤버로 함수를 포함할 수 있다. 클래스와 구조체는 유사한 개념이다. 클래스
는 자료 저장(변수) + 자료 처리(함수) 가 가능한 자료형이고, 특정 용도를 수행하기 위한 변수와 함수를 모아 둔 틀
이라고 말할 수 있다.
CPP의 클래스는 구조체 개념에 딱 한 가지, 접근 제어
라는 개념을 추가했다. 그렇다고 클래스를 그저 구조체의 확장 버전이라고 말하기엔 부족하다. CPP은 기본적으로 객체지향 언어
이기 때문이다. 객체지향의 관점에서 클래스를 이해해야 한다.
CPP에서 말하는 Object의 의미는 우리 주변에 존재하는 사물, 또는 대상
이다. 따라서 객체지향 프로그래밍은 현실에 존재하는 사물과 대상, 그리고 그들의 행동을 있는 그대로 실체화 시키는 형태의 프로그래밍이다. 그리고 이때 클래스는 객체를 만들어 내기 위한 틀로서 사용되는 개념이다.
42 CPP Module00의 My Awesome Phonebook 과제를 예로 들어보자. 이 과제에서는 연락처를 추가하고 조회하는 프로그램을 만들어야 한다. 이 프로그램에는 전화번호부 객체가 존재할 것이다. 그리고, 아래와 같은 일들을 할 것이다.
1은 전화번호부의 행동(behavior)
을 의미한다. 그리고 2와 3은 전화번호부의 상태(state)
를 의미한다.
이처럼 객체는 하나 이상의 상태 데이터와 하나 이상의 행동(기능)으로 구성된다. 상태 정보는 변수(속성)
를 통해, 행동은 함수(메서드)
를 통해 구현한다.
class Contact {
private:
std::string first_name;
std::string last_name;
std::string phone_number;
public:
void set_first_name(const std::string str);
void set_last_name(const std::string str);
void set_phone_number(const std::string str);
std::string get_first_name(void) const;
std::string get_last_name(void) const;
std::string get_phone_number(void) const;
};
위와 같은 모습의 클래스를 선언했다.
객체와 인스턴스라는 말이 혼용되어 사용되는 느낌이 들어 헷갈렸다. 정확한 개념을 찾아보니 객체(Object)는 소프트웨어 세계에 구현할 대상이고, 이를 구현하기 위한 설계도가 클래스(Class)이며, 이 설계도에 따라 소프트웨어 세계에 구현된 실체가 인스턴스(Instance)이다. 출처
즉, 메모리에 올라간 특정 클래스 타입의 객체를 인스턴스(instance) 라고 부른다. 하지만 개념적으로 인스턴스는 객체에 포함되므로 두 단어가 같은 뜻인 것처럼 흔히 사용되는 것 같다. 정확히 구분해서 사용하면 좋을 듯.
CPP에서는 객체 지향 프로그래밍의 기본 규칙인 정보 은닉 을 위해 접근 제어(access control)라는 기능을 제공한다. 아래 3개의 접근 제어 지시자를 통해, 클래스 내부의 멤버에 대한 외부에서의 직접적인 접근을 허용하거나 차단할 수 있다.
참고로 클래스의 기본 접근 제어 권한은 private이며, 구조체 및 공용체는 public 이다.
#include <iostream>
using namespace std;
struct TV {
bool powerOn;
int channel;
int volume;
void setVolume(int vol) {
if (vol >= 0 && vol <= 100)
volume = vol;
}
};
int main() {
TV lg;
lg.powerOn = true;
lg.channel = 11;
lg.setVolume(50);
lg.volume = 400; // 의도치 않은 접근 가능
}
TV 구조체의 각 멤버를 정의하고 메인 함수에서 초기화를 해주고 있다. 만약 개발자가 볼륨의 크기를 0부터 100으로 제한하고 싶다면, 위 예제처럼 구조체 내부에 setVolume이라는 함수를 만들어 사용하도록 할 수 있을 것이다.
하지만 위 코드의 문제점은, 여전히 volume 멤버에 대한 접근이 가능하다는 점이다. 그래서 나온 CPP의 해결책이 접근 지시자이다. 아래 코드 처럼 접근 지시자를 이용해 정보 은닉을 실현하고, 보다 안정적인 프로그램을 개발할 수 있다.
#include <iostream>
using namespace std;
struct TV {
private: //외부 접근 차단
bool powerOn;
int channel;
int volume;
public: //외부 접근 허용
void on() {
powerOn = true;
cout << "TV on." << endl;
}
void off() {
powerOn = false;
cout << "TV off." << endl;
}
void setChannel(int num) {
if (num >= 1 && num <= 999)
channel = num;
cout << "채널을 " << num << "(으)로 변경했습니다." << endl;
}
void setVolume(int vol) {
if (vol >= 0 && vol <= 100)
volume = vol;
cout << "볼륨을 " << vol << "(으)로 변경했습니다." << endl;
}
};
int main() {
TV lg;
lg.powerOn = on();
lg.setChannel(11);
lg.setVolume(50);
}
멤버변수를 private로 선언하고, 해당 변수에 접근하는 함수를 별도로 정의해서, 안전한 형태로 멤버변수의 접근을 유도하는 것이 바로 정보은닉
이며, 이는 좋은 클래스가 되기 위한 기본조건이 된다. 이렇게 어떤 멤버는 보호를 하고, 외부 사용자들에게 접근할 수 있는 인터페이스를 따로 만들어 주는 것을 '캡슐화'라고 부른다.
멤버함수를 정의하는 방법은 두 가지가 있다.
::
연산자 (scope resolution)를 사용#include <iostream>
using namespace std;
class TV {
private:
bool powerOn;
int volume;
public:
// 내부 클래스 정의
void on() {
powerOn = true;
cout << "TV on." << endl;
}
void off() {
powerOn = false;
cout << "TV off." << endl;
}
// 외부 클래스 정의
void setVolume(int vol);
};
// 외부 클래스 정의
void TV::setVolume(int vol) {
if (vol >= 0 && vol <= 100)
volume = vol;
cout << "볼륨을 " << vol << "(으)로 변경했습니다." << endl;
}
int main() {
TV lg;
lg.powerOn = on();
lg.setVolume(50);
}
인스턴스는 모든 멤버 변수를 초기화 하기 전까지는 사용할 수 없다. private 멤버는 일반적인 방식으로 초기화할 수 없기 때문에, CPP에서는 생성자(constructor)
라는 멤버 함수를 제공한다. 클래스에서 객체를 생성할 때마다 해당 클래스의 생성자가 컴파일러에 의해 자동으로 호출된다.
생성자
는 객체의 생성과 동시에 멤버 변수를 초기화해주는 pubilc 함수이다.원형
은 클래스 선언의 public 영역에 포함되어야 한다.#include <iostream>
using namespace std;
class TV {
private:
bool powerOn;
int volume;
public:
TV(void); // 생성자 함수의 원형
}
// 생성자 함수
TV::TV(void){
powerOn = true;
volume = 50;
}
int main() {
TV lg; // 생성자의 암시적 호출
// TV lg = TV // 생성자의 명시적 호출
}
멤버 이니셜라이저는 객체 혹은 멤버변수의 초기화를 보다 간단하게 만들어준다. 아래 예제가 이니셜라이저를 사용한 예제이다. :
(콜론) 기호 뒤의 volume(50)
이 volume을 50으로 초기화하라는 뜻이다.
class TV {
private:
bool powerOn;
int volume;
public:
TV(void) : volume(50) {} //이니셜라이저를 통한 초기화
}
따라서, 객체를 생성할 때는 생성자 함수 몸체에서 초기화하는 것과 이니셜라이저를 이용하는 것 두 가지 방법이 존재한다. 일반적으로 멤버변수 초기화를 할때는 이니셜라이저를 선호한다고 한다. 선언과 동시에 초기화가 이루어지니 대입연산보다 성능 상의 이점이 있는 것 같다.
,
(콤마) 기호로 나열하면 된다.생성자와 반대로 객체 소멸시 반드시 호출되는 것이 소멸자
이다.
~
기호가 붙는 형태의 이름을 갖는다.class Contact {
private:
std::string first_name;
std::string last_name;
std::string phone_number;
public:
void set_first_name(const std::string str);
void set_last_name(const std::string str);
void set_phone_number(const std::string str);
std::string get_first_name(void) const;
std::string get_last_name(void) const;
std::string get_phone_number(void) const;
};
My Awesome Phonebook 과제 예제를 다시 가져와 보면, 멤버함수들에는 set
, get
과 같은 이름이 붙어있는 걸 알 수 있다. CPP에서는 이런 함수명을 자주 쓰는데, 흔히 엑세스 함수
라고 부른다. 이들은 멤버변수를 private로 선언하면서 클래스 외부에서의 멤버변수 접근을 목적으로 정의되는 함수들이다.
그리고 getter함수들 뒤에 const
지시자가 붙어 있는 걸 알 수 있다. const
는 이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다 라는 선언이다. 따라서 const 선언이 추가된 멤버함수 내에서 멤버변수의 값을 변경한다면 컴파일 에러가 발생한다. const 함수 내에서는 const가 아닌 함수의 호출이 제한된다. 아예 멤버변수의 값이 변경될 수 있을 일말의 가능성조차 허용하지 않기 위해서다.
사실 처음에는 CPP의 클래스와 객체지향 프로그래밍 개념을 학습하면서 C에 비해 개발의 자유도가 제한되고 너무 복잡한 기능들이 추가된 것 아닌가 생각했었다. 그런데 바로 그게 이 모든 새 기능들이 추가된 이유인 것 같다. 제한된 방법으로의 접근만 허용을 해서 잘못된 값이 저장되지 않도록 도와주는 것이고, 또 실수를 했을 때 실수가 쉽게 발견되도록 해야 한다는 일관된 원칙이 느껴져서 설계 자체에 재미를 갖게 됐다.