상속

김성진·2024년 1월 29일

1월 29일 수업 정리

c++ 클래스의 주요 기능인 상속에 대해 알아봤다.

상속

클래스 Animal 을 만들었다고 하자. Animal에는 기본 기능인 breath 메소드와 age 변수가 있다. 이때 Animal의 기본 기능들을 포함하는 새로운 클래스 dog, bird 등을 만들어서 숨쉬기, 나이 등은 물론 walk, fly 등의 독자적인 메소드를 추가하면 이들은 Animal 클래스의 자식이 된다. 이렇게 부모 클래스의 멤버를 물려받아 정의하지 않아도 사용할 수 있는것을 상속이라고 한다.

자식클래스 : public 부모클래스 같은 식으로 선언하면 된다.

protected

새로운 접근 제어 지시자 protected는 상속과 관련되어 있다.
이 접근 제어 지시는 외부에서 접근할 수 없으나 자식 클래스에서 접근하는것만큼은 허용한다.

상속이 일어나는 시기

객체를 만들었을때, 그 객체가 상속관계에 있으면 그 위의 부모객체들의 생성자도 1회이상씩 전부 호출된다. 소멸자도 마찬가지로 생성자와 반대의 순서로 호출된다.

Animal, dog, sparrow 클래스를 직접 만들어서 생성자와 소멸자가 나타나는 시기를 관찰해보자.

#include <iostream>

class Animal
{
public:

	Animal() : age(0)
	{
		std::cout << "Animal()" << std::endl;
	}

	~Animal() 
	{
		std::cout << "~Animal()" << std::endl;
	}

	void Breathe() { std::cout << "숨을 쉰다." << std::endl; }
	int age;
};

class Dog : public Animal
{
public :
	Dog()
	{ 
		std::cout << "Dog()" << std::endl;
	}

	~Dog()
	{
		std::cout << "~Dog()" << std::endl;
	}

	void Walk() { std::cout << "걷는다." << std::endl; }
};
class Sparrow : public Animal
{
public:
	Sparrow()
	{
		std::cout << "Sparrow()" << std::endl;
	}

	~Sparrow()
	{
		std::cout << "~Sparrow()" << std::endl;
	}

	void Walk() { std::cout << "걷는다." << std::endl; }

	void Fly() { std::cout << "난다." << std::endl; }

};

int main(void)
{
	Animal animal;
	animal.Breathe();
	

	Dog dog;
	dog.Breathe();
	dog.age = 20;

	std::cout << dog.age << std::endl;
	dog.Walk();
	
	Sparrow sparrow;
	sparrow.Breathe();
	sparrow.age = 10;
	sparrow.Fly();
	return 0;
}

생성자 작성시 주의할 점

부모클래스에서 매개변수를 받는 생성자를 만들었을 경우,
기본 생성자가 없어지게 되고 이후 자식 클래스들이 전부 기본 생성자가 없다는 컴파일 에러가 난다.
자식 클래스는 생성자가 명시되지 않으면 부모 클래스의 기본 생성자를 가져온다.
기본 생성자 작성을 습관화 하자.

또한 생성자와 소멸자는 객체 자신의 것만 건드리는것이 기본이다.

  TextMessage(int sendTime, string sendName, string text) {
        this->sendTime = sendTime;
        this->sendName = sendName;
        this->text = text;

여기서 sendTime과 sendName은 상속받은 변수일 때, 부모의 변수를 초기화 하는것은 오류가 날 수 있다.

class TextMessage : public Message {
public:
    TextMessage(int sendTime, string sendName, string text) : Message(sendTime, sendName), text(text) {
    }

이렇게 Message(sendTime, sendName)로 부모 클래스의 인자를 받아오기만 하면 text만 초기화 할 수 있다.

오버라이딩

부모 클래스와 자식 클래스에 같은 이름의 메소드가 있을 때, 메소드를 호출하면 어떻게 될까? 부모 클래스에서 호출했으면 부모의 것이, 자식 클래스에서 호출하면 자식의 것이 출력된다. 기본적으로 자식에서 선언된 멤버가 부모의 것보다 우선되기 때문이다. 따라서 이렇게 고의로 같은 이름의 멤버를 선언하는것을 오버라이딩이라고 한다.
오버라이딩을 했다면 함수 뒤에 override 를 붙여주면 좋다. 없어도 되지만 보는이가 헷갈리지 않게 써주는것이다.

또한, 부모 클래스 타입을 가리키는 포인터로 자식 클래스의 객체를 가리켜도 문제가 없다. 자식 클래스를 부모처럼 취급해도 되는 것이다. 자식은 기본적으로 부모의 타입을 가지고 있기 때문에 어느정도 잘려나가지도 표현은 제대로 되는것이다.

가상함수

오버라이딩을 알았다면, 가상함수를 사용할 수 있다. 함수 앞에 virtual 을 붙이면 그 함수는 가상함수가 된다. 가상함수의 역할은, 오버라이딩 된 함수가 있다면 가상함수를 무시하고 비가상함수를 실행하는 것이다. 물론 가상함수만 있다면 가상함수를 실행한다. 부모 클래스에서 자식 클래스의 함수를 호출할 수 있는것이다.

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

순수 가상함수는, 실체가 없는 함수이다.
virtual double GetArea() const = 0; 의 꼴로 선언된다.
함수의 앞에 virtual, 뒤에 바디대신 = 0; 을 붙이면 된다.
이런것이 필요한 이유는 도형 클래스 -> 사각형과 삼각형을 구현할 때,
도형 클래스 자체는 설계도일 뿐이지 도형 객체 자체를 생성할 일은 없다.
이런 상황에 도형 클래스에 순수가상함수를 만들어 둔 뒤, 사각형과 삼각형에서 오버라이딩해서 사용하는 것이다.
순수 가상함수가 하나라도 들어가 있다면 추상 클래스라고 하며, 추상 클래스는 객체를 만들 수 없다. 컴파일 에러가 난다.

상속 형변환

  Message* hello3 = new TextMessage(111, "google", "하이");

이런 꼴이 컴파일이 될까? 된다. 어째서일까?
TextMessage가 자식 클래스라 부모 클래스인 Message로 형변환 가능하기 때문이다.
"하이" 부분이 잘려나가고 앞에 두 변수만 출력될 것이다. message는 보낸 시간, 보낸이만 알지 내용의 대한 함수는 가지고 있지 않기 때문이다.

이런식으로 자식에서 부모로 형변환 하는것을 업 캐스팅이라고 부른다. 부작용이 없다.
하지만 부모에서 자식으로 형변환 하는 다운캐스팅은 문제가 생길 수 있다.
묵시적으로 일어날 수도 없고 () 를 통해 강제로 바꿔주는것만이 허용된다.

c++에서는 다른 방법이 있다.
Derived2 d2 = static_cast<Derived2>(b); 처럼
static_cast 를 사용하는 것이다. 정적 형변환인데, b를 Derived2*형으로 바꾸겠다는 뜻이다.
정적이기 때문에 컴파일 시기에 형변환을 강행하지만, 컴파일러는 이것이 제대로 된 것인지 모른다. 따라서 다운 캐스팅을 시도할 때는 정말 신중하게 해야한다.

dinamic_cast

정적인 형변환이 있다면, 동적인 형변환도 있다. dinamic cast는 런타임중에 형변환을 하게 되는데, 형변환을 하는 대상이 올바르면 그대로 성공하고, 실패하면 NULL을 반환하게 된다. 이러한 성질 때문에 정적 형변환보다 더 안전하다.
null체크를 하거나, 혹은 일부러 형태가 다른 여러 객체들을 집어넣고 원하는 결과만 얻고 나머지는 null처리되게 하는 방법도 있다.
단점으로는 프로그램 성능이 굉장히 저하된다는 점이 있다.
동적 형변환은 편하지만 오버라이딩과 가상함수를 잘 사용하면 비슷한 결과를 낼 수 있어 되도록 지양하도록 하자.

업캐스팅 예제

구조체 Animal을 만들어서 1과 2를 가지게 하고, Flying Animal 클래스를 상속시켜서 3을 추가해 1 2 3 을 가지게 했다.
그후 1 2 1 2 1 2 를 반복해서 출력되게 해보자.

#include <iostream>

struct Animal
{
	float xpos = 1;
	float ypos = 2;
};

struct FlyingAnimal : public Animal
{
	float zpos = 3;
};

void PrintAnimals(Animal* a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		std::cout << "(" << a[i].xpos << "," << a[i].ypos << ")" << std::endl;
	}
}

int main()
{
	FlyingAnimal* arr = new FlyingAnimal[5];
	PrintAnimals(arr, 5);
	return 0;
}

결과는 12 31 23 12 31 처럼 123이 두개씩 끊어져 나온다.
원하는 결과가 아니다.
이 코드는 플라잉애니멀로 123123123 의 배열을 만들고 출력한 것이다.
원하는 결과를 위해서는 애니멀의 포인터 배열을 만들어서 배열마다 플라잉애니멀의 배열을 하나씩 넣어서,
1 2 3 4 5 6 7 의 애니멀 배열이 있다면 각 요소마다 123123 의 플라잉애니멀을 넣어두는 것이다.
이 방법의 장점은 중간에 플라잉애니멀이 아닌 그냥 애니멀이 들어가도 상관업다는 점이다.
이후 출력시에 3을 무시하고 1 2만 나오게 하면 된다. 고쳐보자.

#include <iostream>

struct Animal
{
	float xpos = 1;
	float ypos = 2;
};

struct FlyingAnimal : public Animal
{
	float zpos = 3;
};

void PrintAnimals(Animal** a, int n)
{
	for (int i = 0; i < n; ++i)
	{
		std::cout << "(" << a[i]->xpos << "," << a[i]->ypos << ")" << std::endl;
	}
}

int main()
{
	Animal** arr = new Animal* [5]; // 동적 할당한 애니멀은 new Animal[5] 인데 애니멀 포인터니까 동적할당이 없음
	//애니멀 포인터들의 배열이 만들어진거지 애니멀 자체는 안만들어짐
	
	for (int i = 0; i < 5; ++i)
	{
		arr[i] = new FlyingAnimal();   //여기서 동적할당 5번
	}


	PrintAnimals(arr, 5);


	for (int i = 0; i < 5; ++i)
	{
		delete arr[i];  //여기서 5번 해제
	}
	
	delete[] arr; // arr부터 지워버리면 각 배열에 접근 못하니까 마지막에.

	return 0;
}

원하는 결과가 나온다. 동적 할당을 5번 했기에 반드시 5번 해제해야한다.
또한 arr 배열 자체를 먼저 해제하면 이후 배열에 접근할 방법이 없으니 가장 마지막에 해제해야 한다.

profile
듀얼리스트

0개의 댓글