다중 상속이란?
기본 문법
class Lion : public Animal
{
public:
void roar() { std::cout << "Roar!" << std::endl; }
};
class Tiger : public Animal
{
public:
void growl() { std::cout << "Growl!" << std::endl; }
};
class Liger : public Lion, public Tiger // 다중 상속
{
public:
void speak() { std::cout << "Liger sound" << std::endl; }
};
문제 상황
class Animal
{
public:
Animal() { std::cout << "Animal constructor" << std::endl; }
virtual ~Animal() { std::cout << "Animal destructor" << std::endl; }
void breathe() { std::cout << "Breathing" << std::endl; }
protected:
int age;
};
class Lion : public Animal
{
public:
Lion() { std::cout << "Lion constructor" << std::endl; }
};
class Tiger : public Animal
{
public:
Tiger() { std::cout << "Tiger constructor" << std::endl; }
};
class Liger : public Lion, public Tiger
{
public:
Liger() { std::cout << "Liger constructor" << std::endl; }
};
int main()
{
Liger liger;
// 출력:
// Animal constructor ← Lion을 위한 Animal
// Lion constructor
// Animal constructor ← Tiger를 위한 Animal (중복!)
// Tiger constructor
// Liger constructor
// liger.breathe(); // ❌ 컴파일 에러! Lion의 breathe? Tiger의 breathe?
// liger.age = 5; // ❌ 컴파일 에러! 어느 Animal의 age?
return 0;
}
문제점:
1. Animal 생성자가 2번 호출됨 (메모리 낭비)
2. Liger 객체 안에 Animal이 2개 존재 (모호성)
3. Animal의 멤버에 접근할 때 어느 것인지 불명확
메모리 구조 (일반 다중 상속)
Liger 객체:
┌────────────────┐
│ Lion 부분 │
│ - Animal │ ← 첫 번째 Animal
│ - Lion 데이터 │
├─────────────────┤
│ Tiger 부분 │
│ - Animal │ ← 두 번째 Animal (중복!)
│ - Tiger 데이터│
├─────────────────┤
│ Liger 데이터 │
└─────────────────┘
해결 방법: Virtual Inheritance 사용
class Animal
{
public:
Animal() { std::cout << "Animal constructor" << std::endl; }
virtual ~Animal() { std::cout << "Animal destructor" << std::endl; }
virtual void speak() = 0;
protected:
double animalData; // 8 bytes
};
class Lion : public virtual Animal // ✅ virtual 키워드 추가
{
public:
Lion() { std::cout << "Lion constructor" << std::endl; }
void speak() override { std::cout << "Roar!" << std::endl; }
protected:
double lionData; // 8 bytes
};
class Tiger : public virtual Animal // ✅ virtual 키워드 추가
{
public:
Tiger() { std::cout << "Tiger constructor" << std::endl; }
void speak() override { std::cout << "Growl!" << std::endl; }
protected:
double tigerData; // 8 bytes
};
class Liger : public Lion, public Tiger
{
public:
Liger() { std::cout << "Liger constructor" << std::endl; }
void speak() override { std::cout << "Liger sound" << std::endl; }
};
int main()
{
Liger liger;
// 출력:
// Animal constructor ← 단 1번만 호출됨! ✅
// Lion constructor
// Tiger constructor
// Liger constructor
liger.speak(); // ✅ OK - "Liger sound"
// liger.animalData = 5.0; // ✅ 모호성 해결! (protected라 직접 접근은 불가)
return 0;
}
가상 상속의 효과
Animal 생성자가 단 1번만 호출됨Liger 객체 안에 Animal이 1개만 존재일반 상속 vs 가상 상속의 메모리 크기 변화
1. 일반 상속 (Lion만 있을 때)
class Lion : public Animal
{
protected:
double lionData; // 8 bytes
};
// sizeof(Lion) = 24 bytes
// [vptr: 8] [animalData: 8] [lionData: 8]
2. 가상 상속 (Lion만 있을 때)
class Lion : public virtual Animal
{
protected:
double lionData; // 8 bytes
};
// sizeof(Lion) = 32 bytes
// [Lion vptr: 8] [lionData: 8] [Animal vptr: 8] [animalData: 8]
메모리 구조 상세 비교
일반 상속 Lion (24 bytes):
┌─────────────┐
│ vptr (8B) │ → VTable
├─────────────┤
│ animalData │
├─────────────┤
│ lionData │
└─────────────┘
가상 상속 Lion (32 bytes):
┌──────────────┐
│ Lion vptr │ → Lion VTable (offset 포함)
├──────────────┤
│ lionData │
├──────────────┤
│ Animal vptr │ → Animal VTable
├──────────────┤
│ animalData │
└──────────────┘
가상 상속이 더 큰 이유:
일반 상속에서의 함수 호출
Animal* polyLion = new Lion();
polyLion->speak(); // Lion의 speak 직접 호출
Lion의 데이터 위치가 고정되어 있어서 offset 계산 불필요가상 상속에서의 함수 호출
Animal* polyLion = new Lion(); // Animal 포인터로 가상 상속된 Lion 참조
polyLion->speak();
문제: Animal* 포인터는 객체의 Animal 부분을 가리킴. 하지만 가상 상속에서는 Lion 데이터가 Animal 데이터와 떨어져 있음!
해결: Thunk 사용
Lion VTable (가상 상속):
┌─────────────────────────┐
│ offset to Animal: +16 │ ← offset 정보
├─────────────────────────┤
│ &Lion::speak [thunk] │ ← thunk 함수
└─────────────────────────┘
Thunk 함수:
1. this 포인터를 offset만큼 조정
2. 실제 Lion::speak() 호출
Thunk란?
this 포인터에 offset을 더해서 올바른 위치로 이동포인터 타입에 따른 VTable 선택
Lion lion;
// Lion* 타입: offset 계산 불필요
Lion* lionPtr = &lion;
lionPtr->speak(); // thunk 없는 VTable 사용 (빠름)
// Animal* 타입: offset 계산 필요
Animal* animalPtr = &lion;
animalPtr->speak(); // thunk 있는 VTable 사용 (느림)

가상 상속의 성능 비용
다중 상속을 피해야 하는 이유
1. 복잡성 증가: 코드 이해와 유지보수 어려움
2. 다이아몬드 문제: 가상 상속으로 해결 가능하지만 복잡도 증가
3. 성능 오버헤드: 가상 상속은 메모리와 성능 비용 발생
4. 모호성: 같은 이름의 멤버가 여러 부모에 있을 때 충돌
다중 상속이 허용되는 경우
// ✅ OK: 여러 인터페이스 구현
class ISerializable
{
public:
virtual std::string serialize() = 0;
virtual ~ISerializable() = default;
};
class ILoggable
{
public:
virtual void log() = 0;
virtual ~ILoggable() = default;
};
class User : public ISerializable, public ILoggable
{
// 순수 가상 함수만 있는 인터페이스는 다중 상속 OK
};
권장 설계 패턴
// ❌ 나쁜 설계: 구현 클래스 다중 상속
class Liger : public Lion, public Tiger { };
// ✅ 좋은 설계: Composition 사용
class Liger : public Animal
{
Lion lionBehavior; // Lion을 포함
Tiger tigerBehavior; // Tiger를 포함
};
Object Slicing이란?
문제 상황
class Animal
{
public:
virtual void speak() { std::cout << "Animal sound" << std::endl; }
protected:
double weight;
};
class Cat : public Animal
{
public:
void speak() override { std::cout << "Meow~" << std::endl; }
private:
std::string name; // Cat만의 데이터
};
int main()
{
Cat kitty;
kitty.speak(); // "Meow~"
// ✅ 참조: Object Slicing 없음
Animal& animalRef = kitty;
animalRef.speak(); // "Meow~" (가상 함수 정상 동작)
// ❌ 값 복사: Object Slicing 발생!
Animal animalObj = kitty; // Cat의 name이 잘려나감!
animalObj.speak(); // "Animal sound" (Cat의 speak가 아님!)
// sizeof 확인
std::cout << "sizeof(Cat): " << sizeof(Cat) << std::endl; // 40 bytes
std::cout << "sizeof(animalObj): " << sizeof(animalObj) << std::endl; // 16 bytes
// Cat의 name이 사라짐!
return 0;
}
Object Slicing이 발생하는 상황
void feedAnimal(Animal animal) // ❌ 값으로 받음
{
animal.speak(); // 항상 "Animal sound"
}
Cat kitty;
feedAnimal(kitty); // Object Slicing 발생!
메모리 구조
Cat 객체 (원본):
┌──────────┐
│ vptr │
├──────────┤
│ weight │
├──────────┤
│ name │ ← Cat 고유 데이터
└──────────┘
Animal animalObj = kitty 복사 후:

방법 1: Copy Constructor를 Protected로 선언
class Animal
{
public:
virtual void speak() = 0;
virtual ~Animal() = default;
protected:
Animal() = default;
Animal(const Animal&) = default; // ✅ protected 복사 생성자
Animal& operator=(const Animal&) = default;
};
int main()
{
Cat kitty;
Animal animalObj = kitty; // ❌ 컴파일 에러! protected라 접근 불가
return 0;
}
방법 2: Copy Constructor를 Delete
class Animal
{
public:
virtual void speak() = 0;
virtual ~Animal() = default;
Animal(const Animal&) = delete; // ✅ 복사 금지
Animal& operator=(const Animal&) = delete;
protected:
Animal() = default;
};
방법 3: Pure Abstract Class로 만들기 (가장 권장)
class IAnimal // 인터페이스
{
public:
virtual void speak() = 0;
virtual ~IAnimal() = default;
protected:
IAnimal() = default;
IAnimal(const IAnimal&) = delete;
IAnimal& operator=(const IAnimal&) = delete;
};
// 구체 클래스는 복사 가능
class Cat : public IAnimal
{
public:
Cat() = default;
Cat(const Cat&) = default; // ✅ Cat끼리는 복사 가능
void speak() override { std::cout << "Meow~" << std::endl; }
};
int main()
{
Cat kitty1;
Cat kitty2 = kitty1; // ✅ OK - Cat끼리는 복사 가능
IAnimal* ptr = &kitty1;
// IAnimal obj = *ptr; // ❌ 컴파일 에러! IAnimal은 추상 클래스
return 0;
}
올바른 함수 파라미터 설계
// ❌ 나쁜 설계: 값으로 받음 (Object Slicing)
void feedAnimal(Animal animal) { }
// ✅ 좋은 설계: 참조로 받음
void feedAnimal(const Animal& animal) { }
// ✅ 좋은 설계: 포인터로 받음
void feedAnimal(Animal* animal) { }
문제 상황
class Animal
{
public:
virtual void speak() { std::cout << "Animal" << std::endl; }
bool operator==(const Animal& other) const
{
return weight == other.weight;
}
protected:
double weight;
};
class Cat : public Animal
{
public:
void speak() override { std::cout << "Meow~" << std::endl; }
// operator==를 재정의하지 않음!
private:
std::string name;
};
int main()
{
Cat kitty1;
Cat kitty2;
if (kitty1 == kitty2) // ❌ Animal::operator==가 호출됨
{ // name은 비교하지 않음!
std::cout << "Same cats" << std::endl;
}
return 0;
}
문제점:
operator==를 재정의하지 않으면 부모의 연산자가 사용됨해결 방법: 자식 클래스에서 재정의
class Cat : public Animal
{
public:
bool operator==(const Cat& other) const
{
// 부모의 멤버도 비교
return Animal::operator==(other) && name == other.name;
}
private:
std::string name;
};
더 나은 해결: 가상 함수로 만들기
class Animal
{
public:
virtual bool equals(const Animal& other) const
{
return weight == other.weight;
}
protected:
double weight;
};
class Cat : public Animal
{
public:
bool equals(const Animal& other) const override
{
// dynamic_cast로 Cat인지 확인
const Cat* catPtr = dynamic_cast<const Cat*>(&other);
if (!catPtr) return false;
return Animal::equals(other) && name == catPtr->name;
}
private:
std::string name;
};
RTTI (Run-Time Type Information)
dynamic_cast, typeid 연산자 사용Upcasting (업캐스팅): 항상 안전
Cat kitty;
Animal* ptr = &kitty; // ✅ OK - 자식 → 부모 (안전)
Downcasting (다운캐스팅): 위험
Animal* animalPtr = getAnimal(); // 실제로 무엇인지 모름
// ❌ static_cast: 위험! 타입 체크 없음
Cat* catPtr = static_cast<Cat*>(animalPtr);
catPtr->meow(); // animalPtr이 실제로 Cat이 아니면 Undefined Behavior!
// ⚠️ dynamic_cast: 안전하지만 권장하지 않음
Cat* catPtr2 = dynamic_cast<Cat*>(animalPtr);
if (catPtr2) // nullptr 체크 필요
{
catPtr2->meow(); // ✅ 안전
}
else
{
std::cout << "Not a Cat!" << std::endl;
}
dynamic_cast 동작 원리
Animal* ptr = &someObject;
Cat* catPtr = dynamic_cast<Cat*>(ptr);
// 내부 동작:
// 1. ptr이 가리키는 객체의 VTable 확인
// 2. VTable의 type_info 조회
// 3. 상속 계층 구조를 검사
// 4. Cat으로 안전하게 변환 가능하면 포인터 반환
// 5. 불가능하면 nullptr 반환
dynamic_cast를 피해야 하는 이유
성능 오버헤드
설계 문제의 신호
// ❌ 나쁜 설계: 타입 체크
void process(Animal* animal)
{
if (Cat* cat = dynamic_cast<Cat*>(animal))
{
cat->meow();
}
else if (Dog* dog = dynamic_cast<Dog*>(animal))
{
dog->bark();
}
// 새로운 동물이 추가되면 이 코드를 수정해야 함!
}
// ✅ 좋은 설계: 가상 함수 사용
void process(Animal* animal)
{
animal->speak(); // 각 클래스가 자신의 방식으로 speak 구현
}
Open-Closed Principle 위반
올바른 대안: Virtual Function 사용
class Animal
{
public:
virtual void makeSound() = 0;
virtual void play() = 0;
virtual ~Animal() = default;
};
class Cat : public Animal
{
public:
void makeSound() override { std::cout << "Meow~" << std::endl; }
void play() override { std::cout << "Playing with yarn" << std::endl; }
};
class Dog : public Animal
{
public:
void makeSound() override { std::cout << "Woof!" << std::endl; }
void play() override { std::cout << "Playing fetch" << std::endl; }
};
// ✅ 타입 체크 없이 처리
void interact(Animal* animal)
{
animal->makeSound();
animal->play();
// 새로운 동물이 추가되어도 이 코드는 수정 불필요!
}
dynamic_cast가 허용되는 경우
1. Base 클래스는 Pure Abstract Class로 만들기
// ✅ 권장: 인터페이스
class IAnimal
{
public:
virtual void speak() = 0;
virtual void eat() = 0;
virtual ~IAnimal() = default;
protected:
IAnimal() = default;
IAnimal(const IAnimal&) = delete;
IAnimal& operator=(const IAnimal&) = delete;
};
이유:
2. Composition over Inheritance
// ❌ 상속 남용
class Stack : public std::vector<int>
{
// vector의 모든 기능이 노출됨 (insert, erase 등)
};
// ✅ Composition 사용
class Stack
{
public:
void push(int value) { data.push_back(value); }
int pop() { int val = data.back(); data.pop_back(); return val; }
private:
std::vector<int> data; // 포함 관계
};
3. 가상 소멸자 필수
class Base
{
public:
virtual ~Base() = default; // ✅ 필수!
};
4. override 키워드 사용
class Derived : public Base
{
public:
void func() override; // ✅ 오버라이드 의도 명확히
};
5. final 키워드로 상속 제한
class FinalClass final // 더 이상 상속 불가
{
};
class Base
{
public:
virtual void func() final; // 이 함수는 더 이상 오버라이드 불가
};
6. 다중 상속은 인터페이스로만
// ✅ OK: 인터페이스 다중 구현
class Serializable : public ISerializable, public ILoggable { };
// ❌ 피하기: 구현 클래스 다중 상속
class Liger : public Lion, public Tiger { };
다중 상속 관련
Object Slicing
Dynamic Cast와 RTTI
설계 원칙
1. Base 클래스는 Pure Abstract Class로
2. Composition over Inheritance
3. 가상 소멸자 필수
4. override/final 키워드 활용
5. 인터페이스 다중 구현만 허용
코드없는 프로그래밍
C++ Multiple Inheritance
C++ Virtual Inheritance