본문 바로가기

Study/C++

[C++] Polymorphism

다형성(多形性)

  • 다형성은 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 말한다.

1. 객체 포인터 변수

  • 객체의 주소 값을 저장하는 포인터 변수이다.
Student * ptr;		// 포인터 변수 선언
ptr = new Student();	// 포인터 변수의 객체 참조

// ------------------------------------------------

Student * ptr = new StudentHandler();		(O)
StudentHandler * ptr = new StudentHandler();	(O)
  • 이렇듯, C++에서, 어떠한 데이터형의 포인터 변수는 해당 데이터형 객체 또는 해당 데이터형을 직접 혹은 간접적으로 상속하는 모든 객체를 가리킬 수 있다. 즉, 객체의 주소 값을 저장할 수 있다.

2. 함수 오버라이딩

  • 만일, 기초 클래스와 유도 클래스가 동일한 이름과 형태의 함수를 가지고 있다면, 어떻게 작동할까?
    이러한 경우를 오버라이딩이라고 하며, 이렇게 될 경우에 오버라이딩 된 기초 클래스의 함수는, 오버라이딩을 한 유도 클래스의 함수에 가려진다. 즉, 유도 클래스의 함수가 작동한다는 뜻이다.
  • 물론 다음과 같은 형태로 기초 클래스의 함수를 호출하는 것 또한 가능하다. 참고만 하자.
class Student
{
public:
	someFunction
}

class StudentHandler
{
public:
    someFunction
}

int main(void)
{
	StudentHandler stu(...);
    stu.Student::someFunction();
    ...
}

3. 가상함수

  • C++의 컴파일러는 포인터 연산의 가능성 여부를 판단할 때, 포인터의 자료형을 기준으로 판단하지, 실제 가리키는 객체의 자료형을 기준으로 판단하지 않는다. 즉, 다음과 같은 코드는 컴파일 에러가 발생한다.
Class Student
{
	StudentFunc();
}

Class StudentHandler
{
	StudentHandlerFunc();
}

int main(void)
{
	Student * ptr = new StudentHandler();	// Compile Error
	ptr->StudentHandler();			// Compile Error
}
  • 이 이유는 불필요한 포인터 연산을 허용하지 않음으로써, 문제 발생 확률을 최소화 하기 위함이다.
    결론적으로, 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근이 가능하다.

4. 가상함수

  • 가상함수는 기본적으로 객체지향의 개념이다. 가상함수는 유도 클래스에서 재정의할 것을 기대하는 멤버 함수를 의미한다. 가상함수는 다음과 같이 선언한다.
class Student
{
public:
	virtual void SomeFunc() {...};
}
  • 가상함수로 선언된 함수는 포인터의 자료형을 기반으로 호출대상을 결정하는 것이 아닌, 포인터 변수가 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다. (자신을 호출하는 객체의 동적 타입에 따라 실제 호출할 함수가 결정된다.)
    즉, 어떠한 기초 클래스가 존재하고, 해당 기초 클래스의 상속을 받는 여러 유도 클래스가 존재한다고 가정하자. 이렇게 유도된 클래스가 기초 클래스의 함수를 각각 함수 오버라이딩 하고 있고, 클래스별로 포인터를 선언, 해당 함수를 호출하면 각 클래스의 멤버함수를 호출할 것이다.
    하지만, 만일 가상함수로 호출하면, 포인터 변수가 실제로 가리키는 객체를 참조하여 호출 대상을 결정하기 때문에, 마지막으로 오버라이딩된 함수를 호출하게 된다.
    또한, 가상함수를 상속받는 모든 오버라이딩 된 함수는 가상함수로 선언된다.
  • 이러한 가상함수는 상속 과정에서의 에러를 방지한다.

4. 순수 가상함수(Pure Virtual Function) & 추상 클래스(Abstract Class)

  • 클래스 중에서는 객체생성을 목적으로 정의되지 않는 클래스도 존재한다. 가령 기초 클래스로서만 의미를 가지는 클래스가 그러한 경우이다.
    이러한 경우 해당 기초 클래스를 이용해 객체를 생성하면, 당연한게도 컴파일 에러를 발생시키지 않는다. 이때 '순수 가상함수'를 사용하게 되는데, 이는 함수의 몸체가 정의되지 않은 함술르 의미한다. 이를 표현하기 위해 '0의 대입'을 표시하는데 이는 0의 대입을 의미하는 것이 아닌, 명시적으로 몸체를 정의하지 않았음을 컴파일러에게 알려 만일 기초 클래스가 객체를 생성할 경우 컴파일 에러를 발생시킨다. 이를 이요하면 잘못된 객체의 생성을 막을 수 있다.
  • 이렇듯 하나 이상의 멤버함수를 순수 가상함수로 선언한 클래스를 가리켜 '추상 클래스'라고 한다.
// Before
class Person
{
private:
	char name[100];
	int age;
public:
	Person(char *myname, int myage){...}
	void ShowInfo() cosnt{...}
	virtual int TakeTax() const { return 0; }
	virtual void ShowAge() const {}
};

// After
class Person
{
private:
	char name[100];
	int age;
public:
	Person(char *myname, int myage){...}
	void ShowInfo() cosnt{...}
	virtual int TakeTax() const = 0;	// pure virtual function
	virtual void ShowAge() const = 0;	// pure virtual function
};

5. 가상 소멸자, 참조자의 소멸 가능성

  • virtual로 선언된 소멸자의 경우 '가상 소멸자'라고 한다.
    만일, 기초 클래스와 해당 클래스로부터 상속된 유도 클래스가 존재할경우, 기초 클래스의 소멸자를 호출하면 (delete 할 경우) 유도 클래스의 소멸자가 호출되지 않아 메모리 누수가 발생하게 된다. 이러한 경우를 방지하기 위해 기초 클래스에 virtual 선언을 해주면, 이를 상속하는 모든 함수는 별도의  선언 없이 가상함수가 된다. 따라서, 상속의 계층구조상 가장 맨 아래에 존재하는 유도 클래스의 소멸자가 대신 호출된 후 기초 클래스의 소멸자가 순차적으로 호출된다.

 

'Study > C++' 카테고리의 다른 글

[C++] Operator Overloading  (0) 2023.10.13
[C++] Virtual principle and Multiple Inheritance  (1) 2023.10.10
[C++] Inheritance  (0) 2023.10.09
[C++] Static  (0) 2023.10.06
[C++] Friend  (0) 2023.10.06