생성자, 소멸자, 대입 연산자
생성자 : 새로운 객체를 메모리에 만드는 데 필요한 과정을 제어하고 객체의 초기화를 맡는 함수
소멸자 : 객체를 없앰과 동시에 그 객체가 메모리에서 적절히 사라질 수 있도록 하는 과정을 제어하는 함수
대입 연산자 : 기존의 객체에 다른 개체의 값을 줄 때 사용하는 함수입니다.
C++의 어떤 멤버 함수는 클래스 안에 직접 선언해 넣지 않으면 컴파일러가 저절로 선언해 주도록 되어 있다.
- 복사 생성자
- 복사 대입 연산자
- 소멸자
이때 컴파일러가 만드는 함수의 형태는 모두 기본형
생성자조차도 선언되어 있지 않으면 역시 컴파일러가 대신 기본 생성자 선언
이들은 모두 public 멤버이며 inline 함수이다.
컴파일러가 만드는 함수가 하는 일 ?
- 기본 생성자와 소멸자가 하는 일은 일차적으로 컴파일러에게 배후의 코드를 깔 수 있는 자리를 마련하는 것
-> 기본 클래스 및 비정적 데이터 멤버의 생성자와 소멸자를 호출하는 코드가 생긴다.
-> 소멸자는 이 클래스가 상속한 기본 클래스의 소멸자가 가상 소멸자로 되어 있지 않으면 역시 비가상 소멸자로 만들어진다.
복사 생성자와 복사 대입 연산자의 경우?
(1) 컴파일러가 몰래 만들어낸 복사 생성자/복사 대입 연산자가 하는 일
- 원본 객체의 비정적 데이터를 사본 객체 쪽으로 그냥 복사하는 것
-- 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있다.
1. 일반적인 경우만 놓고 볼 때, 어떤 클래스에서 특정한 종류의 기능을 지원하지 않았으면 하는 의도를 반영하는 방법
- 그런 기능을 제공하는 함수를 선언하지 않은 것
-> 복사 생성자와 복사 대입 연산자에 대해서는 해당사항 없음
-> 복사 생성자와 복사 대입 연산자는 선언하지 않고 외부에서 이들을 호출하려고 하면 컴파일러가 대신 선언하기 때문
컴파일러가 생성하는 함수는 도구 공개된다는, 즉 public 멤버가 된다는 사실 -> public 멤버로 두지 말고, 복사 생성자 및 복사 대입 연산자를 private 멤버로 선언하도록 한다.
- 클래스 멤버 함수가 명시적으로 선언되기 때문에 컴파일러는 자신의 기본 버전을 만들 수 없게 된다.
- 함수들이 비공개(private)의 접근성을 가지므로 외부로부터의 호출을 차단할 수 있다.
private 멤버 함수는 그 클래스의 멤버 함수 및 프렌드(friend) 함수가 호출할 수 있다는 점이 허점
- 정의 X
-> 멤버 함수를 private 멤버로 선언하고 일부러 정의하지 않는 방법
2. Uncopyable의 구현과 사용법
- Uncopyable로부터의 상속은 public일 필요가 없다.
- Uncopyable의 소멸자는 가상 소멸자가 아니어도 된다.
정리
- 컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private로 선언한 후에 구현은 하지 않은 채로 둔다.
- Uncopyable과 비슷한 기본 클래스를 쓰는 것도 한 방법
다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
- C++ 규정에 의하면, 기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될 때 그 기본 클래스에 비가상 소멸자가 들어 있으면 프로그램 동작은 미정의 사항이라고 되어 있다.
- 그 객체의 파생 클래스 부분이 소멸되지 않게 된다.
가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조가 하나 들어가야 한다.
- 이 자료구조는 프로그램 실행 중에 주어진 객체에 대해 어떤 가상 함수를 호출해야 하는지를 결정하는 데 쓰이는 정보
-> 실제로는 포인터의 형태를 취하는 것이 대부분이고, 대개 vptr(가상 함수 테이블 포인터)이라는 이름으로 불린다.
vptr
(1) 가상 함수의 주소 -> 포인터들의 배열을 가리키고 있으며 가상 함수 테이블 포인터의 배열은 vtbl(가상 함수 테이블)이라고 불린다.
(2) 가상 함수를 하나라도 갖고 있는 클래스는 반드시 그와 관련된 vtbl을 갖고 있다.
(3) 어떤 객체에 대해 어떤 가상 함수가 호출되려고 하면, 호출되는 실제 함수는 그 객체의 vptr이 가리키는 vtbl에 따라 결정
Point 클래스에 가상 함수가 들어가게 되면 Point 타입 객체의 크기가 커진다.
- 50%에서 100% 까지 커진다.
- C 등의 다른 언어로 선언된 동일한 자료구조와 호환성이 없어진다. -> 다른 언어로 Point와 겉보기가 똑같은 데이터 배치를 써서 선언했다고 해도 vptr만은 어떻게 만들 수 없기 때문이다.
기본 클래스의 손에 가상 소멸자를 쥐어 주자는 규칙은 다형성을 가진 기본 클래스 -> 기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록 설계된 기본 클래스에만 적용된다는 사실
다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 한다.
- 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자야 한다.
기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 한다.
소멸자에서는 예외가 빠져나가면 안된다.
- 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 한다.
어떤 클래스의 연산이 진행되다가 던전 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(소멸자가 아닌 함수)이어야 한다.
객체 생성 및 소멸 과정 중에서 절대로 가상 함수를 호출하지 말자
1. 호출 결과가 원하는대로 돌아가지 않는다.
기본 클래스의 생성자가 호출될 동안에는, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않는다. -> 그 대신 객체 자신이 기본 클래스 타입인 것처럼 동작 한다.
기본 클래스 생성자는 파생 클래스 생성자보다 앞서서 실행되기 때문에 기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아니라는 것이 핵심
파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체의 타입은 바로 기본 클래스이다.
호출되는 가상 함수는 모두 기본 클래스의 것으로 결정될 뿐만 아니라, 런타임 타입 정보를 사용하는 언어 요소를 사용한다고 해도 이 순간엔 모두 기본 클래스 타입의 객체로 취급한다.
파생 클래스의 소멸자가 일단 호출되고 나면 파생 클래스만의 데이터 멤버는 정의되지 않은 값으로 가정하기 때문에 이제부터 C++는 이들을 없는 것처럼 취급하고 진행
기본 클래스 소멸자에 진입할 당시의 객체는 기본 클래스 객체가 되며, 모든 C++ 기능들 역시 기본 클래스 객체의 자격으로 처리
생성자 혹은 소멸자 안에서 가상 함수 호출 X
-> 가상 함수라고 해도 지금 실행 중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로 내려가지 않는다.
출처 : 스콧 마이어스, [Effective C++ 제 3판], 곽용재, 프로텍미디어(2015), p 80-104p