티스토리 뷰

Programming/C++

[c++]가상함수(virtual method)

쩨리쩨리 2018. 3. 13. 17:55
반응형

* 동적 바인딩

함수 호출을 실제 함수의 몸체와 연결하는 것을 바인딩(binding)이라고 한다. 바인딩에는 정적 바인딩과 동적 바인딩이 존재한다. 컴파일 단계에서 모든 바인딩이 완료되는 것을 정적 바인딩(static binding)이라고 한다. 반대로 바인딩이 실행 시까지 연기되고 실행 시간에 실제 호출되는 함수를 결정하는 것을 동적 바인딩(dynamic binding)이라 한다.

 

동적 바인딩을 사용하면 객체 지향의 중요한 특징 중 하나인 다형성을 구현할 수 있다. 즉 객체에 메시지를 보내면 객체가 메시지를 해석해서 가장 적절한 동작을 하게 된다.

 

c++에서 가상 함수가 아니면 모든 함수가 정적 바인딩으로 호출된다. 정적 바인딩은 호출 속도가 빠르지만 호출 함수가 컴파일 단계에서 항상 결정되므로 유연성은 떨어진다. 가상 함수는 동적 바인딩으로 호출된다. 동적 바인딩은 테이블을 사용해서 실제 호출되는 함수를 결정해야 하므로 처리 속도가 늦어진다.

 

 

 

 

* 동적 바인딩 특징

1. 부모의 포인터로 자식을 접근하는 방식 사용(부모가 자식에게 접근하는 방식이라서 포인터가 필요함)

2. is ~ a 상속 관계일때만 사용가능

3. 정적 바인딩과 동적 바인딩은 같은 결과가 나와야 한다.

4. 동적 바인딩을 쓸 수 있는 범위는 부모가 인식할 수 있는 범위까지만 사용 가능

즉, 포인터는 부모만 가지고 있는 경우 또는 오버라이딩한 멤버만 접근이 가능하다.

5. 동적 바인딩은 자식이 여러명일때 부모의 하나의 이름으로 모든 자식을 컨트롤 및 접근을 하기 위해서 많이 쓰인다.(다형성)

 

 

 

 

* 가상 함수

상속 구조 클래스에서 특정 함수에 virtual 키워드를 명시한 함수를 가상 함수라 한다. 가상 함수가 하나라도 존재하는 클래스가 생성될 때에는 멤버필드 외에 가상 함수들의 코드 메모리 주소를 보관하는 테이블이 동적으로 생성되고 이 테이블의 위치 정보를 개체는 갖게 된다. 그리고 자식 클래스에서 자식 생성자를 거치면 해당 함수가 정의된 코드 주소로 변경하는 작업을 수행한다.

 

가상 함수를 호출하는 부분은 컴파일러 과정에서 가상 함수 테이블에 있는 코드 주소를 호출하는 것으로 바뀐다. 이러한 이유로인해 virtual 키워드가 명시된 함수는 관리하는 형식이 아닌 실존하는 클래스의 함수가 호출하게 되는 것이다.

 

여기에서 함수는 호출할 때 사용하는 방법을 의미하고 함수는 수행할 코드가 정의된 정의부 부분을 의미한다.

 

 

 

* 가상 함수 만들어 지는 순서

1. 부모 클래스를 자식 클래스에게 상속한다.

2. 부모 클래스의 함수 하나를 virtual를 앞에 붙여 가상 함수를 만든다.

3. virtual를 붙인 가상 함수가 있는 클래스는 가상 공간이 생긴다. 그 가상 공간에는 가상 배열을 가리키는 가상 포인터가 존재한다.

4. 가상 포인터는 가상 배열을 가리키는데, 그 가상 배열을 가상 테이블이라고도 부른다.

5. 가상 테이블은 동적으로 생성되고, 테이블은 가상 함수들의 코드 메모리 주소를 보관한다.

6. 테이블 안에 있는 가상 함수들의 코드 메모리 주소는 각자 주소를 담고 있기 때문에 어디에 위치해있는지 확실히 가리키고 있다.

7. 즉 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include<iostream>
#include<string>
using namespace std;
 
class A{
public :
    virtual void disp();//함수선언
    void cccc() {cout << "cccc" << endl;}
};
 
class B :public A{
public:
    void disp();//오버라이딩
    void dddd() {cout << "dddd" << endl;}
};
 
void A::disp() {//함수정의//class A의 함수
    cout << "A::disp()" << endl;}
 
void B::disp() {//함수정의//class A의 함수//오버라이딩
    cout << "B::disp()" << endl;
    A::disp();}
 
/*void output() {//외부함수//A,B의 disp()와는 상관 없음
    cout<<"output()"<<endl;}*/
 
void main() {
 
    A *p;
    B bb;
 
    bb.dddd();//정적 바인딩
 
    p = &bb;//타입이 다르지만 오류 안뜸//동적바인딩 방식
    p->disp();//B의 함수를 가리키고 있다
    
    p->dddd();
    //부모의 포인터는 자식의 함수를 부를수 없다. 에러
    //부모가 자식의 함수를 부르려면 오버라이딩 해야함
    //동적바인딩을 했을때...
 
    p->cccc();//원래 부모거는 부를수있다.
 
    /*
    동적바인딩과 정적바인딩은 같은 결과가 나와야함
    가상함수 안쓰면 부모가 우선이라서 부모거부터 잡아버림
    가상함수를 썼을때 자동으로 생기는 Vptr 포인터 는 메모리 배열을 가리킴(함수 포인터 배열)
    메모리상에 존재하고 있는데 보이지 않음
    가상테이블이 오버라이딩된 가상함수를 관리해준다.
    >>포인터가 잡고있는 것을 우선으로 하게끔 한다.
    원래 부모가 인식하고 있는 것만 접근할 수 있다.
    */
    
    /*output();
    A aa;
    aa.disp();
    B bb;
    bb.disp();//bb.B::bisp();
    bb.A::disp();*
    //정적 바인딩으로 클래스 명시해서 함수 호출하는 방식
}
cs

 

 

* 풀이

1. A 타입의 포인터가 있다. 이 포인터는 B 클래슷 안의 bb객체의 주소를 잡고 있다. 포인터는 A 클래스 자체에도 접근이 가능하지만 B클래스 안의 주소를 가리키고 있기 때문에 B 클래스에도 접근이 가능하다.

2. A의 클래스를 B의 클래스가 상속 받고 있고, B 클래스가 A의 disp() 함수를 오버라이딩 해서 재정의 하고 있다. 이때, A와 B 클래스 안(선언부)에는 함수를 선언만 하고 함수의 기능 구현은 클래스 외부(정의부)에서 하고 있다.

3. 원칙적으로 부모를 상속받은 자식은 부모의 기능을 모두 가져올 수 있지만, 부모는 자식의 클래스를 접근 할 수없다. 부모가 자식을 접근하는 방법은 동적 바인딩을 이용하는 것이다.

4. 이때 알고 가야할 원칙이 있는데, A 클래스의 함수에virtual을 쓰지 않으면 A 타입의 포인터는 자식을 가리키고 있지만 굳이 멀리 있는 B의 함수에게 접근하는 것보다 가까운 A 클래스의 함수를 우선적으로 접근한다. 즉, virtual가 없으면 포인터는 부모를 우선적으로 가리킨다.

5. 하지만 A 클래스의 함수앞에 virtual을 쓰면 가상함수로 취급되어 부모보다 자식을 우선해서 가리키게 된다.

6. 아래 그림으로 이해해보자. 상속받은 B 클래스에 가상공간이 생기고 그 가상공간에는 가상 테이블을 가리키는 가상 포인터가 존재한다. 가상 테이블에는 가상공간이 생긴 클래스의 함수를 가리키는 포인터가 들어있다. 이때문에 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
27
28
29
30
#include<iostream>
#include<string>
using namespace std;
 
class A {
 
public :
    A() { cout<<"A::A()"<<endl; }
    virtual ~A() { cout << "~A::A()" << endl; }
};
 
class B : public A {
 
public:
    
    B() { 
        cout << "B::B()" << endl; }
    ~B() { cout << "~B::B()" << endl; }
};
 
void main(){
 
    //B bb;
    
    A *= new B;
 
    delete p;
 
 
}//동적바인딩이 끝나면 메모리를 삭제하기 위해 소멸자를 부른다.//stack
cs

 

 

 

* 풀이

1. A 타입의 포인터는 객체의 일종이기 때문에 객체를 초기화 하러 무조건 생성자를 거쳐야한다.

2. B의 생성자로 올라갔더니 B의 생성자 맨 첫줄에는 부모를 부르는 super 함수가 디폴트 되어있다. 따라서 A 클래스의 생성자로 간다. 뒤에 다시 B 생성자로 돌아 온다.

3. 포인터의 동적 할당이 끝났기 때문에 동적 메모리를 삭제해야한다. main은 stack 구조이기 때문에 LIFO(후입선출)방식을 이용하여 먼저 접근한 B의 메모리 부터 삭제되어야 한다.

4. A의 소멸자 함수에 virtual가 있기 때문에 자식의 소멸자 함수를 우선시로 들어간뒤 A 소멸자로 들려서 메모리를 삭제한다.

 

 

 

* 출력결과

 

반응형
댓글
공지사항