C++ 상속과 다형성 | "virtual 함수" 완벽 가이드

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 멤버
publicpublicprotected접근 불가
protectedprotectedprotected접근 불가
privateprivateprivate접근 불가
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:

관련 글: virtual, override, polymorphism.

한 줄 요약: 상속은 코드 재사용, 다형성은 동일한 인터페이스로 다양한 타입을 처리하는 C++ OOP 핵심 기능입니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 가상 함수 | “Virtual Functions” 가이드
  • C++ 현대적 다형성 설계: 상속 대신 합성·variant
  • C++ override와 final | “가상 함수” 가이드

관련 글

  • C++ 가상 소멸자 |
  • C++ 가상 함수 |
  • C++ 슬라이싱 문제 |
  • C++ 현대적 다형성 설계: 상속 대신 합성·variant
  • C++ 클래스와 객체 |