Doxygen 을 윈도우에 설치하면서 설치 과정을 캡쳐해 남겨 보았습니다. Doxygen 설치 과정 중엔 특별히 할 것이 없습니다.
Doxygen 을 설치한 후 프로그램을 실행시켜 설정을 해 주어야 할 것들 몇 가지가 있습니다.
1) 프로젝트 루트 폴더 지정 2) 소스파일 폴더 지정 3) Scan recursively 체크박스에 체크 4) Destination directory 폴더 지정 (Doxygen이 문서를 생성할 폴더) 5) 프로젝트의 이름과 버전 지정
Doxygen 루트 설정
6) 각 함수마다 사용한 함수로의 링크 생성 체크 7) 해당 언어 선택
8) 문서 왼쪽에 탐색 트리 보이기 체크 9) 소스 간의 관계를 GraphViz로 표현할 것 체크
10) OUTPUT_LANGUAGE 체크 (출력 결과에 쓰여질 언어) 11) ALWAYS_DETAILED_SEC 체크 (항상 상세 정보 출력) 12) INLINE_INHERITED_MEMS 체크
13) EXTRACT_ALL(소스코드의 모든 요소가 문서화 대상으로) 14) EXTRACT_PRIVATE, EXTRACT_STATIC (체크 클래스 내 private, static 멤버 포함할 지) 15) INLINE_SOURCES 체크 (함수 설명 시 함수 코드 보임)
C++에서 복사 생성자란 자신과 같은 클래스 타입의 다른 객체에 대한 참조(reference)를 인수로 전달받아, 그 참조를 가지고 자신을 초기화하는 방법입니다. 복사 생성자는 새롭게 생성되는 객체가 원본 객체와 같으면서도, 완전한 독립성을 가지게 해줍니다. 왜냐하면, 복사 생성자를 이용한 대입은 깊은 복사(deep copy)를 통한 값의 복사이기 때문입니다.
Book(const Book&);
복사 생성자는 다음과 같은 상황에서 주로 사용됩니다.
1. 객체가 함수에 인수로 전달될 때 2. 함수가 객체를 반환값으로 반환할 때 3. 새로운 객체를 같은 클래스 타입의 기존 객체와 똑같이 초기화할 때
연산자 오버로딩, C++의 약속
객체도 기본 자료형 변수처럼 취급하여 덧셈, 뺄셈, 곱셈 등의 연산을 가능하게 한다.
operator 키워드와 연산자를 묶어서 함수의 이름을 정의하면 함수의 이름을 이용한 호출과 연산자를 이용한 함수의 호출을 허용한다.
pos1, pos2가 기본 자료형 변수가 아닌 객체여도 pos1 + pos2는 pos1.operator+(pos2)라는 문장으로 바꿔 해석한다. 객체를 피연산자로 사용한 연산자의 이름 앞에 operator라는 이름을 붙여서 완성되는 이름의 함수를 호출하며 연산자 우측에 있는 피연산자를 인자로 전달
pos1-pos2;는 pos1.operator-(pos2);로 변환된다. 연산자 앞에 operator를 붙여서, operator-라는 이름의 함수를 호출. 연산자 왼쪽의 pos1객체를 대상으로 operator-함수를 호출, -연산자의 우측에 있는 피연산자를 인자로 전달.
오버로딩 된 연산자의 해석
pos1 + pos2
123
pos1.operator+(pos2)
123
1. 멤버함수를 호출할 객체 2. 함수의 이름 3. 함수의 전달인자
연산자를 오버로딩한 함수도 const로 선언하는 게 좋다. 덧셈연산은 연산자의 대상이 되는 피연산자 값의 변경이 아니고 새로운 연산의 결과를 만들어 내는 것이기 때문에 const 선언이 가능하다.
연산자 오버로딩의 두 가지 방법
1. 멤버함수에 의한 연산자 오버로딩 2. 전역함수에 의한 연산자 오버로딩
pos1+pos2;가 멤버함수로 오버로딩되면 pos1.operator+(pos2);
pos1.operator+(pos2);
pos1+pos2;가 전역함수로 오버로딩되면 operator+(pos1, pos2);
operator+(pos1, pos2);
동일한 자료형을 대상으로 + 연산자를 전역함수 기반, 멤버함수 기반으로 동시에 오버로딩 하면 멤버함수 기반으로 오버로딩된 함수가 전역함수 기반으로 오버로딩된 함수보다 우선으로 호출 (가급적 이런 상황은 만들지 x)
전역함수 기반으로 연산자 오버로딩
// GFunctionOverloading.cpp#include<iostream>usingnamespace std;
classPoint
{private:
int xpos, ypos;
public:
Point(int x=0, int y=0): xpos(x), ypos(y)
{ }
voidShowPosition()const//내부출력문 const{
cout << '[' << xpos << ", " << ypos << ']' << endl;
}
friend Point operator+(const Point& pos, const Point& pos2);
// 아래 operator+ 함수에 대해 private 접근을 허용하기 위한 friend 선언
};
// +연산자를 전역함수의 형태로 오버로딩// Point 클래스는 operator+ 함수에 대해 freind선언을 하여 // 함수 내에서는 Point 클래스의 private 멤버에 접근 가능
Point operator+(const Point& pos1, const Point &pos2)
{
Point pos(pos1.xpos + pos2.xpos, pos1.ypos + pos2.ypos);
return pos;
}
intmain(){
Point pos1(3, 4);
Point pos2(10, 20);
Point pos3 = pos1 + pos2; // +연산자가 전역함수의 형태로 오버로딩// operator+(pos1, pos2); 형태로 해석
pos1.ShowPosition();
pos2.ShowPosition();
pos3.ShowPosition();
return0;
}
객체지향이기 때문에 특별한 경우가 아니면 멤버함수 기반으로 연산자를 오버로딩하는 게 낫다. (예외는 아래에서 다룸)
오버로딩이 불가한 연산자의 종류
C++의 문법규칙 보존을 위해 아래 연산자들의 오버로딩을 제한한다.
1. 멤버 접근 연산자 (.) 2. 멤버 포인터 연산자(.*) 3. 범위 지정 연산자 (::) 4. 조건 연산자 (3항연산자 ?:) 5. sizeof (바이트 단위 크기 계산) 6. typeid (RTTI 관련 연산자) 7. static_cast (형변환 연산자) 8. dynamic_cast (형변환 연산자) 9. const_cast (형변환 연산자) 10. reinterpret_cast (형변환연산자)
※ RTTI (runtime type information) : RTTI는 C++ 컴파일러 내에 포함되어 있는 기능으로서, 객체의 유형을 실행 시에 결정할 수 있도록 허용한다. C++이나 다른 고급언어의 컴파일러에서 가지고 있는 이 기능은, 메모리 상주 객체에 유형 정보를 추가한다. 이렇게 함으로써, 실행 시스템은 객체의 캐스트가 유효한 것인지를 확실히 하기 위해, 그 객체가 특정 유형인지를 결정할 수 있다. 객체 기술의 기본 원리 중 하나가, 실행시 객체를 동적으로 변화시킬 수 있는 능력인 polymorphism이다. (출처로 이동)
멤버함수 기반으로만 오버로딩이 가능한 연산자
1. 대입 연산자 (=) 2. 함수 호출 연산자(()) 3. 배열 접근 연산자([]) 4. 멤버 접근을 위한 포인터 연산자 (->)
연산자 오버로딩 시 주의사항
1. 본래의 의도를 벗어난 형태의 연산자 오버로딩은 좋지 않다. 2. 연산자의 우선순위와 결합성은 바뀌지 않는다. 3. 매개변수의 디폴트 값 설정이 불가능하다. - 피연산자의 자료형에 따라서 연산자를 오버로딩 한 함수의 호출이 결정되어 불가능하다. - 매개변수의 디폴트 값이 설정되면 함수 호출관계가 불분명해진다.
4. 연산자의 순수 기능까지 빼앗을 수는 없다. - int 데이터의 + 연산은 의미가 정해져 있고 그를 변경하는 아래와 같은 함수 정의는 허용되지 않는다.
// PointMultipleOperation #include<iostream>#include"Point.h"intmain(){
Point pos(1, 2);
Point cpy;
cpy = pos*3; // pos.operator*(3) 로 해석되며 이 경우 Point객체가 곱셈연산자의 왼편에 와야한다// 교환법칙으로 3*pos;도 가능하게 하려면 오버로딩한 곱셈연산자를 바꿔야 한다.
cpy.ShowPosition();
cpy = pos*3*2; // 3을 곱했을 때 반환되는 객체를 대상으로 다시 2를 곱하고 결과가 cpy에 저장된다
cpy.ShowPosition();
return0;
}
교환법칙 성립을 위한 구현
cpy = 3 * pos; 가 교환법칙이 성립되게 하려면 전역함수로 곱셈연산자를 오버로딩해야 한다. cpy = operator*(3, pos); 처럼 해석되도록 연산자를 오버로딩 해야하고, operator* 함수를 둘 중 하나로 정의해야 한다.
Point operator*(int times, Point& ref)
{
Point pos(ref.xpos * times, ref.ypos * times);
return pos;
}
Point operator*(int times, Point& ref)
{
return ref*times;
}
Point.h
#pragma onceclassPoint
{public:
Point(int x = 0, int y = 0);
private:
int xpos, ypos;
public:
voidShowPosition()const;
Point operator*(int times); //곱셈연산자오버로딩friend Point operator*(int times, Point& ref); //교환법칙
};
콘솔 입출력에 사용되는 키워드 cout과 endl을 유사하게 흉내내서 이해를 돕는 예제이다. 주어진 예제를 특성에 맞게 파일별로 분리해서 실습했다.
mystd.h
#pragma once/**
* cout과 endl 흉내내기를 위해 만든 mystd namespace
* namespace는 소속을 정해 이름을 붙여 구분한다
* 이름이 같아 생길 수 있는 충돌을 방지한다
*/namespace mystd
{
usingnamespace std; // using namespace 이름;은 네임스페이스 모든 요소에 이름 없이 접근 가능// 함수 내부에서 선언해서 적용 범위는 함수 스코프 한정// printf 함수 호출을 위해 삽입 classostream
{public:
voidoperator<<(char* str); // 문자열 출력voidoperator<<(char str); // 문자 출력voidoperator<<(int num); // 숫자 출력voidoperator<<(double e); // 소수점 출력voidoperator<<(ostream& (*fp)(ostream& ostm)); // ??
};
}
ostream.cpp
#include<stdio.h>#include"mystd.h"namespace mystd
{
void ostream::operator<<(char* str)
{
printf("%s", str);
}
void ostream::operator<<(char str)
{
printf("%c", str);
}
void ostream::operator<<(int num)
{
printf("%d", num);
}
void ostream::operator<<(double e)
{
printf("%g", e);
}
// ostream& endl(ostream& ostm) 함수를 인자로 전달받음// 추가공부 : 함수포인터void ostream::operator<<(ostream& (*fp)(ostream& ostm))
{
fp(*this);
}
// endl 함수ostream& endl(ostream& ostm){
ostm << '\n';
fflush(stdout); //버퍼 비우기return ostm;
}
ostream cout; //객체의 이름 // 객체 내에서 다양한 기본자료형 데이터를 대상으로 << 연산자 오버로딩을 하고 있다.
}
main.cpp
// ConsoleOutput : cout과 endl 이해#include<iostream>#include"mystd.h"intmain(){
// using 네임스페이스이름::요소 는// 네임스페이스 이름 없이도 사용하게 해줌// 지역적 using 선언using mystd::cout;
using mystd::endl;
cout << "Simple String";
cout << endl;
cout << 3.14;
cout << endl;
cout << 123;
endl(cout);
return0;
}
아래처럼 연이은 <<연산을 진행하려면 <<연산 결과 cout이 반환되어야 한다. 그래서 예제를 수정해본다.
cout<<123<<endl<<3.14<<endl;
IterateConsoleOutput.cpp
// IterateConsoleOutput #include<iostream>namespace mystd
{
usingnamespace std; // using namespace 이름;은 네임스페이스 모든 요소에 이름 없이 접근 가능// 함수 내부에서 선언해서 적용 범위는 함수 스코프 한정// printf 함수 호출을 위해 삽입 classostream
{public:
//void operator<<(const char* str)
ostream& operator<<(char* str) //cout 객체의 참조값을 반환
{
printf("%s", str);
return *this;
}
ostream& operator<<(char str)
{
printf("%c", str);
return *this;
}
ostream& operator<<(int num)
{
printf("%d", num);
return *this;
}
ostream& operator<<(double e)
{
printf("%g", e);
return *this;
}
ostream& operator<<(ostream& (*fp)(ostream& ostm))
{
returnfp(*this);
}
};
// 원래 endl 처럼 개행하고 출력버퍼를 비우는 작업을 해 준다// 반환된 값을 재 반환하는 형태로 오버로딩ostream& endl(ostream& ostm){
ostm << '\n';
fflush(stdout); //버퍼 비우기return ostm;
}
ostream cout; //객체의 이름. 객체 내에서 다양한 기본자료형 데이터를 대상으로 << 연산자 오버로딩을 하고 있다.
}
intmain(void){
// using 네임스페이스이름::요소 는// 네임스페이스 이름 없이도 사용하게 해줌// 지역적 using 선언using mystd::cout;
using mystd::endl;
cout << 3.14 << endl << 123 << endl;
return0;
}
<<, >> 연산자 오버로딩
<<연산자에 이어 >>연산자도 오버로딩 해 본다.
* 구현 전 알아야 할 것
- cout은 ostream 클래스의 객체 - ostream 은 이름공간 std 안에 선언되어 있으며, 헤더파일 <iostream>을 포함해야 한다. - 멤버함수 형태로 오버로딩하면 cout.operator<<(pos)형태로 해석이 가능해야 한다. - 전역함수 형태로 오버로딩하면 operator<<(cout, pos)형태로 해석이 가능해야 한다.
컨트롤 클래스는 프로그램 전체의 기능을 담당하는 기능적 성격이 강한 클래스이다. 컨트롤 클래스만 봐도 프로그램의전체 기능과 흐름을 파악할 수 있다.
엔터티 클래스는 컨트롤 클래스가 아닌 대부분의 클래스다. 엔터티 클래스는 데이터적 성격이 강해 파일 및 데이터 베이스에 저장되는 데이터를 소유한다. 엔터티 클래스는 프로그램 상 관리되는데이터의 종류를 파악하는 데 도움이 된다. 엔터티 클래스는 프로그램의 기능을 파악하는데 도움을 주지는 못한다.
프로그램의 주요기능은 계좌개설, 입금, 출금, 계좌정보 전체 출력이다. 그리고 전역함수로 구현되어 있다. 객체지향 프로그래밍을 위해 여러 기능의 전역함수들을 하나의 컨트롤 클래스로 묶어서 구현한다. 컨트롤 클래스는 프로그램의 기능적 측면을 담당하게 되어 본래 성격에도 부합한다. 구현 방법은...
1. AccountHandler라는 이름의 컨트롤 클래스를 정의하고 앞서 정의한 전역함수들을 이 클래스의 멤버함수로 포함시킨다. 2. Account 객체의 저장을 위해 선언한 배열과 변수도 이 클래스의 멤버에 포함시킨다. 3. AccountHandler 클래스 기반으로 프로그램이 실행되도록 main 함수를 변경한다.
// ReObjUnder2.cpp#include<iostream>usingnamespace std;
typedefstructData
{int data;
// 함수포인터 변수가 구조체의 멤버로 등장// 반환형식 (*식별자) (파마리터형 목록)void (*ShowData)(Data*);
// ShowData 함수의 주소값을 저장하기 위한 것void (*Add)(Data*, int);
// Add 함수의 주소값을 저장하기 위한 것
} Data;
voidShowData(Data* THIS){
cout << "Data: " << THIS->data << endl;
}
voidAdd(Data* THIS, int num){
THIS->data += num;
}
intmain(){
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);
return0;
}
두 개의 구조체 변수(객체)가 함수를 공유하고 있다. 실제로 C++의 객체와 멤버함수는 이런 관계를 갖는다. 객체가 생성되면 멤버변수는 객체 내에 존재하지만, 멤버함수는 메모리의 한 공간에 별도로 위치하고선 이 함수가 정의된 클래스의 모든 객체가 공유한다.
그리고 객체가 지니고 있는 멤버변수 대상의 연산이 진행되도록 함수를 호출한다;
가상함수의 동작원리와 가상함수 테이블
가상함수의 동작원리를 이해하면 C++이 C보다 느린 이유를 유추 가능하다.
한 개 이상의 가상함수를 포함하는 클래스에 대해서 컴파일러는 가상함수 테이블(V-Table, Virtual Table)이라는 것을 만든다. 가상함수 테이블은 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블이다. AAA 객체의 Func1 함수를 호출해야할 경우 테이블을 참조하여 0x1024번지에 등록된 Func1()를 호출한다.
* key는 호출하고자 하는 함수를 구분지어준다 (구분자) * value는 구분자에 해당하는 함수의 주소정보를 알려준다.
오버라이딩 된 가상함수의 주소정보는 유도클래스의 가상함수 테이블에 포함되지 않는다. => 오버라이딩 된 가상함수를 호출하면 무조건 가장 마지막에 오버라이딩한 유도클래스의 멤버함수가 호출
// VirtualInternal.cpp#include<iostream>usingnamespace std;
classAAA
{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()를 호출virtualvoidFunc1(){
cout << "Func1" << endl;
}
virtualvoidFunc2(){
cout << "Func2" << endl;
}
};
classBBB:public AAA
{
private:
int num2;
public:
// [ BBB 클래스의 가상함수 테이블 ]// key value// ------------------------------------------// void BBB::Func1() 0x3072번지// void AAA::Func2() 0x2048번지// void BBB::Func3() 0x4096번지// AAA클래스의 오버라이딩 된 가상함수 Func1에 대한 정보가 없다.virtualvoidFunc1(){
cout << "BBB::Func1" << endl;
}
voidFunc3(){
cout << "Func3" << endl;
}
};
intmain(void){
AAA* aptr = newAAA();
aptr->Func1();
BBB* bptr = newBBB();
bptr->Func1();
return0;
}
가상함수 테이블이 참조되는 방식
메인함수 호출 이전에 다음 구조로 가상함수 테이블이 메모리공간에 할당된다.
가상함수 테이블은 객체의 생성과 상관없이 메모리 공간에 할당된다. *** 가상함수 테이블이 멤버함수의 호출에 사용되는 데이터 중 하나이기 때문에.
메인함수가 호출되어 객체가 생성되고 나면 다음 구조로 참조 관계를 구성한다. AAA 객체에는 AAA 클래스의 가상함수 테이블의 주소값이, BBB 객체에는 BBB 클래스의 가상함수 테이블의 주소값이 저장된다. (내부적으로 참조되는 주소값이지 우리가 어찌할 수는 없다)
AAA 객체를 통해 Func1 함수가 호출되었다고 가정하면 Func1 함수의 위치 확인을 위해서 AAA 클래스의 가상함수 테이블이 참조되고, 0x1024번지에 위치한 함수가 실행된다. BBB객체의 Func1함수가 호출되면 BBB클래스의 가상함수테이블이 참조되고, 테이블의 위치에 0x3072번지로 기록되어 있으므로 그곳에 위치한 BBB 클래스의 Func1 함수가 실행된다.
오버라이딩 된 AAA 클래스의 Func1 함수에 대한 정보가 없으므로 BBB 클래스의 Func1 함수가 대신 호출된다.
클래스에 가상함수가 포함되면 가상함수 테이블이 생성되고, 테이블을 참조하여 호출할 함수가 결정되기 때문에 실행속도가 감소하는 단점이 있지만 그 속도차는 미미하여 장점이 더 많은 가상함수이다.
Account 클래스에 깊은 복사를 진행하는 복사 생성자를 정의해본다. 깊은 복사를 원칙으로 한다면 클래스의 생성자만 봐도 복사 생성자의 필요성을 판단할 수 있다. 실제 복사 생성자의 호출여부는 중요하지 않으며 클래스는 그 자체로 완성품이 되어야 하기 때문에 당장 필요한 것만으로 채우지 않는다.
모든 a클래스 객체가 하나의 add함수를 공유 (코드 영역 안에 있는 add 함수를 공유한다.) 그렇지만 각각 객체의 n은 구분한다. 어떻게?
voidadd(A* a){ (a->n)++; }
이렇게 변경된다. 자기자신의 주소값 this를 인자값으로 전달한다...
9-2 가상 함수가 동작하는 원리
* 예제를 이해하기 위한 사전지식 : 가상함수 정의, 장점
classA
{int a;
int b;
public:
virtualvoidfct1(){
cout<<"fct1(...)"<<endl; // 멤버함수는 코드영역으로 들어가 공유된다
}
virtualvoidfct2(){
// 클래스의 멤버함수 중 하나라도 virtual로 선언된 멤버함수가 있으면// 그 클래스의 멤버함수 정보를 지니는 a 클래스 객체의 virtual 테이블이 생성된다.// a 클래스 객체의 함수를 호출할 때마다 virtual 테이블을 참조하게 된다// 위치에 대한 정보를 갖고 있다
cout<<"fct2(...)"<<endl;
}
};
classB :public A
{
int c;
int d;
public:
virtualvoidfct1(){ // 오버라이딩 + 재정의
cout<<"overriding fct1(...)"<<endl;
}
voidfct3(){
cout<<"fct3(...)"<<endl;
}
};
클래스의 멤버함수 중 하나라도 virtual로 선언된 멤버함수가 있으면 그 클래스의 멤버함수 정보를 지니는 a 클래스 객체의 virtual 테이블이 생성된다. a 클래스 객체의 함수를 호출할 때마다 virtual 테이블을 참조하게 된다 위치에 대한 정보를 갖고 있다
intmain(){
A a;
a.fct();
}
호출하러 들어오면 테이블의 키값을 찾는다 직접가는 게 아니라 거쳐가게 된다
virtual 테이블에 의해 전에 배웠던 기능이 구현되었던 것!
//은행 계좌 관리 프로그램 3#define _CRT_SECURE_NO_WARNINGS#include<iostream>#include<cstring>usingnamespace std;
classA
{int a;
int b;
public:
virtualvoidfct1(){
cout << "fct1(...)" << endl; // 멤버함수는 코드영역으로 들어가 공유된다
}
virtualvoidfct2(){
// 클래스의 멤버함수 중 하나라도 virtual로 선언된 멤버함수가 있으면// 그 클래스의 멤버함수 정보를 지니는 a 클래스 객체의 virtual 테이블이 생성된다.// a 클래스 객체의 함수를 호출할 때마다 virtual 테이블을 참조하게 된다// 위치에 대한 정보를 갖고 있다
cout << "fct2(...)" << endl;
}
};
classB :public A
{
int c;
int d;
public:
virtualvoidfct1(){ // 오버라이딩 + 재정의
cout << "overriding fct1(...)" << endl;
}
voidfct3(){
cout << "fct3(...)" << endl;
}
};
// 오버라이딩 된 virtual은 테이블에 정보가 없어서 호출이 되지 않는다// intmain(void){
A* aaa = newA();
aaa->fct1();
B* bbb = newB();
bbb->fct1();
return0;
}
생성자는 부모 클래스의 생성자가 먼저 불려지고, 소멸자는 자식 클래스의 소멸자가 먼저 불려지고 나서 부모 클래스의 소멸자가 불려진다.
다형성을 위해 부모 클래스의 포인터로부터 자식 클래스를 호출할 때 가상 함수로 정의되지 않은 자식 클래스의 오버라이딩된 함수를 호출하면 부모 클래스의 멤버 함수가 호출된다 소멸자도 자식클래스에서 오버라이딩된 함수의 일종이라 부모포인터로 객체를 삭제하면 부모클래스 소멸자가 호출된다
소멸자가 가상함수로 안 선언되어 있으면 자식클래스의 소멸자는 호출되지 않고 가상함수 키워드 virtual이 사용되었으면 자식 클래스에서 재정의될 수 있음을 뜻하여 포인터 종류에 상관없이 항상 자식 클래스의 메서드가 호출된다. => 자식클래스의 소멸자가 호출되고나서 부모클래스의 소멸자가 호출된다.
상속관계가 있는 클래스고 소멸자에서 리소스를 해제해야 하는 경우, 반드시 부모 클래스 안의 소멸자를 가상함수로 선언해야 한다.
클래스의 virtual 소멸자 예제
소멸자 앞에 virtual이 붙어있어 동적바인딩이 된다. 자바는 기본이 동적바인딩이만, C++은 기본이 정적바인딩이여서 virtual 키워드를 사용해야 동적바인딩을 구현할 수 있다. virtual이 붙는 함수가 곧 가상함수이다.