본문 바로가기
c, c++/c++로 시작하는 객체지향 프로그래밍

15.5~15.9) 상속과 "다형성"

by 피스타0204 2024. 5. 9.

15.5 함수 재정의

기본 클래스에서 정의된 함수는 파생 클래스에서 재정의될 수 있습니다.

//GeometricObject.cpp
string GeometricObject::toString() const {
  	return "Geometric Object";
}
//Circle.cpp
string Circle::toString() const{
  	return "Circle object";
}
//Rectangle.cpp
string Rectangle::toString() const { return "Rectangle object"; }
#include "GeometricObject.h"
#include "Circle.h"
#include "Rectangle.h"
#include <iostream>  //1)다른 생성자호출로 Circle객체, Rectangle객체 2개씩 더 생성후 출력
using namespace std;  //2)부모클래스의 멤버변수를 protected로 지정
int main() {	    //3)상속된 멤버변수는 부모생성자 통해 초기화함
  	GeometricObject shape;
  	cout << shape.toString() << endl;
    Circle circle(5);
  	circle.setColor("black");  // 상속받은 부모함수 사용
  	cout << circle.toString()<< endl;
    Rectangle rectangle(2, 3, "orange");
  	cout << rectangle.toString()<< endl;
    return 0;
}

만약 하위 클래스에서 재정의된 상위 클래스의 메서드를 원본그대로 가지고 오고 싶다면 

circle1.GeometricObject::toString();  이런 식으로 범위 지정 연산자와 함께 사용하면 됩니다.

 

15.6) 다형성

다형성은 객체지향 프로그래밍을 이루는 3대요소 중 하나입니다. 

이를 이해하기 위해서는 하위 유형(subtype)과 상위 유형(supertype)이라는 용어를 이해해야 합니다. 어떤 클래스유형이 함수의 매개변수나 반환 유형인 것을 본 적이 있을 겁니다. 즉, c++에서는 클래스 이름이 자료형처럼 사용이 됩니다. 이때, 상속으로 인해 부모 클래스와 자식 클래스가 나누어지면, 부모 클래스를 자료형으로 사용하는 것을 supertype, 자식 클래스를 자료형으로 사용하는 것을 subtype이라고 합니다. 

 

이때 다형성은 하위 유형의 객체를 전달받아도 상위 유형의 변수를 사용할 수 있는 것을 말합니다. 엄밀히 말하자면 상위 유형의 변수가 하위 유형 객체를 참조할 수 있는 것을 말합니다.

예제를 봅시다.

#include "GeometricObject.h"
#include "Rectangle.h"
#include "Circle.h"
#include <iostream>
using namespace std;

void displayGeometricObject(const GeometricObject& g) {
	cout << g.toString() << endl;
}
int main() {
	GeometricObject geometricObject;
	displayGeometricObject(geometricObject);

	Circle circle(5);
	displayGeometricObject(circle);

	Rectangle rectangle(4, 6);
	displayGeometricObject(rectangle);
	
	return 0;
}

 

위 코드처럼 기본 클래스의 객체가 사용된다고 명시되어 있는 매개변수에 파생 클래스의 객체를 사용할 수 있습니다.

 

15.7) 가상 함수와 동적 결합

다형성은 상속과 매우 밀접한 관계에 있습니다. 다형성의 개념 자체는 부모 클래스에서 상속된 자식 클래스들이 부모 클래스와 다른 특별한 클래스가 되는 것입니다. 이를 구현하기 위해 가상 함수를 이용한 동적 결합이 사용됩니다. 이런 가상함수를 갖는 클래스를 다형성 유형(polymorphic type)이라고 합니다.

 

GeometricObject.h 의 tostring함수를 가상함수로 만들면 각 클래스의 toString이 출력됩니다.

virtual string toString() const;

부모 클래스에 virtual 로 정의되 toString 함수가 있으면 c++은 실행시 어떤 toString함수를 호출할지 동적으로 결정합니다.

displayGeometricObject(circle);하면 Circle 객체의 toString함수가 전달됩니다. 이렇게 실행시에 어떤 함수를 호출해야 하는 지 결정하는 것을 동적 결합(dynamic binding)이라고 합니다.

 

함수에서 동적결합이 가능하려면 두가지 조건이 충족되어야 합니다.

첫째, 그 함수가 부모 클래스에서 virtual로 정의되어야 합니다.

기본 클래스에서 virtual로 정의된 함수는 파생 클래스에서 자동으로 virtual이 됩니다.

둘째. 객체를 참조하는 변수는 가상 함수에서 참조에 의해 전달되거나 포인터로서 전달되어야 합니다.

즉, 객체 매개변수는 참조에 의해 전달되거나 포인터로 전달되도록 함수를 정의해야 한다는 뜻입니다.

 

함수가 재정의 되지 않을 경우에는 virtual 함수를 사용하지 않는 것이 좋습니다. 동적 결합은 시간과 시스템 자원을 많이 소모합니다.

15.9) 추상 클래스와 순수 가상함수

압서 배운 내용들을 통해 우리는 파생 클래스가 기본 클래스를 더 명시적이고 구체적으로 만들 수 있다는 것을 알게 됐습니다. 반대로 말하자면 기본 클래스가 좀더 뭉뚱그려져 있고 추상적이라는 뜻입니다.

추상 함수; 순수 가상함수는 하위 클래스에서 같은 이름으로 함수를 공유하고, 유사한 개념이지만 상위 클래스에서 정의할 수 없는 함수를 작성하는데 사용합니다. 상위 클래스에서 이름을 적어놓고 하위 클래스에서 복잡한 구현들을 한다고 생각하면 됩니다. 또, 이런 추상함수가 사용되는 클래스를 추상 클래스라고 합니다.

 

추상 클래스는 virtual 뭐시기 =0;으로 나타냅니다.

//GeometricObject.h

	virtual double getArea() const = 0;
	virtual double getPerimeter() const = 0;
#include "GeometricObject.h"
#include "Rectangle.h"
#include "Circle.h"
#include <iostream>
using namespace std;

bool equalArea(const GeometricObject& g1, const GeometricObject& g2) {
	return g1.getArea() == g2.getArea();
}
void displayGeometricObject(const GeometricObject& g) {
	cout << "The area is " << g.getArea() << endl;
	cout << "The perimeter is " << g.getPerimeter() << endl;
}
int main() {
	Circle circle(5);
	Rectangle rectangle(5, 3);

	cout << "Circle info: " << endl;
	displayGeometricObject(circle);

	cout << "\nRectangle info: " << endl;
	displayGeometricObject(rectangle);

	cout << "\nThe two objects have the same area? "
		<< (equalArea(circle, rectangle) ? "Yes" : "No") << endl;
	
	return 0;
}

 

만약 추상클래스가 없었다면 equalArea. displayGeometricObject같은 동적 결합을 사용하는 함수를 만들 수 없습니다. 이것이 추상클래스의 중요한 점입니다.

---

가상 함수를 작성했다면 가상 소멸자를 항상 정의하는 것이 좋습니다. 

Parent*p = new Child;

delete p; //가상 소멸자가 없다면 틀린 코드

 

가상소멸자를작성하지 않으면  p는 Child 객체를 가리키지만 delete p하면 Parent 클래스의 소멸자만 불리고 Child 객체는 소멸자가 불리지 않습니다. 그렇기 때문에 반드시 가상 소멸자를 작성해야 합니다.

class Person {
   public:
  	Person()	{ cout << "Person 생성자" << endl ; }
  	virtual ~Person()	{ cout << "Person 소멸자" << endl; }
};
class Employee : public Person {
   public:
  	Employee() { cout << "Employee 생성자" << endl; }
  	~Employee() { cout << "Employee 소멸자" << endl; }
};
class Faculty: public Employee {
   public:
  	Faculty() { cout << "Faculty 생성자" << endl; }
  	~Faculty() { cout << "Faculty 소멸자" << endl; }
};
int main()
{
  	Person * p = new Faculty;
	delete p
	p = NULL;  	
	return 0;
 }

 

 

15.10) 정적 형변화(static_cast)과 동적 형변환(dynamic_cast)

아래 코드에서 displayGeometricObject는 GeometricObject를 매개변수로 받습니다. 따라서 하위 클래스인 Circle과 Rectangle에서 정의한 getRadius()같은 함수들을 사용할 수 없습니다.

void displayGeometricObjectconst GeometricObject& g) {
	cout << "The radius is " << g.getRadius() << endl; //오류
	cout << "The diaimeter is " << g.getDiameter() << endl;//오류
	cout << "The width is " << g.getWidth() << endl;//오류
	cout << "The height is " << g.getHeight() << endl;//오류
	cout << "The area is " << g.getArea() << endl;
	cout << "The perimeter is " << g.getPerimeter()<< endl;
}

 

 

이를 정적 형변환과 동적 형변환을 이용해 해결할 수 있습니다.

void displayGeometricObject(GeometricObject& g) {
	GeometricObject* p = &g;
	cout << "The radius is " << static_cast<Circle*>(p)->getRadius() << endl;
	cout << "The diaimeter is " << static_cast<Circle*>(p)->getDiameter() << endl;
	cout << "The width is " << static_cast<Rectangle*>(p)->getWidth() << endl;
	cout << "The height is " << static_cast<Rectangle*>(p)->getHeight() << endl;
	cout << "The area is " << g.getArea() << endl;
	cout << "The perimeter is " << g.getPerimeter() << endl;
}

정적 형변환(static casting)과 동적 형변환(dynamic casting)은 똑같이 형변환을 시켜주지만 동적 형변환의 경우 형변환이 실패한 경우 NULL을 반환하기 때문에 코드를 더 쉽게 관리할 수 있습니다. 단, dynamic_casting은 포인터나 다형성 유형의 참조에 대해서만 사용될 수 있습니다. c++에서는 가상함수를 포함하는 클래스유형을 다형성 유형이라 부른다.

int main() {
	
	Rectangle rectangle(5, 3);
	GeometricObject* p = &rectangle;
	Circle* pc = dynamic_cast<Circle*>(p); //pc == NULL 오류 발생
	cout << (*pc).getRadius() << endl;
	
	return 0;
}

 

 

이제 정적 형변환과 동적 형변환을 이용하여 상위 클래스의 객체를 이용해 하위 클래스의 멤버를 사용하고, 하위 클래스의 객체를 이용해 상위 클래스의 멤버를 사용해봅시다.

 

---

그전에 먼저 알아야 할 개념이 있습니다. 업캐스팅과 다운 캐스팅입니다. 파생 클래스 유형의 포인터를 기본 클래스 유형의 포인터로 할당하는 것을 업 캐스팅이라고 합니다. 포인터 대신에 참조변수에 할당하는 것도 똑같이 업캐스팅입니다. 

 

GeometricObject* p = new Circle(); 

Circle* p1 = new Circle(2);

p = p1;

반대로 파생클래스 유형의 포인터가 들어가야하는 자리에 기본 클래스가 할당되는 것을 다운 캐스팅이라고 합니다. 업캐스팅은 정적 형변환 이나 동적 형변환을 사용하지 않고도 암시적으로(자동으로) 수행될 수 있지만 다운 캐스팅은 static_casting이나 dynamic_casting으로 명시적으로 형변환해주어야 합니다.

 

GeometricObject* p = new Circle();

Circle* p1 = new Circle(2);

p1 = static_cast<Circle*>p;

 

void displayGeometricObject(GeometricObject& g) {
	GeometricObject* p = &g;
	Circle* p1 = dynamic_cast<Circle*>(p);
	Rectangle* p2 = dynamic_cast<Rectangle*>(p);
	if (p1 != NULL) {
		cout << "The radius is " << p1->getRadius() << endl;
		cout << "The diaimeter is " << p1->getDiameter() << endl;
	}
	if (p2 != NULL) {
		cout << "The width is " << p2->getWidth() << endl;
		cout << "The height is " << p2->getHeight() << endl;
	}
	cout << "The area is " << g.getArea() << endl;
	cout << "The perimeter is " << g.getPerimeter() << endl;
}
int main() {
	
	Rectangle rectangle(5, 3);
	Circle circle(5);

	cout << "Circle into: " << endl;
	displayGeometricObject(circle);
	cout << "\nREctangle info: " << endl;
	displayGeometricObject(rectangle);
	return 0;
}

 

 

 

 

 

check)

15.9) 함수 오버로딩과 함수 재정의의 차이점은 무엇인가?

함수 오버로딩은 같은 이름을 갖지만 다른 함수를 정의하는 것으로 서로 다른 함수 서명을 한 개 이상 갖게 만들어야 합니다. 하지만 함수를 재정의 할 때는 모두 동일한 함수 서명(function signature)과 반환 유형을 사용합니다.

15.10) 참과 거짓을 판별하라

(1) 기본 클래스에서 정의된 전용함수를 재정의할 수 있다.

(2) 기본 클래스에서 정의된 정적함수를 재정의 할 수 있다.

아니오?

(3) 생성자를 재정의할 수 있다.

아니오?

15.11) 하위 유형과 상위 유형은 무엇인가? 다형성이란 무엇인가?

파생 클래스에 의해 정의된 유형을 하위 유형이라 하고 기본 클래스에 의해 정의된 유형을 상위 유형이라고 합니다.

다형성이란 상위 유형의 변수가 하위 유형 객체를 참조할 수 있는 것을 말합니다.

 

15.12) 다음 질문에 답하시오

(1) 이 프로그램의 출력은 무엇인가?

(2) 만약 void f()를 virtual void f()로 바꾸면 출력은 무엇인가?

(3) 만약 void f()를 virtual void f()로 바꾸고 void p(Parent a)를 void p(Parent& a)로 바꾸면 출력은 무엇인가?

#include <iostream> 
using namespace std;
class Parent{
public:
	void f()
	{
		cout << "invoke f from Parent" << endl;
	}

};
class Child : public Parent{
public:
	void f()
	{
		cout << "invoke f from Child" << endl;
	}

};

void p(Parent a)
{
	a.f();
}

int main(){

	Parent a;
	a.f();
	p(a);
	Child b;
	b.f();
	p(b);

	return 0;
}

(1)

invoke f from Parent

invoke f from Parent

invoke f from Child

invoke f from Parent

(2)

invoke f from Parent

invoke f from Parent

invoke f from Child

invoke f from Parent

(3)

invoke f from Parent

invoke f from Parent

invoke f from Child

invoke f from Child

//다형성 문제는 객체유형을 참조하는 함수에서만 일어납니다.

 

15.13)정적 결합이란 무엇이고 동적 결합이라 무엇인가?

컴파일 하는 동안 컴파일러는 함수의 이름과 매개변수들을 조사하는데 이때 일어나는 결합을 정적 결합(static binding) 또는 초기 결합(early binding)이라고 한다. 그러나 가상 함수를 이용하면 프로그램을 실행할 때 사용자가 객체를 결정한다. 이때 가상메소드를 선택하는 작업을 동적 결합 (dynamic binding) 또는 말기 결합(lately bindin)이라고 한다. 동적 결합은 runtime 중에 일어난다.

 

15.14)가상함수를 선언하는 것만으로 동적 결합을 사용하기에 충분한가?

아니다. 객체유형을 참조하는 함수가 존재해야 실제 동적 결합을 사용할 수 있다.

 

15.15) 다음 코드의 출력은 무엇인가

(a)

#include <iostream> 
#include <string> 
using namespace std;

class Person
{
public:
	void printInfo()
	{
		cout << getInfo() << endl;
	}
	virtual string getInfo()
	{
		return "Person";
	}
};

class Student : public Person
{
public:
	virtual string getInfo()
	{
		return "Student";
	}

};

int main()

{
	Person().printInfo(); 
	Student().printInfo();
}

(b)

#include <iostream> 
#include <string> 
using namespace std;

class Person
{
public:
	void printInfo()
	{
		cout << getInfo() << endl;
	}
	string getInfo()
	{
		return "Person";
	}
};

class Student : public Person
{
public:
	string getInfo()
	{
		return "Student";
	}

};

int main()

{
	Person().printInfo(); 
	Student().printInfo();
}

15.16) 모든 함수를 가상으로 정의하는 것은 좋은 습관인가?

아니다.가상함수는 많은 리소스를 잡아먹는다. 입력이 있기 전까지는 어떤 객체의공간을 잡아놔야 할지 컴파일러가 모르고 올바른 가상메서드를 사용하기 위한 리소스를 더 잡아먹기 때문에 비추다.

15.18) 순수 가상 함수는 어떻게 정의하는가?
virtual 반환형 함수이름 const =0;
15.19 다음 코드에서 잘못된 부분은 무엇인가?

class A
{
public:
	virtual void f() = 0;
};

int main()
{	A a;

	return 0;
}

추상 클래스는 인스턴스화를 할 수 없습니다. 추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 객체를 직접 생성해서 사용할 수 없습니다. 추상 클래스는 새로운 실체 클래스를 만들기 위해 부모 클래스로만 이용됩니다.

15.20)다음 코드를 컴파일하고 실행할 수 있는가? 실행결과는 무엇인가?

#include <iostream> 
using namespace std;

class A
{
public:
	virtual void f() = 0;
};
class B : public A
{
public: 
	void f()
	{ 
		cout << "invoke f from B" << endl; 
	}
};

class C : public B
{
	public: 
		virtual void m() = 0;
};

class D : public C
{
public:
	virtual void m() { cout << "invoke m from D" << endl; }
};

void p(A& a)
{
	a.f();
}

int main()
{
	D d;
	p(d);
	d.m();
	return 0;


}

 

15.21) 15.21 getArea와 getPerimeter 함수를 GeometricObject 클래스에서 제거할 수 있다. GeometricObject 클래스에서 추상 함수로 getArea와 getPerimeter를 정의하는 것 의 장점은 무엇인가?

GeometricObject객체를 매개변수로 전달함으로써 동적결합된 getArea와 getPerimeter 함수를 사용할 수 있습니다.

 

15.22 업캐스팅이란 무엇인가? 다운캐스팅이란 무엇인가?

업캐스팅이란 부모 객체 포인터가 할당되어야 할 장소에 자식 객체 포인터를 할당하는 것이고, 다운 캐스팅은 그 반대다.

업캐스팅은 자동으로 일어나고 다운 캐스팅은 static_casting이나 dynamic_casting으로 명시적으로 작성해주어야 일어난다.

15.23 기본 클래스 유형에서의 객체를 파생 클래스 유형으로 언제 다운캐스팅해야 하는가?

부모 객체 포인터를 자식 객체 포인터에 할당하여야 할때 

 

15.24 다음 문장 실행 후에 p1의 값은 무엇인가?
GeometricObject* p = new Rectangle(2, 3);
Circle* p1 = new Circle(2);
p1 =  dynamic_cast<Circle*>(p);

 

Circle객체 p

15.25 다음 코드를 분석하여라.

#include <iostream>
using namespace std;
class Parent
{
};

class Child : public Parent
{
public:
	void m()
	{
		cout << "invoke m" << endl;
	}
};

int main()

{
	Parent* p = new Child(); 
	return 0;
}


a. 만약 main에 (*p).m(); 를 추가한다면 어떤 컴파일 오류가 발생하겠는가? 

m이 Parent의 멤버가 아니라는 오류가 발생합니다.

b. 만약 (*p).m();  을 다음 코드로 대체할 경우, 어떤 컴파일 오류가 발생하겠는가?
Child* p1 = dynamic_cast<Child*>(p); 

(*p1).m();

 

Parent는 다형성 유형이 아니기 때문에 동작하지 않는다.
c. 만약Child* p1 = dynamic_cast<Child*>(p); (*p1).m();을 다음 코드로 대체할 경우, 프로그램을 컴파일하고 실행할 수 있 는가?
Child* p1 = static_cast<Child*>(p);

(*p1).m();

 

실행된다.
d. 다음 코드는 프로그램을 컴파일하고 실행할 수 있는 가?

#include <iostream>
using namespace std;
class Parent
{
	virtual void m() {};
};

class Child : public Parent
{
public:
	void m()
	{
		cout << "invoke m" << endl;
	}
};

int main()

{
	Parent* p = new Child(); 
	dynamic_cast<Child*>(p)->m();
}

다형성 유형은 가상함수(동적결합)을 포함하는 유형을 말하기 때문에 dynamic_cast로 업 캐스팅할 수 있습니다.

15.26 가상 소멸자를 정의해야 하는 이유는 무엇인가?

보통 소멸자는 연쇄호출에 의해 자식->부모 순으로 호출되어 삭제됩니다. 하지만 업캐스팅이 일어나게 되면 부모 객체가 저장된 포인터를 지웠을때 부모 객체에서 시작하는 것으로 착각하여 자식 객체가 지워지지 않습니다. 그러므로 부모 클래스의 소멸자에 virtual을 붙여주어야 합니다.

예시)

Parent* p = new Child;

delete p;

'c, c++ > c++로 시작하는 객체지향 프로그래밍' 카테고리의 다른 글

13.7~)이진 입출력  (0) 2024.05.24
13.1~ 13.5)파일 입력과 출력  (0) 2024.05.24
15.1~15.4, 15.8) 상속  (0) 2024.05.02
12.1~12.5  (2) 2024.04.25
11.6~11.15  (0) 2024.04.12