I'm FanJae.

[C++ Intermediate] Constructor 생성 원리 본문

C++/Intermediate

[C++ Intermediate] Constructor 생성 원리

FanJae 2024. 9. 10. 22:29

1. 생성자(Constructor)의 생성 원리

1-1. 생성자, 소멸자 호출의 정확한 원리

#include <iostream>
struct BM // BaseMember
{
	BM() { std::cout << "BM()" << std::endl; }
	~BM() { std::cout << "~BM()" << std::endl; }
};
struct DM // DerivedMember
{
	DM() { std::cout << "DM()" << std::endl; }
	DM(int) { std::cout << "DM(int)" << std::endl; }
	~DM() { std::cout << "~DM()" << std::endl; }
};

struct Base // DerivedMember
{
	BM bm;
	Base() { std::cout << "Base()" << std::endl; }
	Base(int a) { std::cout << "Base(int)" << std::endl; }
	~Base() { std::cout << "~Base()" << std::endl; }
};

struct Derived : public Base // DerivedMember
{
	DM dm;
	Derived() { std::cout << "Derived()" << std::endl; }
	Derived(int a) { std::cout << "Derived(int)" << std::endl; }
	~Derived() { std::cout << "Derived()" << std::endl; }
};

int main()
{
	Derived d1;      
	Derived d2(7);
}

- 위와 같은 코드가 존재한다고 가정하자. 그러면 실제로는 아래와 같이 변환해준다.

struct Base
{
    BM bm;
    Base()           : bm() { }
    Base(int a)      : bm() {}
    ~Base()     {...; bm.~BM();   }
};
struct Derived : public Base
{
    DM dm;
    Derived()         : Base(), dm() { }
    Derived(int a)    : Base(), dm() { }
    ~Derived()        { ....; dm.~DM(); ~Base(); }
 }
 int main()
 {
     Derived d1;    // call Derived::Derived()
     Derived d2(7); // call Derived::Derived(int)
 }

컴파일러가 기반 클래스 및 멤버 데이터의 생성자(소멸자)를 호출하는 코드를 생성해주는 것이다.

생성자/소멸자의 호출 순서는 아래와 같은 순서로 실행될 것이다.

1) 기반 클래스 멤버 BM의 생성의 생성자가 먼저 생성된다.

2) 기반 클래스(Base)의 생성자

3) 파생 클래스 멤버(DM)의 생성자

4) 파생 클래스(Derived)의 생성자

 

③ 사용자가 생성자 호출 순서를 변경할 수 없다.

- 항상 기반 클래스 생성자가 먼저 호출되고, 멤버 데이터의 생성자가 불린다.

- 인위적으로 순서를 바꾸는 것이 불가능하다.

struct Derived : public Base // DerivedMember
{
	DM dm;
	Derived() : dm(), Base()  { std::cout << "Derived()" << std::endl; }
	Derived(int a) { std::cout << "Derived(int)" << std::endl; }
	~Derived() { std::cout << "Derived()" << std::endl; }
};

 

컴파일러가 생성한 코드는 항상 디폴트 생성자를 호출한다.

struct DM // DerivedMember
{
	//DM() { std::cout << "DM()" << std::endl; }
	DM(int) { std::cout << "DM(int)" << std::endl; }
	~DM() { std::cout << "~DM()" << std::endl; }
};

struct Base // DerivedMember
{
	BM bm;
	//Base() { std::cout << "Base()" << std::endl; }
	Base(int a) { std::cout << "Base(int)" << std::endl; }
	~Base() { std::cout << "~Base()" << std::endl; }
};

- Base, DM에 대한 Default Constructor가 없다고 나온다.

해결책 : 기반 클래스나 멤버 데이터에 디폴트 생성자가 없는 경우 반드시 사용자가 디폴트가 아닌 다른 생성자를 호출하는 코드를 작성해야 한다.

struct Derived : public Base // DerivedMember
{
	DM dm;
	Derived() : Base(0), dm(0)  { std::cout << "Derived()" << std::endl; }
	Derived(int a) : Base(0), dm(0) { std::cout << "Derived(int)" << std::endl; }
	~Derived() { std::cout << "Derived()" << std::endl; }
};

 

1-2. Compile Explorer

- 어셈블리 레벨에서 확인해보면, Derived 생성자를 부르는 것을 확인할 수 있다.

- 우리가 확인한 것이 맞다면, Derived 내부에서 Base 생성자를 먼저 부를 것이다.

- Derived 실행전에 Base 생성자를 먼저 부르는 것을 확인할 수 있다.

- 또 Base 안에서는 BM 생성자가 불린다. 

- 이는 소멸자에서 동일함을 확인할 수 있다.

- 자신의 작업들을 모두 다 진행한 이후에 DM()의 소멸자를 호출한다.

 

2. Base From Member 

2-1. Base From Member (C++ IDioms)

① 아래 예제의 문제점

#include <iostream>

class Buffer
{
public:
	Buffer(std::size_t sz) {
		std::cout << "initializer buffer" << std::endl;
	}
	void use() {
		std::cout << "user buffer" << std::endl;
	}
};

class Stream
{
public:
	Stream(Buffer& buf) {
		buf.use();
	}
};

int main()
{
	Buffer buf(1024);
	Stream s(buf);
}

- 위와 같은 예제를 만들었다고 가정하고, 아래 예제를 보자.

 

#include <iostream>

class Buffer
{
public:
	Buffer(std::size_t sz) {
		std::cout << "initializer buffer" << std::endl;
	}
	void use() {
		std::cout << "user buffer" << std::endl;
	}
};

class Stream
{
public:
	Stream(Buffer& buf) {
		buf.use();
	}
};

//----------------------
class StreamWithBuffer : public Stream
{
	Buffer buf{ 1024 };
public:
	StreamWithBuffer() : Stream(buf) {}
};

int main()
{
	StreamWithBuffer swb;
}

- 기반 클래스에는 buffer를 요구한다.

- Stream 작업을 하려면 버퍼가 필요하니까 자신의 멤버 데이터로 버퍼 하나를 제공하겠다는 것이다.

- 실제로 실행해보면, 의도대로 작동하지 않는다.

※  멤버 데이터 생성자 보다 기반 클래스의 생성자가 먼저 호출된다.

class StreamWithBuffer : public Stream
{
   Buffer buf;
public:
   StreamWithBuffer() : Stream(buf), buf(1024) { }
};

- 대략 이런 형태라고 생각하면 된다.

 

② 해결 방법

#include <iostream>

class Buffer
{
public:
	Buffer(std::size_t sz) {
		std::cout << "initializer buffer" << std::endl;
	}
	void use() {
		std::cout << "user buffer" << std::endl;
	}
};

class StreamBuffer
{
protected:
	Buffer buf{ 1024 };
};

class Stream
{
public:
	Stream(Buffer& buf) {
		buf.use();
	}
};

//----------------------
class StreamWithBuffer : public StreamBuffer, public Stream
{
	
public:
	StreamWithBuffer() : Stream(buf) {}
};

int main()
{
	StreamWithBuffer swb;
}

- Buffer를 멤버로 가진 별도의 기반 클래스를 설계한 후, 다중 상속을 사용한다.

- Base From Member 라는 이름(C++ IDioms)을 사용한 기술이다.

 

2-2. 생성자와 가상함수

#include <iostream>

class Base
{
public:
	Base() { }
	void foo() {
		vfunc();
	}
	virtual void vfunc() {
		std::cout << "Base vfunc" << std::endl;
	}
};

class Derived : public Base
{
	int data{ 10 };
public:
	virtual void vfunc() override
	{
		std::cout << "Derived vfunc : " << data << std::endl;
	}
};

int main()
{
	Derived d;
	// d.foo();
}

- 기본적으로 생성자에서는 가상함수가 동작하지 않는다.

- Base 생성자에서 vfunc() 호출시 항상 Base의 vfunc() 함수가 호출된다.

 

※ 왜 이런 문법이 생겼을까?

#include <iostream>

class Base
{
public:
	Base() { vfunc(); }
	virtual void vfunc() {	}
};

class Derived : public Base
{
	int data;
public:
	Derived() : Base(), data(10) {}
    
    virtual void vfunc() override
    {
        std::cout << data :: std<<endl;
    }
};

- 위와 같이 변환될 것이다.

- 멤버 데이터는 기반 클래스보다 늦게 초기화 된다.

- 만약 이때 Base()가 Derived::vfunc()를 호출했다면, 초기화 되지 않은 data를 사용하게 되는 문제가 발생할 것이다.

- 그래서 생성자에서는 가상함수가 작동하지 않는다.

 

3. std::initializer_list<T>

- <initializer_list> 헤더, C++11에서 도입.

- const T 타입의 이름 없는 배열(prvalue)를 생성 후, 해당 메모리를 가리키는 객체

- 포인터 2개 또는 포인터와 길이로 구현.

※ C++ 표준에서 이 배열이 무슨 메모리인지는 표준에 정의되어 있지 않다. readonly 라고만 표현되어 있다.

① std::initializer_list<T> 멤버 함수

size() 요소의 갯수 반환
begin() 첫번째 요소를 가리키는 주소(const T*) 반환
end() 마지막 다음요소를 가리키는 주소(const T*) 반환

 

② 사용예시

#include <iostream>
#include <initializer_list>

int main()
{
	std::initializer_list<int> s = { 1,2,3,4,5 };

	// int *first = s.begin(); // error

	const int* first = s.begin();
	const int* lst = s.end();

	for (auto e : s)
	{
		std::cout << e << " ";
	}
}

 

3-1. std::initializer_list<T>는 언제 사용하는가?

- 함수(특히 생성자) 인자로 많이 사용한다.

- 크기가 크지 않은 타입이므로, call by value로 받는 것이 관례이다.

#include <iostream>
#include <initializer_list>

void foo(std::initializer_list<int> s) { }
int main()
{
	std::initializer_list<int> s = { 1,2,3,4,5 };

	// int *first = s.begin(); // error

	foo(s);
	foo({ 1,2,3 });
	foo({ 1,2,3,4,5 });

}

 

 

① 일반 배열 대신 std::initializer_list를 보내는 이유

#include <iostream>
#include <initializer_list>

void f1(int* p) {}
void f2(std::initializer_list<int> s) { }
int main()
{
	int x[5] = { 1,2,3,4,5 };
	f1(x);
	// f1({ 1,2,3 }); error

	std::initializer_list<int> s = { 1,2,3,4,5 };
	f2(s);        // ok
	f2({ 1,2,3 }) // ok
//  f2(x); // error
}

1) 배열을 인자로 보내는 경우 포인터로 받는다. 시작 주소만 알 수 있고, 갯수는 알 수 없다.

2) list initialization({1,2,3}) 형태로 인자를 전달할 수 없다.

 

※ 생성자는 인자로 std::initializer_list를 사용하는 경우 특별한 규칙이 적용된다.

 

② std::initializer_list와 생성자의 규칙

#include <iostream>

class Object
{
public:
	Object() {
		std::cout << "1" << std::endl;
	}
	Object(int, int) {
		std::cout << "2" << std::endl;
	}
	Object(std::initializer_list<int>) {
		std::cout << "3" << std::endl;
	}
};

int main()
{
	Object o1;           // 1
	Object o2(1, 2);     // 2
	Object o3({ 1, 2 }); // 3

	Object o4{ 1, 2 };   // 3

	Object o5(1, 2, 3);
	Object o6{1, 2, 3};
	Object o7{1, 2, 3};

- Object o1(1, 2); -> Object(int, int) 호출

- Object o2( {1, 2} ); -> Object(std::initializer_list) 호출

- Object o3{1,2}; 

① Object(std::initializer_list) 호출

② ①이 없다면 Object(int, int) 호출

 

- 생성자 인자로 std::initializer_list를 사용하면

-> 객체를 배열 처럼 초기화 할 수 있게 된다.

-> { } 초기화를 사용하면 std::initializer_list 버전의 생성자가 우선순위로 선택된다.

-> C++11 부터 STL Container는 배열처럼 초기화가 가능하다.

 

③ STL Container 사용시 () 초기화와 {} 초기화의 차이점을 알아둘 것.

std::vector v1(10, 2) vector(int, int) 생성자
10개의 요소를 2로 초기화
std::vector v2{10, 2} vector(std::initializer_list)
2개의 요소를 10, 2로 초기화

 

std::vector v3 = 10 explicit vector(int)
std::vector v4 = {10} vector(std::initializer_list)
explicit 아님

 

Comments