Programming | 프로그래밍 언어/C++

가상 함수

gyunnnnnn 2023. 1. 16. 20:03

객체 포인터의 참조 관계

객체 포인터 변수

  • 객체의 주소 값을 저장하는 포인터 변수
Person * ptr = new Person();
  • A형 포인터 변수는 A객체 또는 A를 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다.
class Person{};
class Student : public Person{};
class PartTimeStudent : public Student{};
Person * ptr1 = new Student(); // Person형 포인터 변수 ptr
Person * ptr2 = new PartTimeStudent(); // Person형 포인터 변수 ptr
Student * ptr3 = new PartTimeStudent(); // Student형 포인터 변수 ptr
  • 위와 같이 Person형 포인터는 Person 객체뿐만 아니라, Person을 상속하는 파생 클래스의 객체도 가리킬 수 있다.

각 객체 포인터에 대한 참조 가능 여부

cpp9

함수 오버 라이딩

  • 파생 클래스와 기본 클래스에서 동일한 이름과 형태로 두 함수를 정의하는 것
  • 오버 라이딩된 기본 클래스의 함수는 오버 라이딩을 한 파생 클래스의 함수에 가려진다.

오버 로딩 vs 오버 라이딩

  1. 오버 로딩
  • 같은 클래스 내에서 같은 이름의 메서드를 사용하는 것
  1. 오버 라이딩
  • 기본 클래스에서 정의한 메서드를 파생 클래스에서 변경하는 것

가상 함수

  • C++ 컴파일러는 포인터 연산의 가능성 여부를 판단할 때 선언한 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다.
// 컴파일 성공
// 포인터는 Base
Base * bptr = new Derived(); 

// 컴파일 에러
Derived * dptr = bptr; 

// 컴파일 에러
// bptr 포인터는 Base이기 때문에 Derived에 있는 함수는 호출할 수 없다.
bptr -> DerivedFunc() 

// 컴파일 성공
Derived * dptr = new Derived();

// 컴파일 성공
Base * bptr = dptr;

virtual

  • virtual 키워드는 함수에 붙일 수 있다. 부모가 자식을 가리켜 자식에서 새롭게 정의된 기능을 쓸 수 있다.
  • virtual 키워드는 오버 라이딩을 성립할 수 있게 해 주어 다형성을 구현하는 핵심 역할을 한다.
  • 가상 함수 포인터로 인해 클래스 크기에 4Byte가 더 들어간다.(32비트 컴퓨터 기준)
  • memset으로 객체를 초기화하면 가상 함수 테이블 정보까지 같이 초기화 된다.
  • virtual 키워드를 사용해 가상 함수로 선언되면, 해당 함수 호출 시 포인터의 자료형을 기반으로 호출 대상을 결정하지 않고, 포인터 변수가 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.
class A
{
public:
    int a;
    virtual void func()
    {
        cout << "A" << endl;
    }
};

class B : public A
{
public:
    int a;
    virtual void func()
    {
        cout << "B" << endl;
    }
};

class C : public B
{
public:
    int a;
    virtual void func()
    {
        cout << "C" << endl;
    }
};

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

    a->func();
    b->func();
    c->func();

    cout << sizeof(a) << " " << sizeof(b) << " " << sizeof(c) << endl; // 4,4,4
    cout << sizeof(A) << " " << sizeof(B) << " " << sizeof(C) << endl; // 8,12,16
}
  • 실행 결과 : A B C
  • virtual 선언이 안 되어 있다면 A A A로 출력된다.

다형성

  • 모습은 같은데 형태는 다르다 = 문장은 같은데 결과는 다르다.
    • 참조하는 객체의 자료형이 다르기 때문이다.
  • 이름이 같은 함수여도 참조하는 객체에 따라서 결과가 다르다.

순수 가상 함수

  • 함수의 몸체가 정의되지 않은 함수
  • 0의 대입을 통해 표현한다.
  • 잘못된 객체 생성을 막는 용도로 사용한다.
    virtual int AbstractFunc() = 0; // 순수 가상함수

추상 클래스

  • 하나 이상의 멤버 함수를 순수 가상 함수로 선언한 클래스를 추상 클래스라 한다.
  • 추상 클래스는 객체 생성이 불가능한 클래스라는 의미를 지닌다.

가상 소멸자

  • 맨 위에 존재하는 기본 클래스의 소멸자만 virtual로 선언하면 이를 상속하는 파생 클래스의 소멸자들도 모두 가상 소멸자로 선언이 된다.
  • 가상 소멸자가 호출되면, 상속의 계층구조상 맨 아래에 존재하는 파생 클래스의 소멸자가 대신 호출된다.
  • 파생 클래스 -> 기본 클래스의 순으로 소멸자가 호출된다.
class First
{
public:
    string strOne;
    First(string str) : strOne(str){}
    virtual ~First()
    {
        cout << "~First()" << endl;
    }
};

class Second : public First
{
public:
    string strTwo;
    Second(string str1, string str2) : First(str1), strTwo(str2) {}
    ~Second()
    {
        cout << "~Second()" << endl;
    }
};

First * ptr1 = new Second("simple", "complex");
delete ptr1;

Second * ptr2 = new Second("simple", "complex");
delete ptr2;
  • 모두 ~Second() -> ~First() 순으로 소멸자가 호출된다.
  • 소멸자에 virtual 선언이 없다면 1에서는 ~First()만 호출이 된다.