본문 바로가기

복습/C++

[C++] 복습일지 part 2 - 3 #가상함수, 순수가상함수



# 상속 (Inheritance)

# 오버로딩(Overloading)과 오버라이딩(Overriding)

# 가상함수, 순수가상함수 (Virtual Function, Pure Virtual Function)

# 멤버이니셜라이저 (Memeber Initializer)





# 가상함수(Virtual Function)



class A
{
public:
    void Message()
    {
        std::cout << "class A" << std::endl;
    }
};
class B : public A
{
public:
    void Message()
    {
        std::cout << "class B" << std::endl;
    }
};

int main() {
    A a;
    B b;
    A *aa = &a;
    A *bb = &b;

    aa->Message();
    bb->Message();
    
    return 0;
}


출력결과는?
 
class A
class A

나는 두번째 출력에서 
class B가 출력되고싶다.

어떻게 해야 할까?

답은 바로 
'virtual' 키워드이다.



가상함수를 쓰면된다.
다시 코드를 적는다면

class A
{
public:
    virtual void Message()
    {
        std::cout << "class A" << std::endl;
    }
};
class B : public A
{
public:
    void Message()
    {
        std::cout << "class B" << std::endl;
    }
};

int main() {
    A a;
    B b;
    A *aa = &a;
    A *bb = &b;

    aa->Message();
    bb->Message();
    
    return 0;
}


4행의 virtual void Message() 를 주목한다.
이번엔 출력결과가
class A
class B
가 출력이 된다.

이렇게되는 이유가 무엇일까?
그이유는 동적바인딩 때문이다.
동적바인딩, 정적바인딩이란 무엇일까?

정적바인딩(Static Binding) - 컴파일타임에 성격이 결정됨
동적바인딩(Dynamic Binding) - 런타임에 성격이 결정됨

뭔말이냐 하면
함수를 호출 할 때에는 그함수를 호출한 '주소' 가 저장된다.  (http://secretroute.tistory.com/entry/140819 참조)

프로그램실행 -> 함수호출 -> 함수가 저장된 주소로 점프 -> 함수실행 -> 원래위치

함수를 호출하는부분에 그 함수가 위치한 주소로 연결시켜주는것을 '바인딩(Binding)' 이라고 한다.

동적바인딩과 정적바인딩의 예시코드를 들어보겠습니다.

[동적바인딩코드]

int main() {
    int num;
    Monster *monster = nullptr;
    std::cin >> num;
    switch(num)
    {
        case 1:
            monster = new Pig();
            break;
        case 2:
            monster = new Slime();
            break;
    }
    return 0;
}


[정적바인딩코드]

int main() {
    int num;
    std::cin >> num;
    switch(num)
    {
        case 1:
        {
            Pig *p = new Pig();
            break;
        }
        case 2:
        {
            Slime *s = new Slime();
            break;
        }
    }
    return 0;
}


차이점이 보이시나요?


다시 가상함수로 돌아와서

클래스에 가상함수가 하나라도있다면
그 클래스에 맞는 vtable이라는 배열이 생성이 됩니다.
각 클래스마다 다른 vtable 의 시작주소를 알리는 4바이트의 vptr도 각 클래스에 숨겨진채로 생성이됩니다.

그렇다면 상속을 하면 어떻게 될까요

일단 순서는 이렇습니다.

파생클래스는 기본클래스의 가상함수테이블을 상속받습니다.
기본클래스의 가상함수테이블을 복사하여 파생클래스의 가상함수테이블에 가상함수의 주소를 재정의합니다.
재정의 하지않았다면 기본클래스의 함수를 그대로 사용합니다.
새로운 가상함수가 등록이됐다면 가상함수테이블의 맨 뒤쪽에 추가합니다.  http://wonjayk.tistory.com/244 참고



#include <iostream>
using namespace std;

class A
{
public:
    virtual void function1()
    {
        std::cout << "A의 function1" << std::endl;
    }
    virtual void function2()
    {
        std::cout << "A의 function2" << std::endl;
    }
    virtual void function3()
    {
        std::cout << "A의 function3" << std::endl;
    }
};
class B : public A
{
    virtual void function2()
    {
        std::cout << "B의 function2" << std::endl;
    }
    virtual void function3()
    {
        std::cout << "B의 function3" << std::endl;
    }
    virtual void function4()
    {
        std::cout << "B의 function4" << std::endl;
    }
};


int main() {
    A *b = new B();
    b->function1();
    b->function2();
    b->function3();
//    b->function4();
    
    
    return 0;
}



이코드에서 출력은 어떻게될까?

답은 


여기서 자세한 설명을 하겠습니다.



A클래스에 virtual이 있다.

A클래스의vtable 생성

vtable을 가리키는 vptr생성

vtable에는

function1()

function2()

function3()

저장


B클래스에 virtual이 있다.

B클래스의 vtable을 생성

A클래스를 상속받으므로 A클래스의 vtable도 상속

A클래스의 vtable에

function1() 

function2()

function3() 가 있는데

B클래스에는 

function2()

function3() 이 재정의됨

function4() 추가

B클래스의 vtable에 

function1() B클래스에없으므로 A를 가리키고잇음

function2() B를 가리킴

function3() B를 가리킴

function4() B를 가리킴


이렇게 진행이된다.

그렇지만 코드에서

A *b = new B(); 

a는 A포인터이다.

마지막행에서

b->function4()는 할수 없다. 컴파일에러이다

왜냐하면 

A에는 function4()함수가 없기 때문이다.



그림을 참고하면 이해가 쉬울것이다.

[출처] http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/ 



여기서 또 하나 알아야 할 점이 있습니다.

생성자, 소멸자에 관련된건데요


생성자 : 기본클래스 -> 파생클래스 

소멸자 : 파생클래스 -> 기본클래스


가 된다는걸 공부했는데요


#include <iostream>
using namespace std;

class Animal
{
public:
    Animal()
    {
        std::cout << "Animal() 생성자" << std::endl;
    }
    virtual ~Animal()
    {
        std::cout << "~Animal() 소멸자" << std::endl;
    }
};
class Dog : public Animal
{
public:
    Dog()
    {
        std::cout << "Dog() 생성자" << std::endl;
    }
    ~Dog()
    {
        std::cout << "~Dog() 소멸자" << std::endl;
    }
};

int main() {
    Animal *a = new Dog();
    delete a;
}


virtual ~Animal()을 해서 소멸자에도 virtual을 붙였는데요
그이유가 무엇일까요?
일단 출력을 보겠습니다.


생성자의 개수만큼 소멸자가 호출된 것을 볼 수 있는데요

virtual을 뺀다면 어떻게 바뀔까요?



~Dog() 소멸자가 보이지않습니다.



[Animal클래스의 소멸자에 virtual이 없다]

-> Dog클래스의 생성자호출을위해 Animal클래스의 생성자호출

-> Dog클래스의 생성자 호출

-> delete a;

-> a는 Animal의 포인터이기때문에 a의 소멸자가 호출


* 그렇다면 Dog()는 생성되고 제거가 되지않아서 heap메모리에 남아있게 됩니다. 뭔가 찝찝하고 상식적으로 이렇게 되면 안됩니다.

그래서 기본클래스인 Animal의 소멸자에 virtual을 붙입니다.


[Animal클래스의 소멸자에 virtual이 있다]

-> Dog클래스의 생성자호출을위해 Animal클래스의 생성자호출

-> Dog클래스의 생성자 호출

-> delete a;

-> a는 Animal의 포인터이기때문에 a의 소멸자가 호출

-> Animal의 소멸자가 virtual이기때문에 상속받는 파생클래스인  Dog의 소멸자를 호출

virtual을 쓰면 파생클래스에서 재정의될수 있으므로 탐색을 함.

그런데 파생클래스에 재정의한 소멸자가 있다면,

-> Dog클래스의 소멸자호출

-> Animal클래스의 소멸자호출



# 순수가상함수 (Pure Virtual Function)


순수가상함수란 : 구현이없는 가상함수.


그렇다면 어떻게 쓰는것일까요?

virtual void 메서드이름() = 0;


함수인데 구현이 없습니다. 

이렇다는것은 실제구현은 파생클래스에서 한다는 것입니다. 

주의할점은 반드시 파생클래스에서 '재정의' (= Overriding) 해야 한다는 것입니다.


또한 순수가상함수(Pure Virtual Function) 을 가지고 있는 클래스를

추상 클래스(Abstract Class) 라고 부릅니다.

여기서 추상 클래스가 되면 객체를 생성할 수 없게 됩니다.




#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void sound() = 0;
};
class Dog : public Animal
{
public:
    void sound()
    {
        std::cout << "멍멍" << std::endl;
    }
};

int main() {
    Animal *a = new Dog();
}


class Animal은 순수가상함수를 가지고있다 (= Animal 클래스는 추상클래스이다.)
class Dog에서 sound()메서드를 재정의하기 전에는 Animal *a = new Dog(); 를 할 수없다. (= 순수가상함수는 반드시 파생클래스에서 재정의해야함)



* 파생클래스가 클래스 구조에서 가장 말단에 있다면, virtual을 생략해도된다. 
 왜냐하면 그 파생클래스가 없기때문에 탐색을 할 필요가 없기 때문이다.


[알게된 점]
- vTable과 vPtr 
- virtual의 작동원리
- 파생클래스가 있다면 기본클래스의 소멸자를 반드시 virtual로 해야함. 그렇지않으면 메모리가 남아있음.
- 동적바인딩(Dynamic Binding)과 정적바인딩(Static Binding) 의 차이와 원리

[알아야할 점]

- vTable과 vPtr의 심화



지적사항이나 부족한점 댓글로 달아주시면 고치겠습니다!

감사합니다