|9.1| 상속 관계에서의 함수 중복


< 예시 : 상속 관계에서 함수를 중복하는 경우 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
 
class Base {
public:
    void f() { cout << "Base::f() called" << endl; }
};
 
class Derived : public Base {
public:
    void f() { cout << "Derived::f() called" << endl; }
};
 
void main() {
    Derived d, *pDer;
    pDer = &d; // 객체 d를 가리킨다.
    pDer->f(); // Derived의 멤버 f() 호출 
 
    Base* pBase;
    pBase = pDer; // 업캐스팅. 객체 d를 가리킨다.
    pBase->f(); // Base의 멤버 f() 호출
}
cs


실행 결과


Derived::f() called

Base::f() called


=> 파생 클래스에서 기본 클래스와 동일한 식의 함수를 중복 작성하는 겨우, 

기본 클래스에 대한 포인터로는 기본 클래스의 함수를 호출하고(up-casting의 경우에도),

파생 클래스의 포인터로는 파생 클래스에 작성된 함수를 호출한다.




|9.2| 가상 함수와 오버라이딩


: 가상 함수(virtual function)오버라이딩(overriding)은 상속에 기반을 둔 기술로 객체 지향 언어의 꽃이다!



● 오버라이딩


: 오버라이딩은 파생 클래스에서 기본 클래스에 작성된 가상 함수를 중복 작성하여, 기본 클래스에 작성된 가상 함수를 무력화시키고,

객체의 주인 노릇을 하는 것이다.


기본 클래스의 포인터를 이용하든 파생 클래스의 포인터를 이용하든 가상 함수를 호출하면, 파생 클래스에 오버라이딩된 함수가 항상 실행된다.


파생 클래스에서 기본 클래스의 가상 함수와 완전히 동일한 원형의 함수를 재정의 하는 것을 '함수 오버라이딩(function overriding)' 이라고 한다.



● 가상 함수


: 가상 함수(virtual function)란 virtual 키워드로 선언된 멤버 함수로,

virtual 키워드는 컴파일러에게 자신에 대한 호출 바인딩을 실행 시간까지 미루도록 지시하는 키워드이다.



● Overloading(함수 중복) vs Overriding 



< overloading >


1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
    void f() {
        cout << "BASE::f() called" << endl;
    }
};
 
class Derived : public Base {
public:
    void f() {
        cout << "Derived::f() called" << endl;
    }
};
cs


=> Base의 f()와 Derived 의 f()는 각각 동등한 호출 기회를 가짐



< overriding >


1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {
public:
    virtual void f() {
        cout << "BASE::f() called" << endl;
    }
};
 
class Derived : public Base {
public:
    virtual void f() {
        cout << "Derived::f() called" << endl;
    }
};
cs


=> Base의 f()는 존재감을 잃고, 항상 Derived의 f()가 호출됨



※ 변수 오버라이딩이란 용어는 없다. 항상 멤버 함수 에게만 적용된다.




< 예시 : 오버라이딩 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
 
class Base {
public:
    virtual void f() { cout << "Base::f() called" << endl; }
};
 
class Derived : public Base {
public:
    virtual void f() { cout << "Derived::f() called" << endl; }
};
 
void main() {
    Derived d, *pDer;
    pDer = &d; // 객체 d를 가리킨다.
    pDer->f(); // Derived::f() 호출 
 
    Base* pBase;
    pBase = pDer; // 업캐스팅. 객체 d를 가리킨다.
    pBase->f(); // 동적 바인딩 발생!! Derived::f() 실행
}
cs


실행결과


Derived::f() called

Derived::f() called



함수 오버로딩의 경우 pBase가 Base 타입의 포인터이므로 다음 코드는 Base의 f()을 호출할 것 으로 예상되지만,

pBase가 가리키는 객체는 overriding한 Derived의 f()를 포함하므로 동적 바인딩 통해 Derived의 f()가 호출된다.


-> Base의 f()에 대한 모든 호출은 실행 시간 중에 Derived의 f()함수로 동적 바인딩된다.




● 오버라이딩의 목적


: 파생 클래스들이 자신의 목적에 맞게 가상 함수를 재정의 하도록 하는 것이다. ( 객체 지향 언어의 다형성(polymorphism)을 실현한다. )


 

< 예시 :  ' 오버라이딩을 통한 다형성 '  의 이해 >



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Shape {
protected:
    virtual void draw(){ }
};
 
class Circle : public Shape {
protected:
    virtual void draw() { } // Circle 그린다.
    
};
 
class Rect : public Shape {
protected:
    virtual void draw() { } // Rect 그린다.
 
};
 
class Line : public Shape {
protected:
    virtual void draw() { } // Line 그린다.
 
};
 
void paint(Shape *p) {
    p->draw();
}
 
paint(new Circle()); // Circle을 그린다
paint(new Rect()); // Rect를 그린다
paint(new Line()); // Line을 그린다
cs




● 동적 바인딩 : 오버라이딩된 함수가 무조건 호출


: 가상 함수를 호출하는 코드를 컴파일 할 때, 컴파일러는 바인딩을 실행 시간에 결정하도록 미루어둔다.


  나중에 가상 함수가 호출되면, 실행 중에 객체 내에 오버라이딩된 가상 함수를 동적으로 찾아 호출한다.


  이 과정을 동적 바인딩(dynamic binding) 이라고 부른다. 


  오버라이딩은 파생 클래스에서 재정의한 가상 함수의 호출을 보장받는 선언이다.



● 동적 바인딩이 발생하는 구체적 경우


: 동적 바인딩은 파생 클래스의 객체에 대해, 기본 클래스의 포인터로 가상 함수가 호출될 때 일어난다.


- 기본 클래스 내의 멤버 함수가 가상 함수 호출

- 파생 클래스 내의 멤버 함수가 가상 함수 호출

- main()과 같은 외부 함수에서 기본 클래스의 포이터로 가상 함수 호출

- 다른 클래스에서 가상 함수 호출



● 동적 바인딩 예시



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;
 
class Shape {
public:
    void paint() { 
        draw();                        // 얘가
    }
    virtual void draw() {            // 얘를 호출함
        cout << "Shape::draw() called" << endl
    }
};
 
int main() {
    Shape * pShape = new Shape();
    pShape->paint(); 
    delete pShape;
}
cs


실행 결과


Shape::draw() called



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;
 
class Shape {
public:
    void paint() { 
        draw();                                // 얘가
    }
    virtual void draw() {                    // 얘를 호출하지 않고
        cout << "Shape::draw() called" << endl
    }
};
 
class Circle : public Shape {
public:
    virtual void draw() {                    // 얘를 호출함
        cout << "Circle::draw() called" << endl
    }
};
 
int main() {
    Shape *pShape = new Circle();
    pShape->paint(); 
    delete pShape;
}
cs


실행 결과


Circle::draw() called



-> 기본 클래스에서 자신의 멤버를 호출하더라도 그것이 가상 함수이면 역시 동적 바인딩이 발생한다.




● 오버라이딩의 성공 조건


: 함수 이름, 매개 변수 타입, 개수 , 그리고 리턴 타입까지 일치해야 오버라이딩이 성공한다.




● virtual 지시어 생략 가능


: 클래스의 virtual 키워드는 파생 클래스로 상속되므로, 파생 클래스에서 virtual 키워드를 생략할 수 있다.



● 범위 지정 연산자(::)로 존재감을 상실한 함수를 호출하자



< 예시 >


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;
 
class Shape {
public:
    virtual void draw() { 
        cout << "--Shape--"
    }
};
 
class Circle : public Shape {
public:
    int x;
    virtual void draw() { 
        Shape::draw(); // 기본 클래스의 draw() 호출
        cout << "Circle" << endl;
    }
};
 
int main() {
    Circle circle;
    Shape * pShape = &circle;
 
    pShape->draw();  // 동적 바인딩 발생. draw()는 virtual이므로
    pShape->Shape::draw(); // 정적 바인딩 발생. 범위지정연산자로 인해
}
cs




● 가상 소멸자


: 파생 클래스의 객체가 기본 크래스에 대한 포인터로 delete되는 상황에서도 정상적인 소멸이 되도록 하기 위해서

  기본 클래스의 소멸자를 만들 때 가상 함수로 작성할 것을 권한다.



1) 소멸자를 가상 함수로 선언하지 않은 경우


1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Base {
public:
    ~Base();
};
 
class Derived : public Base {
public:
    ~Derived();
};
 
int main() {
    Base *= new Derived();
    delete p; // ~Base()만 실행됨
}
cs


-> p가 Base 타입이므로 ~Base() 소멸자는 실행되나,  ~Derived()는 실행되지 않는다.



2) 소멸자를 가상 함수로 선언한 경우


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base {
public:
    virtual ~Base();
};
 
class Derived : public Base {
public:
    virtual ~Derived();
};
 
int main() {
    Base *= new Derived();
    delete p; /* 1. ~Base() 호출
                 2. ~Derived()실행(동적 바인딩)
                 3. ~Base() 실행*/
}
cs


-> 소멸자를 가상 함수로 선언하면, 객체를 기본 클래스의 포인터로 소멸하든, 파생 클래스의 포인터로 소멸하든 파생 클래스와 기본 클래스의 소멸자를 모두 실행하는 정상적인 소멸 과정이 진행된다.


따라서 클래스의 소멸자를 작성할 떄 고민없이 무조건 virtual로 선언 하는 것이 뒤탈이 없겠다.



< 예시 : 소멸자를 가상 함수로 선언 >



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
 
class Base {
public:
    virtual ~Base() { cout << "~Base()" << endl; }
};
 
class Derived: public Base {
public:
    virtual ~Derived() { cout << "~Derived()" << endl; }
};
 
int main() {
    Derived *dp  = new Derived();
    Base *bp = new Derived();
 
    delete dp; // Derived의 포인터로 소멸
    delete bp; // Base의 포인터로 소멸
}
 
cs


실행 결과


~Derived()

~Base()

~Derived()

~Base()


※ 생성자는 가상 함수가 될 수 없으며, 생성자에서 가상 함수를 호출해도 동적 바인딩이 일어나지 않는다.

  하지만 소멸자는 가상 함수가 될 수 있으며 가상 함수로 만드는 것이 바람직하다.



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

pair 사용  (0) 2019.05.17
#8-5 가상 상속  (0) 2018.10.07
#8-3 상속과 생성자, 소멸자  (0) 2018.10.07
#8-2 상속과 객체 포인터  (0) 2018.10.07
#8-1 상속  (0) 2018.10.07