C++ 상속과 다형성 | "virtual 함수" 완벽 가이드
이 글의 핵심
C++ 상속과 다형성에 대한 실전 가이드입니다.
상속과 다형성이란?
상속 (Inheritance) 은 기존 클래스의 속성과 메서드를 재사용하는 기법이고, 다형성 (Polymorphism) 은 동일한 인터페이스로 다양한 타입을 처리하는 능력입니다.
왜 필요한가?:
- 코드 재사용: 공통 기능을 기본 클래스에
- 확장성: 새 타입 추가 용이
- 유연성: 런타임에 타입 결정
- 추상화: 인터페이스와 구현 분리
// ❌ 상속 없이: 중복 코드
class Dog {
string name;
public:
void eat() { cout << name << " 먹습니다\n"; }
void bark() { cout << "멍멍\n"; }
};
class Cat {
string name;
public:
void eat() { cout << name << " 먹습니다\n"; } // 중복
void meow() { cout << "야옹\n"; }
};
// ✅ 상속 사용: 재사용
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
void eat() { cout << name << " 먹습니다\n"; }
};
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
void bark() { cout << "멍멍\n"; }
};
상속 구조:
flowchart TD
Animal["Animal (기본 클래스)"]
Dog["Dog (파생 클래스)"]
Cat["Cat (파생 클래스)"]
Animal --> Dog
Animal --> Cat
Animal -.-> |eat| A1["eat()"]
Dog -.-> |bark| D1["bark()"]
Cat -.-> |meow| C1["meow()"]
기본 상속
class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
void eat() {
cout << name << "이(가) 먹습니다" << endl;
}
};
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
void bark() {
cout << name << "이(가) 짖습니다: 멍멍!" << endl;
}
};
int main() {
Dog dog("바둑이");
dog.eat(); // 상속받은 메서드
dog.bark(); // Dog만의 메서드
}
접근 지정자:
| 상속 방식 | public 멤버 | protected 멤버 | private 멤버 |
|---|---|---|---|
public | public | protected | 접근 불가 |
protected | protected | protected | 접근 불가 |
private | private | private | 접근 불가 |
class Base {
public:
int pub;
protected:
int prot;
private:
int priv;
};
class Derived : public Base {
void func() {
pub = 1; // OK: public
prot = 2; // OK: protected
// priv = 3; // 에러: private
}
};
virtual 함수와 다형성
class Animal {
public:
virtual void speak() {
cout << "동물 소리" << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "멍멍!" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "야옹!" << endl;
}
};
int main() {
Animal* animals[3];
animals[0] = new Animal();
animals[1] = new Dog();
animals[2] = new Cat();
for (int i = 0; i < 3; i++) {
animals[i]->speak(); // 각자의 speak 호출
}
// 메모리 해제
for (int i = 0; i < 3; i++) {
delete animals[i];
}
}
추상 클래스 (순수 가상 함수)
class Shape {
public:
virtual double area() = 0; // 순수 가상 함수
virtual double perimeter() = 0;
virtual ~Shape() {} // 가상 소멸자
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
double perimeter() override {
return 2 * 3.14159 * radius;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() override {
return width * height;
}
double perimeter() override {
return 2 * (width + height);
}
};
int main() {
Shape* shapes[2];
shapes[0] = new Circle(5.0);
shapes[1] = new Rectangle(4.0, 6.0);
for (int i = 0; i < 2; i++) {
cout << "넓이: " << shapes[i]->area() << endl;
delete shapes[i];
}
}
실전 예시
예시 1: 게임 캐릭터 시스템
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Character {
protected:
string name;
int hp;
int attackPower;
public:
Character(string n, int h, int ap)
: name(n), hp(h), attackPower(ap) {}
virtual ~Character() {}
virtual void attack(Character* target) {
cout << name << "의 공격!" << endl;
target->takeDamage(attackPower);
}
virtual void takeDamage(int damage) {
hp -= damage;
cout << name << "이(가) " << damage << " 데미지를 받았습니다. (HP: " << hp << ")" << endl;
}
virtual void useSkill() = 0; // 순수 가상 함수
bool isAlive() { return hp > 0; }
string getName() { return name; }
};
class Warrior : public Character {
public:
Warrior(string n) : Character(n, 150, 30) {}
void useSkill() override {
cout << name << "이(가) 강타를 사용합니다!" << endl;
attackPower += 20;
}
};
class Mage : public Character {
public:
Mage(string n) : Character(n, 80, 50) {}
void useSkill() override {
cout << name << "이(가) 파이어볼을 시전합니다!" << endl;
attackPower += 30;
}
};
class Healer : public Character {
public:
Healer(string n) : Character(n, 100, 15) {}
void useSkill() override {
cout << name << "이(가) 힐을 사용합니다!" << endl;
hp += 50;
cout << "HP 회복! (현재 HP: " << hp << ")" << endl;
}
};
int main() {
vector<Character*> party;
party.push_back(new Warrior("전사"));
party.push_back(new Mage("마법사"));
party.push_back(new Healer("힐러"));
for (auto& character : party) {
character->useSkill();
}
// 메모리 해제
for (auto& character : party) {
delete character;
}
return 0;
}
설명: 다형성을 활용하여 다양한 캐릭터 타입을 하나의 컨테이너로 관리합니다.
예시 2: 결제 시스템
#include <iostream>
#include <string>
using namespace std;
class PaymentMethod {
public:
virtual bool pay(double amount) = 0;
virtual string getMethodName() = 0;
virtual ~PaymentMethod() {}
};
class CreditCard : public PaymentMethod {
private:
string cardNumber;
public:
CreditCard(string num) : cardNumber(num) {}
bool pay(double amount) override {
cout << "신용카드 결제: " << amount << "원" << endl;
cout << "카드번호: " << cardNumber << endl;
return true;
}
string getMethodName() override {
return "신용카드";
}
};
class BankTransfer : public PaymentMethod {
private:
string accountNumber;
public:
BankTransfer(string acc) : accountNumber(acc) {}
bool pay(double amount) override {
cout << "계좌이체: " << amount << "원" << endl;
cout << "계좌번호: " << accountNumber << endl;
return true;
}
string getMethodName() override {
return "계좌이체";
}
};
class PaymentProcessor {
public:
void processPayment(PaymentMethod* method, double amount) {
cout << "\n=== 결제 처리 ===" << endl;
cout << "결제 수단: " << method->getMethodName() << endl;
if (method->pay(amount)) {
cout << "결제 완료!" << endl;
} else {
cout << "결제 실패!" << endl;
}
}
};
int main() {
PaymentProcessor processor;
CreditCard card("1234-5678-9012-3456");
processor.processPayment(&card, 50000);
BankTransfer transfer("123-456-789012");
processor.processPayment(&transfer, 30000);
return 0;
}
설명: 전략 패턴을 사용하여 다양한 결제 방법을 유연하게 처리합니다.
예시 3: 파일 포맷 변환기
#include <iostream>
#include <string>
using namespace std;
class Document {
protected:
string content;
public:
Document(string c) : content(c) {}
virtual ~Document() {}
virtual void save(const string& filename) = 0;
virtual string getFormat() = 0;
};
class PDFDocument : public Document {
public:
PDFDocument(string c) : Document(c) {}
void save(const string& filename) override {
cout << "PDF로 저장: " << filename << ".pdf" << endl;
cout << "내용: " << content << endl;
}
string getFormat() override {
return "PDF";
}
};
class WordDocument : public Document {
public:
WordDocument(string c) : Document(c) {}
void save(const string& filename) override {
cout << "Word로 저장: " << filename << ".docx" << endl;
cout << "내용: " << content << endl;
}
string getFormat() override {
return "Word";
}
};
class DocumentConverter {
public:
void convert(Document* doc, const string& filename) {
cout << "\n=== 문서 변환 ===" << endl;
cout << "포맷: " << doc->getFormat() << endl;
doc->save(filename);
}
};
int main() {
DocumentConverter converter;
PDFDocument pdf("PDF 문서 내용");
converter.convert(&pdf, "report");
WordDocument word("Word 문서 내용");
converter.convert(&word, "letter");
return 0;
}
설명: 다형성으로 다양한 문서 포맷을 통일된 인터페이스로 처리합니다.
자주 발생하는 문제
문제 1: 가상 소멸자 누락
증상: 파생 클래스 소멸자가 호출 안됨 (메모리 누수)
원인: 기본 클래스 소멸자가 virtual이 아님
해결법:
// ❌ 위험한 코드
class Base {
public:
~Base() { cout << "Base 소멸" << endl; }
};
class Derived : public Base {
private:
int* data;
public:
Derived() { data = new int[100]; }
~Derived() {
delete[] data;
cout << "Derived 소멸" << endl;
}
};
Base* ptr = new Derived();
delete ptr; // Derived 소멸자 호출 안됨! 누수!
// ✅ 올바른 코드
class Base {
public:
virtual ~Base() { cout << "Base 소멸" << endl; }
};
문제 2: override 키워드 누락
증상: 오버라이드 의도했으나 새 함수 생성됨
원인: 함수 시그니처 불일치
해결법:
// ❌ 실수하기 쉬운 코드
class Base {
public:
virtual void func(int x) {}
};
class Derived : public Base {
public:
void func(double x) {} // 오버라이드 아님! 새 함수!
};
// ✅ override로 명시
class Derived : public Base {
public:
void func(double x) override {} // 컴파일 에러!
void func(int x) override {} // OK
};
문제 3: 슬라이싱 (Slicing)
증상: 파생 클래스 정보 손실
원인: 값으로 복사
해결법:
// ❌ 슬라이싱 발생
Dog dog("바둑이");
Animal animal = dog; // Dog 정보 손실!
animal.speak(); // Animal::speak 호출
// ✅ 포인터나 참조 사용
Dog dog("바둑이");
Animal* animal = &dog;
animal->speak(); // Dog::speak 호출
실무 패턴
패턴 1: 템플릿 메서드
class DataProcessor {
public:
void process() {
loadData();
validateData();
transformData();
saveData();
}
virtual ~DataProcessor() = default;
protected:
virtual void loadData() = 0;
virtual void validateData() {} // 기본 구현
virtual void transformData() = 0;
virtual void saveData() = 0;
};
class CSVProcessor : public DataProcessor {
protected:
void loadData() override {
std::cout << "CSV 로드\n";
}
void transformData() override {
std::cout << "CSV 변환\n";
}
void saveData() override {
std::cout << "CSV 저장\n";
}
};
// 사용
CSVProcessor processor;
processor.process();
패턴 2: 인터페이스 분리
// 읽기 인터페이스
class IReadable {
public:
virtual std::string read() = 0;
virtual ~IReadable() = default;
};
// 쓰기 인터페이스
class IWritable {
public:
virtual void write(const std::string& data) = 0;
virtual ~IWritable() = default;
};
// 읽기/쓰기 모두 구현
class File : public IReadable, public IWritable {
public:
std::string read() override {
return "파일 내용";
}
void write(const std::string& data) override {
std::cout << "파일 쓰기: " << data << '\n';
}
};
// 읽기 전용
class ReadOnlyFile : public IReadable {
public:
std::string read() override {
return "읽기 전용 내용";
}
};
패턴 3: 팩토리 메서드
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};
class ConcreteProductA : public Product {
public:
void use() override {
std::cout << "Product A\n";
}
};
class ConcreteProductB : public Product {
public:
void use() override {
std::cout << "Product B\n";
}
};
class Creator {
public:
virtual std::unique_ptr<Product> createProduct() = 0;
virtual ~Creator() = default;
void operation() {
auto product = createProduct();
product->use();
}
};
class CreatorA : public Creator {
public:
std::unique_ptr<Product> createProduct() override {
return std::make_unique<ConcreteProductA>();
}
};
FAQ
Q1: virtual 함수는 느린가요?
A: 약간의 오버헤드가 있지만 대부분의 경우 무시할 수 있는 수준입니다. 유연성의 이점이 훨씬 큽니다.
// 간접 참조 1회 (vtable)
animal->speak(); // vtable 조회 → 함수 호출
Q2: 모든 함수를 virtual로 만들어야 하나요?
A: 아니요, 오버라이드가 필요한 함수만 virtual로 만드세요. 소멸자는 항상 virtual 로 만드는 것이 좋습니다.
class Base {
public:
virtual ~Base() = default; // 항상 virtual
virtual void func() {} // 오버라이드 필요 시만
void helper() {} // virtual 불필요
};
Q3: 다중 상속은 언제 사용하나요?
A: 가능하면 피하세요. 대신 인터페이스 (추상 클래스) 를 사용하거나 컴포지션을 고려하세요.
// ✅ 인터페이스 다중 상속: OK
class IReadable { virtual std::string read() = 0; };
class IWritable { virtual void write(const std::string&) = 0; };
class File : public IReadable, public IWritable { };
// ❌ 구현 다중 상속: 복잡
class A { int x; };
class B { int y; };
class C : public A, public B { }; // 다이아몬드 문제
Q4: override vs final?
A:
override: 오버라이드 의도 명시, 컴파일 타임 검증final: 더 이상 오버라이드 불가
class Base {
virtual void func() {}
};
class Derived : public Base {
void func() override {} // 오버라이드
};
class Final : public Base {
void func() final {} // 더 이상 오버라이드 불가
};
Q5: 추상 클래스 vs 인터페이스?
A: C++에는 인터페이스가 없습니다. 순수 가상 함수만 있는 클래스를 인터페이스처럼 사용합니다.
// 인터페이스 (순수 가상 함수만)
class IShape {
public:
virtual double area() = 0;
virtual ~IShape() = default;
};
// 추상 클래스 (일부 구현 포함)
class Shape {
public:
virtual double area() = 0;
void print() { std::cout << "Shape\n"; } // 구현 포함
};
Q6: 상속 vs 컴포지션?
A:
- 상속 (“is-a”): Dog is an Animal
- 컴포지션 (“has-a”): Car has an Engine
일반적으로 컴포지션이 더 유연합니다.
// 상속: is-a
class Dog : public Animal { };
// 컴포지션: has-a
class Car {
Engine engine;
};
Q7: 가상 소멸자를 왜 사용하나요?
A: 파생 클래스 소멸자가 호출되도록 보장합니다.
// ❌ 가상 소멸자 없음: 메모리 누수
class Base {
public:
~Base() { } // virtual 없음
};
class Derived : public Base {
int* data_;
public:
Derived() { data_ = new int[100]; }
~Derived() { delete[] data_; } // 호출 안됨!
};
Base* ptr = new Derived();
delete ptr; // Derived 소멸자 호출 안됨!
// ✅ 가상 소멸자: 안전
class Base {
public:
virtual ~Base() { }
};
Q8: 상속 학습 리소스는?
A:
- “Effective C++” by Scott Meyers (Item 7, 32-40)
- “C++ Primer” by Stanley Lippman
- cppreference.com - Inheritance
관련 글: virtual, override, polymorphism.
한 줄 요약: 상속은 코드 재사용, 다형성은 동일한 인터페이스로 다양한 타입을 처리하는 C++ OOP 핵심 기능입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 가상 함수 | “Virtual Functions” 가이드
- C++ 현대적 다형성 설계: 상속 대신 합성·variant
- C++ override와 final | “가상 함수” 가이드
관련 글
- C++ 가상 소멸자 |
- C++ 가상 함수 |
- C++ 슬라이싱 문제 |
- C++ 현대적 다형성 설계: 상속 대신 합성·variant
- C++ 클래스와 객체 |