본문 바로가기

Programing/C++

[C++] 가상 함수(상속)

728x90
반응형

오늘은 상속 중에서도 가상 함수에 대해서 공부를 하였다.

바로 예제로 넘어가자

#include <iostream>

using namespace std;

class Base
{
public:
    void func()
    {
        cout << "Base" << endl;
    }
};

class Derived : public Base
{
public:
    void func()
    {
        cout << "Derived" << endl;
    }
};

int main()
{
    Base b;
    Derived d;

    b.func(); // Base
    d.func(); // Derived

    Base& b0 = d; // 부모 객체가 자식 클래스르 가르키게 함.
    b0.func(); // Derived일것 같지만 Base가 나오게 된다

    Base* b1 = &d; 
    b1->func(); // Derived일것 같지만 Base가 나오게 된다
}

이렇게 부모 클래스(Base)가 자식클래스(Derived)를 가르키게 한 후에, func()함수를 사용하면 부모 클래스의 func()가 호출 되게 된다.

만약, 자식 클래스(Derived)의 func()를 쓰고 싶으면 virtual 키워드를 사용하면 된다.

 

이렇게 쓰게되면 상속 받은 자식 클래스의 동일한 func()함수는 오버라이딩을 하는 것으로 보는 것이다.

#include <iostream>

using namespace std;

class Base
{
public:
    virtual void func()
    {
        cout << "Base" << endl;
    }
};

class Derived : public Base
{
public:
    void func() // 오버 라이딩
    {
        cout << "Derived" << endl;
    }
};


void foo(Base& base)
{
    base.func();
}

int main()
{
    Base b;
    Derived d;

    b.func(); // Base
    d.func(); // Derived

    Base& b0 = d; // 부모 객체가 자식 클래스르 가르키게 함.
    b0.func(); // Derived

    Base* b1 = &d; 
    b1->func(); // Derived

    foo(d); // Derived
}

추가 예제,

#include <iostream>

using namespace std;

class Base
{
public:
    virtual void func()
    {
        cout << "Base" << endl;
    }
};

class Derived : public Base
{
public:
    // 키워드르 적지 않아도 암시적으로 virtual 키워드가 있는것과 동일하다. 
    // 그러나 관례적으로 virtual키워드를 적어 주는게 좋다.
    // override를 적으면 부모클래스에는 반드시 virtual 키워드가 있어야한다. 
    // 만약 virtual키워드가 없으면 override를 키워드를 사용할 수 없다.
    // override를 적으면 부모클래스와 함수 이름이 틀리면 컴파일이 되지 않는다.
    virtual void func() override 
    {
        cout << "Derived" << endl;
    }
};

class Derived1 : public Derived
{
public:
    void func() // 오버 라이딩
    {
        cout << "Derived1" << endl;
    }
};

void foo(Base& base)
{
    base.func();
}

int main()
{

    Derived1 d;
    foo(d); // Derived1
}

 

게임을 구현하여 예제를 들어보자,

#include <iostream>

using namespace std;

class Character
{
private:
    int _health;
    int _power;
public:
    Character(int health, int power):_health(health), _power(power)
    {

    }
    virtual void damaged(int power)
    {
        _health -= power;
    }
    void attack(Character& target) const
    {
        target.damaged(_power);
    }
};

class Player : public Character
{
public:
    Player(int health, int power) : Character(health, power)
    {

    }
    virtual void damaged(int power) override
    {
        Character::damaged(power);
        cout << "으악" << endl;
    }
};

class Monster : public Character
{
public:
    using Character::Character; // 부모클래스의 생성자를 상속 받음
    virtual void damaged(int power) override
    {
        Character::damaged(power);
        cout << "꽤액" << endl;
    }
};



int main()
{
    Player player(200, 100);
    Monster monster(100, 50);
    player.attack(monster);  // 꽤액, 오버라이딩 되어 monster의 damaged가 호출됨
    monster.attack(player); // 으악, 오버라이딩 되어 player의 damaged가 호출됨
}

 

다음은 virtual 키워드가 꼭 필요한 이유를 소멸자를 통해서 설명해보겠다,

virtual키워드가 없으면, 부모 포인터 객체가 자식 클래스로 동적할당하여 가르키게 할 경우, delete로 포인터 객체를 삭제할때 선언할때의 타입만 보기 때문에 부모 객체만 삭제가 된다.

자식 클래스는 삭제가 되지 않고, 메모리 누수가 발생하는 셈이다.

 

#include <iostream>

using namespace std;

class Character
{

public:
    ~Character()
    {
        cout << "~Character" << endl;
    }
};

class Player : public Character
{
public:
    ~Player()
    {
        cout << "~Player" << endl;
    }
};


int main()
{
    Character* ch = new Player;
    delete ch;
    // ~Character

}

 

그래서 virtual 키워드를 쓰게 되면 컴파일러가 ch의 타입만 보는게 아니라, ch가 가르기고 있는 대상의 타입 까지 보게 해준다.

#include <iostream>

using namespace std;

class Character
{

public:
    virtual ~Character()
    {
        cout << "~Character" << endl;
    }
};

class Player : public Character
{
public:
    virtual ~Player()
    {
        cout << "~Player" << endl;
    }
};


int main()
{
    Character* ch = new Player;
    delete ch;
    // ~Character

}

 

가상함수에 관련하여 몇가지 주의사항을 안내하려 한다.

#include <iostream>

using namespace std;

class Character
{

public:
    virtual int get()
    {
        return 1;
    }
};

class Player : public Character
{
public:
    virtual int get(int num) override // 함수의 프로토타입이 다르면, (이름은 같으나 매개변수가 있고, 없고 차이, const가 달려있어도 다른 함수로 취급함.) override가 되지 않는다.,
    {
        return num;
    }
};


int main()
{
    Character c;
    c.get();

    Player p;
    p.get(); // 부모클래스에서 매개변수가 없는 get이 없기 때문에 오류가 난다. 즉 get()함수를 가리게 되는 것이다.
}

이거 문제를 해결하면,

 

 

#include <iostream>

using namespace std;

class Character
{

public:
    virtual int get()
    {
        return 1;
    }
};

class Player : public Character
{
public:

    using Character::get; // 부모클래스 get 함수를 상속 받아오면 된다.
    virtual int get(int num)
    {
        return num;
    }
};


int main()
{
    Character c;
    c.get();

    Player p;
    p.get(); // 컴파일 오류 없이 동작한다.
}

추가로 보면, 

#include <iostream>

using namespace std;

class Character
{

public:
    virtual Character* get()
    {
        return nullptr;
    }
};

class Player : public Character
{
public:

    virtual Player* get() override // 리턴형이 부모, 자식 관계에 있으면 부모,자식 관계에 있는 포인터나 레퍼런스는 함수 모양이 달라도 가상함수를 허용한다(공변형)
    {
        return nullptr;
    }
};


int main()
{
    Character c;
    c.get();

    Player p;
    p.get(); 
}

 

#include <iostream>

using namespace std;

class Character
{

private:
    int num;


};

class Player : public Character
{
private:
    int num;
    void fun()
    {
        num = 10; 
        // 이 num은 어디의 num일까?  이거는 Player클래스의 num이다. 
        // 만약 부모 글래스의 num이 public으로 선언되어도 Player클래스의 num을 가르킨다. 
        // 이러면 부모 클래스의 num을 가리게 된다.
    }
};


int main()
{
    Player player;
    player.num; // 컴파일 에러가 발생한다. num이 2개 선언된거와 동일하기 때문
    // Player클래스에 using Character::num;을 선언하면 컴파일러 오류는 해결된다.
}

 

반응형

'Programing > C++' 카테고리의 다른 글

[C++] 클래스 템플릿  (0) 2021.01.20
[C++] 템플릿(Template) - 2  (0) 2021.01.19
[C++] 템플릿(Template) - 1  (0) 2021.01.18
[C++] 코딩 테스트 개 문제 풀이  (0) 2021.01.17
[C++] 코딩테스트 준비(백준, 고양이문제)  (0) 2021.01.14