많은 분들 — 특히 고수준의 스크립트 언어 사용자거나, 고수준/저수준 추상화가 부족한 C, Java 사용자들[1] — 이 흔히 C++에서 하는 삽질 중의 하나는, C++의 객체 생성이 어떻게 이루어지는지 오해하는 것. IRC에서 과 후배들에게 좀 물어보니 명확히 아는 사람은 드물더라(정확한 동작을 아는 사람이 없었다).
C++에서 객체를 생성한다는 것은 크게 2 단계의 작업이 필요하다.[2]
이렇게 두 가지이고, C++에서는 이 두 가지를 구분할 수 있는 문법적인 도구를 제공한다.
예제로 사용하기 위해서, 이런 클래스가 하나 있다고 하자.
class Base { protected: int x; public: Base() { x = 1; } virtual ~Base() { } virtual void DoSomething() { cout << "Base:" << x << endl; } };
우선 두 단계가 “동시에” 일어나는 코드를 하나 보자.
Base* x = new Base();
new가 메모리를 할당해주고, Base() 함수(?)가 데이터를 초기화한다.
예제를 하나 들어보면 이런 것.
Base* y = new (static_cast<Base*>( operator new(sizeof(Base)) )) Base(); // or equivalent allocation/initialzation Base* p = static_cast<Base*>( operator new( sizeof(Base) ) ); // 생성 new (p) Base(); // 초기화(생성자호출)
참고로 처음 한 줄 / 나중 2줄이 같은 의미다. operator new( 크기 ) 로 호출한 코드가 전역 new를 써서 메모리 할당[4] + 그러고 나서 new (메모리 위치) 생성자() 호출로 초기화를 수행한다.
다만 이런 동작을 하고나면 소멸자도 직접 호출해줘야하고, 메모리 해제도 따로 해야한다. 즉, 이런 코드를 사용해야 한다.((대조적으로 동시에 할당/초기화 했던 코드는 delete x로 끝이지))
p->~Base(); delete static_cast<void*>(p);
이걸 알면 뭐에 써먹을 수 있냐고? 메모리 할당과 객체 생성이 분리되었다는 사실을 알면,
뭐 이런 것들이 가능하다.[8] 반면에 이렇게 사실 상 두 단계인 객체생성을 이해 하지 못하면 다음과 같은 경우를 볼 수 있다.
1의 경우는 학부생 시절에 프로젝트 수업에서 같은 팀원의 코드 — C 프로그래머고 C++은 잘 모르는 — 에서 발견했다. 생성자가 없는 C 의 부족한 추상화에 당한 경우라고 생각한다.
2의 경우는 사실 이 토픽과는 약간 거리가 있긴하지만, 놀랍게도 회사원 생활 중에 발견. C++의 추상화가 애매한 수준이라 발생한 문제인 것 같다. OS/Compiler에 따라선 virtual function table pointer를 잘 조작해서(…) 실제로 객체가 생성된 것처럼 동작하게 만들 수 있지만, “확신이 서도” 왠간하면 하지 말 짓이겠지. 근데 저게 왜 안되는지 이해하지 못하고 있는건 좀 보고있기 괴로웠다(…).
여튼 한 줄 요약.
C++의 객체 생성은 할당/초기화 2 단계고, 이걸 잘 이용하면 성능을 올리거나 편의성을 증대할 수 있다.
ps. 뭔가 요즘 주석이 주저리주저리 달린 글을 자주 쓰게되는데 뭔가 주석이 필요없거나 / 주석이 잘 이해되게 글을 쓰는 좋은 방법이 없을까;
굳이 1과 2 사이에 하나쯤 끼워 넣자면
1. 메모리를 할당한다.
1.1. 상속을 통해 만들어진 클래스의 경우, 함수 테이블 포인터를 갱신함으로써 “정체성” 문제를 해결한다. 이 포인터는 클래스 초반의 4바이트에 저장되는 것으로 기억.
2. 생성자 호출. 변수 초기화.
정도 되겠네요.
가상 포인터 문제는 성능 등 이런저런 데서 자주 다루어지는 데다가 c++과 c#의 차이점을 논할 때도 자주 이야기되는 것인지라 생각나는 게 이 정도네요. 그런데, 평소에 잘만 쓰다가 갑자기 위 조건들을 딱딱 끊어서 이야기하는 것은 좀 당황스러워서라도 말이 잘 안나올 듯.(그래도 memcpy는 좀 심하지만요. 클래스 정체성에 대해 잘 모른다는 얘기?)
Written by 고어핀드 on March 25, 2008 at 10:18pm
굳이 끼워넣는다기보단 vftbl을 초기화해주는 작업은 생성자에서 무조건 일어난다. 그것도 상속단계 따라 각 생성자마다 무려 각각 자기가 아는 vftbl의 주소로 새로 덧씌우는 작업이 일어나지. (그래서 생성자 안에서 가상함수를 부르면 쳐죽일놈이 될 수 있다(…))
Written by rein on March 25, 2008 at 10:56pm
아, 그 작업을 생성자가 해주는 것이었군요. 생성자 안에서 가상함수 불렀다가 피보는 이야기는 Effective c++에서 본 듯. c++에서는 안되지만 c#에서는 되는 경우의 대표적인 예인 듯.
Written by 고어핀드 on March 25, 2008 at 11:02pm
그렇지 생성자에서 해주는 것이지.
가상함수가 생성자의 현재까지 생성된 지점에 의존적(…)으로 가상함수를 호출하기 때문에, 추상 기반 클래스에서 하위 클래스의 가상함수를 호출했다간
“Pure Virtual Function Call” 같은 아스트랄한 에러를 볼 수 있음;
Written by rein on March 26, 2008 at 9:19am
stack alloc (C alloca() 함수) -> displacment new
소멸자가 필요없다면 stack rewind때 알아서 사라져주시기까지함(…)
Written by rein on March 26, 2008 at 9:50am
Jump to comments