가티있는블로그

[C++] 가상함수

2021. 8. 21. 22:52 | 프로그래밍/C++

upcasting

기반 클래스 타입의 포인터로 파생 클래스를 가리킬 수 있다.

기반 클래스 타입의 참조로 파생 클래스를 가리킬 수 있다.

 

class Shape
{
public:
    int color;
};
class Rect : public Shape
{
public:
    int x, y, w, h;
};

int main()
{
    Rect rect;

    Shape* p = ▭ 
    
    p->color = 0; // ok
    p->x = 0;     // error
    static_cast<Rect*>(p)->x = 0; // ok
    
}

기반 클래스 타입의 포인터로 파생 클래스를 가리킬 때

기반 클래스의 멤버는 접근할 수 있지만 파생 클래스가 추가한 멤버는 접근할 수 없다.

파생 클래스가 추가한 멤버에 접근 하려면 포인터를 파생 클래스 타입으로 캐스팅 해야한다.

 

위의 코드에서 p는 Shape타입이기 때문에 Shape는 x를 가지고 있지 않기 때문에 실제로 메모리에 x가 존재하더라도 컴파일러는 에러로 표시하게 된다.

 

upcasting 활용

- 동종(동일한 기반 클래스를 사용하는 클래스)을 처리하는 함수를 만들 수 있다.

- 동종을 보관하는 컨테이너를 만들 수 있다.

 

 

function overried

기반클래스가 가진 함수를 파생클래스가 다시 만드는 것.

기반 클래스 포인터로 파생 클래스를 가리킬때 overried된 함수를 호출하면

C++, C# 등의 언어는 기반 클래스 함수 호출, java, swift 등의 언어는 파생 클래스 함수 호출 (반대라서 햇갈린다..)

 

class Shape
{
public:
    void Draw() { std::cout << "Shape::Draw" << std::endl; }
};

class Rect : public Shape
{
public:
    void Draw() { std::cout << "Rect::Draw" << std::endl; }
};

int main()
{
    Shape s; s.Draw(); // Shape::Draw
    Rect r;  r.Draw(); // Rect::Draw
    
    Shape* p = &r;     //
    p->Draw();         // Shape::Draw
}

 

 

함수 바인딩

C++은 기본적으로 컴파일 할 때 함수 호출을 결정한다. static binding

따라서 컴파일러는 컴파일 시간에 실제로 p가 어느 객체를 가리키는지 알 수 없다. 

그래서 포인터의 타입으로 함수 호출을 결정하게된다.

 

 

가상 함수

어느 함수를 호출할지는 컴파일 시간에 하지말고 실행할때 결정해달라는 것.

메모리에 있는 객체를 조사한 후 호출. -> 실제 메모리에 놓은 객체 타입으로 함수 호출 결정

 

따라서 원칙적으로 파생클래스가 재정의한 메소드는 virtual로 설정해야한다.

가상함수가 아닌 함수를 재정의 하지말라

class Shape
{
public:
    virtual void Draw() { std::cout << "Shape::Draw" << std::endl; }
};
class Rect : public Shape
{
public:
    virtual void Draw() { std::cout << "Rect::Draw" << std::endl; }
};

int main()
{
    Shape s; 
    Rect r;  
    
    Shape* p = &r;
    p->Draw();    // Rect::Draw
}

위의 코드처럼 함수를 virtual로 선언하게 되면 Rect classd의 Draw를 호출하게된다.

 

파생클래스에서 가상 함수 재정의(override)할 때 virtual 키워드는 붙여도 되고 붙이지 않아도 된다.

되도록이면 붙이는것이 가독성이 좋다.

 

override 키워드

C++ 11부터 지원을 하는데 실수를 방지하기 위해 override를 붙이는 것이 좋다.

  
class Base
{
public:
    virtual void f1()    {}
    virtual void f2(int) {}
    virtual void f3() const {}
    virtual void f4() {}
};

class Derived : public Base
{
public:
    virtual void ff1() override {} //error
    virtual void f1() override {}
    virtual void f2(double) override {} //error
    virtual void f3() override {} //error
    
    virtual void f4() final {}
};
class Derived2 : public Derived
{
public:
    virtual void f4()  {} //error 재정의 불가능
};

 

final 키워드

객체지향에서 많이 사용하게된다.

파생클래스에서 가상함수를 재정의 할 수 없도록 하기 위해서 

 

virtual override final 키워드는 선언부에만 표기하고 구현부에는 포기하지 않는다.

 

 

가상 소멸자

어떤 클래스가 기반 클래스로 사용되면 소멸자를 반드시 가상함수로 만들어야 한다.

만약 소멸자를 가상함수로 만들어주지 않으면 파생 클래스의 소멸자가 아닌 기반클래스가 호출이 될 수 있다.

 

기반클래스만 virtual로 해주게되면 파생클래스의 소멸자들은 자동으로 불려지게된다.

 

public:
    virtual void Draw() { cout << "Shape::Draw" << endl;} 
    
    virtual ~Shape() {} //가상소멸자
};

 

가상함수의 오버헤드 - 메모리 사용량

- 가상함수 테이블. 객체당 한개의 가상함수 테이블을 가리키는 포인터 추가.

 

가상함수의 오버헤드 - 성능

- 함수 호출시 실행시간에 메모리를 한번 참조 후 호출. 가상함수는 인라인함수가 될 수 없다.

 

=> 작은 임베디드 프로그램 등에서는 문제가 될 수 있다.

'프로그래밍 > C++' 카테고리의 다른 글

[C++] exception 예외처리  (0) 2021.08.22
[C++] 추상클래스, 인터페이스  (0) 2021.08.21
[C++] 상속  (0) 2021.08.21
[C++] this 포인터 개념  (0) 2021.08.20
[C++] 상수 멤버 함수 const member function  (0) 2021.08.20