동족의 묶음 UpCasting에 대해 알아보자🔤

안녕하세요! 두두코딩 입니다 ✋
오늘은 Design pattern 중 upcasting 개념에 대해 알아보겠습니다.

C++ 디자인 패턴 강의를 듣고 정리하고자 합니다. 자세한 내용은 여기를 클릭해주세요.

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

UpCasting

UpCasting이라는 개념은 C++ 책을 좀 읽어 봤다면 한번은 접해봤을 개념이다. 해당개념은 너무 중요하기 때문에 자세하게 다뤄보도록하자.

#include<iostream>
using namespace std;

class Animal
{
	int age;
};

class Dog : public Animal
{
	int color;
};

int main()
{
	Dog d;

	Dog* dp1 = &d;		// ok.

	double* dp2 = &d;		// error.

	Animal* ap1 = &d;		// ok.
}

위의 예시를 보자.

첫번째 정의부분에서 Dog* dp1 = &d;는 사용가능하다. 당연히 동일한 타입이니 포인팅이 가능하다.

두번째 정의 부분인 double으로casting이 될까? double* dp2 = &d; 해당 정의를 수행하면 에러가 발생한다. 당연히 타입이 달라 에러가 발생하고, 조금더 생각해보면, 우리가 할당받은 double 크기에 Dog라는 객체를 담을수 없기 때문이라고 생각해볼 수도 있다.

마지막으로, Animal* ap1 = &d; 해당 케이스는 될까? 된다. Dog 객체는 Animal 객체를 상속받아 작성되었기 때문에 포인팅이 된다.

우리는 이와 같이 기반클래스파생클래스를 가리킬수 있는 개념을 UpCasting이라고 부른다.

근데 왜 상속받으면 포인팅이 가능할까? (메모리 관점에서 생각해보자. 🤔)

memory

위의 그림을 보면, 우리가 Animal을 상속받아 Dog 객체를 생성하면, Animal의 멤버 변수를 취하고 아래 자신의 멤버 변수를 취하는 것을 볼 수 있다. 따라서, Animal을 갖고 포인팅을 해도 Animal은 자신의 멤버를 가리키고 있는 것이기 때문에 포인팅이 가능한 것이다. 메모리 관점에서 보면 UpCasting 은 당연시 할 수 있는 내용이다.

그렇다면 UpCasting이 왜 필요하고, 언제 사용되는지 알아보자.🙄

UpCasting의 사용이유

기반클래스를 활용해 파생클래스를 가리킬 수 있는 개념을 Upcasting이라고 했다. 그렇다면, UpCasting은 언제 활용할까? 아래의 예시를 보자.

upcasting1

위의 그림과 같이, 폴더 구조를 만든다고 생각해보자.

우선, 폴더 내 자료들을 관리하기 위해 우리는 Vector 자료구조를 활용해야한다. 해당 폴더 내 자료구조들을 담아야하는데 어떻게 담아야할까? 현재 폴더 내에는 sample이라는 파일 객체와 test라는 폴더 객체가 존재한다.

만약 vector자료구조를 sample 타입만 받도록 한다면, 파일객체만 받을 수 있을 것이다. (test 객체만 설정해도 마찬가지이다..😓) 이렇게 구성하게 된다면, 우리는 폴더 구조를 만들지 못할 것이다. 폴더 내에는 sample이라는 파일과 test라는 폴더가 존재하기 때문이다. 이 둘의 객체를 한번에 묶어서 관리하기 위해선 어떻게해야할까? 이때 사용해야되는 개념이 UpCasting이다. 둘을 folder라는 기반 클래스로 묶어 관리하면된다.

구체적으로, folder라는 기반클래스를 하나 생성하고, sampletestfolder를 상속받는 파생클래스로 구성하면 된다. 그렇다면, 기반클래스에서 파생클래스를 가리킬 수 있는 UpCasting 개념을 활용할 수 있다.

vectorfolder라는 기반클래스가 되는 객체로 묶게될 경우 sampletest를 모두 vector내 담을 수 있게 된다.

만약, folder를 또 다른 folder에서 사용하게 된다면 이 역시 또다른 기반클래스를 하나 생성해 상속받은 후 UpCasting으로 사용하면 된다.

❗ 상속의 개념은 변수 및 함수를 재사용하기 위해서만 사용하는 것이 아니라, 디자인 관점에서 보듯 동족끼리 묶을 때도 많이 사용한다는 점을 기억해라.

Virtual 가상함수

우리가 위의 UpCasting 개념을 배우게 될 시점에는 상속 이라는 개념과 같이 배우게 된다. 상속 개념을 말할 때 가장 많이 따라오는 이야기가 virtual함수의 사용법이다.

보통 요즘 흔히 쓰는 Java 혹은 C#에서는 너무 자명한 이야기이지만, Pointer를 사용하는 C++에서는 조금 생각해 볼 필요가 있는 문제이다. 아래의 예시를 보자.

#include <iostream>
using namespace std;

class Animal
{
	int age;
public:
	void Cry() { cout << "Animal Cry" << endl; }
};

class Dog : public Animal
{
	int color;
public:
	void Cry() { cout << "Dog Cry" << endl; }
};

int main()
{
	Dog d;
	Animal* p = &d;

	p->Cry();
}

위의 코드를 봤을 때, p->Cry()가 어디를 호출할 것인가? 흔히 사용하는 Java 혹은 C# 에서는 재정의된 함수인 Dog 내 Cry()를 호출할 것이다. 하지만, C++에서는 포인터를 따라가서, 호출하기 때문에, Dog가 아닌 Animal 의 Cry()를 호출 할 것이다. (위의 메모리 그림을 봤듯이, 기반 클래스의 포인터는 기반 클래스로 인식해버린다.. 😱)

위와 같은 문제 즉, 기반 클래스가 포인터로 파생 클래스를 가리킬 때, 파생클래스가 재정의 한 함수를 호출 하려면 반드시 Virtual (가상함수)로 선언해야한다.

class Animal
{
	int age;
public:
	virtual void Cry() { cout << "Animal Cry" << endl; }
};

class Dog : public Animal
{
	int color;
public:
	virtual void Cry() { cout << "Dog Cry" << endl; }
};

위와 같이, 선언할 경우 기반클래스가 파생클래스를 가리켜도 파생클래스가 재정의 한 함수를 부르게 된다. (이게 어떻게 가능한지 알고 싶으면, virtual table을 구글에 검색해보자! 🤗)

Override 키워드

우리가 보통 상속을 받아 사용할 경우 기반 클래스의 함수를 재정의해서 사용한다. 이를 overriding이라고 부른다.

하지만 재정의를 할 경우, 실수로 우리가 인자를 더 넣거나, 반환형을 다르게 만들어도 컴파일러는 해당 문제를 식별하지 못한다. 결국 실행이 됐다가 나중에 프로그래머만 밤새서 이 말도 안되는 에러를 찾는일을 해야한다 ㅠㅠ,, 😫

이를 막기위해 C++11에서는 override라는 키워드가 등장했다. 해당 키워드를 사용할 경우 가독성도 좋아지고, 실수도 미리 잡아줘 프로그래머의 생산성을 향상시킬 수 있다. 위의 예시를 통해 어떻게 붙이는지 확인해보자.

class Animal
{
	int age;
public:
	virtual void Cry() override { cout << "Animal Cry" << endl; }
};

class Dog : public Animal
{
	int color;
public:
	virtual void Cry() override { cout << "Dog Cry" << endl; }
};

가상함수의 마지막 즉, 함수 본문 시작 전 override라는 키워드를 적어주면 된다. 파생 클래스에만 보통 붙여주면 되는데, 기반 클래스에도 붙여 습관화 하자.

override 키워드는 오픈소스 혹은 큰 프로젝트를 하다보면 많이 만날 수 있다. 당연히 상속을 받고 재정의를 하게 될 경우 사용해야한다. 하지만, 포스팅을 하는 입장에서.. 소스코드가 너무 길어지고 하니 해당 키워드는 “생략” 하도록 하겠다. (있다고 생각해주길 바란다.. 착한 사람 눈에는 보일 것이다.. 😇)