PART 2. 객체 지향 프로그래밍
- chapter 9. 다형성
1. 다형성(Polymorphism)이란?
- 다형성은 객체들의 타입이 다르면 똑같은 메시지가 전달되더라도 서로 다른 동작을 하는 것을 말한다.
- 메시지를 보내는 측에서는 객체가 어떤 타입인지 알 필요가 없어야 한다.
- 다형성은 객체 포인터나 객체 참조자를 통하여 이루어진다.
- 객체 포인터도 기본적으로 타입이 맞는 객체만을 가리킬 수 있지만, 부모 클래스 포인터는 자식 클래스 객체를 가리킬 수 있다.
- 이를 상향 형변환이라고 한다.
- 상향 형변환을 하면 상속 받은 부분만을 사용할 수 있고 나머지는 사용할 수 없다. 즉 참조할 수 있는 영역의 크기가 축소된다.
- 반대로 부모 클래스를 가리키는 포인터가 자식 클래스를 가리키도록 형변환하는 것을 하향 형변환이라고 한다.
- 하향 형변환은 부모 클래스가 가리키는 객체가 자식 클래스일 때만 사용해야한다. 그렇지 않으면 없는 멤버를 참조하게 될 수 있다.
- 함수의 매개변수도 부모클래스의 포인터나 참조로 작성하는 것이 유리하다.
2. 가상함수(Virtual Funtion)
- 부모 클래스의 포인터로 자식 클래스의 객체를 가리키고 상속된 함수를 호출하면 기본적으로는 부모 클래스의 함수가 호출된다.
- 이때 실제로 가리키는 객체에 따라 다른 함수가 호출되어야 다형성이 있다고 할 수 있다.
- 가상함수를 사용하면 포인터가 가리키는 실제 객체에 따라 서로 다른 멤버 함수가 자동적으로 선택된다.
- 상속된 가상 함수를 자식 클래스에서 재정의할 때는 virtual키워드를 붙이지 않아도 가상 함수로 상속된다.
- 함수 호출문과 함수 몸체를 연결하는 것을 바인딩이라고 한다.
- 일반 함수는 컴파일 단계에서 바인딩이 모두 완료되며 이를 정적 바인딩(static binding)이라 한다.
- 가상 함수는 실행 중에 함수 호출문과 함수 몸체가 연결되며 이를 동적 바인딩(dynamic binding) 또는 지연 바인딩(late binding)이라 한다.
3. 가상 소멸자
- 부모 클래스의 포인터로 자식 클래스의 객체를 가리키고 이를 삭제하면 부모 클래스의 소멸자만 호출되는 문제가 있다.
- 이를 방지하기 위해서는 부모 클래스의 소멸자를 가상 함수로 선언해줘야 한다.
- 가상 소멸자를 사용하면 위의 포인터를 삭제할 때 정상적으로 자식 클래스의 소멸자가 호출된 뒤 부모 클래스의 소멸자가 호출되게 된다.
4. 순수 가상 함수(pure virtual funtion)
- 순수 가상 함수는 헤더만 존재하고 몸체는 없는 함수를 말한다.
- 문법 : virtual 반환형 함수이름(매개변수리스트) = 0;
- 순수 가상 함수를 하나라도 가지고 있는 클래스를 추상 클래스(abstract class)라고 한다.
- 추상 클래스로는 객체를 생성할 수 없다.
- 추상 클래스를 상속받은 자식 클래스는 반드시 순수 가상 함수를 구현해야 한다.
Header.h
#pragma once #include <iostream> #include <string> using std::cout; using std::cin; using std::endl; using std::string;
Animal.h
#pragma once #include "Header.h" class Animal { protected: int speed; public: virtual void speak() = 0; virtual int walk(int here) = 0; virtual void setSpeed(int s) = 0; virtual int getSpeed() = 0; virtual void printSpeed() = 0; virtual void printName() = 0; public: //virtual Animal(); //생성자는 재정의될 수 없으니 가상함수로 만들수도 없다. Animal(); virtual ~Animal(); };
Animal.cpp
#include "Animal.h" //어차피 오버라이딩 해야하는 함수이므로 이렇게 안에 내용이 있어도 의미가 없다. //순수 가상으로 정의하고 내용을 만들면? void Animal::setSpeed(int s) { speed = s; } Animal::Animal() { speed = 0; } Animal::~Animal() { }
Dog.h
#pragma once #include "Animal.h" class Dog : public Animal { private: string name; public: //오버라이딩 함수 //vitual속성은 상속되므로 재정의한 가상 함수는 vitual키워드가 안 붙어있어도 가상함수이다. void speak(); int walk(int here); void setSpeed(int s); int getSpeed(); void printSpeed(); void printName(); //추가한 함수 virtual void bite(); public: Dog(); virtual ~Dog(); };
Dog.cpp
#include "Dog.h" //클래스 외부에서는 virtual키워드를 쓸 수 없다. void Dog::speak() { cout << "멍멍" << endl; } int Dog::walk(int here) { return here + speed; } void Dog::setSpeed(int s) { speed = s; } int Dog::getSpeed() { return speed; } void Dog::printSpeed() { cout << speed << endl; } void Dog::printName() { cout << name << endl; } void Dog::bite() { cout << "개는 물수 있다." << endl; } Dog::Dog() { name = "개!"; speed = 3; } Dog::~Dog() { }
Cat.h
#pragma once #include "Animal.h" class Cat : public Animal { private: string name; public: //오버라이딩 함수 //vitual속성은 상속되므로 재정의한 가상 함수는 vitual키워드가 안 붙어있어도 가상함수이다. void speak(); int walk(int here); void setSpeed(int s); int getSpeed(); void printSpeed(); void printName(); //추가한 함수 virtual void claw(); public: Cat(); virtual ~Cat(); };
Cat.cpp
#include "Cat.h" void Cat::speak() { cout << "야옹" << endl; } int Cat::walk(int here) { return here + speed; } void Cat::setSpeed(int s) { speed = s; } int Cat::getSpeed() { return speed; } void Cat::printSpeed() { cout << speed << endl; } void Cat::printName() { cout << name << endl; } void Cat::claw() { cout << "고양이는 할퀼 수 있다." << endl; } Cat::Cat() { name = "고양이!"; speed = 4; } Cat::~Cat() { }
JumpingCat.h
#pragma once #include "Cat.h" class JumpingCat : public Cat { private: string name; public: //오버라이딩 함수 //부모에 vitual이 안 붙어있어도 부모의 부모에 붙은 virtual이 상속된다. void speak(); int walk(int here); void printName(); //추가한 함수 void jump(); public: JumpingCat(); virtual ~JumpingCat(); };
JumpingCat.cpp
#include "JumpingCat.h" //오버라이딩 함수 void JumpingCat::speak() { cout << "grrrrrrr" << endl; } int JumpingCat::walk(int here) { //부모의 멤버 변수를 그대로 쓸 수 있다. return here + speed; } void JumpingCat::printName() { cout << name << endl; } //추가한 함수 void JumpingCat::jump() { cout << "JumpingCat은 점프도 할 수 있다!" << endl; } JumpingCat::JumpingCat() { //멤버 변수를 오버라이딩 하면 자식 클래스의 멤버 변수가 부모 클래스의 멤버 변수의 이름을 완전히 가리게된다. name = "점핑 캣!"; //부모의 멤버 변수를 새롭게 초기화해서 그대로 쓸 수 있다. speed = 5; } JumpingCat::~JumpingCat() { }
main.cpp
#include "Dog.h" #include "JumpingCat.h" //매개변수를 부모클래스의 참조나 포인터로 받으면 자식 클래스를 다 받을 수 있다. void temp(Animal& ani) { ani.printSpeed(); } void temp1(Animal* ani) { ani->printSpeed(); } //추상클래스를 매개변수로 사용할 수 없다. //void temp2(Animal ani) //{ // ani.printSpeed(); //} int main() { //추상 클래스로는 객체를 만들 수 없다. //Animal ani; Animal* ani[3]; ani[0] = new Dog; ani[1] = new Cat; ani[2] = new JumpingCat; //부모 클래스의 포인터로 자식 클래스를 가리켜서 사용하기 for (int iCnt = 0; iCnt < 3; iCnt++) { ani[iCnt]->printName(); ani[iCnt]->speak(); cout << ani[iCnt]->walk(10) << endl; //참조를 받는 함수에 포인터를 쓸 수 없다. //temp(ani[iCnt]); temp1(ani[iCnt]); } //부모 클래스의 포인터로 자식 클래스의 멤버 함수를 호출할 수는 없다. //ani[0]->bite(); //ani[1]->claw(); //하향 형변환을 하면 가능하다. Dog* ani_0 = dynamic_cast<Dog*> (ani[0]); ani_0->bite(); Cat c; temp(c); JumpingCat jc; temp1(&jc); }
가상 소멸자가 필요한 이유
Header.h
#pragma once #include <iostream> #include <string> using std::cout; using std::cin; using std::endl; using std::string;
DynamicParents.h
#pragma once #include "Header.h" class DynamicParents { protected: char* s; public: virtual void printString(); public: DynamicParents(const char* p); virtual ~DynamicParents(); }; class DynamicParents_noVirtual { protected: char* s; public: virtual void printString(); public: DynamicParents_noVirtual(const char* p); ~DynamicParents_noVirtual(); };
DynamicParents.cpp
#include "DynamicParents.h" void DynamicParents::printString() { cout << s << endl; } DynamicParents::DynamicParents(const char* p) { cout << " Parents 생성자" << endl; s = new char[strlen(p) + 1]; strcpy_s(s, strlen(p) + 1, p); } DynamicParents::~DynamicParents() { cout << " Parents 소멸자" << endl; delete[] s; } void DynamicParents_noVirtual::printString() { cout << " " << s << endl; } DynamicParents_noVirtual::DynamicParents_noVirtual(const char* p) { cout << " Parents 생성자" << endl; s = new char[strlen(p) + 1]; strcpy_s(s, strlen(p) + 1, p); } DynamicParents_noVirtual::~DynamicParents_noVirtual() { cout << " Parents 소멸자" << endl; delete[] s; }
DynamicChild.h
#pragma once #include "DynamicParents.h" class DynamicChild : public DynamicParents { private: char* tag; public: void printString(); public: DynamicChild(const char* t, const char* p); virtual ~DynamicChild(); }; class DynamicChild_noVirtual : public DynamicParents_noVirtual { private: char* tag; public: void printString(); public: DynamicChild_noVirtual(const char* t, const char* p); virtual ~DynamicChild_noVirtual(); };
DynamicChild.cpp
#include "DynamicChild.h" void DynamicChild::printString() { cout << " " << tag << s << tag << endl; } DynamicChild::DynamicChild(const char* t, const char* p) : DynamicParents(p) { cout << " child 생성자" << endl; tag = new char[strlen(t) + 1]; strcpy_s(tag,strlen(t)+1,t); } DynamicChild::~DynamicChild() { cout << " child 소멸자" << endl; delete[] tag; } void DynamicChild_noVirtual::printString() { cout << " " << tag << s << tag << endl; } DynamicChild_noVirtual::DynamicChild_noVirtual(const char* t, const char* p) : DynamicParents_noVirtual(p) { cout << " child 생성자" << endl; tag = new char[strlen(t) + 1]; strcpy_s(tag, strlen(t) + 1, t); } DynamicChild_noVirtual::~DynamicChild_noVirtual() { cout << " child 소멸자" << endl; delete[] tag; }
main.cpp
#include "DynamicChild.h" int main() { { cout << "가상 소멸자가 없으면" << endl; DynamicParents_noVirtual* x = new DynamicChild_noVirtual("--", "메모리 누수 있음"); x->printString(); delete x; cout << "자식 소멸자가 호출되지 않음" << endl; } cout << endl; { cout << "가상 소멸자가 있으면" << endl; DynamicParents* o = new DynamicChild("--", "메모리 누수 없음"); o->printString(); delete o; cout << "부모 생성자, 자식 생성자, 자식 소멸자, 부모 소멸자 다 호출 됨" << endl; } }