다형성: 다형성은 객체 지향 프로그래밍에서 객체들이 다양한 타입을 가질 수 있음을 나타내는 개념입니다. 이는 동일한 인터페이스를 공유하는 여러 객체를 사용하여 유연한 코드 작성과 코드 재사용성을 촉진합니다.
다형성은 늦은 묶기(동적묶기:dynamic binding)라고 부르는 특징을 사용하여 구현된다. 어느 함수를 호출할지 컴파일 시간에 이루어지는 것이 아니라 프로그램이 실제로 실행되는 실행 시간에 결정되기 때문이다.
LSP의 핵심 : 공개 상속은 바탕 클래스 멤버를 (파생 클래스 안에서) 재사용하는 데 있지 않고 파생 클래스의 멤버가 재사용되는 데 있다. (바탕 클래스 멤버를 재구현한 파생 클래스 멤버를 바탕 클래스가 다형적으로 사용하는 데 있다).
다형성으로 바탕 클래스의 멤버를 재구현할 수 있고 그렇게 재구현된 멤버들을 바탕 클래스의 참조나 포인터를 기대하는 코드에서 사용할 수 있다. 다형성을 사용하면 바탕 클래스의 적절한 멤버들을 재구현한 파생 클래스가 기존의 코드를 재사용할 수도 있다.
메서드나 소멸자를 virtual
로 지정하면 모든 파생 클래스에도 virtual
상태를 유지한다.
클래스 멤버가 가상으로 선언되어 있으면 이 멤버에 override
표시가 있건 없건 상관없이 모든 파생 클래스에서 가상 멤버가 된다. 그러나 키워드는 꼭 사용해야 한다. 그래야 컴파일러가 파생 클래스 인터페이스를 써 내려갈 때 식자 오류를 잡아낼 수 있기 때문
재사용이 가능한 인터페이스를 바탕 클래스의 인터페이스에 추가하면 재사용성이 개선된다. 재사용이 가능한 인터페이스로 파생 클래스는 사용자 인터페이스에 영향을 미치지 않고 자신만의 구현을 채워 넣을 수 있다. 동시에 사용자 인터페이스는 바탕 클래스의 기본 구현은 물론이고 파생 클래스의 의도에 맞게 행위할 것이다.
늦은 묶기(다형성)의 효과를 보여준다.
void showInfo(Vehicle &vehicle)
{
cout << "Info: " << vehicle << '\n';
}
int main()
{
Car car(1200); // 무게가 1200인 자동차
Truck truck(6000, 115, // 캐빈 무게가 6000인 트럭,
"Scania", 15000); // 속도는 115, 제조사는 Scania,
// 무게가 15000인 트레일러
showInfo(car); // 아래 (1) 참고
showInfo(truck); // 아래 (2) 참고
Vehicle *vp = &truck;
cout << vp->speed() << '\n';// 에러 발생, 아래 (3) 참고
}
Car
와 Truck
클래스는 Vehicle
클래스를 상속한 파생 클래스
vp
라는 Vehicle
포인터를 생성하고, 이 포인터를 truck
객체를 가리키도록 초기화합니다. 이것은 다형성의 중요한 예시입니다. vp
가 truck
객체를 가리키고 있지만, vp
의 타입은 Vehicle
vp
는 Vehicle
의 포인터이므로 speed()
함수가 어떤 클래스에서 호출될지를 결정할 때 "늦은 바인딩"이 사용됩니다. 따라서 truck
객체가 실제로 호출되는 함수를 결정하게 됩니다. 그러나 speed()
함수가 Vehicle
클래스에서 가상 함수로 정의되지 않았기 때문에 컴파일러에서 에러가 발생합니다.
mass가 virtual 로 선언되어 있고 늦은 묶기가 사용된다.
Car
의 무게를 보여준다.Truck
의 무게를 보여준다.speed
멤버는 Vehicle
클래스의 멤버가 아니다. 그래서 Vehicle*
포인터를 통하여 호출할 수 없다.예제를 보면 클래스를 가리키는 포인터가 사용될 때 그 클래스의 멤버만 호출할 수 있다
멤버의 가상적 특징은 (이른 vs. 늦은) 묶기 유형에만 영향을 미친다. 포인터를 통하여 보이는 멤버 함수 집합에는 영향을 미치지 않는다.
가상 멤버를 통하여 파생 클래스는 바탕 클래스 멤버로부터 또는 바탕 클래스를 가리키는 포인터나 참조로부터 호출된 함수가 수행하는 행위를 재정의할 수 있다. 이렇게 바탕 클래스의 멤버를 파생 클래스가 재정의하는 것을 멤버를 재정의한다라고 부른다.
소멸자를 virtual로 선언하지 않으면 객체가 소멸할 때 메모리가 해제되지 않는 경우가 발생할 수 있음
일반적으로 C++에서 기본 클래스와 파생 클래스 간에 가상 함수를 사용하면 런타임에 객체의 실제 타입에 따라 올바른 함수가 호출
그러나 소멸자의 경우에도 마찬가지로 가상 소멸자를 사용하면 동일한 원리가 적용
Vehicle *vp = new Land(1000, 120);
delete vp; // 객체 소멸
delete vp
는 ~Vehicle
을 호출
Land
가 메모리를 할당하고 있다면 메모리 누수
이 문제를 가상 소멸자로 해결할 수 있다
소멸자를 virtual
로 선언하면
class Vehicle {
public:
virtual ~Vehicle() {
// 기본 클래스 소멸자
}
};
class Land : public Vehicle {
public:
~Land() {
// 파생 클래스 소멸자
}
};
int main() {
Vehicle* vp = new Land();
delete vp; // Land의 소멸자 호출
return 0;
}
위의 코드에서 vp
는 Vehicle
클래스 포인터이지만, 실제로 Land
클래스의 객체를 가리킵니다. 가상 소멸자를 사용하면 delete vp;
를 호출할 때 Land
클래스의 소멸자가 호출되어 객체의 리소스가 올바르게 정리됩니다. ~Land
는 먼저 자신의 서술문을 실행한 다음에 ~Vehicle
을 호출한다. 그리하여 위의 delete vp
서술문은 늦은 묶기를 사용하여 ~Vehicle
을 호출하고 이 시점부터 평소대로 객체가 소멸된다.
가상 멤버 함수는 반드시 바탕 클래스에 구현되어 있어야 할 필요가 없다.
추상 클래스 (Abstract Class): 추상 클래스는 일부 멤버 함수가 순수 가상 함수로 정의된 클래스를 의미합니다. 이 클래스는 객체를 직접 생성할 수 없습니다.
파생 클래스가 순수 가상 함수들을 구현하여 기능을 정의해야 합니다. 추상 클래스는 보통 인터페이스를 정의하거나, 공통된 기능을 가진 클래스들의 기본 클래스로 사용됩니다.
추상 클래스는 객체를 만들 수 없기 때문에 이 클래스를 직접 사용하는 것이 아니라 파생 클래스에서 구현을 제공해야 합니다.
순수 가상 함수 (Pure Virtual Function): 순수 가상 함수는 함수 선언 끝에 = 0;
을 붙여 선언된 가상 함수입니다.
이 함수는 Base 클래스에서는 구현되지 않고, 파생 클래스에서 반드시 구현되어야 합니다. 순수 가상 함수를 가지고 있는 클래스는 추상 클래스가 됩니다.
이것은 파생 클래스에서 필수적으로 구현해야 하는 동작을 정의할 수 있도록 합니다.
C++에서는 순수 가상 함수가 있는 클래스를 직접 객체화할 수 없습니다.
예를 들어, 아래는 추상 클래스 Vehicle
를 정의하고 그 안에 mass
를 설정하고 setMass
라는 순수 가상 함수를 선언한 예입니다:
class Vehicle {
public:
double mass; // 멤버 변수
// 순수 가상 함수, 파생 클래스에서 반드시 구현되어야 함
virtual void setMass(double newMass) = 0;
};
이제 Vehicle
클래스는 객체를 직접 생성할 수 없고, 파생 클래스에서 setMass
함수를 구현해야 합니다.
이렇게 하면 프로토콜(인터페이스)을 정의하고, 파생 클래스에서 이를 준수하여 구체적인 동작을 정의할 수 있게 됩니다.
바탕 클래스의 가상 소멸자는 언제나 순수 가상 함수가 될 수 있을까? ㄴㄴ.
파생 클래스에 소멸자를 강제할 필요가 없다. (소멸자를 = delete
속성으로 선언하지 않는 한) 소멸자는 기본으로 주어지기 때문이다.
둘째, 소멸자가 순수 가상 함수라면 구현이 존재하지 않는다. 그렇지만 파생 클래스의 소멸자는 결국 바탕 클래스의 소멸자를 호출한다. 구현이 없는데 어떻게 바탕 클래스의 소멸자를 호출할 수 있겠는가?
순수 가상 멤버 함수는 반드시는 아니지만 const
멤버 함수인 경우가 있다. 이렇게 하면 불변 파생 클래스 실체를 생성할 수 있다. const
멤버 함수에 대한 일반 규칙은 순수 가상 함수에도 적용된다.
추상 바탕 클래스는 멤버 데이터가 없는 경우가 많다. 그렇지만 바탕 클래스가 순수 가상 멤버를 선언하면 파생 클래스에도 동일하게 선언해야 한다. 파생 클래스의 순수 가상 함수의 구현이 파생 클래스 실체의 데이터를 변경하면 그 함수는 const
멤버로 선언할 수 없다. 그러므로 추상 바탕 클래스의 저자는 순수 가상 멤버가 const
멤버 함수인지 아닌지 주의깊게 살펴야 한다.
class Base
{
public:
virtual ~Base();
virtual void pureimp() = 0;
};
Base::~Base()
{}
void Base::pureimp()
{
std::cout << "Base::pureimp() called\n";
}
class Derived: public Base
{
public:
virtual void pureimp();
};
inline void Derived::pureimp()
{
Base::pureimp();
std::cout << "Derived::pureimp() called\n";
}
int main()
{
Derived derived;
derived.pureimp();
derived.Base::pureimp();
Derived *dp = &derived;
dp->pureimp();
dp->Base::pureimp();
}
바탕 클래스에서 순수 가상 멤버 함수를 구현하는 것은 가능하지만, 그것이 항상 파생 클래스에서 호출될 것이 보장되지 않으며, 파생 클래스에서 구현하는 것과 비교해서 일관성을 유지하기 어렵다
main
함수에서 Derived
클래스의 객체 derived
를 생성합니다.derived.pureimp()
:derived.pureimp()
를 호출하면 Derived
클래스에서 재정의된 pureimp()
함수가 실행되고, 먼저 부모 클래스인 Base
클래스의 pureimp()
가 호출된 후에 "Derived::pureimp() called"
가 출력됩니다.derived.Base::pureimp()
:derived.Base::pureimp()
를 호출하면 Base
클래스의 pureimp()
가 직접 호출되어 "Base::pureimp() called"
가 출력됩니다. 이렇게 하면 파생 클래스의 재정의된 버전이 아니라 부모 클래스의 버전이 호출됩니다.dp
포인터:Derived
클래스의 포인터 dp
를 생성하고, 이 포인터를 derived
객체를 가리키도록 초기화합니다.dp->pureimp()
:dp->pureimp()
를 호출하면 역시 Derived
클래스에서 재정의된 pureimp()
함수가 실행됩니다.dp->Base::pureimp()
:dp->Base::pureimp()
를 호출하면 Base
클래스의 pureimp()
가 직접 호출되어 "Base::pureimp() called"
가 출력됩니다. 마찬가지로 파생 클래스의 재정의된 버전이 아니라 부모 클래스의 버전이 호출됩니다.결과적으로 코드는 다양한 방식으로 가상 함수를 호출하고, 파생 클래스와 부모 클래스의 함수가 어떻게 작동하는지를 보여주고 있습니다.
final
식별자를 클래스 선언에 적용하면 그 클래스가 바탕 클래스로 사용되면 안된다고 지시할 수 있다.
class Base1 final // 바탕 클래스 불가
{};
class Derived1: public Base1 // error !!
{};
//
class Base2 // 바탕 클래스로 OK
{};
class Derived2 final: public Base2 // OK
{};
class Derived: public Derived2 // error !!
{};
final
식별자를 가상 멤버 선언에도 추가할 수 있다. 그런 가상 멤버들을 파생 클래스가 재정의하면 안 된다는 뜻
class Base
{
virtual int v_process();
virtual int v_call();
virtual int v_call2() const;
virtual int v_display();
};
class Derived: public Base
{
virtual int v_process() final;
};
class Derived2: public Derived
{
// int v_process(); // error !!
virtual int v_call2() override; //error !! const 아님
virtual int v_display(); // 재정의 OK
};
final
키워드는 더 이상의 상속을 방지하고 override
키워드는 정확하게 똑 같은 서명으로 재정의되었는지 컴파일러에게 점검해 달라고 요청하는 것
C++는 실행 시간 식별을 dynamic_cast
와 typeid
연산자를 통하여 제공한다.
Base 클래스의 포인터나 참조를 파생 클래스의 포인터나 참조로 변환한다.하향-형변환이라고 한다.
dynamic_cast은 실행 시간에 결정됨 - RTTI(Run Time Type Information)
런타인에서 클래스의 type_info를 보고 해당 클래스가 올바른 type의 형태인지 아닌지 판단
dynamic_cast 사용 가능 조건은 virtual function 사용
RTTI의 type_info 때문입니다.
virtual function을 사용하게 되면 해당 클래스에는 Vtable이 생성이 됩니다.
이 v-table에는 override된 자식 클래스의 함수를 가르킬 수 있는 주소값이 들어있는데
이 v-table에 클래스의 type_info 정보가 들어가게 됩니다.
때문에 dynamic_cast를 사용하게 되면 좀 더 안전한 형변환을 할 수 있습니다.
하지만 단점은 RTTI는 자원을 좀 먹기 때문에 퍼포먼스 측면에서 static_cast보다 좀 떨어지는 것이 사실입니다.
사용방법
dynamic_cast<변환형>(변환할 내용)
저는 Animal이라는 부모 클래스를 만들고 자식 클래스로 Cat 과 Dog를 만들고 싶습니다.
그리고 Cat 객체와 Dog객체 각 100개씩 총 200개를 백터에 넣어 관리하고 싶습니다.
하지만 저는 vector<Cat>, vector<Dog>
이렇게 2개의 백터로 관리하고 싶지 않고
하나의 백터로 관리하고 싶습니다.
#include <iostream>
#include <string>
#include <vector>
class Animal {
public:
virtual void sound() = 0;
void info(){
cout << "동물은 숨을 쉽니다.";
}
};
class Dog : public Animal{
private:
string name;
public:
Dog(string s) : name(s) {};
void sound() { cout << "멍멍"; }
void name_print() {cout << name < endl; }
void only_dog() {cout << "이건 개 클래스"; }
};
class Cat : public Animal {
private:
string name;
public:
Cat(std::string s) : name(s) {};
void sound() {cout << "냐옹"; }
void name_print() {cout << name << endl; }
void data() {cout << this << endl; }
void only_cat() {cout << "이건 고양이 클래스"; }
};
int main() {
vector<Animal*> v;
v.emplace_back(new Cat("나비"));
v.emplace_back(new Dog("멍멍이"));
// Animal 부모객체 선언 후 upcast를 통해 다양한 동물 클래스를 관리
// 빼낼 땐 downcast
Cat* cat = static_cast<Cat*>(v[0]); // 벡터에서 첫 번째 요소인 Cat 객체를 추출
Dog* dog = (Dog*)v[1];// 벡터에서 두 번째 요소인 Dog 객체를 추출
cat->name_print();// Cat 클래스의 name_print 메서드를 호출
cat->sound();
dog->name_print();
dog->sound();
delete cat;
delete dog;
return 0;
}
백터에 들어가있는 클래스는 Cat클래스 인데 Dog클래스로 받으면??
객체 간에 상속 계층이 다르기 때문에 컴파일 오류가 발생
Dog* cat = static_cast<Dog*>(v[0]);
Cat* dog = (Cat*)v[1];
cat->name_print();
cat->sound();
cat->only_dog();
dog->name_print();
dog->sound();
dog->only_cat();
delete cat;
delete dog;
return 0;
}
따라서 dynamic cast 사용
int main() {
std::vector<Animal*> v;
v.emplace_back(new Cat("나비"));
v.emplace_back(new Dog("멍멍이"));
Cat* cat; Dog* dog; // Cat 클래스 포인터를 선언
for (size_t idx = 0; idx < v.size(); idx++) {
if (cat = dynamic_cast<Cat*>(v[idx])) { //런타임에 형 변환의 유효성
//확인해야 하는 경우 dynamic_cast
cat->name_print();
cat->sound();
cat->only_cat();
}
else {
dog = dynamic_cast<Dog*>(v[idx]);
dog->name_print();
dog->sound();
dog->only_dog();
}
}
delete cat;
delete dog;
return 0;
}
https://hwan-shell.tistory.com/213
class Base
{
public:
virtual ~Base();
};
class Derived: public Base
{
public:
char const *toString();
};
inline char const *Derived::toString()
{
return "Derived object";
}
int main()
{
Base *bp;
Derived *dp,
Derived d;
bp = &d;
dp = dynamic_cast<Derived *>(bp);
if (dp)
cout << dp->toString() << '\n';
else
cout << "dynamic cast conversion failed\n";
}
Base
클래스와 Derived
클래스 정의:Base
클래스는 가상 소멸자(~Base()
)를 선언하고 있습니다. 소멸자는 파생 클래스에서 메모리 관리를 위해 필요한 경우 자원을 해제하는 데 사용됩니다.Derived
클래스는 Base
클래스를 상속하고 있으며, toString
함수를 선언하고 있습니다. 이 함수는 문자열을 반환합니다.toString
함수 구현:Derived
클래스에서 toString
함수를 구현했습니다. 이 함수는 "Derived object"라는 문자열을 반환합니다.main
함수:Base
포인터 bp
를 선언하고 Derived
포인터 dp
를 선언합니다.
Derived
클래스의 객체 d
를 생성합니다.
bp
를 d
객체의 주소로 설정합니다. 이것은 다형성의 예시입니다. Base
클래스의 포인터로 Derived
클래스의 객체를 가리킬 수 있습니다.
dynamic_cast
를 사용하여 bp
를 Derived
클래스의 포인터인 dp
로 변환하려 시도합니다. 이렇게 하면 bp
가 실제로 Derived
클래스의 객체를 가리키는지 확인하게 됩니다.
dp
가 성공적으로 Derived
클래스로 변환된 경우, dp->toString()
를 호출하여 "Derived object"를 출력합니다.
그렇지 않은 경우 "dynamic cast conversion failed"를 출력합니다.
static_cast
와 dynamic_cast
의 동작 및 차이점을 설명하고 있습니다.
static_cast
:
static_cast
는 가상 멤버 함수 여부와 상관없이 사용될 수 있으며, 컴파일러는 컴파일 시에 유효성을 확인합니다.dynamic_cast
:
static_cast
는 컴파일 시간에 형 변환을 수행합니다.dynamic_cast
는 실행 시간에 형 변환을 수행합니다.dynamic_cast
는 주로 다형성을 활용하여 상속된 클래스 간의 형 변환을 처리할 때 사용됩니다.dynamic_cast
는 가상 멤버 함수를 가진 클래스 간에 하향 형 변환에 사용될 수 있으며, 실행 시간 보호책을 제공합니다.dynamic_cast
는 실행 시간에 형 변환을 수행하며, 객체의 실제 유형을 확인하고 유효성을 검사합니다. 따라서 dynamic_cast
를 사용하면 객체의 실제 유형과 요청된 유형이 부합하지 않는 경우 형 변환이 실패합니다. 이것은 런타임 보호책을 제공하며, 논리적으로 유효하지 않은 형 변환을 방지할 수 있습니다.
따라서 static_cast
를 사용하여 bp
를 Derived2
로 형 변환하는 것은 기술적으로 가능하지만, 이것은 객체의 실제 유형과 부합하지 않을 수 있으므로 의미적으로 부합하지 않는 경우입니다. 그렇기 때문에 논리적으로 유효하지 않은 형 변환을 피하기 위해 dynamic_cast
를 사용하라는 것이 강조되는 것입니다.
요약하면, static_cast
와 dynamic_cast
를 사용할 때 논리적 의미와 안전성을 고려해야 하며, dynamic_cast
를 사용하면 보다 안전하고 의미적으로 유효한 형 변환을 수행할 수 있습니
// c++ 서적
static_cast
는 명시적 변환 기능 수행 실행 시간에 타입 검사를 수행하지 않음 타입을 안전하게 캐스팅하도록 실행 시간에 타입 검사를 적용하려면 다이나믹 캐스트 사용dynamic_cast
는 같은 상속 계층에 속한 타입끼리 캐스팅할 때 실행 시간에 타입을 검사한다. 차이점은 dynamic_cast
는 실행 시간에 타입 검사를 수행하는 반면 dynamic_cast는 문제가 되는 타입도 그냥 캐스팅한다는 것.typeid
는 가상 멤버가 있는 바탕 클래스에만 사용해야 함
typeid
연산자는 type_info
유형의 객체를 돌려줌
class type_info
{
public:
virtual ~type_info();
int operator==(type_info const &other) const; //같은 타입인지
int operator!=(type_info const &other) const; //다른 타입인지
bool before(type_info const &rhs) const // 타입 비교인데 "앞에" 있는지
char const *name() const; //타입의 이름을 문자열로 반환
private:
type_info(type_info const &other);// 비공개 복사 생성자
type_info &operator=(type_info const &other); //비공개 할당 연산자
};
이 클래스는 비공개 복사 생성자와 비공개 중복정의 할당 연산자가 있음
비공개로 만들어 type_info
객체를 복사하거나 할당하는 것을 방지합니다. 이렇게 함으로써 type_info
객체를 생성하거나 할당하는 것을 막아서 객체의 타입 정보의 무결성을 보장
class Base;
class Derived: public Base;
Derived d;
Base &br = d;
cout << typeid(br).name() << '\n';
br
은 Base
클래스의 참조이며, 이 참조는 d
객체를 가리킴
**typeid
** 연산자에 바탕 클래스 참조를 넘겨줌
“Derived” 텍스트를 출력하는데 **br**
이 실제로 참조하는 클래스의 이름
**Base**
에 가상 함수가 없다면 “Base”텍스트 출력
typeid
연산자는 객체의 실제 타입을 확인하기 위해 가상 함수 테이블 사용
가상 함수가 없는 클래스는 typeid
연산자가 가상 함수 테이블을 통해 타입 정보 확인 불가
대신, 컴파일러는 컴파일 시간에 Base
클래스를 인식하고 "Base" 출력
class Base; // 적어도 가상 함수가 하나는 있다 가정
class Derived: public Base;
Base *bp = new Derived; // 파생 객체를 가리키는 바탕 클래스 포인터
if (typeid(bp) == typeid(Derived *)) // 1: Base * != Derived *
...
if (typeid(bp) == typeid(Base *)) // 2: Base * == Base *
...
if (typeid(bp) == typeid(Derived)) // 3: Base * != Derived
...
if (typeid(bp) == typeid(Base)) // 4: Base * == Base
...
if (typeid(*bp) == typeid(Derived)) // 5: *bp == Derived
...
if (typeid(*bp) == typeid(Base)) // 6: *bp != Base
• 일반적으로 합성은 결합도가 더 느슨하다. 그러므로 상속보다 더 좋다.
std::string
멤버를 가진 클래스를 생각해 보라). 상속으로는 이런 클래스를 실현할 수 없다.