일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- std::ostream
- virtual function
- std::vector
- std::endl
- constructor
- suffix return type
- this call
- virtual inheritance
- base from member
- increment operator
- C++
- placement new
- pointer to member data
- std::cout
- c++ multi chatting room
- dynamic_cast
- return by reference
- discord bot
- vector size
- virtual function table
- 더 지니어스 양면포커
- vector capacity
- operator overloading
- delete function
- diamond inheritance
- new&delete
- c++ basic practice
- conversion constructor
- virtual destructor
- member function pointer
- Today
- Total
I'm FanJae.
[C++ Intermediate] Constructor 생성 원리 본문
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 아님 |
'C++ > Intermediate' 카테고리의 다른 글
[C++ Intermediate] Member Function Pointer (2) | 2024.09.23 |
---|---|
[C++ Intermediate] this call (1) | 2024.09.22 |
[C++ Intermediate] Trivial (4) | 2024.09.19 |
[C++ Intermediate] new/delete, placement new (0) | 2024.09.18 |
[C++ Intermediate] 객체의 변환(Conversion) (0) | 2024.09.13 |