황현석 일지

객체 지향 설계 프로그래밍 C++, 중간 겸 고찰 본문

황현석 어록

객체 지향 설계 프로그래밍 C++, 중간 겸 고찰

황현석 2025. 10. 20. 04:30

우리 대학생들은 C++을 공부하고, 문법을 외우고, 작동방식을 배운다. 하지만 더 나아가서, 소프트웨어를 설계하고, 매커니즘을 설계하는 철학적 사고를 하여야 하지 않겠는가?

 

당신을 위해, 심도있는 문제를 가져 왔다. C++ 순례에 큰 기여가 됬으면 좋겠다.

 

1. C++에서 RTTI (RunTime Type Information) 기능이 꺼져 있다면, 당신은 어떻게 안전하게 다운캐스팅을 하실 건가요?

 

RTTI란, C++내에서, 각 객체가 무슨 타입인지, 객체마다 고유한 메모리 주소에 추가적인 정보를 넣어놓는 기술입니다. 이것이 없으면, C++내에서 유일무이한 안전한 타입 변환 dynamic_cast<T*>()를 사용할 수 없습니다.

 

당신은 그럼 어떻게 안전하게 다운 캐스팅 할 것인가요?

 

2. static dispatch 와 dynamic dispatch에 대해 설명하세요.

 

이것은 함수의 링킹과 관련이 있습니다. 당신이 call하는 함수는 어떤 방식으로 결정되는지 알고 계신가요? 무슨 함수를 부르는지 동작을 모르는데, 감으로 부르는 것은 저는 썩 내키지 않다고 생각합니다.

 

3. derived가 아닌 base 클래스에 함수가 구현되어 있습니다. 이 클래스를 인스턴스화 해서 함수를 부른다는 것에 대해 자세하게 설명하세요.

 

함수를 부를 때는 그 함수에 this도 같이 컴파일러가 넣어서 호출됩니다. 이것을 중점적으로 설명해보면 됩니다.

 

4. class 내에는 friend라는 키워드를 넣어, 함수나 클래스를 만들 수 있습니다. 아직 있지도 않은 함수를 friend하는게 말이 안되지 않나요? 왜 가능한지 설명하세요.

 

클래스 내에 friend 키워드는 해당 함수나, 클래스가 본인 클래스의 접근 제어자를 public 으로 생각하게 되게끔 합니다. 그렇게 어려운 질문은 아닙니다. 기초를 물어보는 것이니, 너무 깊게는 생각하지 않으셔도 됩니다.

 

#include<bits/stdc++.h>
using namespace std;

class A {
public:
    int number_[20] = {0};
};

class B : public A {
public:
    void print() {
        cout << number_[19] <<'\n';
    }          
};

int main() {
    int value = 91;
    ((B*)((&value) - 19))->print();
}
5. 위 코드의 실행과정을 정확하게 설명하세요.

 

위 코드는 사실 런타임 에러가 날 수 있습니다. 하지만 나지 않을 경우, 무슨일이 어떻게 벌어지는지 정확하게 아는것은 코드 내부 작동원리의 핵심에 근접했다고 볼 수 있습니다.

 

6. 앞의 코드에서 A가 만약 polymorphic 클래스라고 합시다. 그럼, 위의 코드 와 같이 print()가 정상적으로 작동하지 않습니다. 어떻게 수정해야 정상적으로 작동할까요?

 

polymorphic 클래스가 뭔지 아시나요? dynamic dispatch를 위한 v-table을 가지는데, 이를 위한 포인터가 필요합니다. 그럼 이 포인터를 어디에 저장하는게 굉장히 합리적일까요?

 

 

7. Parent 클래스와 derived class Child가 있습니다. 이때 Parent는 nonPolymorphic 클래스, child는 Polymorphic클래스 입니다. Child클래스로 생성된 객체가 있을 때, Parent 클래스로 upCasting시에, 완벽하게 Parent class 의 맴버변수를 사용 할 수 있습니다.

어떻게 그것이 가능한지, 컴파일러는 뭘 하고 있는지 설명하세요.

 

생각해보니 그렇습니다. 왜 우리는 알게모르게 이상한짓을 하고 있었고, 심지어 잘 작동하고 있었습니다. 레거시 클래스와의 호환이 잘 되고 있었습니다. 생각해보면 컴파일러는 알게모르게 도움을 주고 있던 것 같습니다.

 

#include<bits/stdc++.h>
using namespace std;

class A {
public:
    long long number_ = 612;
};

class B : public A {
    virtual ~B() = default;
};

int main() {
    B *b = new B();

    A *force = reinterpret_cast<A*>(b);
    A *a = static_cast<A*>(b);

    cout << force->number_ <<'\n';
    cout << a->number_ <<'\n';
}

 

8. 둘의 출력값은 다른데, 왜 다른지 설명하세요.

 

7번 문제와 정확하게 동일한 지식을 묻는 문제입니다. 이건 그 정확한 예시를 들어 설명하라는 것이군요.

 

#include<bits/stdc++.h>
using namespace std;

class A {
public:
    long long number_ = 612;
    void print() {}
};

class B : public A {
public:
    virtual void print() {}
};

class C {
    virtual void print() {
        cout << "Why call me?\n";
        long long number = *((long long*)(this+1));
        cout << number << '\n';
    }
};

int main() {
    B *b = new B();
    C *c = new C();

    long long *bb = reinterpret_cast<long long*>(b);
    long long *cc = reinterpret_cast<long long*>(c);

    *bb = *cc;

    b->print();
}

 

9. 8번에 대한 고민이 끝나셨나요? 그럼 지금 이 코드가 무엇을 어떻게 실행하고 있는지 설명하세요. (64-bit 컴퓨터에서 실행하고 있다고 가정하세요)

 

꽤 흥미로운 주제입니다. 스스로 생각하기 어렵지만, 또 생각해보면 어려운 문제는 아닙니다. 단지, 저런 행위가 가능함을 깨닫게 되는 프로그래밍 관점에서의 지평을 넓히기에는 좋은 예시라고 생각했습니다.

 

주된 vptr의 이야기 그리고 현학적이고 지루한 기행은 이쯤 끝내도록 하겠습니다.

 

#include<bits/stdc++.h>

using namespace std;

class A {
public:
    int number_=1;

    void print() {
        cout << number_ << '\n';
    }
};

class B {
  public:
    int number2_=2;

    void print2() {
        cout << number2_ << '\n';
    }
};

class C : public A, public B {
};


int main() {
    C *c = new C();
    B *force_b = reinterpret_cast<B*>(c);
    B *b = (B*) c;
    A *a = (A*) c;

    b->print2();
    a->print();
    
    force_b->print2();
}

 

10. 9번문제를 응용한 문제입니다. 다중상속 일 때, 각 클래스 별 메모리 할당 순서, 그때 함수가 실행시킨다는 것의 원리를 분석하고 설명하세요.

 

 

여기까지입니다. 

 

당신의 C++, 더 나아가 코드의 내부구조, 더 나아가 컴퓨터 그리고 하드웨어를 이해하고 당신의 구현으로 세상을 이롭게 하길 바랍니다.