카테고리 없음

C++ 기초 - 상속과 다형성

이개 2026. 4. 7. 20:58

자식 클래스 정의 방법

class 자식클래스명 : public 부모클래스명
{
};

class Cat: public Animal
{
};

 

 

상속 관계에서 생성자/소멸자

생성자

부모 클래스의 생성자 호출 -> 자식 클래스의 생성자 호출

소멸자

자식클래스의 소멸자 호출 -> 부모 클래스의 소멸자 호출 

 

 

다형성

// Animal.h

#pragma once

class Animal
{
public:
	Animal();
	
	virtual ~Animal();

	//void MakeSound();
	virtual void MakeSound();
		// 자식 클래스마다 행동을 다르게 구현해야하는 멤버 함수 앞에 virtual 키워드를 붙여 줍니다.

	//void Eat();
	virtual void Eat();
		// virtual 키워드가 붙은 멤버 함수를 "가상 함수"라고 합니다.
	
};

 

가상함수로 선언한 함수는 자식 클래스에서 자식의 함수가 호출된다.

 

 

순수 가상함수

// Animal.h

#pragma once

class Animal
{
public:
	Animal();

	virtual ~Animal();

	virtual void MakeSound();

	//virtual void Eat();
	virtual void Eat() = 0;
		// 가상 함수의 맨 뒤에 '= 0'을 붙이면 순수 가상 함수가 됩니다.

};

 

자식 클래스에서 반드시 구현해야하는 함수는 선언 뒤에 0을 붙여준다.

 

 

추상 클래스

순수 가상 함수를 1개 이상 가지고 있는 클래스는 추상 클래스라고 한다.

추상 클래스는 객체를 만들 수 없다.

 

인터페이스

C++에서는 인터페이스라는 클래스를 지원하고 있지 않고, 추상 클래스를 인터페이스처럼 사용하고 있다.

 

인터페이스가 없는 클래스

// Monster.h

class Monster
{


};

// Orc.h

class Orc : public Monster
{

};


// Zombie.h

class Zombie : public Monster
{


};

// 가고일.h

class 가고일 : public Monster
{


};

// Main.cpp

int main()
{
	// 기획자님이 오셔서 "발 묶기"스킬을 구현하자고 의논했다.
	// 발이 달린 몬스터들에게 스턴을 거는 스킬.
	
	Monster* Monsters[1000];
	
	// 주변에 있는 모든 몬스터를 탐색.
	
	for (int i = 0; i < 1000; ++i)
	{
		if (Monster[i] == Orc)
		{
			// 발 묶기 스킬 적용!!!
		}
		if (Monster[i] == Zombie)
		{
			// 발 묶기 스킬 적용!!!
		}
		if (Monster[i] == 가고일)
		{
			// 발 묶기 스킬 실패...
		}
		
		// 실제 게임에서 위 세 종류의 몬스터만 있을까?... No
		// 300 종류의 몬스터는 있을듯..? 300개의 if문 필요. 더 늘어나면 문제...
	}

	return 0;
}

 

인터페이스가 있는 클래스

// Monster.h

class Monster
{


};

// IWalkable.h

class IWalkable
{
public:
	virtual void BindFoot() = 0;

}

// Orc.h

class Orc 
	: public Monster
	, public IWalkable
{

	virtual void BindFoot()
	{
		// 발묶기 성공! 관련 로직이 있습니다...
	}
	
};


// Zombie.h

class Zombie 
	: public Monster
	, public IWalkable
{
	virtual void BindFoot()
	{
		// 발묶기 성공! 관련 로직이 있습니다...
	}

};

// 가고일.h

class 가고일 
	: public Monster 
{

};

// Main.cpp

int main()
{
	// 기획자님이 오셔서 "발 묶기"스킬을 구현하자고 의논했다.
	// 발이 달린 몬스터들에게 스턴을 거는 스킬.
	
	IWalkable* WalkableMonsters[100];
	
	// 주변에 있는 걷는 애들만 모을 수 있음.
	
	for (int i = 0; i < 100; ++i)
	{
		WalkableMonsters[i]->BindFoot();
	}

	return 0;
}

 

IWalkable의 I는 인터페이스라는 뜻의 접두어다.

Monster를 상속받고, 다중 상속을 통해 IWalkable 인터페이스를 상속받으면 Monster의 기능과 IWalkable의 걷기 인터페이스 구현 의무를 가진 클래스가 된다.

 

IWalkable* WalkableMonsters[100];
// IWalkable을 상속받은 애들만 담을 수 있는 배열

for (int i = 0; i < 100; ++i)
{
    WalkableMonsters[i]->BindFoot(); // 각자의 BindFoot 호출
}

 

// 인터페이스 없이 구현한다면?
if (Orc)    { Orc->BindFoot(); }
if (Zombie) { Zombie->BindFoot(); }
// 몬스터 추가될 때마다 if문 추가해야 함 ❌

// 인터페이스 사용하면?
WalkableMonsters[i]->BindFoot();
// 몬스터가 100개 추가돼도 코드 변경 없음 ✅

 

추상클래스는 객체 생성이 불가해서 IWalkable* walk; 처럼 포인터로 선언해주어야 한다.

 

상속 관계를 is-a 관계라고도 한다.

 

접근 제어자별 상속 방법

class 자식클래스명 : 접근제어자 부모클래스명 {};

class Cat : public Animal {};

class Honda : private Car {};

class AndroidPhone : protected Phone {};

 

 

다중 상속

// Artist.h
class Artist
{
...
};

// Programmer.h
class Programmer
{
    ...
};

// TechnicalArtist.h
class TechnicalArtist : public Artist, public Programmer
{
    ...
};

 

클래스 명 뒤에 콜론+접근제어자+클래스명+쉼표... 로 구분한다.

 

자식 클래스에서 상속받는 클래스 순서대로 호출된다. 초기화 리스트의 순서와는 상관없다.

class Liger
	: public Lion  // Lion() 생성자가 먼저 호출됨.
	, public Tiger // 뒤이어 Tiger() 생성자가 호출됨.
{
public:
	Liger()
		: Tiger() // 아무리 초기화 리스트에서 이 순서로 호출한다 해도 상속 받는 순서대로 호출됨.
		, Lion()
	{
	}

}

 

 

다중 상속의 문제점

MakeSound()가 Lion 클래스와 Tiger 클래스에 모두 정의되어 있다면?

범위 지정 연산자를 꼭 써줘야한다.

Liger* Liger01 = new Liger();
Liger01->Lion::MakeSound();

 

다중 상속은 최대한 쓰지 않고, 쓰더라도 인터페이스를 통해 상속받도록 한다.

 

 

메모리 관점에서의 상속

// Animal.cpp
Animal::Animal(int InAge)
	: Age(InAge)
{
}

// Cat.cpp
Cat::Cat(int InAge)
	: Animal(InAge)
	, Flexibility(999.0f)
{
}

// Main.cpp
Cat* MyCat = new Cat(3);
	// 1. 힙 메모리에 Cat 객체의 크기만큼 동적 할당됨.
	// 2. Animal 클래스 생성자가 먼저 호출됨. 동적 할당 받은 메모리 시작 주소의 앞부분부터 초기화됨.
	// 3. 그다음 Cat 클래스 생성자가 호출됨. 그 다음 위치부터 연속적으로 초기화.
	// 4. 동적 할당된 메모리 시작 주소가 스택 메모리에 선언된 MyCat 변수에 담김.

 

 

가상 상속

상속받을 때 virtual 키워드를 붙이면, 최하위 자식 클래스에서 조상 클래스의 인스턴스를 단 하나만 생성하여 공유

class Animal { public: int age; };

// 가상 상속 사용
class Lion : virtual public Animal { ... };
class Tiger : virtual public Animal { ... };

// 이제 Liger 내부에는 Animal이 딱 하나만 존재함
class Liger : public Lion, public Tiger { ... };

 

 

this 키워드

해당 멤버 함수를 호출한 객체 자신의 메모리 주소를 담은 포인터. 객체 자신을 가리킨다고도 한다.

 

멤버 함수의 저장 위치

멤버 함수는 코드 섹션에 있다. 멤버 함수는 인자로 해당 객체(this)를 받는 전역함수처럼 동작한다. 즉, 코드 섹션에 하나의 전역함수처럼 두고 모든 객체가 공유해서 쓴다고도 볼 수 있다.

// 아래는 수도코드입니다. 컴파일러에 따라 다릅니다.

MyCat->GetAge();
    // movl MyCat, %ecx           // MyCat 주소를 ecx에 저장하고
    // call Cat::GetAge           // GetAge() 멤버 함수를 호출합니다.

YourCat->GetAge();
    // movl YourCat, %ecx         // YourCat 주소를 ecx에 저장하고
    // call Cat::GetAge           // GetAge() 멤버 함수를 호출합니다.
    
// 즉, 만약 Animal::GetAge()을 구현 한 뒤, Cat은 상속만 받고 해당 함수를 오버라이딩하지 않았다면
// MyCat 객체가 호출하든 YourCat 객체가 호출하든 같은 함수를 호출하는 것입니다.

 

 

정적 바인딩

C++은 기본적으로 어떤 멤버 함수가 호출될 것인지 컴파일 타임에 결정. 이를 정적 바인딩이라고 하고, 컴파일 타임의 자료형(정적 자료형)에 따라 어떤 멤버함수가 호출될 것인지 결정

 

위와 같이 선언하면 Cat01의 정적 자료형은 Animal*이므로 MakeSoudn() 호출 시 Animal::MakeSound()가 호출된다.

Cat*로 선언해야만 Cat의 MakeSound()가 호출된다.

 

자료형으로 따지면 Animal* 자료형이기 때문에 Animal 크기만큼만 읽어온다.

 

 

동적 바인딩

멤버 함수에 virtual 붙여주면 가상함수가 되어 오버라이드할 수 있다.

C++ 컴파일러는 가상 함수 호출을 구현하기 위해 가상 함수 테이블을 활용한다.

 

VS 컴파일러 기준, _vfptr이라는 멤버가 자동으로 추가된다. 가상 함수 테이블은 클래스 단위에 생성된다. 객체는 _vfptr 멤버를 통해 해당 테이블 주소를 소유한다.

 

 

모든 함수를 가상 함수로 만들지 않는 이유

가상 함수 호출은 가상 함수 테이블을 통해 함수 주소를 읽어온 뒤 그 주소로 이동하여 호출하게 된다.

이 과정에서 일반 멤버 함수 호출보다 약간의 오버헤드가 발생한다. 자원 낭비를 막기위해 필요한 만큼만 선언한다.

 

다형성

다형성을 활용한다면 Animal* Animals[60] 배열을 만들어 모두 저장하고, 각 가상 함수 구현을 호출하면 실체에 맞게 호출된다.

 

final 키워드

final 키워드가 붙은 클래스는 더이상 상속 불가능하다.

final 키워드가 붙은 클래스에 정의된 가상 멤버 함수를 재정의할 자식 클래스는 존재할 수 없다.

방어적 키워드

 

가상 소멸자

상속될 가능성이 있는 클래스라면 무조건 virtual 키워드를 소멸자에 붙여야한다.

 

상속 관계에서 객체가 소멸할 때 자식 소멸자가 먼저 실행되고, 그 다음에 부모 소멸자가 실행되는 것이 정상적인 순서지만, 문제는 Animal* cat = new Cat();처럼 부모 타입의 포인터로 자식을 가리키고 있을 때 발생한다.

 

만약 virtual 소멸자가 없다면, delete Cat;을 했을 때 부모 클래스(Animal) 부분만 메모리에서 해제되고, 자식 클래스(Cat)에만 있는 데이터는 메모리에 그대로 남는 메모리 누수(Memory Leak)가 발생한다.

// Main.cpp

#include <iostream>
#include <crtdbg.h> // _CrtSetDbgFlag(), _CrtSetBreakAlloc()

#include "Animal.h"
#include "Cat.h"

int main()
{
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	Animal* Cat01 = new Cat(5);

	Cat01->MakeSound();

	delete Cat01;
	Cat01 = nullptr;

	return 0;
}

 

 

추상 클래스와 인터페이스

순수 가상함수가 1개 이상 선언된 클래스를 추상 클래스라 한다.

추상 클래스는 객체를 만들 수 없다.

객체를 만들 수 없어도, 포인터나 참조형으로는 사용 가능하다.

// Animal.h

class Animal
{
public:
	virtual void Eat() = 0;
	
};

// Main.cpp

Animal MyAnimal01;                 // 컴파일 될까요 안될까요?
Animal* MyAnimal02;                // 컴파일 될까요 안될까요?
Animal* MyAnimal03 = new Animal(); // 컴파일 될까요 안될까요?
Animal* MyCat = new Cat();         // Cat 클래스에 Eat() 순수 가상 함수가 재정의 되어 있다는 가정하에 컴파일 됩니다.
Animal& MyCatRef = *MyCat;         // 컴파일 될까요 안될까요?

 

 

인터페이스

C++은 인터페이스를 지원하지 않지만, 추상 클래스를 인터페이스처럼 사용할 수 있다.

순수 가상 함수만 가지게끔 작성하고, 멤버 변수도 두지 않는다. 다만 따로 문법이 있는 게 아니므로, 멤버 변수가 꼭 없어야만 하는 것은 아니다.

 

다중 상속으로 인터페이스를 사용하는 예시

// MonsterBase.h
class MonsterBase
{
};

// IWalkable.h
class IWalkable
{
public:
	virtual void OnBound() = 0;
};

// Orc.h
class Orc
	: public MonsterBase
	, public IWalkable
{
	virtual void OnBound() { bCannotMove = true; }
};

// Troll.h
class Troll
	: public MonsterBase
	, public IWalkable
{
	virtual void OnBound() { bCannotMove = true; }
};

// 가고일.h
class 가고일
	: public MonsterBase
{
};

// Main.cpp

int main(void)
{
	IWalkable* AllWalkableMonster[1024];

	// ... 대충 근처의 "걸어다니는" 몬스터 개체를 AllWalkableMonster[]에 모으는 코드 ...

	for (int i = 0; i < 1024; ++i)
	{
		AllWalkableMonster[i]->OnBound();
	}

	return 0;
}

 

IWalkable을 상속받는지는 dynamic_cast로 확인할 수 있다.

for (MonsterBase* monster : AllMonstersInWorld)
{
    // [핵심] dynamic_cast: 
    // "이 몬스터(MonsterBase*)가 IWalkable을 상속받은 놈이면 주소를 주고, 아니면 NULL을 줘!"
    IWalkable* walkablePointer = dynamic_cast<IWalkable*>(monster);

    if (walkablePointer != nullptr) // NULL이 아니면 "걷는 놈" 확정!
    {
        if (walkableCount < 1024) 
        {
            AllWalkableMonster[walkableCount] = walkablePointer;
            walkableCount++;
        }
    }
}