09-1 멤버함수와 가상함수의 동작원리
앞의 내용에서는 객체 내에 멤버함수가 존재한다고 했고 앞으로도 객체 그림을 그릴 때 그렇게 그려도 무방하지만,
(객체지향적 논리로는 이렇게 이해하는 게 옳다) 사실은 멤버함수가 객체 안에 존재하지 않는다.
ReObjUnder1.cpp
객체의 멤버함수와 멤버변수가 어떠한 형태로 구성되는지 설명하기 위해
모델이 되는 C++ 코드를 보고 C언어 스타일의 구조체와 전역함수를 이용해서 구현해본다.
// ReObjUnder1.cpp
#include <iostream>
using namespace std;
class Data
{
private:
int data;
public:
Data(int num) :data(num)
{ }
void ShowData()
{
cout << "Data: " << data << endl;
}
void Add(int num)
{
data += num;
}
};
int main()
{
Data obj(15);
obj.Add(17);
obj.ShowData();
return 0;
}
* 함수 포인터 (참고)
ReObjUnder2.cpp
C언어 스타일의 구조체와 전역함수를 이용해서 구현
// ReObjUnder2.cpp
#include <iostream>
using namespace std;
typedef struct Data
{
int data;
// 함수포인터 변수가 구조체의 멤버로 등장
// 반환형식 (*식별자) (파마리터형 목록)
void (*ShowData)(Data*);
// ShowData 함수의 주소값을 저장하기 위한 것
void (*Add)(Data*, int);
// Add 함수의 주소값을 저장하기 위한 것
} Data;
void ShowData(Data* THIS)
{
cout << "Data: " << THIS->data << endl;
}
void Add(Data* THIS, int num)
{
THIS->data += num;
}
int main()
{
Data obj1 = { 15, ShowData, Add };
Data obj2 = { 7, ShowData, Add };
// 두 객체는 ShowData, Add 함수를 공유하는 것과 같다
// 두 함수를 이용해 멤버인 함수 포인터 변수를 초기하기 떄문에
obj1.Add(&obj1, 17); //멤버함수를 호출하는 것과 유사
obj2.Add(&obj2, 9);
obj1.ShowData(&obj1);
obj2.ShowData(&obj2);
return 0;
}
두 개의 구조체 변수(객체)가 함수를 공유하고 있다. 실제로 C++의 객체와 멤버함수는 이런 관계를 갖는다.
객체가 생성되면 멤버변수는 객체 내에 존재하지만, 멤버함수는 메모리의 한 공간에 별도로 위치하고선 이 함수가 정의된 클래스의 모든 객체가 공유한다.
그리고 객체가 지니고 있는 멤버변수 대상의 연산이 진행되도록 함수를 호출한다;
가상함수의 동작원리와 가상함수 테이블
가상함수의 동작원리를 이해하면 C++이 C보다 느린 이유를 유추 가능하다.
한 개 이상의 가상함수를 포함하는 클래스에 대해서 컴파일러는 가상함수 테이블(V-Table, Virtual Table)이라는 것을 만든다. 가상함수 테이블은 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블이다.
AAA 객체의 Func1 함수를 호출해야할 경우 테이블을 참조하여 0x1024번지에 등록된 Func1()를 호출한다.
* key는 호출하고자 하는 함수를 구분지어준다 (구분자)
* value는 구분자에 해당하는 함수의 주소정보를 알려준다.
오버라이딩 된 가상함수의 주소정보는 유도클래스의 가상함수 테이블에 포함되지 않는다.
=> 오버라이딩 된 가상함수를 호출하면 무조건 가장 마지막에 오버라이딩한 유도클래스의 멤버함수가 호출
// VirtualInternal.cpp
#include <iostream>
using namespace std;
class AAA
{
private:
int num1;
public:
// 한 개 이상의 가상함수를 포함하는 클래스에 대해서
// 컴파일러는 가상함수 테이블(V-Table, Virtual Table)이라는 것을 만든다.
// : 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블
// [ AAA 클래스의 가상함수 테이블 ]
// key value
// ------------------------------------------
// void AAA::Func1() 0x1024번지
// void AAA::Func2() 0x2048번지
// * key는 호출하고자 하는 함수를 구분지어준다 (구분자)
// * value는 구분자에 해당하는 함수의 주소정보를 알려준다.
// AAA 객체의 Func1 함수를 호출해야할 경우
// 테이블을 참조하여 0x1024번지에 등록된 Func1()를 호출
virtual void Func1() {
cout << "Func1" << endl;
}
virtual void Func2() {
cout << "Func2" << endl;
}
};
class BBB: public AAA
{
private:
int num2;
public:
// [ BBB 클래스의 가상함수 테이블 ]
// key value
// ------------------------------------------
// void BBB::Func1() 0x3072번지
// void AAA::Func2() 0x2048번지
// void BBB::Func3() 0x4096번지
// AAA클래스의 오버라이딩 된 가상함수 Func1에 대한 정보가 없다.
virtual void Func1() {
cout << "BBB::Func1" << endl;
}
void Func3()
{
cout << "Func3" << endl;
}
};
int main(void)
{
AAA* aptr = new AAA();
aptr->Func1();
BBB* bptr = new BBB();
bptr->Func1();
return 0;
}
가상함수 테이블이 참조되는 방식
메인함수 호출 이전에 다음 구조로 가상함수 테이블이 메모리공간에 할당된다.
가상함수 테이블은 객체의 생성과 상관없이 메모리 공간에 할당된다. ***
가상함수 테이블이 멤버함수의 호출에 사용되는 데이터 중 하나이기 때문에.
메인함수가 호출되어 객체가 생성되고 나면 다음 구조로 참조 관계를 구성한다.
AAA 객체에는 AAA 클래스의 가상함수 테이블의 주소값이, BBB 객체에는 BBB 클래스의 가상함수 테이블의 주소값이 저장된다. (내부적으로 참조되는 주소값이지 우리가 어찌할 수는 없다)
AAA 객체를 통해 Func1 함수가 호출되었다고 가정하면 Func1 함수의 위치 확인을 위해서 AAA 클래스의 가상함수 테이블이 참조되고, 0x1024번지에 위치한 함수가 실행된다. BBB객체의 Func1함수가 호출되면 BBB클래스의 가상함수테이블이 참조되고, 테이블의 위치에 0x3072번지로 기록되어 있으므로 그곳에 위치한 BBB 클래스의 Func1 함수가 실행된다.
오버라이딩 된 AAA 클래스의 Func1 함수에 대한 정보가 없으므로 BBB 클래스의 Func1 함수가 대신 호출된다.
클래스에 가상함수가 포함되면 가상함수 테이블이 생성되고, 테이블을 참조하여 호출할 함수가 결정되기 때문에 실행속도가 감소하는 단점이 있지만 그 속도차는 미미하여 장점이 더 많은 가상함수이다.
09-2 다중상속에 대한 이해
다중상속은 둘 이상의 클래스를 동시 상속하는 것이다.
다중상속의 기본방법
// MultiInheri1.cpp
#include <iostream>
using namespace std;
class BaseOne
{
public:
void SimpleFuncOne()
{
cout << "BaseOne" << endl;
}
};
class BaseTwo
{
public:
void SimpleFuncTwo()
{
cout << "BaseTwo" << endl;
}
};
// 다중상속
class MultiDerived : public BaseOne, BaseTwo
{
public:
void ComplexFunc()
{
SimpleFuncOne();
SimpleFuncTwo();
}
};
int main(void)
{
MultiDerived mdr;
mdr.ComplexFunc();
return 0;
}
다중상속의 모호성
두 기초 클래스에 동일한 이름의 멤버가 존재할 경우 문제의 소지가 있다.
// MultiInheri2.cpp
#include <iostream>
using namespace std;
class BaseOne
{
public:
void SimpleFunc()
{
cout << "BaseOne" << endl;
}
};
class BaseTwo
{
public:
void SimpleFunc()
{
cout << "BaseTwo" << endl;
}
};
// 다중상속
class MultiDerived : public BaseOne, protected BaseTwo
{
public:
void ComplexFunc()
{
// 어느 클래스에 정의된 함수의 호출을 원하는 지 명시해야 한다.
BaseOne::SimpleFunc();
BaseTwo::SimpleFunc();
}
};
int main(void)
{
MultiDerived mdr;
mdr.ComplexFunc();
return 0;
}
가상상속
//은행 계좌 관리 프로그램 3
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
//#include <cstring>
using namespace std;
/***** class Base ****/
class Base //부모클래스
{
public:
Base(); //생성자
void SimpleFunc(); //멤버함수
};
Base::Base()
{
cout << "Base Constructor" << endl;
}
void Base::SimpleFunc()
{
cout << "BaseOne" << endl;
}
/***** class MiddleDerivedOne ****/
class MiddleDerivedOne :virtual public Base //가상상속으로 LastDerived 객체 생성 시 Base가 한 번만 호출
{
public:
MiddleDerivedOne(); //생성자
void MiddleFuncOne(); //멤버함수
};
MiddleDerivedOne::MiddleDerivedOne() : Base()
//기초 클래스의 생성자 호출을 명시하지 않아도 되나
//기초 클래스의 생성자가 호출됨을 강조하기 위해 별도로 :Base()라고 명시
{
cout << "MiddleDerivedOne Constructor" << endl;
}
void MiddleDerivedOne::MiddleFuncOne() // 멤버함수
{
SimpleFunc();
cout << "MiddleDerivedOne" << endl;
}
/***** class MiddleDerivedTwo ****/
class MiddleDerivedTwo :
virtual public Base
{
public:
MiddleDerivedTwo(); //생성자
void MiddleFuncTwo(); //멤버함수
};
MiddleDerivedTwo::MiddleDerivedTwo() : Base() //기초 클래스의 생성자가 호출됨을 강조하기 위해 별도로 :Base()라고 명시
{
cout << "MiddleDerivedTwo Constructor" << endl;
}
void MiddleDerivedTwo::MiddleFuncTwo()
{
SimpleFunc();
cout << "MiddleDerivedTwo" << endl;
}
/***** class LastDerived ****/
class LastDerived
: public MiddleDerivedOne, public MiddleDerivedTwo
{
// 가상으로 Base 클래스를 상속하는 두 클래스를 다중으로 상속한다
public:
LastDerived();
//: MiddleDerivedOne(), MiddleDerivedTwo();
void ComplexFunc();
};
LastDerived::LastDerived()
: MiddleDerivedOne(), MiddleDerivedTwo() //생성자
{
cout << "LastDerived Constructor" << endl;
}
void LastDerived::ComplexFunc() //멤버함수
{
MiddleFuncOne();
MiddleFuncTwo();
SimpleFunc(); // 가상상속으로 하나만 존재한다
}
/***** main ****/
int main()
{
cout << "객체생성 전....." << endl;
LastDerived ldr;
cout << "객체생성 후....." << endl;
ldr.ComplexFunc();
return 0;
}
Q. 헤더와 cpp 파일분할로 분할하면 오류가 생긴다 ---- 질문
LastDerived 클래스가 Base클래스를 간접적으로 두 번 상속하는 구조이다.
virtual 키워드가 없으면 SimpleFunc()를 호출 시 어느 클래스를 통해서 간접 상속한 Base 클래스의 멤버함수를 호출할 지 명시해야 한다.
MiddleDerivedOne::SimpleFunc();
Base 클래스의 멤버가 LastDerived 객체에 하나씩만 존재하는 게 타당한 경우가 대부분이고,
MiddleDerivedOne::SimpleFunc();보다 Base 클래스가 딱 한번만 상속하는 게 더 현실적인 해결책이며
그를 위한 문법이 가상상속이다.
실제로 Base 클래스의 생성자도 한번만 호출된다. 가상상속을 하지 않으면 Base 클래스의 생성자는 두 번 호출된다.
'C, C++ > 열혈 C++ 프로그래밍' 카테고리의 다른 글
[윤성우 열혈 c++] OOP 단계별 프로젝트 05단계 (0) | 2021.11.04 |
---|---|
[윤성우 열혈 c++] OOP 단계별 프로젝트 04단계 (0) | 2021.11.04 |
[윤성우 열혈 c++] OOP 단계별 프로젝트 03단계 (0) | 2021.11.01 |
[윤성우 열혈 c++] OOP 단계별 프로젝트 02단계 (0) | 2021.11.01 |
[윤성우 열혈 C++] 09. Virtual의 원리와 다중상속 (영상) (0) | 2021.11.01 |