프로그래밍 언어/C++

C++ OPP 3대요소 : 다형성

순정법사 2023.09.14

C. OPP 다형성

1. 가상 함수

a. 가상 함수(virtual function)

🌟 파생 클래스에서 재정의할 것으로 기대하는 멤버 함수

 

자신을 호출하는 객체의 동적 타입에 따라 실제 호출할 함수가 결정

 

📘 문법

virtual 멤버함수의원형;

 

기초 클래스에서 virtual 키워드를 사용해 선언하면, 파생 클래스에서 재정의된 멤버 함수도 자동으로 가상 함수가 됨

 

👉 파생 클래스에서도 virtual 키워드를 사용해 가상함수라는것을 명확히 하는 것도 나쁘지 않음!

 

b. 동적 바인딩(dynamic binding)

🌟 바인딩(binding)  : 함수를 호출하는 코드에서 어느 블록에 있는 함수를 실행하라는 의미

 

C++에서는 함수가 오버로딩될 수 있으므로 이 작업이 조금 복잡

 

  • 정적 바인딩(static binding)  / 초기 바인딩(early binding)  : 함수를 호출하는 코드는 컴파일 타임에 고정된 메모리 주소로 변환

➡ 가상 함수가 아닌 멤버 함수는 모두 이러한 정적 바인딩

 

하지만 가상 함수의 호출은 컴파일러가 어떤 함수를 호출해야 하는지 미리 알 수 없음

왜냐하면, 가상 함수는 프로그램이 실행될 때 객체를 결정하므로, 컴파일 타임에 해당 객체를 특정할 수 없기 때문

 

  • 동적 바인딩(dynamic binding) / 지연 바인딩(late binding) : 가상 함수의 경우 런 타임에 올바른 함수가 실행될 수 있도록 하는 것

➡  가상 함수도 결합하는 타입이 분명할 때에는 일반 함수와 같이 정적 바인딩

➡  이러한 가상 함수는 기초 클래스 타입의 포인터나 참조를 통하여 호출될 때만 동적 바인딩

 

⏳ 예제 : 가상 함수를 사용한 동적 바인딩을 간략하게 표현

#include <iostream>
using namespace std;

class A
{
public:
	virtual void Print() { cout << "A 클래스의 Print() 함수" << endl; }
};

class B : public A
{
	virtual void Print() { cout << "B 클래스의 Print() 함수" << endl; }
};

int main(void)
{
	A* ptr;
	A obj_a;
	B obj_b;
	
	ptr = &obj_a;
	ptr->Print();
	ptr = &obj_b;
	ptr->Print();
	return 0;
}

✨ 실행결과

A 클래스의 Print() 함수
B 클래스의 Print() 함수

 

c. 가상 함수 테이블(virtual function table,vtbl)

C++에서는 가상 함수의 정의와 동작 방식만을 규정하고 있고, 그에 따른 구현은 컴파일러마다 다르지만

컴파일러가 가상 함수를 다루는 가장 일반적인 방식은 가상 함수 테이블(virtual function table)을 이용하는 것

 

각각의 객체마다 가상 함수 테이블을 가리키는 포인터를 저장하기 위한 숨겨진 멤버를 하나씩 추가합니다.

이와 함께 가상 함수를 단 하나라도 가지는 클래스에 대해서 가상 함수 테이블을 작성

 

🌟 이렇게 작성된 가상 함수 테이블에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장

 

가상 함수를 호출하면, C++ 프로그램은 가상 함수 테이블에 접근하여 자신이 필요한 함수의 주소를 찾아 호출

가상 함수를 사용하면 이처럼 함수의 호출 과정이 복잡해지므로, 메모리와 실행 속도 측면에서 약간의 부담

 

➡ 따라서 기본 바인딩은 정적 바인딩이며, 필요한 경우에만 가상 함수로 선언

 

💙 파생 클래스가 재정의할 가능성이 있는 함수는 모두 가상 함수로 선언하는 편이 좋음
 

d. 가상 소멸자

🌟 기초 클래스의 소멸자는 반드시 가상으로 선언

 

⏳ 예제

Person* hong = new Student;

...

delete hong;	// ~Student() 소멸자는 호출되지 않음

 

위와 같은 경우 ~Person()만 호출되기 때문에 메모리가 정상적으로 해제되지 않음

 

👉 따라서 기초 클래스는 명시적으로 소멸자를 선언할 필요가 없어도, 아무 일도 하지 않는 가상 소멸자를 선언해야 함!

 

 

2. 추상 클래스

a. 순수 가상 함수(pure virtual function)

가상 함수는 반드시 재정의해야만 하는 함수가 아닌, "재정의가 가능한 함수"

 

🌟 순수 가상 함수(pure virtual function) : 파생 클래스에서 "반드시 재정의해야 하는 멤버 함수" 를 의미

 

일반적으로 함수의 동작을 정의하는 본체가 ❌

따라서 재정의하지 않으면 사용할 수 없음

 

📘 문법

virtual 멤버함수의원형=0;

//함수만 있고 본체가 없다는 의미로 함수 선언부 끝에 "=0"을 추가

 

b. 추상 클래스(abstract class)

🌟 하나 이상의 순수 가상 함수를 포함하는 클래스

 

객체 지향 프로그래밍에서 중요한 특징인 다형성을 가진 함수의 집합을 정의할 수 있게 함

 

👉 즉, 반드시 사용되어야 하는 멤버 함수를 추상 클래스에 순수 가상 함수로 선언해 놓으면,

이 클래스로부터 파생된 모든 클래스에서는 이 가상 함수를 반드시 재정의

 

순수 가상 함수를 포함하고 있어 인스턴스를 생성할 수 없음

➡ 따라서 상속을 통해 파생 클래스를 만들고, 그 클래스에서 순수 가상 함수를 오버라이딩 후 파생 클래스의 인스턴스를 생성 가능

 

💥 하지만 추상 클래스 타입의 포인터와 참조는 바로 사용 가능

 

⏳ 예제

#include <iostream>
using namespace std;

class Animal
{
public:
	virtual ~Animal() {}	// 가상 소멸자의 선언
	virtual void Cry()=0;	// 순수 가상 함수의 선언
};

class Dog : public Animal
{
public:
	virtual void Cry() { cout << "멍멍!!" << endl; }	//오버라이딩 하지 않으면 인스턴스 생성 불가
};

class Cat : public Animal
{
public:
	virtual void Cry() { cout << "야옹야옹!!" << endl; }	//오버라이딩 하지 않으면 인스턴스 생성 불가
};

int main(void)
{
	Dog my_dog;
	my_dog.Cry();
	Cat my_cat;
	my_cat.Cry();
	return 0;
}

✨ 실행결과

멍멍!!
야옹야옹!!

 

c. 추상 클래스의 용도 제한

  1. 변수 또는 멤버 변수
  2. 함수의 전달되는 인수 타입
  3. 함수의 반환 타입
  4. 명시적 타입 변환의 타입

 

 

 


출처 : http://www.tcpschool.com/cpp/cpp_polymorphism_virtual

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com