본문 바로가기

프로그래밍 언어들/C++

CHAPTER04 - 클래스의 완성-1

1. 정보은닉(Informaiton Hiding)의 이해

멤버범수를 'private'으로 선언하고, 해당 변수에 접근하는 함수를 별도로 정의해서 안전한 형태로

멤버변수의 접근을 유도하는 것이다. 이는 좋은 클래스가 되기 위한 기본조건이 된다.


(1) 엑세스 함수(Access Function)

외부에서 'private'으로 선언된 멤버변수에 접근하기 위한 목적으로 정의되는 함수들이다.

보통 정수형 데이터 XXX 변수에 대해서 아래와 같이 선언한다.

    int getXXX( void) const;
    void setXXX( const int);


(2) 상수형 함수(Const)

위의 액세스 함수들에 'const' 선언이 추가되어 있다. 이는 "이 함수 내에서 멤버변수의 값을 변경하지 않겠다"라는

의미이다. 따라서 const 선언이 추가된 멤버함수 내에서 멤버변수의 값을 변경하는 코드가 삽입되면, 컴파일 에러가 발생한다.

이는 프로그래머 실수를 최소화하기 위한 매우 의미있는 선언이다. 다음 예제를 주목해 보자.

public:
    int getNum( void)
    {
        return num;
    }
    void showinfo( void) const
    {
        cout << getNum() << endl;
    };


겉보기에는 문제가 없어보이지만, showinfo() 에서 컴파일 에러가 발생한다. 그 이유는 showinfo 함수를 상수로

선언했지만, 그 안에서 호출하는 getNum이라는 멤버함수가 'const'로 선언되지 않았기 때문에,

멤버변수의 값을 변경시킬 가능성이 있기 때문이다.

※ 즉, const 함수 내에서는 const가 아닌 함수의 호출이 제한된다.


두 번째 예제도 한 번 보도록 하자.

class First
{
private:
    int num;
public:
    int getnum( void) // const로 선언해야 한다.
    {
        return num;
    }
};
class Second
{
private:
    int num;
public:
    void initnum( const First &f)
    {
        num = f.getnum(); // 컴파일 에러 발생
    }
};


위 예제는 'f.getnum()' 부분에서 컴파일 에러를 발생 시킨다. 'initnum()' 함수가 'const'가 아닌데 왜 그럴까?

그것은 멤버변수 f가 '참조자이면서 const'로 선언되어 있기 때문이다. 참조자면 해당 변수의 데이터를 변경할 수 있지만,

const로 선언했기 때문에 변경하면 안된다. 그러나 First의 멤버함수인 'getnum()'이 const로 선언되지 않았기 때문

f의 값을 변경시킬 가능성이 있기 때문이다.

즉, "const 참조자를 대상으로 값의 변경 능력을 가진 함수의 호출을 허용하지 않는다."

이러한 습관을 통해 코드의 안정성을 높이도록 하자.


2. 캡슐화(Encapsulation)

클래스 설계에서 중요한 단계로, 관련이 있는 함수와 변수를 하나의 클래스 안에 묶는 것이다.

하지만 캡슐화의 범위를 결정하는 일이 쉽지 않다. 경험 많은 객체지향 프로그래머를 구분하는 첫 번째 기준은

캡슐화이다. 캡슐화는 일관되게 적용할 수 있는 단순한 개념이 아니고, 구현하는 프로그램의 성격과 특성에 따라서

적용하는 범위가 달라지는, 흔히 하는 말로 정답이라는 것이 딱히 없는 개념이다.

즉, 감기약을 예로 들면 "코감기는 항상 콧물, 재채기, 코막힘을 동반한다."

그렇다면 코감기를 낫기 위해서는 콧물약, 재채기약, 코막힘약을 복용해야 한다.

저 기능들을 묶으면 하나의 캡슐화가 되는 것이다.

그렇다면, "감기는 코감기, 목감기, 몸살감기가 함께 동반되기도 하고, 개별적으로 진행되기도 한다."

이 경우에는 답을 내릴 수가 없다. 보다 구체적인 정보와 가정이 필요하므로, 종합감기약이 답일 수도 있고

감기약이 따로 존재하는 것이 답을 수도 있다. 그래서 클래스를 캡슐화 시키는 능력은 오랜 시간을 두고 다양한 사례를

접하면서 길러져야 한다.


3. 생성자(Constructor)와 소멸자(Destructor)

지금까지는 객체를 생성하고, 정보은닉을 목적으로 'private'로 선언한 멤버변수의 초기화를 위해 별도의 함수를 정의하고

호출하였다. 그러나 이는 여간 불편한 것이 아니다. 다행히 '생성자'라는 것을 이용하면 객체도 생성과 동시에 초기화할 수 있다.


(1) 생성자의 이해

객체 생성시 딱 한번 호출되는 함수로 다음과 같은 특징을 지닌다.

- 클래스의 이름과 함수의 이름이 동일하다.

- 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.

class TestClass
{
private:
    int num;
public:
    TestClass( int n)
    {
        num = n;
    }
};


이전에 생성자를 정의하지 않았을 때, 우리는 다음과 같은 방식으로 객체를 생성하였다.

TestClass tc;
TestClass *tc = new TestClass;


그러나 이제 생성자가 정의되었으니, 객체과정생성에서 자동으로 호출되는 생성자에게 전달할 인자를 추가하자.

TestClass tc(20);
TestClass *tc = new TestClass(40);


사실 생성자는 어렵지 않다. 따라서 다음과 같은 두 가지 사실을 추가로 설명하겠다.

- 생성자도 함수의 일종이니 오버로딩이 가능하다.

- 생성자도 함수의 일종이니 매개변수에 '디폴트 값'을 설정할 수 있다.

class TestClass
{
private:
    int num;
public:
    TestClass( void)
    {
        num = 10;
    }
    TestClass( int n)
    {
        num = 20;
    }
    TestClass( int n, int m)
    {
        num = n * m;
    }
    /* using the value of default.
    TestClass( int n, int m = 20)
    {
        num = n * m;
    }
    */

};


주석 부분을 포함하여, 생성자의 오버로딩이 가능하다는 것이 증명됐다.

그러나 만약 주석을 풀고 컴파일하면, "어떤 생성자를 호출할지 아주 애매모호합니다."라는 에러가 발생한다.

주석을 풀고 아래와 같이 선언을 해보자.

TestClass tc(20);


즉, TestClass( int n)의 생성자와 TestClass( int n, int m = 20)의 생성자, 둘 다 호출이 가능하기 때문이다.


그리고 다음으로 TestClass( void) 생성자를 호출하는 객체를 보자.

TestClass tc; // 가능
TestClass tc(); // 불가능
TestClass *tc = new TestClass; // 가능
TestClass *tc = new TestClass(); // 가능


왜 4번 째 줄은 가능한데, 2번 째 줄은 불가능한 것인가?

class TestClass
{
private:
    int num;
public:
    TestClass( void)
    {
        num = 10;
    }
    TestClass( int n)
    {
        num = n;
    }
};

int main( void)
{
    TestClass tc(); // 함수의 선언으로 인식
    TestClass tc2 = tc();

    return 0;
}

TestClass tc()
{
    TestClass temp(10);

    return temp;
}


그것은 'TestClass tc();'가 함수의 원형으로 인식되기 때문이다. 보통 함수의 원형은 전역변수로 선언하지만

이렇게 특정 함수 내에 지역적으로 선언도 가능하다. 그러므로 객체 생성이 아니라 '함수 선언'으로 인식되는 것이다.


(2) 멤버 이니셜라이저(Member Initializer)를 이용한 멤버 초기화
Class Second가 Class First를 포함하고 있다고 가정할 때, Second를 생성하면 First 객체도 함께 생성된다.

따라서, "Second 객체를 생성하는 과정에서 First 생성자를 통해 First 객체를 초기화할 수 없을까?"

라는 생각을 하게 될 것이다.

class First
{
private:
    int x;
    int y;
public:
    First( const int &xpos, const int &ypos)
    {
        x   = xpos;
        y   = ypos;
    }
};

class Second
{
private:
    First f;
public:
    Second( const int &x, const int &y)
    {

    }
};


위의 예제에서 Second 객체가 포함하고 있는 First 객체를, First 생성자를 이용해서 초기화하는 방법은 아래와 같다.

Second( const int &x, const int &y) : f(x, y){}


Second 생성자로 넘어온 인자값 x, y으로 First 클래스의 객체인 f의 생성자를 호출하는 것이다.


따라서 객체 생성과정을 다음과 같이 정리할 수 있다.

- 메모리 공간의 할당

- 이니셜라이저를 이용한 멤버변수(객체)의 초기화

- 생성자의 몸체부분 실행


C++의 모든 객체는 위의 세가지 과정을 순서대로 거쳐서 생성이 완성된다. 물론 이니셜라이저가 선언되지 않았다면

메모리 공간의 할당과 생성자의 몸체부분의 실행으로 객체생성은 완성된다.

※ 생성자를 선언하지 않았다고 해서 생성자가 호출이 되지 않는 것은 아니다!

생성자를 정의하지 않으면 '디폴트 생성자(default constructor)'가 자동으로 삽입되어 호출된다.


(3) 멤버 이니셜라이저(Member Initializer)'를 이용한 변수 및 const 상수(변수) 초기화

'멤버 이니셜라이저'는 객체가 아닌 멤버의 초기화에도 사용할 수 있다.

class Second
{
private:
    int num1;
    int num2;
public:
    Second( const int &n, const int &m) : num1(n)
    {
        num2    = m;
    }
};


위의 소스에서 Second 클래스의 멤버 이니셜라이저를 보면, 'num1(n)'으로 되어 있다.

즉, num1 멤버변수를 n으로 초기화하라는 의미이다.

따라서 프로그래머는 생성자의 몸체에서 초기화 하는 방식과 이니셜라이저를 이용하는 방법 중 선택이 가능하다.

일반적으로 멤버변수의 초기화는 이니셜라이저를 선호하는 편이다.

- 초기화의 대상을 명확히 인식할 수 있다.

- 성능에 약간의 이점이 있다. ( 추 후에 설명 )

'num1(n)'은 다음의 문장에 비유할 수 있다.

=> int num1 = n

이니셜라이저를 통해서 초기화되는 멤버는 선언과 동시에 초기화가 이뤄진다.

그러나 생성자 몸체를 통해서 이루어지는 다음의 초기화는

num2 = n;

=> int num2;

=> num2 = n;

위와 같이 선언과 초기화가 따로 이루어진다.

즉, 이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성된다.

반면, 생성자의 몸체부분에서 대입연산을 통한 초기화를 진행하면, 선언과 초기화를 각각 별도로 진행하는 형태로

바이너리 코드가 생성된다.

따라서, "이니셜라이저를 이용하면 const 멤버변수도 초기화가 가능하다."

class Second
{
private:
    const int num1;
public:
    Second( const int &n) : num1(n) {}
};


* 참고 : const는 변수를 상수화 시키는 키워드이다. 즉 const 변수와 const 상수는 같은 의미이다.


출처 : 윤성우 열혈 C++ 프로그래밍