GONII 2015. 2. 20. 12:15

26.1 생성자

26.1.1 생성자

:: 객체 초기화

클래스의 객체를 선언하면 메모리에 이 객체가 즉시 생성된다. 그러나 메모리만 할당 될 뿐이지 초기화는 되지 않으므로 객체 내의 멤버 변수들은 모두 쓰레기값을 가지고 있을 것이다. 쓰레기값을 가지고 있는 객체는 쓸모가 없으며 그래서 객체 선언문 다음에는 통산 객체를 원하는 상태로 초기화하는 대입문이 따라온다.

   

객체를 초기화하는 이 특별한 함수를 생성자(Constructor)라고 부른다.

생성자는 클래스 스스로 자신을 초기화하는 방법을 정의하며 클래스를 기본 타입과 동등하게 만드는 언어적 장치이다. 생성자의 이름은 항상 클래스의 이름과 동일하며 필요할 경우 초기화에 사용할 인수를 받아들일 수 있지만 리턴값은 가질 수 없다.

   

  • 예제 Constructor

#include <iostream>

using namespace std ;

   

class position

{

private :

int x ;

int y ;

char ch ;

public :

position ( int ax, int ay, char ach)

{

x = ax ;

y = ay ;

ch = ach ;

}

void outPosition()

{

cout << x << endl ;

cout << y << endl ;

cout << ch << endl ;

}

} ;

   

void main ( void )

{

position here(30, 10, 'a') ;

here.outPosition() ;

}

:: 생성자 호출

  1. 암시적인 방법 : position here(30, 10, 'a') ; // 간단하고 직관적, 더 많이 사용됨
  2. 명시적인 방법 : position here=position(30, 10, 'a') ;

       

::생성자의 인수

  1. 형식 인수에 일정한 접두를 붙여 멤버 이름과 구분되도록 한다.
  2. 멤버 이름을 작성하는 특별한 규칙을 정하고 이 규칙대로 멤버 이름을 짓는다.
  3. 형식 인수 이름과 멤버 이름을 같이 쓰되 함수의 본체에서 멤버 변수를 참조할 때 범위 연산자를 사용한다.

       

:: 생성자 오버로딩

생성자도 분명히 함수의 일종이다. 그러므로 오버로딩이 가능하며 디폴트 인수를 사용할 수도 있고 인라인으로 선언할 수도 있다.

  • 예제 ConstructOverload

#include <iostream>

using namespace std ;

   

class position

{

private :

int x ;

int y ;

char ch ;

   

public :

position ( char a_ch)

{

x = 80 ;

y = 24 ;

ch = a_ch ;

}

position ( int a_x, int a_y, char a_ch='s')

{

x = a_x ;

y = a_y ;

ch = a_ch ;

}

void outPosition()

{

cout << x << endl ;

cout << y << endl ;

cout << ch << endl ;

}

} ;

   

void main ( void )

{

position here(30, 10, 'a') ;

position there(40, 10) ;

position where1('k') ;

   

here.outPosition() ;

there.outPosition() ;

where1.outPosition() ;

}

   

26.1.2 파괴자(소멸자)

생성자는 주로 멤버 변수의 값을 원하는 값으로 대입하는 작업을 하지만 그 외 객체가 동작하는데 필요한 모든 초기화 처리를 담당하기도 한다.

예를 들어 네트워크 통신을 하는 객체의 경우 이 객체가 동작하려면 네트워크 연결을 먼저 해야 하며 데이터베이스를 액세스하는 객체라면 서버와 연결해야 하는데 이런 동작 환경 초기화도 생성자의 임무에 속한다.

요컨데 생성자는 객체가 제대로 동작하기 위한 모든 처리를 담당하는 함수이다.

객체가 사라질 때 반대의 처리를 할 함수도 필요하다.

이러한 뒷처리를 하는 특별한 멤버 함수를 파괴자(Destructor)라고 하며 객체가 소멸될 때 컴파일러에 의해 자동으로 호출된다. 파괴자의 이름은 클래스 이름 앞에 ~를(tilde라고 읽는다) 붙인 것으로 고정되어 있으며 인수와 리턴값을 가지지 않는다.

  • 예제 Person1

#include <iostream>

using namespace std ;

   

class person

{

private :

char *name ;

int age ;

public :

person(const char *a_name, int a_age)

{

name = new char[strlen(a_name)+1] ;

strcpy(name, a_name) ;

age = a_age ;

}

~person()

{

delete[] name ;

}

void outPerson()

{

cout << "이름 : " << name << ", 나이 : " << age << endl ;

}

} ;

   

void main ( void )

{

person boy("을지문덕", 28) ;

boy.outPerson() ;

}

파괴자는 객체가 사라질 때 컴파일러에 의해 자동으로 호출되는데 객체가 바꿔 놓은 환경을 원래대로 돌려 놓거나 할당한 자원을 회수하는 역할을 한다.

   

position 클래스는 파괴될 때 특별히 할 일이 없으므로 파괴자가 불필요하지만 person 클래스는 생성자가 메모리를 도적으로 할당하므로 이 메모리를 해제할 파괴자가 반드시 필요하다. 메모리를 아무도 해제하지 않으면 메모리 누수가 발생할 것이다.

26.1.3 생성자, 파괴자의 특징

  1. 이름이 정해져 있다.
  2. 리턴값이 없다.
  3. 반드시 public 액세스 속성을 가져야 한다.
  4. 생성자는 인수가 있지만 파괴자는 인수가 없다.
  5. friend도 static도 될 수 없다.
  6. 파괴자는 가상 함수로 정의할 수 있지만 생성자는 가상 함수로 정의될 수 없다.
  7. 둘 다 디폴트가 있다.

26.1.4 객체의 동적 생성

실행 중에 객체를 동적으로 생성할 때는 new 연산자를 사용한다.

  • 예제 Personmalloc

#include <iostream>

using namespace std ;

   

class person

{

private :

char *name ;

int age ;

   

public :

person( const char *a_name, int a_age)

{

name = (char *) malloc(strlen(a_name)+1) ;

strcpy(name, a_name) ;

age = a_age ;

}

   

~person()

{

free(name) ;

}

   

void outPerson()

{

cout << "이름 : " << name << ", 나이 : " << age << endl ;

}

} ;

   

void main ( void )

{

person boy("을지문덕", 25) ;

boy.outPerson() ;

   

person *pGirl ;

pGirl = new person("신사임당", 19) ;

pGirl->outPerson() ;

delete pGirl ;

}

malloc이 생성자를 호출하지 않는 것과 마찬가지로 free는 파괴자를 호출하지 않는다.

26.2여러 가지 생성자

26.2.1 디폴트 생성자

디폴트 생성자(기본 생성자)란 인수를 가지지 않는 생성자이다.

  • 예제 DefConstructor

#include <iostream>

using namespace std ;

   

class position

{

private:

int x ;

int y ;

char ch ;

   

public :

position()

{

x = 0 ;

y = 0 ;

ch = ' ' ;

}

void outPosition()

{

if ( ch != ' ' )

{

cout << x << endl ;

cout << y << endl ;

cout << ch << endl ;

}

}

} ;

   

void main ( void )

{

position here ;

   

here.outPosition() ;

   

}

디폴트 생성자는 호출부에서 어떤 값으로 초기화하고 싶은지를 전달하는 수단인 인수가 없다.

   

인수를 받아들이지 않기 때문에 객체의 멤버에 의미 있는 어떤 값을 대입하지는 못하며 주로 모든 멤버를 0이나 -1또는 NULL이나 빈 문자열로 초기화한다.

   

생성자가 없을 경우 컴파일러가 디폴트 생성자를 만들기 때문에 생성자를 전혀 정의하지 않아도 객체를 선언할 수 있다.

   

  • 예제 NoDefCon

#include <iostream>

using namespace std ;

   

class position

{

private :

int m_x ;

int m_y ;

char m_ch ;

public :

position( int x, int y, char ch )

{

m_x = x ;

m_y = y ;

m_ch = ch ;

}

void outPosition()

{

cout << m_x << endl ;

cout << m_y << endl ;

cout << m_ch << endl ;

}

} ;

   

void main ( void )

{

// position there[3] ;                // 사용할 수 있는 생성자가 없다고 나옴

}

position there[3] = {position(1,2,'x'), position(3,4,'y'), position(5,6,'x')} ;

   

객체 배열을 선언하면서 초기화 해줘야 한다.

   

아니면 디폴트 생성자를 정의해줘야함

   

26.2.2 복사 생성자

클래스가 int와 동일한 자격을 가지는 타입이 되기 위해서는 이미 생성되어 있는 같은타입의 객체로부터 초기화 될 수 있어야 한다.

  • 예제 person2

#include <iostream>

   

using namespace std ;

   

class person

{

private :

char *name ;

int age ;

   

public :

person(const char *aname, int aage)

{

name = new char[strlen(aname)+1] ;

strcpy( name, aname ) ;

age = aage ;

}

person ( const person &other )

{

name = new char[strlen(other.name)+1] ;

strcpy (name, other.name) ;

age = other.age ;

}

~person()

{

delete []name ;

}

void outPerson()

{

cout << "이름 : " << name << ", 나이 : " << age << endl ;

}

} ;

   

void main ( void )

{

person boy("강감찬", 22) ;

person young = boy ;

young.outPerson() ;

}

복사 생성자는 자신과 같은 타입의 다른 객체에 대한 레퍼런스를 전달받아 이 레퍼런스로부터 자신을 초기화한다.

   

person 복사 생성자는 동일한 타입의 other를 인수로 전달받아 자신의 name에 other.name의 길이만큼 버퍼를 새로 할당하여 복사한다. 새로 메모리를 할당해서 내용을 복사했으므로 이 메모리는 완전한 자기 것이며 안전하게 따로 관리할 수 있다. age는 물론 단순 변수이므로 값만 대입받으면 된다.

   

   

young과 boy는 타입만 같을 뿐 완전히 다른 객체이고 메모리도 따로 소유하므로 각자의 name을 마음대로 바꿀 수 있고 파괴자에서 메모리를 해제해도 문제가 없다. 복사 생성자에 의해 두 객체가 완전한 독립성을 얻은 것이다.

:: 객체가 인수로 전달될 때

void printAbout ( person anyBody )

{

anyBody.outPerson() ;

}

void main ( void )

{

person boy ("강감찬", 22) ;

printAbout(boy) ;

}

함수 호출 과정에서 형식 인수가 실인수로 전달되는 것은 일종의 복사생성이다. 함수 내부에서 새로 생성되는 형식인수 anyBody가 실인수 boy를 대입받으면서 초기화되는데 이때 복사생성자가 없다면 anyBody가 boy를 얕은 복사하며 두 객체가 동적 버퍼를 공유하는 상황이 된다.

함수의 인수로 사용되거나 리턴값으로 사용되는 객체는 반드시 복사 생성자를 제대로 정의해야 한다.

   

:: 복사 생성자의 인수

복사 생성자의 인수는 반드시 객체의 레퍼런스여야 하며 객체를 인수로 취할 수는 없다.

복사 생성자 자신도 함수이므로 실인수를 전달 할 때 값의 복사가 발생할 것이다. 객체 자체를 인수로 전달하면 복사 생성자로 인수를 넘기는 과정에서 다시 복사 생성자가 호출될 것이고 이 복사 생성자는 인수를 받기 위해 또 다시 복사 생성자를 호출한다. 결국 자기가 자신을 종료조건없이 호출해대는 무한 재귀 호출이 발생

   

Class 클래스의 복사 생성자 원형은 Class( const Class & ) 여야 한다.

   

:: 디폴트 복사 생성자

디폴트 복사 생성자는 멤버끼리 1:1로 복사함으로써 원본과 완전히 같은 사본을 만들기만 할 뿐 깊은 복사는 하지 않는다.

   

26.2.3 멤버 초기화 리스트

객체 초기화의 임무를 띤 생성자가 하는 주된 일은 멤버 변수의 값을 초기화 하는 것이다. 그래서 생성자의 본체는 보통 전달받은 인수를 멤버 변수에 대입하는 대입문으로 구성된다. 멤버에 단순히 값을 대입하기만 하는 경우 본체에서 = 연산자를 쓰는 대신 초기화 리스트(Member Initialization LIst) 라는 것을 사용 할 수 있다. 초기화 리스트는 함수 선두와 본체 사이에 : 을 찍고 멤버와 초기값의 대응 관계를 나열하는 것이다.

position(int ax, int ay, char ach) : x(ax), y(ay), ch(ach)

{

// 더 하고 싶은 일

}

   

:: 상수 멤버 초기화

상수는 선언할 때 반드시 초기화 해야 한다.

  • 예제 InitConstMember

#include <iostream>

using namespace std ;

   

class some

{

private :

public :

const int value ;

some (int i ) : value( i ) {}

void outValue()

{

cout << value << endl ;

}

};

   

void main ( void )

{

some s(5) ;

s.outValue() ;

}

value 멤버는 상수이므로 값을 변경할 수 없으며 대입 연산 자체가 인정되지 않는다. 그래서 초기화 리스트라는 특별한 문법이 필요하다.

   

클래스 선언문은 컴파일러에게 클래스가 어떤 모양을 하고 있다는 것을 알릴 뿐이지 실제 메모리를 할당하지는 않는다. 그러므로 value 멤버는 아직 메모리에 실존하지 않으며 존재하지도 않는 대상의 값을 초기화할 수는 없다. 상수는 객체가 생성될 때 반드시 초기화되어야 하며 상수 멤버 초기화의 책임은 생성자에게 있다. 따라서 상수 멤버를 가지는 클래스의 모든 생성자들은 상수 멤버에 대한 초기화 리스트를 가져야 한다.

   

:: 레퍼런스 멤버 초기화

레퍼런스는 변수에 대한 별명이며 선언할 때 반드시 누구에 대한 별명인지를 밝혀야 한다.

  • 예제 InitRefMember

#include <iostream>

using namespace std ;

   

class some

{

private :

public :

int &ri ;

some(int &i) : ri(i) { }

void outValue()

{

cout << ri << endl ;

}

} ;

   

void main ( void )

{

int i = 5 ;

some s( i ) ;

s.outValue() ;

}

   

:: 포함된 객체 초기화

구조체 끼리 중첩할 수 있듯이 클래스도 다른 클래스의 객체를 멤버로 가질 수 있다.

포함된 객체를 초기화할 때도 초기화 리스트를 사용한다.

  • 예제 InitEmbeded

#include <iostream>

using namespace std ;

   

class position

{

private :

public :

int x, y ;

position ( int ax, int ay )

{

x = ax;

y = ay;

}

} ;

   

class some

{

public :

position pos ;

some( int x, int y ) : pos(x, y) { }

void outValue()

{

cout << pos.x << ", " << pos.y << endl ;

}

} ;

   

void main ( void )

{

some s(3, 4) ;

s.outValue() ;

}

만약 포함된 객체가 디폴트 생성자를 정의 한다면 초기화 리스트에서 초기화하지 않아도 컴파일러가 디폴트 생성자를 호출하며 에러는 발생하지 않는다. 그러나 디폴트 생성자는 쓰레기를 치우느 정도 밖에 할 수 없으므로 원한느 초기화는 아닐 확률이 높다.

   

이 외에 상속받은 멤버를 초기화할 때도 초기화 리스트를 사용한다.

26.3 타입 변환

26.3.1 변환 생성자

일반 타입의 변수끼리 값을 대입할 때는 산술 변환 규칙에 따라 암시적으로 상호 변환된다.

클래스가 일반 타입과 완전히 동등해지려면 타입을 변환할 수 있는 문법적 장치가 있어야 한다. 그 첫 번째 장치가 바로 변환 생성자(Conversion Constructor)이다.

변환 생성자는 기본 타입으로부터 객체를 만드는 생성자이며 인수를 하나만 취한다.

  • 예제 Convert1

#include <iostream>

using namespace std ;

   

class time

{

private :

int hour ;

int min ;

int sec ;

public :

time() {}

time(int abssec)

{

hour = abssec/3600 ;

min = (abssec/60) % 60 ;

sec = abssec % 60 ;

}

void outTime()

{

cout << "현재 시간은 " << hour << " : " << min << " : " << sec << "입니다." << endl ;

}

} ;

   

void main( void )

{

time now(3723) ;

now.outTime() ;

}

   

26.3.2 변환 함수

  • 예제 convert2

#include <iostream>

using namespace std ;

   

class time

{

private :

int hour, min, sec ;

public :

time() { }

time(int abssec)

{

hour = abssec/3600 ;

min = (abssec/60)%60 ;

min = abssec%60 ;

}

time(int h, int m, int s)

{

hour = h;

min = m ;

sec = s ;

}

operator int()

{

return hour*3600+min*60+sec ;

}

void outTime()

{

cout << "현재 시간은 " << hour << ":" << min << ":" << sec << endl ;

}

};

   

   

void main ( void )

{

time now(18,25,12) ;

int i = now ;

cout << i << endl ;

}

   

  • 예제 convert3

#include <iostream>

using namespace std ;

   

class time

{

private :

int hour, min, sec ;

public :

time() {}

time( int h, int m, int s)

{

hour = h, min = m, sec = s ;

}

void outTime()

{

cout << "현재 시간은 " << hour << ":" << min << ":" << sec << "입니다." << endl ;

}

int timeToInt()

{

return hour*3600+min*60+sec ;

}

void intToTime( int abssec )

{

hour = abssec/3600 ;

min = (abssec/60) % 60 ;

sec = abssec % 60 ;

}

} ;

   

void main ( void )

{

time now(18, 25, 12) ;

int i = now.timeToInt() ;

cout << "i = " << i << endl ;

   

time now2 ;

now2.intToTime( i ) ;

now2.outTime( ) ;

}

   

26.3.3 클래스간의 변환

  • 예제 CelFah

#include <iostream>

using namespace std ;

   

class fahrenheit ;

class celsius

{

public :

double tem ;

celsius() {}

celsius(double aTem) : tem(aTem) {}

operator fahrenheit() ;

void outTem()

{

cout << "섭씨 = " << tem << endl ;

}

} ;

   

class fahrenheit

{

public :

double tem ;

fahrenheit() {}

fahrenheit(double aTem) : tem(aTem) {}

operator celsius() ;

void outTem()

{

cout << "화씨 = " << tem << endl ;

}

} ;

   

celsius::operator fahrenheit()

{

fahrenheit F ;

F.tem = tem*1.8+32 ;

return F ;

}

   

fahrenheit::operator celsius()

{

celsius C ;

C.tem = (tem-32) / 1.8 ;

return C ;

}

   

void main ( void )

{

celsius C(100) ;

fahrenheit F = C ;

C.outTem() ;

F.outTem() ;

   

cout << endl ;

   

fahrenheit F2 = 120 ;

celsius C2 = F2 ;

F2.outTem() ;

C2.outTem() ;

}

두 클래스가 서로를 상호 참조하므로 순서를 정할 수 없으며 나중에 선언된 클래스에 대한 정방 선언이 필요하다.

   

두 클래스의 객체끼리는 암시적인 변환이 가능하여 상호 초기식에 사용할 수 있고 언제든지 상대편의 객체를 대입할 수 있다.

   

변환 함수만으로도 필요한 변환을 다 할 수 있는데 변환 생성자를 굳이 만들어 놓는 이유는 무엇일까? 그 이유는 기본 타입은 컴파일러에 내장되어 있어 마음대로 수정할 수 있는 대상이 아니며 변환 함수를 정의할 수 없기 때문이다.

 

반응형