관리 메뉴

Kim's Programming

C++ - 클래스 생성자/파괴자(3/3) 본문

Programming/Cplusplus

C++ - 클래스 생성자/파괴자(3/3)

Programmer. 2015. 9. 3. 17:30

::레퍼런스 멤버 초기화


레퍼런스는 변수에 대한 별명이며 선언할 때 반드시 누구에 대한 별명인지를 밝혀야 합니다.단 예외적으로 함수의 형식 인수, 클래스의 멤버, extern 선언시는 대상체를 지정하지 않을 수 있는데 이때는 함수 호출시나 객체 생성시로 초기화가 연기됩니다. 레퍼런스 멤버를 가지는 클래스는 생성자에서 이 멤버를 초기화해야 하는데 다음 예제처럼 초기화 리스트를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string.h>
using namespace std;
class Some
{
public:
    int &ri;
    Some(int &i) :ri(i){}
    void OutValue()
    {
        cout << ri << endl;
    }
};
void main()
{
    int i = 5;
    Some S(i);
    S.OutValue();
}
cs

Some 클래스는 정수형 레퍼런스 변수 ri를 멤버로 가지고 있으며 생성자는 ri가 참조할 실제 변수를 인수로 전달 받아 ri가 이 변수의 별명이 되도록 합니다. 레퍼런스 멤버는 다음과 같이 대입 연산자로 초기화 할 수 없습니다.

1
2
3
4
Some(int &i)
{
    ri=i;
}
cs

왜냐하면 레퍼런스에 대한 대입 연산은 레퍼런스 그 자체의 대상체를 지정하는 것이 아니라 레퍼런스가 참조하고 있는 변수에 값을 대입하는 것으로 정의되어 있기 떄문입니다. 레퍼런스 멤버는 대입 연산자로 초기화할 수 없으며 반드시 초기화 히스트에서 대상체를 지정해야 합니다. 레퍼런스 멤버는 대입 연산자로 초기화 할 수 없으며 반드시 초기화 리스트에서 대상체를 지정해야합니다. 레퍼런스 멤버 초기화 문법은 사실 앞의 상수 멤버 초기화 규칙과 동일한 것이라고 볼 수 있습니다. 왜냐하면 레퍼런스는 일종의 상수 포인터 이기 때문입니다.


레퍼런스는 생성 직후부터 별명으로 동작해야 하므로 선언할 때 짝이 될 변수를 반드시 지정해야합니다. 하지만 레퍼런스를 초기식없이 선언할 수 있는 세가지 예외가 있는데 그중 하나가 클래스 멤버로 선언될 떄입니다.


::레퍼런스 멤버 초기화


구조체 끼리도 중첩할 수 있듯이 클래스도 다른 클래스의 객체를 멤버로 가질 수 있습니다. 포함된 객체를 초기화할 때도 초기화 리스트를 사용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<iostream>
using namespace std;
 
class Position
{
public:
    int x, y;
    Position(int ax, int ay)
    {
        x = ax;
        y = ay;
    }
};
 
class Some
{
public:
    Position Posit;
    Some(int x, int y) :Posit(x, y){}
    void Output()
    {
        cout << Posit.x << " , " << Posit.y << endl;
    }
};
void main()
{
    Some S(45);
    S.Output();
}
cs

Some클래스가 Position 클래스의 객체 Posit을 포함하고 있는데 포함된 Pos객체를 초기화 하기 위해 다ㅏ음과 같이 작성은 할 수 없습니다.

1
2
3
4
Som(int x, iny y)
{
    Posit(x, y);
}
cs

왜냐하면 생성자는 객체를 생성할 떄만 호출할 수 있으며 외부에서 명시적으로 호출할 수 없기 떄문입니다. 그래서 멤버로 포함된 객체를 초기화할 때도 초기화 리스트를 사용해야합니다. 다음 코드는 어떨 까요?

1
2
3
4
Some(int x, int y)
{
    Position Posit(x,y);
}
cs

생성자를 호출하는 문장처럼 보이지만 Posit은 생성자 함수 내에서 임시적으로 만들어지는 지역 객체일 뿐이며 포함된 객체 Posit과는 이름만 같을 뿐 서로 상관이 없습니다. 이 코드는 객체 Pos를 초기화하는 것이 아니라 쓰지도 않는 지역 객체를 멤버와 같은 이름으로 하나 만들 뿐이며 이 객체는 생성자가 종료될 때 자동으로 파괴됩니다. 기본 타입의 멤버 변수도 일종의 포함된 객체로 볼 수 있으며 x(ax), y(ay)식으로 초기화 리스트에서 초기화 할 수 있습니다. 물론 기본 타입은 대입 연산자에 의해 값을 대입할 수도 있으므로 생성자 본체에서 초기화 하는것도 가능합니다


만약 포함된 객체가 디폴트 생성자를 정의한다면 초기화 리스트에서 초기화 하지 않아도 컴파일러가 디폴트 생성자를 호출하며 에러는 발생하지 않습니다. 그러나 디폴트 생성자는 쓰레기를 치우는 정도 뿐 원하는 초기화는 아닐 수 있습니다. 그렇지 않은 경우에는 반드시 초기화 리스트에서 적절한 생성자를 호출하여 객체를 초기화하여야합니다.


타입 변환


변환 생산자


일반 타입의 변수끼리 값을 대입할 때는 산술 변환 규칙에 따라 암시적으로 상호 변환 됩니다. 물론 모든 타입들이 다 상호 변환되는 것은 아니며 호환되는 타입들끼리만 그렇게 됩니다. 다음 소스를 보겠습니다.

1
2
int i =  'C';
double d = 12;
cs

'C'는 문자형 상수이지만 정수형 변수 i에 대입할 수 있으며 12는 정수형 상수이지만 실수형 변수 d에 대입할 수 있습니다. 문자형이 정수형의 큰 타입으로 변환될 때는 암시적으로 상승 변환이 되며 반대의 경우는 하강 변환이 일어납니다. 변수끼리 대입할 떄나 함수의 인수로 전달될 때도 별다른 거부없이 암시적 변환이 적용됩니다. 물론 정수형 변수에 실수값을 대입하는 식의 하강 변환의 경우 약간의 정확도 손실이 발생하기 떄문에 컴파일러에서 경고처리 하게 됩니다.


클래스의 객체들도 일반 타입과 마찬가지로 암시적 변환이 가능은 한데 클래스가 일반 타입과 완전히 동등해지려면 타입을 변환할 수 있는 문법적 장치가 있어야합니다. 그 첫 번쨰 장치가 변환생성자(Conversion Constructor)입니다. 변환 생성자는 기본 타입으로부터 객체를 만드는 생성자이며 인수를 하나만 취합니다. 인수가 둘 이상이면 변환생성자가 아닙니다. 다음 소스의 Time생성자는 정수값으로부터  Time객체를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
using namespace std;
class Time
{
private:
    int hour, min, sec;
public:
    Time(){}
    Time(int abssec)
    {
        hour = abssec / 3600;
        min = (abssec / 60) % 60;
        sec = abssec % 60;
    }
    void Output()
    {
        cout << "현재시간은 " << hour << " : " << min << " : " << sec << "입니다" << endl;
    }
};
void main()
{
    Time Now(3723);
    Now.Output();
}
cs


Time 클래스는 시간을 표현하며 시, 분, 초의 요소들을 멤버 변수로 가집니다. 두 개의 생성자가 정의되어 있는데 디폴트 생성자와 변환 생 성자입니다. 시간이라는 값은 시, 분, 초의 3차원으로 표현하지만 자정 이후 경과한 시간을 절대초로 정의하고 절대초로 표현할 수도 있습니다. 가령 정오는 43200이 절대초이며 절대초 33956은 오전 9시 25분 56초가 됩니다. 절대초는 시간끼리 계산에 유리한 표현법이며 실용적인 가치가 있습니다.Time(int) 생성자는 정수형의 abssec인수 하나만을 취하는데 절대초 abssec으로부터 시, 분, 초를 구해 객체를 초기화합니다. 절대초로부터 시, 분, 초의 요  소를 분리해내는 수식은 간단합니다. 시간은 3600으로 나누고 초는 60으로 나눈 나머지입니다. 정수 값 하나를 변환하여 객체를 생성하므로 이런 생성자를 변환 생성자라고 합니다. main함수의 첫번쨰 문장 Time Now(2723);은 정수 상수 3723이라는 값으로부터 1:2:3 이라는 Time형 객체를 생성합니다. 이 문장은 객체 선언 문법으로 변환생성자를 직접적으로 호출하는 것이고 다음과 같이 간접적으로 호출할 수도 있습니다.

1
Time Now=3723;
cs

int와 Time은 원래 호환되지 않지만 변환생성자가 정의되어 있으면 컴파일러에 의해 자동으로 변환됩니다. 초기식의 우변이 정수이므로 컴파일러는 정수를 Time 객체로 변환할 수 있는 변환 생성자를 찾아 호출합니다. 변환 생성자가 정의되어 있으면 초기화 뿐만아니라 언제든지 정수값을 Time 객체에 대입할 수도 있습니다. Now=1000대입문은 정수값 1000을 Time형 객체로 만들기 위해 Time(int) 생성자를 호출하여 임시 객체를 만들고 이 객체를 Now에 대입합니다.

Time클래스가 절대초라는 정수형 개념을 지원하므로 정수를 암시적으로 변환하여 TIme객체를 만들 수 있는 표현은 매우 편리합니다. 그러나 이런 기능의 부작용의 원인도 되는데 다음 경우를 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<iostream>
using namespace std;
class Time
{
private:
    int hour, min, sec;
public:
    Time(){}
    Time(int abssec)
    {
        hour = abssec / 3600;
        min = (abssec / 60) % 60;
        sec = abssec % 60;
    }
    void Output()
    {
        cout << "현재시간은 " << hour << " : " << min << " : " << sec << "입니다" << endl;
    }
};
void func(Time When)
{
    When.Output();
}
void main()
{
    Time Now(3723);
    func(Now);
    func(1234);
}
cs

func함수는 Time형의 객체를 인수로 전달받아 그 시간을 출력하는데 정수값을 전달해도 잘 동작합니다. 실인수가 형식인수로 전달되는 과정은 일종의 대입 연산이며 이 과정에서 변환 생성자가 작동하여 정수값을 Time형의 임시 객체로 변환하기 떄문입니다. main에서 func(1234)를 호출했는데 1234가 절대초의 의미를 가지는 값이라면 문제가 없습니다.


하지만 만약 이것이 의도된 호출이 아니라 단순한 실수였다면 대단히 잡기 힘든 버그의 원인이 될 수 있습니다. Time형의 객체를 전달해야하는데 정수값을 잘못 전달해도 컴파일러가 아무런 군말없이 변환을 해버리니 디버깅을 해 보기 전에는 잘못을 알기 어렵습니다. 뿐만아니라 func('S');나 func(123.123);같은 호출문조차도 에러로 처리되지 않습니다. 문자형이나 실수형은 정수형으로 암시적 변환이 가능하고 이렇게 변환된 정수형은 다시 변환 생성자에 의해 Time형 객체로 변환이 가능하기 떄문입니다.


변환 생성자는 편리하기도 하지만 클래스와 일반 타입간의 구분을 모호하게 만들어 버리는 맹점이있습니다. 변환생성자의 존재는 컴파일러에게 더 많은 암시적 변환 수단을 제공하여 엄격한 타입 체크를 방해하며 이는 버그의 원인이 되기에 충분합니다. 이런 부작용이 우려되면 explicit 키워드를 앞에 붙여 줍니다.     

1
2
3
4
5
6
7
8
9
10
11
12
class Time
{
private:
    int hour, min, sec;
public:
    Time(){}
    explicit Time(int abssec)
    {
        hour = abssec / 3600;
        min = (abssec / 60) % 60;
        sec = abssec % 60;
    }
cs

explicit로 지정된 생성자는 암시적인 형 변환에 사용할 수 없도록 금지됩니다. 즉 컴파일러가 임의적인 판단을 하지 못하도록 합니다. 하지만 명시적인 형 변환이나 캐스트 연산자를 사용하는 것은 가능합니다.

1
2
3
Time Now = 3723;
Time Now(3723);
Time Now = (Time)3723;
cs

명시적인 생성자 호출이나 캐스트 연산자는 사용자가 변환하라는 의지를 분명히 밝힌것이므로 explicit 키워드와는 상관없이 허용됩니다. 사용자가 책임을 지겠다고 변환을 지시한 것이기 때문에 컴파일러는 이 지시를 거부 하지는 않습니다. 하지만 대입이나 함수 호출에 의한 암시적인 변환은 컴파일 에러로 처리됩니다.

변환생성자는 필요한 만큼 정의할 수 있습니다. 만약 실수값으로부터 Time 객체를 생성하도록 하고싶다면 실수 하나를 인수로 취하는 생성자를 정의하면 됩니다. 다음 생성자를 Time 클래스에 추가해보겠습니다.

1
2
3
4
5
6
Time(double d)
{
    hour = int(d)%24;
    min=int((d-int(d))*100)%60;
    sec=0;
}
cs

실수를 어떻게 Time객체로 바꿀 것인가에 대한 명확한 규칙이 필요한데 Time(double)생성자의 경우 정수부를 시간으로, 소수부를 분으로하고 초를 상수 0으로 고정시키는 규칙을 적용했습니다. 변환 규칙이 좀 억지스럽기는 하지만 이런 식으로 필요한 변환 생성자를 정의하면됩니다. 이후 Time객체는 Time A(12.34)등의 선언문에 의해 실수값으로부터 변환 생성 될 수 있습니다.


변환생성자는 반드시 인수를 하나만 취해야 하며 둘 이상을 취할 경우 변환 생성자가 아닙닌다. 왜냐하면 변환이란 원칙적으로 일대일 연산이며 TimeA(1234);선언문이나 A=1234; 대입문에서 보다시피 객체 초기화에 필요한 피연산자가 하나밖에 없습니다. 변환 생성자가 적용되는 초기화, 대입연산은 이항 연산을 하는데 좌변은 객체 자신으로 정해져 있으므로 나머지 우변이 되는 반환 대상에 대해서만 인수를 전달 받아야 합니다. 단, 복사 생성자는 인수를 하나만 취하지만 동일 타입으로부터 사본을 생성ㅇ하므로 변환생성자라고는 할 수 없습니다.


변환 함수


변환 생성자를 정의하면 정수값으로부터 Time형 객체를 만들 수 있고 Time형 객체에 정수값을 대입할 수도 있습니다. 이것이 가능하다면 반대의 변환, 즉 Time형 객체로부터 정수값을 만들어내는 것도 가능할 것입니다. 정수가 Time이 될 수 있다면 Time도 정수가 될 수 있어야 비로소 두 타입이 완전히 호환된다고 표현할 수 있습니다. 호환이 되면 다음 코드가 제대로 동작하여야합니다.

1
2
3
Time Now(18,25,19);
int i = Now;
printf("%d\n", i);
cs

18:25:19라는 시간이 절대초로 얼마인가를 계산한 후 정수값으로 출력해보는 코드입니다. 하지만 아직 이 코드는 동작하지 않습니다. 왜냐하면 Time클래스는 정수를 Time으로 바꾸는 변환생성자만 제공할뿐 자신을 정수로 바꾸는 방법은 제공하지 않기 때문입니다. 객체를 일반타입으로 역변환 하려면 변환 함수(Conversion Function)를 정의하여야합니다. 변환 함수의 형식은 다음과 같습니다.

1
2
3
4
operator 변환타임()
{
    내용
}
cs

키워드 operator 다음에 변환하고자 하는 타입의 이름을 밝히고 본체에는 변환 방법을 작성합니다. 변환 함수는 인수를 취하지 않으며 리턴 타입도 지정하지 않습니다. 왜냐하면 연산 대상은 자기 자신으로 고정되어 있고 변환 결과는 지정한 타입임을 이미 알고 있기 때문입니다. 객체 자신을 다른 타입으로 변환하는 동작을 하므로 작업거리와 결과가 이미 정해져있는 것입니다. 다음 소스는 Time클래스에 시분초를 전달받는 생성자와 변환 함수를 추가해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include<iostream>
using namespace std;
class Time
{
private:
    int hour, min, sec;
public:
    Time(){}
    explicit Time(int abssec)
    {
        hour = abssec / 3600;
        min = (abssec / 60) % 60;
        sec = abssec % 60;
    }
    Time(int h, int m, int s)
    {
        hour = h;
        min = m;
        sec = s;
    }
    operator int()
    {
        return hour * 3600 + min * 60 + sec;
    }
    void Output()
    {
        cout << "현재시간은 " << hour << " : " << min << " : " << sec << "입니다" << endl;
    }
};
void func(Time When)
{
    When.Output();
}
void main()
{
    Time Now(18,25,19);
    int i = Now;
    printf("%d\n", i);
}
cs

operator int() 변환 함수가 Time 클래스의 멤버 함수로 작성되어 있습니다. 변환 함수의 본체는 아주 단순한데 시간에 3600을 곱한값, 분에 60을 곱한값, 그리고 초를 모두 더하면 절대초를 쉽게 구할 수 있으며 이 구한 정수값을  턴합니다. 변환 함수의 원형에는 리턴타입이 없지만 어디까지나 생략된 것일 뿐이므로 본체에서 return을 사용할 수 있습니다. 변환 함수에 의해 객체를 int로 반환할 수 있는 방법이 정의되었으므로 이제 Time형 객체는 정수형 변수에 대입할 수 있습니다. main함수에서 Time형 객체 Now를 18,25,19로 초기화를 하고 정수형 변수 i를 Now로 초기화했습니다. 이떄 변환 함수가 호출되어 Now 객체의 멤버값으로부터 절대초를 계싼하여 리턴할 것이며 i는 그 결과값을 가집니다. 출력결과는 18:25:19의 절대값인 66319가 됩니다. int와 호환이 된다는 것은 사실상 모든 수치형과 호환될 수 있다는 의미이고 Time형 객체는 char, double, float, long등의 타입과도 상호변환 가능합니다.


변환함수와 변환 생성자는 하는 일이 비슷하기 떄문에 닮은 점이 많습니다. 우선 변환 생성자와 마찬가지로 변환 함수도 필요한 만큼 얼마든지 정의할 수 있습니다. Time객체를 실수나 문자형으로도 변환하도록 하고 싶다면 operator double(), operator char()함수를 더 정의하면 됩니다. 또한 변환 함수도 변환 생성자와 똑같은 이유로 다소 위험한 면이 있습니다.

1
2
3
4
void func(int i)
{
 
}
cs

func 함수는 정수형 인수를 하나 받아들이는데 이 함수에 대해 func(Now)를 호출할 수도 있습니다. 왜냐하면 변환함수에 의해 Time형 객체가 정수로 변환될 수 있기 떄문입니다. 의도적인 호출이라면 물론 변환 함수의 서비스를 받게되지만 단순한 실수일 경우는 문제가 커지게 됩니다.

1
2
3
int Nox, Noy;
Time Now;
gotoxy(Nox,Now);
cs

gotoxy의 두 번쨰 인수는 Noy를 잘못적은 것이기도 하지만 문법적으로 적법합니다. Now가 정수가 될 수 있으므로 gotoxy의 y좌표로 이용하더라도 문제가 없습니다. 심지어 ar[Now]=0;같이 배열의 첨자에도 Time형 객체를 쓸 수 있으며 ptr+Now 같은 포인터에 Time형 객체를 더하는것도 허용됩니다. 변환 함수가 있으니 이렇게 수상해 보이는 코드는 경고처리가 되지 않습니다.

게다가 변환함수는 변환 생성자처럼 explicit로 암시적 변환은 금지하는 장치도 없어 주의 깊게 사용하는 수 밖에 없습니다. 암시적 변환이 정 문제가 된다면 아예 변환 생성자나 변환 함수를 만들지 말고 TimeToInt, IntToTime같은 명시적인 함수를 만들어 사용하는 편이 안전합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include<iostream>
using namespace std;
class Time
{
private:
    int hour, min, sec;
public:
    Time(){}
    void IntToTime(int abssec)
    {
        hour = abssec / 3600;
        min = (abssec / 60) % 60;
        sec = abssec % 60;
    }
    Time(int h, int m, int s)
    {
        hour = h;
        min = m;
        sec = s;
    }
    operator int()
    {
        return hour * 3600 + min * 60 + sec;
    }
    void Output()
    {
        cout << "현재시간은 " << hour << " : " << min << " : " << sec << "입니다" << endl;
    }
};
void func(Time When)
{
    When.Output();
}
void main()
{
    Time Now(18,25,19);
    int i = Now;
    printf("%d\n", i);
 
    Time Now2;
    Now2.IntToTime(i);
    Now2.Output();
 
}
cs

이렇게 되면 변환이 필요할 때 사용자가 멤버 함수를 명시적으로 호출해야만 하며 컴파일러가 어떠한 변환도 하지 않으므로 좀 불편은 하지만 위험하진 않습니다.


클래스간의 변환



앞의 것들은 클래스와 기본 타입간의 변환에 대해 보았는데 이런 변환장치들은 클래스에게 가급적 기본타입과 동등한 자격을 주기위해 마련된 것들입니다. 이번에는 클래스끼리의 변환에 대해 알아보되 클래스 타입이므로 기본 타입간의 변환과 다르지는 않습니다. 섭씨 클래스를 화씨 클래스간 변환하는 간단한 소스 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include<iostream>
using namespace std;
class Farenheit;
class Celcius
{
public:
    double Tem;
    Celcius(){}
    Celcius(double aTem) :Tem(aTem){}
    operator Farenheit();
    void Output()
    {
        cout << "섭씨 = " << Tem << endl;
    }
};
class Farenheit
{
public:
    double Tem;
    Farenheit(){}
    Farenheit(double aTem) :Tem(aTem){}
    operator Celcius();
    void Output()
    {
        cout << "화씨 = " << Tem << endl;
    }
};
Celcius::operator Farenheit()
{
    Farenheit F;
    F.Tem = Tem*1.+ 32;
    return F;
}
Farenheit::operator Celcius()
{
    Celcius C;
    C.Tem = (Tem - 32/ 1.8;
    return C;
}
 
void main()
{
    Celcius C(100);
    Farenheit F = C;
    C.Output();
    F.Output();
 
    cout << endl;
    Farenheit F2 = 120;
    Celcius C2 = F2;
    F2.Output();
    C2.Output();
}
cs

두 크래스가 서로를 상호 참조하므로 순서를 정할 수 없으며 나중에 선언되는 클래스에 대한 정방 선언이 필요합니다. class Farenheit;선언문은 클래스의 일종이라는 것을 미리 알리며 변환함수의 원형 선언을 위해 전방 선언이 먼저 되어 있어야 합니다. 각 클래스의 멤버 함수들은 상대방 클래스의 모양을 정확하게 알아야 하므로 외부 정의만 가능합니다.

각 변환 함수는 상대편의 임시 객체를 만든 후 변환 공식대로 초기화하여 리턴합니다. 지역변수를 리턴하는 코드가 어색해 보이지만 잠시 사용하고 호출 객체에 대입되면 사라져도 상관없으므로 문제는 되지 않습니다.

'Programming > Cplusplus' 카테고리의 다른 글

C++ - 캡슐화(2/3)  (0) 2015.09.19
C++ - 캡슐화(1/3)  (0) 2015.09.19
C++ - 클래스 생성자/파괴자(2/3)  (2) 2015.08.31
C++ - 클래스 생성자/파괴자(1/3)  (1) 2015.08.30
C++ - 파괴자(Destructor)  (0) 2015.08.30