상세 컨텐츠

본문 제목

항목 5) C++가 자동으로 만들고 호출하는 함수들

C++/Effective C++

by DeaKyungLee 2021. 12. 5. 10:12

본문

컴파일러가 암시적으로 선언하고 호출하는 함수들을 정확하게 인지하자.

 

자동으로 생성되는 클래스 멤버 함수들

 

프로그래머가 굳이 만들어 주지 않아도 컴파일러가 자동으로 생성시키는 함수들이 존재한다.

대표적으로,

  1. 기본 생성자
  2. 복사 생성자
  3. 복사 대입 연산자
  4. 소멸자

이렇게 4가지 종류를 말할 수 있다.

즉, 아래의 두 코드는 사실상 같은 코드라는 말이다.

// 빈 클래스
class Empty{};
// 자동으로 생성되는 함수들
class Empty
{
public:
	Empty() {  };  // 기본 생성자
	Empty(const Empty& rhs) {  }; // 복사 생성자 
	~Empty() {  }; // 소멸자
	Empty& operator=(const Empty& rhs) {}; // 복사 대입 연산자
};

그리고 각 함수가 호출되는 시점은 아래와 같다.

Empty e1; // 기본 생성자
Empty e2(e1); // 복사 생성자
e2 = e1; // 복사 대입 연산자
더보기
// 실제 실행 예시

#include <iostream>
using namespace std;
class Empty
{
public:
	Empty() { cout << "기본 생성자 호출\n"; };  // 기본 생성자
	Empty(const Empty& rhs) { cout << "복사 생성자 호출\n"; }; // 복사 생성자 
	~Empty() { cout << "소멸자 호출\n"; }; // 소멸자
	Empty& operator=(const Empty& rhs) { // 복사 대입 연산자
		cout << "복사 대입 연산자 호출\n"; 
		Empty re; // 기본 생성자 (re)
		return re;
		// 소멸자 (re)
	}; 
};
int main()
{
	Empty e1; // 기본 생성자 (e1)
	Empty e2(e1); // 복사 생성자
	e2 = e1; // 복사 대입 연산자
	return 0;
	// 소멸자 (e1)
	// 소멸자 (e2)
}

여기서 소멸자에 대한 언급이 하나 필요한데,

기본적으로 생성되는 소멸자는 상속한 기본 클래스의 소멸자가 가상 소멸자가 아니라면, 이것 역시 비가상 소멸자로 만들어진다.

( 가상 소멸자에 대한 내용은 항목 7) 참고 )

(기본) 복사 생성자 및 복사 대입 연산자

자동으로 만들어지는 복사 생성자는 기본적으로 Swallow Copy ( 얕은 복사 )형식으로 작동한다.

때문에 아래 코드는 실행 도중에 에러를 발생시킨다.

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
 
using namespace std;
 
template<class T>
class NameObject {
public:
	NameObject(const char* name, const T& value) : nameValue(name), objectValue(value) 
	{
		cout << "NameObject(const char* name, const T& value) 호출\n";
	}
	NameObject(const string name, const T& value) : nameValue(name), objectValue(value) 
	{
		cout << "NameObject(const string name, const T& value) 호출\n";
	}
 
public:
	string nameValue;
	T objectValue;
};
 
int main()
{
	NameObject<const char*> no1("Smallest Prime Number", "test");
    
    /* 	복사 생성자 호출
 		자동으로 생성되는 기본 복사생성자에서,
        각 비트만 그대로 복사함 : 얕은복사가 일어남. */
	NameObject<const char*> no2(no1);		
	cout << no1.nameValue << endl;
	cout << no2.nameValue << endl;
 
	delete no1.objectValue;
    /*	Error 발생 
    	기본 복사생성자에서 얕은복사로 인해,
        no1.objectValue와 no2.objectValue는 같은 주소를 갖게됨 */
	cout << no2.objectValue << endl; 
	return 0;
}

복사 대입 연산자 역시 복사 생성자와 작동되는 방식은 비슷하다.

하지만 (기본) 복사 대입 연산자는 참조자(&)와 상수(const)가 그 대상일 때는 컴파일 단계에서 생성을 거부해버린다.

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
 
using namespace std;
 
template<class T>
class NameObject {
public:
	NameObject(string& name, const T& value) : nameValue(name), objectValue(value) {}
 
public:
	string& nameValue; // 참조자
	const T objectValue; // 상수
};
 
int main()
{
	string newDog("Persephone");
	string oldDog("Satch");
 
	NameObject<int> d1(newDog, 2);
	NameObject<int> d2(oldDog, 31);
 
	d2 = d1;	// Error : 참조자는 원래 자신이 참조하고 있는것과 다른 객체를 참조할 수 없음. 
				// 또한 상수객체도 동일한 에러가 발생
 
	return 0;
}

곰곰이 생각해보면, 컴파일러 입장에서 컴파일을 거부하는 것은 당연한 상황이다.

보다시피 d1, d2 객체가 선언되는 시점에서 이미 nameValue 참조자 및 ObjectValue 상수에 대한 초기화가 완료되었다. ( 즉, 값이 이미 정해졌다. )

그런데 얕은 복사 형식으로 동작하는 (기본) 복사 대입 연산자에서는 당연히 참조자와 상수에 대한 직접적인 변경을 요구하게 되고, 이것을 뻔히 아는 컴파일러가 가만있을 리가 없다.

그리고 복사 대입 연산자와는 다르게, 복사 생성자는 동작한다.

더보기
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;
template<class T>
class NameObject {
public:
	NameObject(string& name, const T& value) : nameValue(name), objectValue(value) {}
public:
	string& nameValue; // 참조자
	const T objectValue; // 상수
};
int main()
{
	string newDog("Persephone");
	string oldDog("Satch");
	NameObject<int> d1(newDog, 2);
	NameObject<int> d2(d1);
	cout << d2.nameValue << "\n";
	cout << d2.objectValue << "\n";
	//d2 = d1;	// Error : 참조자는 원래 자신이 참조하고 있는것과 다른 객체를 참조할 수 없음. 
				// 또한 상수객체도 동일한 에러가 발생
	return 0;
}

참조자 및 상수에 대한 초기화가 복사 생성자 단계에서 이루어지기 때문에, 아무런 문제 없이 동작한다.

결론적으로,

참조자 및 상수를 데이터 멤버로 가지고 있는 클래스에 대입 연산을 지원하려면, 프로그래머가 직접 복사 대입 연산자를 정의해 주어야 한다.

그리고 추가적으로

복사 대입 연산자를 private 접근 지정자로 선언한 기본 클래스에서 파생된 클래스들은 영원히 복사 대입 연산자를 암시적으로 가질 수 없다.

파생 클래스 입장에서는 어떤 식으로든 해당 함수에 접근할 수 있는 방법이 없기 때문이다.

즉, 바꿔서 말하자면 파생 클래스의 암시적 복사 대입 연산자 생성을 막고 싶다면 기본 클래스의 복사 대입 연산자를 private로 선언해버리면 된다.

 

Things to Remember

  • 컴파일러는 경우에 따라서 클래스에 대한 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만든다.
  • 암시적으로 생성되는 복사 생성자, 복사 대입 연산자는 기본적으로 Swallow Copy 형식으로 동작한다. 때문에 참조자 및 상수를 데이터 멤버로 가지는 경우는 주의해야 한다.

관련글 더보기

댓글 영역