1. 사용자 정의 생성자가 있으면, 기본 생성자를 자동 제공하지 않는다

 

  • 이는 클래스의 인스턴스가 묵시적으로 기본 값으로 초기화 되는 것을 방지하기 위함이다.

    따라서, 기본 생성자가 제공되지 않는 경우에는 인자 없이 객체를 생성하려고 할 때 오류가 발생한다.

  • 그러나 복사 생성자와, 복사 할당자는 기본적으로 여전히 생성된다.

    다만 특정 조건 하에서는 기본 복사 생성자와 복사 할당 연산자가 생성되지 않을 수 있다.
    이동 생성자 또는 이동 할당 연산자가 사용자 정의된 경우(2번 참고)


  • std::vector와 같은 표준 컨테이너는 요소의 복사 생성자를 호출하여 요소를 복사한다.

    만약 복사 생성자가 사용할 수 없거나 삭제된 경우에만 오류가 발생합니다.

2. 이동 생성자 or 이동 할당 연산자가 명시적으로 선언된 경우
    컴파일러는 복사 생성자를 자동으로 정의하지 않는다.

 

  • 이동 할당(Move Assignment) 연산자의 명시적 선언  시 →  컴파일러가 복사 할당 (Copy Assignment) 연산자를 자동으로 정의하지 않는다.

    이는 클래스에 사용자 정의 이동 할당 연산자가 필요한 경우,
    기본 복사 할당 연산자가 적절하지 않을 수 있음을 의미합니다.

 

3.  복사 생성자, 복사 할당 연산자, 이동 생성자, 이동 할당 연산자 또는 소멸자 중 하나라도 명시적으로 선언되면,  컴파일러는 기본 이동 생성자와 이동 할당 연산자를 자동으로 생성하지 않는다.

 

  • 이는 클래스가 자원을 관리하고 있을 경우,
    기본적으로 이동 세맨틱스 수행되는 것이 적절하지 않을 수 있음을 가정하기 때문
    이다.


4. 컴파일러에 의해 암시적으로 생성된 default 복사 생성자는
    얕은 복사(Shallow Copy)로 동작한다.

- 포인터만 복사하기 때문에 같은 대상을 가리키는 인스턴스가 생성되어 버린다.

 따라서 깊은 복사(Deep Copy)를 원할 경우 복사 생성자를 직접 정의해 주어야한다.

 

5. explicit 키워드를 붙이면 객체 생성 시
    컴파일러가 자동으로 형변환을 시도하지 않는다.

struct A
{
    A(int) { }      // converting constructor
    A(int, int) { } // converting constructor (C++11)
    operator bool() const { return true; }
};
 
struct B
{
    explicit B(int) { }
    explicit B(int, int) { }
    explicit operator bool() const { return true; }
};
 
int main()
{
    A a1 = 1;      // OK: copy-initialization selects A::A(int)
    A a2(2);       // OK: direct-initialization selects A::A(int)
    A a3 {4, 5};   // OK: direct-list-initialization selects A::A(int, int)
    A a4 = {4, 5}; // OK: copy-list-initialization selects A::A(int, int)
    A a5 = (A)1;   // OK: explicit cast performs static_cast
    if (a1) ;      // OK: A::operator bool()
    bool na1 = a1; // OK: copy-initialization selects A::operator bool()
    bool na2 = static_cast<bool>(a1); // OK: static_cast performs direct-initialization
 
//  B b1 = 1;      // error: copy-initialization does not consider B::B(int)
    B b2(2);       // OK: direct-initialization selects B::B(int)
    B b3 {4, 5};   // OK: direct-list-initialization selects B::B(int, int)
//  B b4 = {4, 5}; // error: copy-list-initialization does not consider B::B(int,int)
    B b5 = (B)1;   // OK: explicit cast performs static_cast
    if (b2) ;      // OK: B::operator bool()
//  bool nb1 = b2; // error: copy-initialization does not consider B::operator bool()
    bool nb2 = static_cast<bool>(b2); // OK: static_cast performs direct-initialization
}

 

6.  최상위 객체의 소멸자에 virtual 키워드를 붙이지 않으면
     하위 객체의 소멸자가 호출되지 않는다.


아래 코드는 상속 관계에 있는 객체 A, B, C를 다형성을 이용하여

하나의 컨테이너 std::list<A*> 에서 관리하는 코드이다.

class A
{
public:
	A() = delete;
	A(int _value) :a(new int(_value))
	{
		cout << "A+(" << *a << ")\n"; 
	}
	A(const A& _a) :a(new int(*_a.a)) 
	{
		cout << "A+ Copy(" << *a << ")\n"; 
	}
	virtual void Print() const { cout << "A::Print()" << *a << '\n'; }
	virtual ~A()
	{
		cout << "A-(" << *a << ")\n"; 
		delete a;
	}

	int* a;
};

class B :public A
{
public:
	B() = delete;
	B(int _value) :A(_value)
	{
		b = new int(_value);
		cout << "B+(" << _value << ")\n"; 
	}
	B(const B& _b) :A(*this), b(new int(*_b.b)) 
	{
		cout << "B+ Copy(" << *b << ")\n"; 
	}
	void Print() const 
	{
		cout << "B::Print()" << *b << '\n'; 
		A::Print();
	}
	~B() 
	{
		cout << "B-(" << *b << ")\n"; 
		delete b;
	}
	int* b;
};

class C : public B
{
public:
	C() = delete;
	C(int _value) :B(_value)
	{ 
		c = new int(_value);
		cout << "C+(" << _value << ")\n"; 
	}

	C(const C& _c) :B(*this), c(new int(*_c.c))
	{ 
		cout << "C+ Copy(" << *c << ")\n"; 
	}
	void Print() const 
	{ 
		cout << "C::Print()" << *c << '\n';
		B::Print();
	}
	~C() 
	{
		cout << "C-(" << *c << ")\n"; 
		delete c;
	}

	int* c;
};

void Print(std::list<A*> list);
void Delete(std::list<A*> list);

int main()
{
	std::list<A*> list;

	cout << "==========            Step 1            ==========\n";
	list.push_back(new A(1));

	cout << "==========            Step 2            ==========\n";
	list.push_back(new B(2));

	cout << "==========            Step 3            ==========\n";
	list.push_back(new C(3));

	Print(list);
	Delete(list);

}


void Print(std::list<A*> list)
{
	cout << "\n==========            Print            ==========\n";
	
	for (auto it = list.begin(); it != list.cend(); it++)
	{
		(*it)->Print();
		cout << "\n";
	}

	cout << "\n\n";
}

void Delete(std::list<A*> list)
{
	for (auto it = list.begin(); it != list.cend(); it++)
	{
		cout << "delete\n";
 		delete* it;
		cout << "\n";
	}
}
 

위 코드를 실행하면 아래와 같은 결과가 나온다.

소멸자에 virtual 키워드를 붙인 경우

 
 

그런데 만약 최상위 객체 A의 소멸자에 virtual 키워드가 붙어있지 않으면 어떻게 될까?

virtual 키워드 없는 경우

생성자는 최하위 객체에서 부터 호출되지만, 소멸자는 최상위 객체에서 부터 호출된다.

따라서 최상위 객체의 소멸자에 virtual 키워드를 붙이지 않으면

최상위 객체의 소멸자만 호출되고 끝나버리기 때문에 하위 객체의 리소스가 해제되지 않는다.

그 결과 메모리 누수가 발생하게 된다.

 

Posted by Elan
: