- 상속과 다형성 몰아보기
더보기
- 상속(Inheritance)
공통 기능을 부모에 모아두고, 자식이 그걸 기반으로 확장하는 방식입니다.
자식 클래스는 부모 클래스의 멤버 변수와 멤버 함수를 그대로 사용할 수 있으면서,
본인 클래스만의 멤버 변수와 멤버 함수도 추가할 수 있습니다. - 상속은 왜 필요할까요?
- 코드 재사용성
똑같은 멤버 변수와 똑같은 멤버 함수들을 가진 두 클래스가 있다면
해당 멤버 변수와 멤버 함수를 부모 클래스로 묶어버릴 수 있습니다. - 확장성
공통 부분은 부모 클래스에 두고, 자식 클래스만의 멤버 함수와 멤버 변수들을 가지면서
자식 클래스의 기능을 좀 더 확장 할 수 있습니다. - 유지보수에 유리함
공통 부분을 부모 클래스에 묶어두면, 부모 클래스 한 곳만 수정하면
많은 자식 클래스의 부분에도 자동으로 적용됩니다.
잘못 바꾸면 영향을 미치는 곳이 많다는 것도 단점이긴 하나,
제대로 바꾼다면 오히려 장점입니다
- 코드 재사용성
- 자식 클래스를 정의하는 방법
class 자식클래스명 : public 부모클래스명
{
};
class Cat : public Animal
{
}; - 멤버 함수와 멤버 변수
OOP에서의 멤버 함수는 해당 객체의 “행동” 혹은 “동작”을 의미합니다.
멤버 변수는 해당 객체의 “속성”을 의미합니다
class Animal
{
public:
void MakeSound();
void Eat();
// 동물이라면 할만한 행동들을 멤버 함수로 정의합니다.
public:
unsigned int Age;
// 동물이라면 가질만한 속성들을 멤버 변수로 정의합니다.
}; - 상속 관계에서 생성자/소멸자
상속 관계에 있다면, 자식 클래스의 객체가 만들어지거나 지워질 때
부모 클래스의 생성자와 소멸자도 같이 호출됩니다.
생성자는 부모 클래스의 생성자가 먼저 호출됩니다.
그 다음으로 자식 클래스의 생성자가 호출됩니다.
마치 자식은 부모님이 밥숟가락 들기 전에 밥숟가락을 들지 않는 것과 비슷합니다.
반대로 소멸자는 자식 클래스의 소멸자가 먼저 호출됩니다.
// 명시적 호출.
Cat::Cat(int InAge)
: Animal(InAge)
{
}
// 암시적 호출.
Cat::Cat(int InAge)
{
}
delete MyCat;
// ~Cat()이 실행된 뒤에 ~Animal()이 실행됨. - 다형성
우리는 개나 고양이는 본 적 있지만, “동물”이라는 것을 실제로 본 적이 없습니다.
그러니 동물이 먹는 행동을 하거나, 소리를 내는 행동을 어떻게 하는지 정의할 수 없었습니다
즉, “무늬는 같으나(똑같이 동물인데), 행동은 다르게(먹거나 소리 내는건 다르게)”가 필요합니다.
자료형은 부모 자료형으로 같으나, 실제 멤버 함수는 다른 멤버 함수가 호출되는 성질을 다형성이라고 합니다. - 다형성을 구현하는 방법
// Animal.h
#pragma once
class Animal
{
public:
Animal();
virtual ~Animal();
//void MakeSound();
virtual void MakeSound();
// 자식 클래스마다 행동을 다르게 구현해야하는 멤버 함수 앞에 virtual 키워드를 붙여 줍니다.
//void Eat();
virtual void Eat();
// virtual 키워드가 붙은 멤버 함수를 "가상 함수"라고 합니다.
};
// Dog.h
#pragma once
#include "Animal.h"
class Dog : public Animal
{
public:
Dog();
virtual ~Dog();
virtual void MakeSound();
// 부모 클래스의 가상 함수를 재정의합니다. 이를 "오버라이드(Override)"라고 합니다.
virtual void Eat();
}; - 가상 함수를 의역하자면,
가상이란 “실체가 없다”는 뜻입니다.
OOP에서의 가상은 “뭘 할지 정해진게 없다.”는 뜻입니다.
”자식들 중 누가 호출하는지에 따라 다른 로직이 수행된다.”
ex) 동물이라면 소리를 내곤 하는데, 어떻게 소리 낼지는 자식들(개, 고양이, ...)에 따라 다르다. - 순수 가상 함수
무릇, 동물이라 함은 무조건 먹어야 합니다. 소리는 못 내더라도, 먹어야 생명이 유지됩니다.
이처럼 무조건 이 행동을 자식 클래스에서 정의하게끔 강제할 필요가 있을때, 순수 가상 함수 문법을 사용합니다.
순수 가상 함수는 자식 클래스에서 구현하지 않으면 컴파일 에러가 납니다.
// Animal.h
#pragma once
class Animal
{
public:
Animal();
virtual ~Animal();
virtual void MakeSound();
//virtual void Eat();
virtual void Eat() = 0;
// 가상 함수의 맨 뒤에 '= 0'을 붙이면 순수 가상 함수가 됩니다.
}; - 순수 가상 함수를 의역하자면,
순수는 “없다.”는 뜻. 여기서는 “함수의 정의가 없다.”는 뜻. 혹은 “함수를 정의할 수 없다.”
여기에 가상 함수 의역까지 붙여서 정리하자면,
“부모 클래스에서는 정의 못하겠고, 자식들 중 누가 호출하는지에 따라 다른 로직이 수행된다.” - 추상 클래스
순수 가상 함수를 1개 이상 가지고 있는 클래스를 추상 클래스라고 합니다.
추상 클래스는 객체를 만들 수 없습니다.
쉽게 말해서, 클래스에 정의할 수 없는 함수가 있는 클래스입니다.
클래스를 설계도에 비유한다면, 구멍난 부분이 있는 설계도가 추상 클래스입니다.
구멍난 부분이 있는데 제품, 즉 객체를 만들 수 있을까요? 안됩니다.
또한 “동물”은 개념입니다. 진짜 실제로 돌아다니는 물질이 아닙니다. 객체를 만들 수 없는게 당연합니다. - “인터페이스”
사실, C++에서 인터페이스라는 클래스를 문법적으로 지원하지는 않습니다.
추상 클래스를 마치 인터페이스처럼 사용하고 있다고 보면 됩니다.
- 상속
더보기
- 상속(Inheritance)
공통 기능을 부모에 모아두고, 자식이 그걸 기반으로 확장하는 방식입니다.
자식 클래스는 부모 클래스의 멤버 변수와 멤버 함수를 그대로 사용할 수 있으면서,
본인 클래스만의 멤버 변수와 멤버 함수도 추가할 수 있습니다.
부모 클래스 == 베이스 클래스(Base Class)
자식 클래스 == 파생 클래스(Derived Class) - 상속 관계를 다른 말로 is-a 관계라고도 합니다.
ex) Cat is-a Animal, Dog is-a Animal.
단, Animal is-a Cat은 아닙니다. - 자식 클래스를 정의하는 방법
class 자식클래스명 : 접근제어자 부모클래스명 {};
class Cat : public Animal {};
class Honda : private Car {};
class AndroidPhone : protected Phone {}; - 자식 클래스의 상속 접근 제어자
거의 모든 경우에 public을 사용합니다.
- 상속 접근 제어자 예시
Animal 클래스에 public 접근 제어자로 Move(); 메서드가 구현되어 있다고 가정해봅시다.
이때 Cat 클래스가 Animal 클래스를 public 접근제어자로 상속 접근을 했다면 아래와 같은 코드는 전혀 문제 없이 작동합니다.
하지만 private 접근제어자로 상속 접근을 했다면 Move(); 메서드 역시 private을 따릅니다.
따라서 아래 코드는 컴파일 에러가 납니다.
Cat MyCat;
MyCat.Move(); - 메모리 관점에서의 상속
// 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 변수에 담김. - 상속 관계에서 생성자 호출 순서
부모 클래스의 생성자가 먼저 호출됩니다. (명시적 혹은 암시적으로)
그 다음으로 자식 클래스의 생성자가 호출됩니다.
부모 클래스의 특정 생성자를 호출할 때는 반드시 초기화 리스트를 사용해야 합니다. - 상속과 소멸자
자식클래스의 소멸자가 먼저 호출된 후에 부모 클래스의 소멸자가 호출됩니다.
생성자 호출 순서와 정반대입니다.
delete MyCat;
// ~Cat()이 실행된 뒤에 ~Animal()이 실행됨.
- this 키워드와 멤버 함수
더보기
- this 키워드
해당 멤버 함수를 호출한 객체 자신의 메모리 주소를 담은 포인터입니다.
또는 객체 자신을 가리킨다고도 합니다.
멤버 함수는 혼자 실행되지 않습니다. 반드시 어떤 객체에 대해 호출됩니다.
이 어떤 객체가 누군지 알려주는 키워드가 this 키워드입니다.
MyCat->GetAge();
// 멤버 함수 GetAge()를 호출한 객체는 MyCat.
// this는 MyCat의 메모리 주소를 담은 포인터.
int Animal::GetAge()
{
return Age;
// 윗 줄의 Age 멤버 변수는 "누구의" 멤버 변수인지 생략되어 있습니다.
// 윗 줄은 return this->Age;와 같습니다.
} - this 키워드의 필요성
void Animal::SetAge(int Age)
{
Age = Age;
// 매개변수 Age에 매개변수 Age를 대입하므로, 의미가 없습니다.
// this를 활용해서, this->Age = Age;라고 적어주거나
// 매개변수명을 InAge로 수정해주면 좋습니다.
} - 멤버 함수의 비밀
int Animal::GetAge()
{
return this->Age;
// 겉으로 봤을 땐, this를 인자값으로 받는것도 아닌데
// 어떻게 갑자기 함수 안에서는 this를 사용할 수 있는지 궁금함.
}
MyAnimal->GetAge();
int GetAge(Animal* InThis)
{
return InThis->Age;
// 컴파일러 입장에서는 사실상 이 함수처럼 동작하게 됨.
}
GetAge(MyAnimal); - 멤버 함수의 저장 위치는 어딜까요?
멤버 변수들은 스택 메모리, 힙 메모리, 데이터 섹션에 있을 수 있다는 것을 배웠습니다.
그렇다면 멤버 함수는 어디에 존재할까요?
멤버 함수도 메모리 어딘가에 위치해 있습니다.
모든 것은 메모리 어딘가에 위치해 있어야만 합니다. 실은 코드 섹션에 있습니다. - 각 객체마다 멤버 함수의 메모리가 따로 따로 잡혀 있을까요?
MyCat->GetAge();이나 YourCat->GetAge();이나 동작이 완전 일치하는데도 따로 잡혀 있을까요?
아주 러프하게 보면 “멤버 함수는 인자로 해당 객체(this)를 받는 전역함수처럼 동작한다.”고도 말할 수 있습니다.
즉, 코드 섹션에 하나의 전역 함수처럼 두고 모든 객체가 공유해서 쓴다고도 볼 수 있습니다.
int GetAge(Animal* InThis)
{
return InThis->Age;
}
// 위와 같은 함수를 코드 섹션 어딘가에 하나만 두고 공유 - 저수준에서 바라본 멤버 함수의 호출
// 아래는 수도코드입니다. 컴파일러에 따라 다릅니다.
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 객체가 호출하든 같은 함수를 호출하는 것입니다.
- 정적 바인딩
더보기
- 정적 바인딩(Static Binding)
C++은 기본적으로 어떤 멤버 함수가 호출될 것인지 컴파일 타임에 결정합니다.
이를 정적 바인딩이라고 합니다.
즉, 컴파일 타임의 자료형(정적 자료형)에 따라 어떤 멤버 함수가 호출될 것인지 결정합니다.
- 다형성과 동적 바인딩, virtual 키워드
더보기
- 동적 바인딩(Dynamic Binding)
런타임 중에 어떤 함수를 호출할지 결정하는 것을 뜻합니다.
늦은 바인딩(Late Binding)이라고도 불립니다. - virtual 키워드와 가상 함수
virtual 키워드가 달린 멤버 함수를 가상 함수라고 합니다. - virtual 키워드가 붙은 부모 클래스의 가상 함수를 자식 클래스에서 재정의하는 것을
오버라이딩(Override)이라고 합니다.
오버로딩은 같은 이름의 함수를 매개변수 목록만 다르게해서 재정의하는 것입니다.
오버라이딩은 같은 이름의 멤버 함수를 같은 매개변수 목록과 같은 반환 자료형으로 재정의하는 것입니다.
둘 다 “함수를 재정의 한다.”는 것은 같습니다. 그래서 헷갈릴 수 있습니다. - 그럼 모든 멤버 함수를 가상 함수로 만들면 고민할 필요 없지 않나요?
가상 함수 호출은 가상 함수 테이블을 통해 함수 주소를 읽어온 뒤 그 주소로 이동하여 호출하게 됩니다.
이 과정에서 일반 멤버 함수 호출보다 약간의 오버헤드가 발생하게 됩니다.
매 초마다 60~300번씩 호출되는 함수도 보게 될 텐데,
모든 멤버 함수에 virtual 키워드를 남용하는 건 올바르지 않습니다. - 다형성
무늬는 하나인데, 실행 해 보니 다른 행동을 하더라.
여기서 무늬는 자료형(클래스)을 뜻하고 행동은 함수(멤버 함수)를 뜻합니다. - 다형성을 그래서 어떻게 활용하나요?
만약 새, 호랑이, 사람이 각각 20개씩 있다고 한다면,
다형성을 활용하지 않는다면 20개씩 총 3개의 새 / 호랑이 / 사람 배열을 만들어야합니다.
그리고 3개의 배열을 돌면서 MakeSound() 메서드를 호출하게 됩니다.
다형성을 활용한다면, Animal* Animals[60] 배열을 만들어서 싹다 저장할 수 있습니다.
그리고 순회를 돌며 Animals[i]->MakeSound()라고 호출하면 각 실체에 맞게 호출되게 됩니다. - final 키워드
final 키워드가 붙은 클래스는 더 이상 상속될 수 없습니다.
따라서 final 키워드가 붙은 클래스에 정의된 가상 멤버 함수를 재정의할 자식 클래스는 존재할 수 없습니다.
방어적인 프로그래밍을 위해 가능하다면 final 키워드를 사용하는 것이 좋습니다
- 가상 소멸자
더보기
- 가상 소멸자
virtual 키워드가 붙은 소멸자.
상속될 가능성이 있는 클래스라면 무조건 virtual 키워드를 소멸자에 붙여야 합니다.
모든 클래스는 final 키워드가 붙지 않은 이상, 언젠가 누군가가 상속 할 가능성은 열려 있기 때문입니다.
- 다중 상속
더보기
- C++에서는 다중 상속을 지원합니다.
// 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()
{
}
} - 다중 상속의 문제점 첫 번째 문제점
만약 Lion 클래스에도 MakeSound() 멤버 함수가 정의되어 있고,
Tiger 클래스에도 MakeSound() 멤버함수가 정의되어 있다면
Liger 객체는 어떤 MakeSound() 멤버 함수를 호출해야할까요?
범위 지정 연산자를 꼭 써줘야 알 수 있습니다
Liger* Liger01 = new Liger();
Liger01->Lion::MakeSound(); - 다중 상속의 두 번째 문제점
Lion 클래스와 Tiger 클래스가 서로 또 Animal 클래스를 상속 받는다면,
Liger에는 Animal 관련 메모리가 몇 개 있을까요?
당연히 2개이고, 모호함으로 인한 컴파일 에러가 나고 메모리 낭비이기도 합니다. - 가상 부모 클래스를 활용하면 다이아몬드 문제를 해결할 수 있습니다?
근데 사자와 호랑이를 섞는 라이거 클래스를 미래에 만들거라고 가정하고
미리 virtual 키워드로 상속 받는다? 이거도 말이 안됩니다.
// Animal.h
class Animal
{
...
};
// Tiger.h
class Tiger : virtual public Animal
{
...
};
// Lion.h
class Lion : virtual public Animal
{
...
};
// Liger.h
class Liger : public Tiger, public Lion
{
...
}; - 다중 상속을 최대한 쓰지 맙시다.
필요할 때는 인터페이스를 사용하여 다중 상속 받도록 합시다.
인터페이스에 대해서는 뒤에서 배우게 됩니다.
- 추상 클래스와 인터페이스
더보기
- 추상 클래스(Abstract Class ↔ Concrete Class)
순수 가상 함수가 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++은 자체적으로 인터페이스를 지원하지는 않습니다.
다만, 추상 클래스를 인터페이스처럼 사용할 순 있습니다.
순수 가상 함수만 가지게끔 작성하고, 멤버 변수도 따로 두지 않는 식으로 사용합니다.
물론 C++이 자체적으로 인터페이스를 지원하지는 않으므로, 지켜야 할 문법은 없습니다
그러니 멤버 변수가 꼭 없어야 한다던가 하는 제약은 없습니다.
class IFlyable
{
public:
virtual void Fly() = 0;
}; - 그래서 인터페이스가 왜 필요한걸까요?
MonsterBase를 상속 받은 Orc, Troll, 가고일이 있다고 가정해봅시다.
기획자님이 와서 “발 묶기”라는 플레이어 캐릭터의 스킬을 추가하고 싶다고 합니다.
근처에 “걸어 다니는” 몬스터에게만 적용되는 스킬이라고 합니다.
인터페이스가 없다면 아래와 같이 할 수 밖에 없습니다.
// MonsterBase.h
class MonsterBase
{
};
// Orc.h
class Orc : public MonsterBase
{
};
// Troll.h
class Troll : public MonsterBase
{
};
// 가고일.h
class 가고일 : public MonsterBase
{
};
// Main.cpp
int main(void)
{
MonsterBase* AllMonster[1024];
// ... 대충 근처의 몬스터 개체를 AllMonster[]에 모으는 코드 ...
for (int i = 0; i < 1024; ++i)
{
if (AllMonster[i]가 오크인가 ? ) { 발 묶기 성공 }
if (AllMonster[i]가 트롤인가 ? ) { 발 묶기 성공 }
if (AllMonster[i]가 가고일인가 ? ) { 발 묶기 실패 }
// 몬스터 종류가 3개 뿐인 게임은 거진 없습니다. 100개면 if문도 100개..
}
return 0;
} - 인터페이스를 활용하여 위 소스코드를 개선할 수 있습니다.
Orc, Troll, Zombie, ... 공통점은 "걸을 수 있다"는 것.
발묶기 스킬은 이들 모두에게 적용 되어야 합니다.
"걸어 다닐 수 있는"이라는 자료형을 만듭시다.얘네는 꼭 발이 묶여야 합니다.
IWalkable 이라는 인터페이스를 만들어서 다중상속 시켜봅시다.
// 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
, public IWalkable // 이게 맞을까요?
{
};
// Main.cpp
int main(void)
{
IWalkable* AllWalkableMonster[1024];
// ... 대충 근처의 "걸어다니는" 몬스터 개체를 AllWalkableMonster[]에 모으는 코드 ...
for (int i = 0; i < 1024; ++i)
{
AllWalkableMonster[i]->OnBound();
}
return 0;
}
'복습용 > C++' 카테고리의 다른 글
| [Unreal Engine 8기] C++ std::string과 File I/O (0) | 2026.03.25 |
|---|---|
| [Unreal Engine 8기] C++ Casting, inline 키워드, static 키워드, 예외처리 (0) | 2026.03.24 |
| [Unreal Engine 8기] C++ 복사 생성자와 오버로딩 (0) | 2026.03.20 |
| [Unreal Engine 8기] C++ 생성자와 소멸자 (0) | 2026.03.19 |
| [Unreal Engine 8기] C++ Console IO (0) | 2026.03.18 |