[C++] 객체와 클래스
클래스와 구조체
클래스 서술은 public이나 private 레이블과, 멤버 함수들이 추가된 형태의 구조체 선언처럼 보인다. 사실 C++는 클래스가 가지고 있는 특징들을 구조체에까지 확장시켰다. 유일한 차이는 구조체에 대한 디폴트 접근 제어는 public이고, 클래스에 대한 디폴트 접근 제어는 private이라는 것이다. C++ 프로그래머들은 클래스를 일반적으로 클래스 서술을 구현하는 데 사용하고, 순수한 데이터 객체를 정의하는 데에는 구조체를 사용한다.(종종 plain-old data 구조라고 불리며, POD 구조라고도 불린다.)
클래스 멤버 함수의 구현
멤버 함수의 함수 머리는 그 함수가 어느 클래스에 속하는지 나타내기 위해서 사용 범위 결정 연산자(::)를 사용해야 한다. 예를 들어, update() 멤버 함수의 머리는 다음과 같아야 한다.
void Stock::update(double price)
사용 범위 결정 연산자가 그 메서드 정의가 어느 클래스에 적용되는 것인지를 알려 준다. 이것을 우리는 "update()가 클래스 사용 범위(class scope)를 가지고 있다."라고 말한다.
Stock::update()는 그 함수의 검증된 이름(qualified name)이라 부른다. 한편 간단한 update()는 그 전체 이름의 약식 표기(unqualified name, 검증되지 않은 이름)로 그 클래스 사용 범위 안에서만 사용할 수 있다.
인라인 메서드
클래스 선언 안에 정의를 가지고 있는 모든 함수는 자동으로 인라인 함수가 된다. 그러므로 Stock::set_tot()은 인라인 함수이다. 클래스 선언은 짧은 멤버 함수들에 대해서 인라은 함수를 사용하는 경우가 많다. set_tot()이 그러한 한 예이다.
원한다면 클래스 선언의 외부에 멤버 함수를 정의하고, 그것을 인라인 함수로 만들 수 있다. 그렇게 하려면, 클래스 세부 구현 부분에서 그 함수를 정의할 때 inline이라는 제한자를 앞에 붙이면 된다.
멤버 함수를 호출한다는 말은, 다른 OOP 언어에서 메시지를 보낸다.(sending a message)는 말과 같은 뜻이다. 그러므로, 두 개의 서로 다른 객체에 같은 메시지를 보낸다는 말은, 같은 메서드를 호출하여 그것을 두 개의 서로 다른 객체에 적용한다는 말과 같은 것이다.
클라이언트-서버 모델
OOP 프로그래머들은 프로그램 설계를 클라이언트-서버 모델로 자주 이야기한다. 이 개념에서 클라이언트는 클래스를 사용하는 프로그램이다. 클래스 메서드를 포함하여, 클래스 선언이 서버를 구성한다. 서버는 그것을 필요로 하는 프로그램들이 사용할 수 있는 리소스이다. 클라이언트는 public으로 정의된 인터페이스를 통해서만 서버를 사용한다. 이것은, 클라이언트의 유일한 책임이 그 인터페이스를 이해하는 것임을 의미한다. 서버 책임은 그 서버가 그 인터페이스에 따라 신뢰성 있고 정확하게 수행되는지 확인하는 것이다. 서버 설계자기 그 클래스 설계에 가하는 모든 변경은 인터페이스가 아니라 세부 구현이어야 한다.
디폴트 생성자
디폴트 생성자(default constructor)는 명시적인 초기화 값을 제공하지 않을 때 객체를 생성하는 데 사용하는 생성자이다. 즉, 다음과 같은 선언에 사용되는 생성자이다.
Stock fluffy_the_cat;
이 구분이 동작하는 이유는, 사용자가 생성자를 제공하지 않아도 C++가 자동으로 디폴트 생성자를 제공하기 때문이다. 그것은 디폴트 생성자의 암시적인 버전인다, 아무런 일도 하지 않는다. Stock 클래스의 경우에, 디폴트 생성자는 다음과 같이 될 것이다.
Stock::Stock() { }
파괴자
객체를 생성하기 위해서 생성자를 사용할 때, 프로그램은 객체의 수명이 다할 때까지 그 객체를 추적하는 책임을 맡는다. 객체의 수명이 끝나는 시점에서, 프로그램은 파괴자(destructor)라는 무서운 별칭을 가진 특별한 멤버 함수를자동으로 호출한다.
생성자와 마찬가지로, 파괴자도 특별한 이름을 가진다. 그것인 앞에 틸데(~)가 붙은 클래스 이름으로부터 만들어진다. 그래서, Stock 클래스의 파괴자는 ~Stock()이다.
파괴자는 언제 호출되는가? 컴파일러가 이 결정을 처리한다. 일반적으로 사용자가 코드에 명시적으로 파괴자를 호출하면 안된다. 정적 기억 공간의 클래스 객체를 생성한다면, 프로그램이 종료될 때 파괴자가 자동으로 호출된다.
자동 기억 공간의 클래스 객체를 생성한다면, 예제들어세 보았듯이, 그 객체가 정의된 코드 블록을 프로그램이 벗어날 때 파괴자가 자동으로 호출된다.
new를 사용하여 객체를 생성한다면, 그 객체는 힙 메모리 또는 자유 기억 공간에 저장되므로, 그것의 메모리를 해제하기 위해 delete를 사용할 때 파괴자가 자동으로 호출된다.
마지막으로, 프로그램은 어떤 작업들을 수행하기 위해 임시적인 객체를 생성할 수 있다. 그러한 경우에, 프로그램은 그 객체의 사용을 마쳤을 때 파괴자를 자동으로 호출한다.
const 멤버 함수
다음과 같은 코드 단편이 있다고 생각해 보자.
const Stock land = Stock("Kludgehorn Properties");
land.show();
현재의 C++에서, 컴파일러는 두 번째 라인에 대한 이의를 제기할 것이다. 왜냐하면 const이기 때문에 변경하면 안 되는 호출 객체를 show()에 해당하는 코드가 수정하지 않는다는 보장을 하지 않기 때문이다. 우리는 전에 함수의 매개변수를 const 참조나 const를 지시하는 포인터로 선언함으로써 이러한 문제를 해결하였다. 그러나 여기에서는 구문상의 문제 때문에 그와 같은 방식으로 해결할 수 없다.
show() 메서드는 매개변수를 갖지 않는다. 대신에 show() 메서드가 사용하는 객체는 메서드 호출에 의해 암시적으로 제공되고 있다. 그러므로 함수가 호출 객체를 변경하지 않는다고 약속하는 새로운 문법이 필요하다. C++가 이것을 해결하는 방법은 함수 괄호 뒤에 const 키워드를 넣는 것이다. 즉, show() 선언이 다음과 같이 된다.
void show() const; // 호출 객체를 변경하지 않는다고 약속한다.
마찬가지로, show() 함수 정의의 시작 부분도 다음과 같이 된다.
void Stock::Show() const // 호출 객체를 변경하지 않는다고 약속한다.
이 방식으로 선언되고 정의된 클래스 함수들을 const 멤버 함수라고 한다. 필요할 때 함수의 형식 매개변수로 const 참조와 포인터를 사용해야 하듯이, 호출 객체를 변경하면 안 되는 클래스 메서드들은 const로 만들어야 한다. 이 책에서도 앞으로는 이 규칙을 따르기로 한다.
객체 들여다보기, this 포인터
지금까지 클래스의 각 멤버 함수는 그것을 호출하는 하나의 객체만을 처리했다. 그러나 가끔은 하나의 메서드가 두 개의 객체를 동시에 처리할 필요가 있다. 그것을 가능하게 하기 위해 C++는 this라는 특별한 포인터를 제공한다.
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s; // 매개변수로 전달받은 객체
else
return ?????; // 메서드를 호출한 객체
}
여기에서 s.total_val은 매개변수로 전달된 객체의 주식 총 가치이다. 그리고 total_val은 메시지를 전달 받은 객체의 주식 총 가치이다. s.total_val이 total_val보다 크면 그 함수는 s를 리턴한다. 그렇지 않으면 그 메서드를 호출하는 데 사용된 객체(OOP식으로 말하면 topval 메시지를 전달받은 객체)를 리턴한다. 문제는 "그 객체를 무었이라고 부를 것인가?"이다.
C++가 이 문제를 해결하는 방법은 this라는 포인터를 사용하는 것이다. this 포인터는 멤버 함수를 호출하는 데 사용된 객체를 지시한다. (기본적으로 this는 그 메서드에 숨은 매개변수로 전달된다.) 그러므로 stock1.topval(stock2)함수 호출은 this 포인터를 stock1 객체의 주소로 설정하고, topval 메서드에서 그 포인터를 사용할 수 있게 만든다. 실제로 topval()에 사용되고 있는 total_val은 this -> total_val의 약식 표기이다.
그러나 this는 그 객체의 주소이기 때문에 우리가 리턴하려는 것은 this가 아니다. 우리가 리턴하려는 것은 그 객체 자체이다. 그 객체는 *this에 의하여 표현된다. 이제 호출한 객체의 대용 이름으로 *this를 사용하여 그 메서드의 정의를 끝낼 수 있다.
클래스 사용 범위
C++ 클래스는 클래스 사용 범위(class scope)라는 시로운 종류의 사용 범위를 도입한다.
클래스 사용 범위는 클래스 데이터 멤버들의 이름이나 클래스 멤버 함수들의 이름과 같이, 클래스 안에서 정의되는 이름들에 적용된다. 클래스 사용 범위를 갖는 것들은 클래스 안에는 알려지지만 클래스 바깥에는 알려지지 않는다. 그래서 클래스 멤버들을 서로 다른 클래스에 같은 이름으로 선언해도 충돌하지 않는다.
그 밖의 경우에는 클래스 멤버 이름을 사용할 때에는, 상황에 따라 직접 멤버 연산자(.)나 간접 멤버 연산자(->) 또는 사용 범위 결정 연산자(::)를 사용해야 한다.
클래스 사용 범위 상수
클래스 사용 범위를 가진 기호 상수를 사용하는 것이 좋을 때가 있다. 예를 들어, Stock 클래스 선언은 company 배열의 크기를 지정하기 위해 리터럴 30을 사용한다. 그 상수 30이 모든 객체들에 대해 동일하기 때문에, 모든 객체들이 공유하는 하나의 상수로 만드는 것도 나쁘지 않다. 그래서 다음 하나의 해결책으로 잘못 생각할 수도 있다.
class Stock
{
private:
const int Months = 12;
double const[Months];
이것은 동작하지 않는다. 왜냐하면 클래스를 선언하는 것은 객체가 어떻게 생겼는지 서술하는 것이지, 그 객체를 생성하는 것은 아니기 때문이다. 따라서 값을 저장 할 기억 공간은 객체가 생성될 때까지 마련되지 않는다.
그러나, 지금 원하는 것과 동일한 효과를 내는 두 가지 방법이 있다.
첫째, 클래스 안에 열거체를 선언할 수 있다. 클래스 선언 안에 주어지는 열거체는 클래스 사용 범위를 가진다. 따라서 열거체를 사용함으로써 클래스 사용 범위를 가지는 기호 이름들을 정수 상수들에 제공할 수 있다. 즉, Bakery 선언을 다음 같이 시작할 수 있다.
class Bakery
{
private:
enum {Months = 12};
double consts[Months];
...
이러한 방식으로 열거체를 선언하는 것은, 클래스 데이터 멤버를 생성하지 않는다.
최근에 C++는 키워드 static을 사용하여 클래스 안에 상수를 정의하는 제 2의 방법을 도입했다.
class Bakery
{
private:
static const int Months = 12;
double consts[Months];
...
이것은 객체 안이 아니라 다른 정적 변수들과 함께 저장되는 Months라는 하나의 상수를 생성한다. 그래서 모든 Bakery 객체들이 하나의 Months 상수를 공유한다.
범위가 정해진 열거형
전통적인 열거는 몇 가지 문제점을 안고 있다. 그중 하나는, 두 개의 다른 enum 정의로부터 온 열거자는 충돌한다는 것이다.
enum egg {Small, Medium, Large, Jumbo};
enum t_shirt {Small, Medium, Large, Xlarge};
이 구문은 제대로 동작하지 못한다. 왜냐면, egg Small과 t_shirt Small은 모두 동일한 범위에 있어서 서로 충돌이 발생하기 때문이다. C++11에서는 새로운 열거자 형식을 제공하여 열거자에게 클래스 범위를 갖게 함으로써 이러한 문제를 해결한다. 다음의 내용을 통해서 이러한 해결 내용을 예시하고 있다.
enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, Xlarge};
또 다른 방법으로, 클래스 대신에 struct 키워드를 사용할 수 있다. 두 가지의 경우 모두 열거자를 충족하기 위해서는 enum 이름을 사용해야 한다.
egg choice = egg::Large;
t_shirt Floyd = t_shirt::Large;
열거자는 클래스 범위를 지니고 있기 때문에 다른 enum 정의로 부터 온 열거자와 더 이상 이름이 충돌할 가능성이 없다.
범위가 지정된 열거는 int형으로 암시적 전환이 이루어지지 않는다. 그러나 꼭 필요하다면 명시적 형 변환을 할 수 있다.
int Frodo = int(t_shirt::Small); //Frodo는 0에 세팅된다.
열거는 몇 가지 기본 정수형에 의해 표현된다. 자동적으로, C++11의 범위가 정해진 열거의 내적 형태는 int이다. 또한, 다른 선택을 의미하는 구문이 있다.
// pizza의 기본형은 short이다.
enum class : short pizza {Small, Medium, Large, XLarge};
: short는 기본형이 short가 되어야 한다는 것을 의미한다. 기본 변수형은 정수형이 되어야만 한다. C++11에서는 범위가 정해지지 않은 열거에 대해서도 기본 변수형을 명시하기 위해서 이 구문을 사용할 수 있다.