2016년 3월 2일 수요일

[C++] 생성자, 소멸자

생성자: 객체를 생성할 때 자동적으로 호출되는 함수
  - 클래스와 같은 이름의 형태
  - 리턴 하지도 않는 특징
왜 필요한가? 객체를 생성과 동시에 초기화를 해주기 위해 필요하다.  

  객체를 생성과 동시에 초기화 주는 것이 클래스의 좋은 구조이기 때문에, C++는 정보은닉과 더불어 생성과 동시에 초기화라는 안전성을 제공하는 생성자라는 문법을 제공해 주는 것이다. 

생성자의 형태
   1. 디폴트 생성자 (Default Constructor)
   2. 인자가 있는 생성자 
   3. 복사 생성자 (Copy Constructor) 


- 디폴트 생성자  
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person{  
  5. public:  
  6.     int number;  
  7.     char *name;  
  8.       
  9.     //생성자  
  10.     Person()  
  11.     {  
  12.         number =0; name = "Noname";   
  13.     }     
  14.     void print()  
  15.     {  
  16.         cout<< "Number : "<<number <<" Name :" <<name <<endl;  
  17.     }  
  18. };  
  19.   
  20. int main()  
  21. {  
  22.     Person P;  
  23.     P.print();  
  24.   
  25.     return 0;  
  26. }  
 위에서 생성자 부분을 보면, 생성자의 원형만 적어 주었다. 생성자는 클래스와 동일한 이름을 가진 멤버 함수로, 다른 멤버 함수와 같이 클래스 안이나 바깥쪽에서 정의하는 것이 가능하다.  객체를 생성하면 기본 적으로 생성자가 호출 되는데, 이렇게 인자가 없는 생성자를 디폴트 생성자라고 부른다. 
  굳이 생성자를 저렇게 명시적으로 적어 주지 않더라도 클래스에서는 디폴트 생성자를 호출한다. (하지만 초기화에 대한 기대는 버려야 할 것이다.)

- 인자가 있는 생성자
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person{  
  5. public:  
  6.     int number;  
  7.     char *name;  
  8.       
  9.     //인자가 있는 생성자  
  10.     Person(int _number, char* _name)  
  11.     {  
  12.         number =_number; name = _name;  
  13.     }     
  14.     void print()  
  15.     {  
  16.         cout<< "Number : "<<number << endl<<"Name :" <<name <<endl;  
  17.     }  
  18. };  
  19.   
  20. int main()  
  21. {  
  22.     Person P(1, "myname");  
  23.     P.print();  
  24.   
  25.     return 0;  
  26. }  
 인자가 있는 생성자는 별도로 정의되어 있는 용어는 아니지만, 말그대로 생성자에 인자값을 가진것이 바로 인자가 있는 생성자이다. 디폴트 생성자와 크게 다를바가 없지만, 인자가 있기 때문에 어떻게 인자를 넘겨줄 것인지가 문제가 된다.. 
  생성자는 객체를 생성할 때, 호출되기 때문에 인자를 넘겨주는 것도 객체를 생성할때 위와 보는 것과 같이 main 함수에서 해주어야 한다.  이렇게 인자가 있는 생성자를 사용하면 객체의 생성과 동시에 초기화 하는 것이 편해진다. 

- 복사 생성자
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Person{  
  5. private:  
  6.     int number;  
  7.     char *name;  
  8. public:  
  9.       
  10.     Person()  
  11.     {  
  12.         number = 0; name = "noname";  
  13.     }  
  14.     Person(int _number, char* _name)  
  15.     {  
  16.         number =_number; name = _name;  
  17.     }  
  18.       
  19.     //복사 생성자  
  20.     Person(const Person& p)  
  21.     {  
  22.         cout<<"Copy Constructor Call"<<endl<<endl;  
  23.         number = p.number;  
  24.         name = p.name;  
  25.     }  
  26.     void print()  
  27.     {  
  28.         cout<< "Number : "<<number << endl<<"Name :" <<name <<endl;  
  29.     }  
  30. };  
  31.   
  32. int main()  
  33. {  
  34.     //객체 생성  
  35.     Person P1(1, "myname"), P2(2, "your name");  
  36.     Person P3 = P1;  
  37.   
  38.     P3.print();  
  39.     P3 = P2;  
  40.       
  41.     cout<<endl;  
  42.     P3.print();  
  43.   
  44.     return 0;  
  45. }  
 복사 생성자는 다른 객체로부터 값을 복사해서 초기화하는데 사용하며, 자신과 동일한 타입의 객체에 대한 레퍼런스를 인자로 받는 생성자이다. 인자값에 Const를 써준 이유에 대해서는 아래의 링크를 참조 하면 도움이 될 것이다. 

  복사 생성자는 36번째 줄처럼 객체를 사용해서 초기화하는 경우에 호출이 된다 .디폴트 생성자를 호출하고 나서 복사 생성자를 또 호출하는게 아니라, 오직 복사 생성자만 호출한다. 

  우리가 복사 생성자를 만들지 않아도, 36번째 줄처럼 초기화는 잘 작동을 한다. 그럼 왜 복사 생성자를 만들어야 하나? 그 이유는 1:1 복사 하는 것 말고 다른 방식으로 복사하고 싶은 경우가 있기 때문에 그런 경우에 복사 생성자를 재정의 해서 사용하면 될것이다. 


* 객체가 소멸되는 시점 : 함수 내에 지역적으로 변수가 선언되면 함수 호출이 끝남과 동시에 소멸이 된다. 이와 마찬가지로 객체도 함수내에서 선언된다고 하면, 함수 호출이 끝나면 소멸되게 된다. 

* 전역적으로 선언된 객체 
: 전역변수는 프로그램 시작과 동시에 메모리에 올라갔다가, 프로그램이 종료될때 소멸된다. 객체도 마찬가지로 똑같은 동작을 하지만, 객체는 이렇게 생성할 일은 거의 없다고 보면 된다. 

* 전역 : 일반적인 객체지향에 전역이란 개념은 존재 하지 않는다. 이를 대신하기 위한 static 멤버 변수, 멤버 함수가 존재 한다. (이거 대해서는 나중에...)


소멸자 : 객체의 메모리 반환을 위해서 객체 소멸시 자동 호출되는 함수
   -  클래스의 이름 앞에는 ~가 붙은 형태
   - 리턴하지 않고, 리턴 타입도 없다. 
   - 전달인자는 항상 Void 형으로 오버로딩, 
   - 디폴트 매개변수의 선언이 불가능하다는 특징

  그럼 아래의 동적 할당 예제를 통해서 소멸자가 어떻게 동작하고 있는지 한번 자세히 알아 보도록 하자. 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class DynamicArray{  
  5. public:  
  6.     int *arr;  
  7.   
  8.     DynamicArray(int arraySize)  
  9.     {  
  10.         arr = new int [arraySize]; //인자로 받는 크기만큼 메모리를 할당한다.  
  11.     }  
  12.       
  13.     //소멸자, 메모리를 해제한다.   
  14.     ~DynamicArray()  
  15.     {  
  16.         delete[] arr;  
  17.         arr = NULL;  
  18.     }  
  19. };  
  20.   
  21. void main()  
  22. {  
  23.     int size;  
  24.     int i;  
  25.     cout<< "몇개의 정수를 입력하겠는가? ";  
  26.     cin>>size;  
  27.   
  28.     DynamicArray da(size);  
  29.   
  30.     cout<< size <<"개의 정수를 입력 하시오. "<<endl;  
  31.     for(i=0; i< size; i++)  
  32.         cin>>da.arr[i];  
  33.   
  34.     for(i = size -1 ; i>=0; --i)  
  35.         cout<< da.arr[i]  << " ";  
  36.   
  37.     cout<<endl;     
  38. }  
- 결과값 

  DynamicArray 클래스는 내부적으로 동적 메모리 할당을 사용한다. 28번째 줄에서 DynamicArray 타입의 객체 da를 생성하면서 필요한 메모리의 크기를 인자로 전달하는데, 10번째 줄처럼 메모리를 동적으로 할당하고 그 주소를 arr 멤버 변수에 보관해준다. 아마 DynamicArray를 생성하면 아래와 같은 모습을 띄고 있을 것이다. 

 결국 main() 함수가 끝나면 da 객체가 자동적으로 소멸하게 된다. 함수 안에서 정의한 변수는 함수가 종료와 동시에 소멸되므로, 객체가 소멸하면서 자동적으로 소멸자가 호출되고, 16번째 줄처럼 소멸자 안에서 arr이 가리키는 메모리를 해제 한다. 
  만약 소멸자가 없었다 라고 한다면, 우리는 클래스를 만들고 프로그램을 짤때마다 일일히 다 해제를 해줘야 한다. 만약 프로그램의 크기가 커진다고 생각하면, 참 큰일이 아닐수 없다. 

객체의 소멸 순서는 소멸자 호출 -> 메모리 반환 순서로 객체가 소멸되고, 소멸자의 호출을 먼저 해주는 이유는 소멸자의 호출을 먼저 해줌으로서, 메모리가 반환되어질때, 반환되어지지 않은 메모리 공간을 명시적으로 반환해주기 위해서이다. 

 기본적으로 소멸자를 명시해주지 않아도 디폴트 소멸자가 사용되고, 디폴트 소멸자는 디폴트 생성자와 같은 특징을 가지고 있다. 소멸자의 명시적 제공은 첫번째, 생성자에서 메모리를 동적으로 할당하는 경우나 디버깅시 사용자가 객체의 소멸되는 시점을 알고 싶을때 사용을 한다. 

댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...