본문 바로가기
연구소👨‍💻/CS연구소

[C++] 다형성과 오버로딩, 오버라이딩

by 신그자체김상범 2024. 6. 22.

다형성이란

부모객체를 선언하고 자식객체로 초기화해서 런타임시 다양한 실제타입에 따라 다른 메서드를 사용할 수 있게 하는 동적 다형성과 한 스코프 내에서 함수를 선언할 때 같은 이름의 함수를 다른 타입의 파라미터로 정의하는 정적 다형성이있다.

오버라이딩과 가상함수 virtual

오버라이딩은 부모의 객체의 메서드를 자식 객체가 재정의 할 수 있게 하는 기능이다.

그런데 다형성을 지원하는 C++에서 부모 개체로 선언된 자식 객체의 경우 특정 오버러이딩 메소드를 호출하면 이 메소드는 부모의 메소드를 불러올까 아니면 자식의 메소드를 불러올까?

Virtual은 여기서 작동한다. Virtual을 함수 앞에 선언하면 함수를 선언할 때 자식 클래스의 메소드가 이 메소드를 덮어 쓸 수 있게 해준다.

그런데 무슨원리로?

이런 기능을 들었을 때 아 참 편리하구나라고 생각했지만 어떻게 가능한거지? 라는 생각이 버로든다.

V 포인터와 V 테이블

C++에서 멤버함수에 Virtual이 선언된 모든 클래스는 클래스별로 V테이블이라는 자료형이 컴파일러에 의해서 생성된다. 이 자료형은 데이터 세그먼트에 저장된다.

V 테이블안에는 포인터들이 내장되어있다. 그 포인터는 클래스에서 선언된 virtual 함수들의 주소를 담고있다.

클래스 인스턴스가 생성되면 모든 인스턴스는 자신의 V테이블을 가리키는 V포인터를 가진다.

그래서 부모 클래스로 선언된 자식 클래스 인스턴스가 오버라이딩된 함수를 콜하게되면 그 클래스는 V포인터에 저장된 V테이블로가서 그곳에 저장된 함수를 정확히 호출할 수 있게된다.

V포인터, V테이블의 예시

#include <iostream>
using namespace std;

class Parent {
public:
    virtual void show() {
        cout << "Parent's show function" << endl;
    }
};

class Child : public Parent {
public:
    void show() override {
        cout << "Child's show function" << endl;
    }
};

int main() {
    Parent* p = new Child();
    p->show();  // Child's show function 호출

    delete p;
    return 0;
}

위 코드에서 p->show()가 호출될 때, 다음과 같은 순서로 동작한다.

  1. p는 Parent 타입의 포인터지만, 실제로 Child 객체를 가리킨다.
  2. p->show() 호출 시, p의 V 포인터를 통해 Child 클래스의 V 테이블에 접근한다.
  3. Child 클래스의 V 테이블에는 Child 클래스에서 재정의된 show 함수의 주소가 저장되어 있다.
  4. 따라서, p->show()는 Child 클래스의 show 함수를 호출하게 된다.

버추얼 기반 클래스

전처리기 지시자인 #ifdef 같은 곳에서도 그랬지만 상속이나, 참조, 연결을 하는 로직들은 항상 순환, 중복과 관련된 로직을 조심해야한다.

C++의 상속의 경우에도 이런 문제를 쉽게 떠올릴 수 있다.

같은 부모 클래스를 상속한 두 개의 다른 클래스 A, B가 또 다른 자식 클래스 C에게 다중 상속될 때

Roadmap 에서는 이런 문제를 다이아몬드 상속이라고 정의했다. 이런 상속은 호출 과정에서 모호함을 일으킬 수 있다고 나와있다.

버추얼 클래스는 이를 상속 과정에서 클래스 앞에 virtual을 붙이면서 해결한다. 아마 이러면 v테이블에서 클래스에 대한 주소가 A, B에 담길 것이고, 이 두개를 다중상속 받는 C의 V테이블에도 A, B에 선언된 같은 부모클래스 주소를 받아 중복 없이 저장할 수 있게된다.

Virtual 클래스 예시

class A {
public:
    int x;
    A() : x(0) {}
    void show() {
        cout << "Class A, x = " << x << endl;
    }
};

class B : virtual public A {
public:
    B() { x = 1; }
};

class C : virtual public A {
public:
    C() { x = 2; }
};

class D : public B, public C {
public:
    D() { x = 3; }
};

int main() {
    D obj;
    obj.show(); // Class A, x = 3

    return 0;
}

여기서 D클래스는 B, C를 상속받는데 그과정에서 B, C가 상속받은 A를 2번 상속받을 예정이었다. 그러나 V테이블을 통해 주소값을 접근하기 때문에 D는 자신이 받아야하는 2개의 A가 사실 하나라는 사실을 알고 불필요하고 모호한 상속을 줄일 수 있다.

나머지 virtual 이외 클래스 관련 접근 지정자들.

일단 확실히 해야하는 것은 C++의 클래스에서의 접근 지정자는 클래스 단위다, 같은 클래스의 다른 인스턴스가있다면 두 인스턴스는 서로의 private 변수에 접근할 수 있다. 그 대신 private은 같은 클래스를 가진 인스턴스만

protected

protected는 객체지향에서 상속을 할 때 상속된 클래스가 해당 클래스의 변수에 접근하게 해줄 수 있는 선언자다. private를 포함하는 영역을 갖고 있다고 할 수 있다.

friend

private, protected 접근지정자로 설정된 변수, 함수는 외부 스코프에서 접근하지 못하게되어있다. 그러나 friend설정을 통해 특정함수나 클래스가 이 변수에 접근할 수 있게 해준다.

'연구소👨‍💻 > CS연구소' 카테고리의 다른 글

[C++] 예외 처리  (0) 2024.06.26
[네트워크] TCP, UDP 프로토콜  (0) 2024.06.19
[네트워크] IP프로토콜 분석  (1) 2024.06.16
[OS] 멀티프로세스, 멀티스레딩  (1) 2024.06.13
[C++] 람다 함수  (0) 2024.06.12