학습 개요
- 상속을 활용하여 기초 클래스와 파생 클래스를 선언함으로써 클래스 계층 구조를 만들었을 경우, 기초 클래스의 포인터는 기초 클래스의 객체뿐만 아니라 해당 클래스 계층 구조에 속하는 파생 클래스들의 객체를 가리키게 할 수 있음
- 이 때 기초 클래스 포인터가 가리키고 있는 객체가 정확히 어느 클래스의 객체인가에 따라 그 객체에 맞는 동작이 이루어지게 하는 것이 필요함
- 이러한 클래스 계층 구조에 따른 포인터 활용 및 멤버 함수의 자동 선택이 이루어지게 하는 방법에 대하여 학습함
학습 목표
- 클래스의 포인터로 객체를 가리켜서 포인터를 통해 객체를 사용할 수 있음
- 동적 연결을 사용하여 포인터가 가리키는 객체에 맞는 멤버 함수가 자동적으로 선택되어 동작하게 할 수 있음
dynamic_cast
연산자를 이용하여 안전하게 다운 캐스트를 할 수 있음
주요 용어
- 정적 연결(static binding)
- 이름과 그 이름에 해당하는 대상의 연결이 프로그램의 실행이 시작되기 전에 이루어지게 하는 것
- 동적 연결(dynamic binding)
- 이름과 그 이름에 해당하는 대상의 연결이 프로그램의 실행되는 동안 결정되게 하는 것
- 가상 함수(virtual function)
- 클래스 계층 구조에서 함수를 재 정의 할 때 동적 연결 방식으로 함수가 실행되도록 선언된 함수
- 업 캐스팅(upcasting)
- 파생 클래스 포인터를 기초 클래스 포인터로 변환하는 것
- 다운 캐스팅(downcasting)
- 기초 클래스 포인터를 파생 클래스 포인터로 변환하는 것
강의록
상속과 포인터
클래스 계층 구조와 포인터
포인터(참조)로 가리킬 수 있는 대상
1 2 3 4 5 6 7 8 9 10 11 12 13
int main() { Person *pPrsn1, *pPrsn2; Person dudley("Dudley"); Student *pStdnt1, *pStdnt2; Student harry("Harry", "Hogwarts"); pPrsn1 = &dudley; // 부모 포인터에 부모 객채 OK pStdnt1 = &harry; // 자식 포인터에 자식 객체 OK pPrsn2 = &harry; // 부모 포인터에 자식 객체(업캐스팅) OK pStdnt2 = &dudley; // 자식 포인터에 부모 객체(다운캐스팅) Error! return 0; }
예제: 객체 포인터 배열
- 객체 포인터의 배열
- Person 및 Student 클래스의 객체를 가리키는 포인터를 저장하는 배열을 선언하여 객체를 가리키게 하고, 배열에 저장된 객체들을 출력하는 함수를 통해 출력하는 프로그램을 작성하라.
예제: 객체 포인터 배열 - Person.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef PERSON_H_INCLUDED
#define PERSON_H_INCLUDED
#include <iostream>
#include <string>
using namespace std;
class Person {
string name;
public:
Person(const string& n) : name(n) { }
string getName() const { return name; }
void print() const { cout << name; }
};
#endif // PERSON_H_INCLUDED
예제: 객체 포인터 배열 - Student.h
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "Person.h"
class Student : public Person {
string school;
public:
Student(const string& n, const string& s) :
Person(n), school(s) { }
string getSchool() const { return school; }
void print() const { // print() 함수 오버라이딩
Person::print(); // 부모 클래스의 print() 함수를 명시적으로 호출
cout << " goes to " << school;
}
};
예제: 객체 포인터 배열 - PArrMain.cpp
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
#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;
void PrintPerson(const Person * const p[], int n)
{
for (int i=0; i < n; i++) {
p[i]->print();
cout << endl;
}
}
int main()
{
Person dudley("Dudley");
Student harry("Harry", "Hogwarts");
Student ron("Ron", "Hogwarts");
dudley.print();
cout << endl;
harry.print();
cout << endl << endl;
Person *pPerson[3];
pPerson[0] = &dudley;
pPerson[1] = &harry;
pPerson[2] = &ron;
PrintPerson(pPerson, 3);
return 0;
}
// Dudley
// Harry goes to Hogwarts
// Dudley
// Harry
// Ron
가상 함수
객체 포인터와 재정의 된 멤버 함수 호출
정적 연결 - Sbinding.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include "Person.h"
#include "Student.h"
using namespace std;
int main()
{
Person *p1 = new Person("Dudley");
p1->print(); // Person::print() 호출
cout << endl;
Person *p2 = new Student("Harry", "Hogwarts");
p2->print(); // Person::print() 호출
cout << endl;
((Student *)p2)->print(); // Student::print() 호출 -> p2가 Student 객체를 가리키고 있으리라는 보장이 없음
cout << endl;
return 0;
}
// Dudley
// Harry
// Harry goes to Hogwarts
동적 연결
- 동적 연결(dynamic binding)
- 객체 포인터를 통해 객체의 멤버 함수를 호출할 경우 포인터가 가리키는 실제 객체가 무엇인 가에 따라 실행 중에 멤버 함수를 결정하는 것
- C++에서는 가상 함수(virtual function)로 동적 연결을 구현함
- 기초 클래스에서 가상 함수로 선언한 멤버 함수를 재 정의한 파생 클래스의 함수는 역시 가상 함수이며, 동적 연결이 적용됨
정적 연결로 컴파일하여 동작하는 객체
1 2 3 4 5
class Person { void print() const { cout << name; } };
1 2 3 4 5 6 7
class Student : public Person { void print() const { Person::print(); cout << " goes to " << school; } };
동적 연결로 동작하는 객체
1 2 3 4 5
class Person { virtual void print() const { cout << name; } };
1 2 3 4 5 6 7
class Student : public Person { void print() const { Person::print(); cout << " goes to " << school; } };
예: 동적 연결의 활용 - PArrMain.cpp
1
2
3
4
5
6
7
void PrintPerson(const Person* const p[], int n)
{
for (int i=0; i < n; i++) {
p[i]->print();
cout << endl;
}
}
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
int main()
{
Person dudley("Dudley");
Student harry("Harry", "Hogwarts");
Student ron("Ron", "Hogwarts");
dudley.print();
cout << endl;
harry.print();
cout << endl;
Person *pPerson[3];
pPerson[0] = &dudley;
pPerson[1] = &harry;
pPerson[2] = &ron;
PrintPerson(pPerson, 3);
return 0;
}
// Dudley
// Harry goes to Hogwarts
// Dudley
// Harry goes to Hogwarts
// Ron goes to Hogwarts
소멸자의 동적 연결
- 소멸자를 가상 함수로 선언하지 않은 경우
- 기초 클래스의 포인터에 연결된 파생 클래스 객체를 제거할 때 기초 클래스의 소멸자만 동작함
- 파생 클래스의 소멸자가 동작하지 않아 필요한 작업이 누락 됨
1 2 3 4 5 6 7 8
class BaseClass { int *ptB; public: BaseClass(int n) { ptB = new int[n]; } ~BaseClass() { delete [] ptB; } };
1 2 3 4 5 6 7
class DrvClass : public BaseClass { int *ptD; public: DrvClass(int n1, int n2) : BaseClass(n1) { ptD = new int[n2]; } ~DrvClass() { delete [] ptD; } };
1 2 3 4 5
BaseClass *pB1 = new BaseClass(5); BaseClass *pB2 = new DrvClass(10, 15); delete pB1; // 기초 클래스의 소멸자 동작 delete pB2; // 기초 클래스의 소멸자만 동작 (메모리 누수 발생)
- 기초 클래스의 포인터에 연결된 파생 클래스 객체를 제거할 때 기초 클래스의 소멸자만 동작함
- 소멸자를 가상 함수로 선언한 경우
- 기초 클래스의 포인터에 연결 된 파생 클래스 객체를 제거할 때 파생 클래스의 소멸자가 동작할 수 있게 함
1 2 3 4 5 6 7 8 9
class BaseClass { int *ptB; public: BaseClass(int n) { ptB = new int[n]; } virtual ~BaseClass() { delete [] ptB; } // virtual 선언 };
1 2 3 4 5 6 7
class DrvClass : public BaseClass { int *pD; public: DrvClass(int n1, int n2) : BaseClass(n1) { pD = new int[n2]; } ~DrvClass() { delete [] pD; } };
1 2 3 4 5
BaseClass *pB1 = new BaseClass(5); BaseClass *pB2 = new DrvClass(10, 15); delete pB1; // 기초 클래스의 소멸자 동작 delete pB2; // 파생 클래스와 기초 클래스의 소멸자가 모두 동작
업 캐스팅과 다운 캐스팅
- 업 캐스팅(upcasting)
- 파생 클래스 포인터를 기초 클래스 포인터로 변환하는 것
- 묵시적 형 변환을 통해 업 캐스팅을 할 수 있음
- 다운 캐스팅(downcasting)
- 기초 클래스 포인터를 파생 클래스 포인터로 변환하는 것
- 묵시적 형 변환을 할 수 없으며, 형 변환 연산자로 명시적 형 변환을 해야 함
1 2 3 4 5 6
Person *pPrsn1 = new Person("Dudley"); Student *pStdnt1 = new Student("Harry", "Hogwarts"); Person *pPrsn2 = pStdnt1; // upcasting Student *pStdnt2 = pPrsn2; // downcasting - Error(묵시적 형 변환 불가)
static_cast
연산자를 사용한 다운 캐스팅Person 클래스의 포인터 pPrsn2가 Student 클래스의 객체를 가리키고 있었기 때문에 정상적인 동작을 함
1 2 3 4 5
Person *pPrsn1 = new Person("Dudley"); Student *pStdnt1 = new Student("Harry", "Hogwarts"); Person *pPrsn2 = pStdnt1; // upcasting Student *pStdnt2 = static_cast<Student*>(pPrsn2); cout << pStdnt2->getSchool() << endl;
pPrsn2가 Student 클래스의 객체를 가리키고 있지 않다면 부적절한 변환을 하게 되는 문제가 있음
1 2 3 4 5
Person *pPrsn1 = new Person("Dudley"); Student *pStdnt1 = new Student("Harry", "Hogwarts"); Person *pPrsn2 = pPrsn1; Student *pStdnt2 = static_cast<Student*>(pPrsn2); cout << pStdnt2->getSchool() << endl; // 부적절한 호출
static_cast
연산자를 사용하여 다운 캐스팅을 하는 것은 부적절한 변환의 위험이 있음
dynamic_cast
연산자를 사용한 다운 캐스팅dynamic_cast
는 pPrsn1이 가리키는 대상이 Student 객체가 아니므로nullptr
을 반환함1 2 3 4 5 6
Person *pPrsn1 = new Person("Dudley"); Student *pStdnt1 = new Student("Harry", "Hogwarts"); Person *pPrsn2 = pPrsn1; // upcasting Student *pStdnt2 = dynamic_cast<Student*>(pPrsn1); // nullptr if (pStdnt2) cout << pStdnt2->getSchool() << endl;
pPrsn2가 Student 객체를 가리키고 있으므로
dynamic_cast
로 정상적인 다운 캐스팅이 됨1 2 3 4 5 6
Person *pPrsn1 = new Person("Dudley"); Student *pStdnt1 = new Student("Harry", "Hogwarts"); Person *pPrsn2 = pStdnt1; // upcasting Student *pStdnt2 = dynamic_cast<Student*>(pPrsn2); if (pStdnt2) cout << pStdnt2->getSchool() << endl;
dynamic_cast
를 사용하려면 클래스 선언문에 가상 함수를 포함하고 있어야 함
심화 학습
override
- 가상 함수의 재정의
virtual
은 기초 클래스의 가상 함수를 재 정의할 때는 지정할 필요가 없으며, 파생 클래스에서 새롭게 가상 함수를 선언하고자 할 때만 사용하는 것이 좋음- 파생 클래스에서 어떤 함수가 가상 함수인지 명확히 알기 어려울 수 있으므로, 가상 함수를 재 정의함을 명시적으로 알리고자 할 때에는
override
를 지정함
1 2 3 4 5 6 7 8 9 10 11 12 13
class A { virtual void f() { } }; class B : public A { void f() { } virtual void g() { } };
1 2 3 4 5 6 7 8 9 10 11 12 13
class A { virtual void f() { } }; class B : public A { void f() override { } virtual void g() { } };
final
- 가상 함수의 재정의 금지
- 가상 함수를 더 이상 재 정의 하지 못하게 하려면
final
을 지정함
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class A { virtual void f() { } }; class B : public A { void f() override final { // final 지정으로 재정의 금지 } }; class C : public B { void f() override { // error! } };
- 가상 함수를 더 이상 재 정의 하지 못하게 하려면
연습 문제
DrvClass가 BaseClass의 파생 클래스이고, 이 클래스들의 객체와 포인터가 위 지문과 같이 정의 되었을 때 사용할 수 없는 문장을 모두 나열한 것은?
1 2 3 4 5 6 7 8
BaseClass baseObj; DrvClass drvObj; BaseClass* basePt; DrvClass* drvPt; basePt = &baseObj; // ㈀ basePt = &drvObj; // ㈁ drvPt = &baseObj; // ㈂ drvPt = &drvObj; // ㈃
a.
drvPt = &baseObj;
- 기초 클래스 포인터는 파생 클래스 객체를 가리킬 수 있으나, 파생 클래스 포인터는 기초 클래스 객체를 가리킬 수 없음
클래스 B과 D에 대해 위 지문 문장의 실행 결과 출력 되는 결과는?
1 2 3 4 5 6 7
class B { public: virtual void f() { cout << "B "; } };
1 2 3 4 5 6 7
class D : public B { public: void f() { cout << "D "; } };
1 2 3 4
D *pD = new D; B *pB = pD; pB->f(); pD->f();
a. D D
f()
를 가상 함수로 선언하였으므로 포인터에 연결된 객체가 무엇인가에 따라 함수가 선택되어 실행 됨
DrvClass가 BaseClass의 파생 클래스이고, drvPt와 basePt가 각각 DrvClass와 BaseClass의 포인터라고 하자. 위 지문의 문장을 실행하였을 때 만일 basePt가 BaseClass의 객체를 가리키고 있다면
nullptr
이 drvPt에 저장되도록 하려면 ㈀에 무엇을 넣어야 하는가?1
drvPt = // ____㈀____(basePt);
a.
dynamic_cast<DrvClass*>
dynamic_cast
는 포인터를 다운 캐스팅 할 때 만일 형 변환이 안전하게 일어날 수 없을 때는nullptr
를 반환함으로써 포인터를 오용 할 가능성을 예방할 수 있게 함
위 지문은 클래스 A의 소멸자를 선언하는 문장의 일부이다. 클래스 B가 A의 파생 클래스이고, A의 포인터 pA가 동적 할당된 B의 객체를 가리키고 있다.
delete
연산자로 pA가 가리키고 있는 객체를 반납할 때 클래스 B의 소멸자가 동작하게 하려면 공란에 어떤 단어를 넣어야 하는가?1 2 3 4 5 6 7
class A { // ___________~A() { } };
a.
virtual
- 소멸자를 가상 함수로 선언함으로써 포인터에 연결된 객체에 맞는 소멸자가 동작하도록 함
정리 하기
- 클래스 계층 구조에서 기초 클래스의 포인터는 해당 클래스의 객체 뿐 아니라 파생 클래스의 객체도 가리킬 수 있음
- 그러나 파생 클래스의 포인터로 기초 클래스의 객체를 가리키게 하면 안 됨
- 파생 클래스에서 재 정의하는 함수를 기초 클래스에서 가상 함수로 선언하면 기초 클래스의 포인터에 연결된 객체에 따라 해당 함수를 선택하여 동작하게 하는 동적 연결을 할 수 있음
- 클래스의 계층 구조에서 클래스의 소멸자는 가상 함수로 선언하여 동적 연결에 따라 소멸자가 동작할 수 있게 함
- 포인터의 업 캐스팅은 묵시적 형 변환을 할 수 있으나, 다운 캐스팅은 명시적으로 형 변환을 지정해야 함
- 가상 함수를 포함하고 있는 클래스의 경우
dynamic_cast
연산자를 이용하여 더 안전하게 다운 캐스팅을 할 수 있음