C++ 객체 지향 프로그래밍(OOP)의 진정한 강력함은 상속(Inheritance)과 다형성(Polymorphism)에서 나옵니다. 이 두 개념은 코드의 중복을 획기적으로 줄여줄 뿐만 아니라, 시스템을 유연하고 확장 가능하게 만들어줍니다.
오늘은 C++ 상속의 기본부터 다형성을 구현하는 가상 함수, 그리고 실무에서 자주 쓰이는 추상 클래스까지 핵심 내용을 정리해 보겠습니다.

1. 상속 (Inheritance): 코드의 재사용
상속이란 기존에 정의된 클래스(부모 클래스)의 기능을 이어받아 새로운 클래스(자식 클래스)를 만드는 것을 말합니다.
상속의 기본 문법
class Base {
public:
void display() {
std::cout << "부모 클래스의 기능입니다." << std::endl;
}
};
// Base를 상속받는 Derived 클래스
class Derived : public Base {
// 아무 내용이 없어도 부모의 display()를 물려받습니다.
};
상속 접근 지정자 (꼭 기억해야 할 점)
많은 입문자가 헷갈려 하는 부분입니다. 상속 시 사용하는 public, protected, private 키워드는 부모 멤버의 자식 내 접근 권한을 결정합니다.
| 접근 지정자 | 부모 멤버의 자식 내 성격 | 비고 |
| public 상속 | public → public, protected → protected | 가장 일반적인 상속 형태 |
| protected 상속 | 모두 protected로 변경 | 외부 접근 차단, 손자 클래스까지는 허용 |
| private 상속 | 모두 private으로 변경 | 'IS-A' 관계가 아닌 내부 구현용 |
2. 다형성 (Polymorphism): 하나의 인터페이스, 다양한 동작
다형성은 "하나의 기호가 여러 의미를 갖는 것"을 뜻합니다. C++에서는 부모 클래스의 포인터나 참조자로 자식 객체를 가리킬 때, 실제 객체의 타입에 맞는 함수가 호출되도록 하는 동적 바인딩이 핵심입니다.
가상 함수 (Virtual Function)
자식 클래스에서 재정의(Override)할 함수라면 부모 클래스에서 반드시 virtual 키워드를 붙여야 합니다.
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() { cout << "동물이 소리를 냅니다." << endl; }
virtual ~Animal() {} // [중요] 상속 시 소멸자는 반드시 가상 함수로!
};
class Dog : public Animal {
public:
void speak() override { cout << "멍멍!" << endl; }
};
int main() {
Animal* ptr = new Dog();
ptr->speak(); // "멍멍!" 출력 (다형성)
delete ptr; // 가상 소멸자 덕분에 Dog 소멸자도 정상 호출됨
return 0;
}
임베디드 팁: 가상 함수를 선언하면 메모리에 vtable(가상 함수 테이블)이 생기므로 아주 미세한 메모리 오버헤드가 발생합니다. 하지만 대규모 설계의 유연성을 위해선 필수적입니다.
3. 추상 클래스와 순수 가상 함수
때로는 부모 클래스에서 함수를 직접 구현할 필요가 없고, 자식들이 반드시 구현하도록 '강제'만 하고 싶을 때가 있습니다. 이때 순수 가상 함수를 사용합니다.
- 순수 가상 함수: virtual void func() = 0; 형태의 함수
- 추상 클래스: 순수 가상 함수를 하나라도 포함한 클래스 (직객 생성 불가)
class Shape {
public:
virtual void draw() = 0; // 순수 가상 함수
};
class Circle : public Shape {
public:
void draw() override { cout << "원을 그립니다." << endl; }
};
4. 실전 활용: 객체 지향적 설계 예제
상속과 다형성을 활용하면, 새로운 타입의 객체가 추가되어도 메인 로직(Loop)을 수정할 필요가 없는 확장성 높은 코드를 짤 수 있습니다.
#include <iostream>
#include <vector>
#include <memory>
class Sensor {
public:
virtual void readData() = 0;
virtual ~Sensor() {}
};
class TempSensor : public Sensor {
public:
void readData() override { std::cout << "온도 데이터를 읽는 중..." << std::endl; }
};
class HumidSensor : public Sensor {
public:
void readData() override { std::cout << "습도 데이터를 읽는 중..." << std::endl; }
};
int main() {
// 스마트 포인터를 사용한 다형성 구현
std::vector<std::unique_ptr<Sensor>> sensors;
sensors.push_back(std::make_unique<TempSensor>());
sensors.push_back(std::make_unique<HumidSensor>());
for (const auto& s : sensors) {
s->readData(); // 각 센서 타입에 맞는 함수가 자동 호출됨
}
return 0;
}
결론: 상속과 다형성을 잘 쓰려면?
- 가상 소멸자를 잊지 마세요. 메모리 누수의 주범이 됩니다.
- override 키워드를 명시하여 컴파일러의 도움을 받으세요.
- 상속 깊이가 너무 깊어지면 코드가 복잡해지므로, 상속보다는 포함(Composition)을 고려해야 할 때도 있습니다.
C++의 객체 지향 원리를 이해하면 임베디드 환경에서도 드라이버 계층이나 미들웨어를 훨씬 체계적으로 설계할 수 있습니다. 오늘 포스팅이 C++ 공부에 도움이 되셨길 바랍니다!
'Core Programming > Modern C++ & System Design' 카테고리의 다른 글
| C++ 네임스페이스(Namespace) 사용법 총정리: 이름 충돌 방지와 모듈화 (0) | 2024.12.20 |
|---|---|
| C++ 연산자 오버로딩 완벽 정리: 복소수 예제로 배우는 연산자 재정의 (0) | 2024.12.19 |
| C++ 클래스와 객체 완벽 정리: 객체 지향 프로그래밍(OOP) 핵심 개념 (0) | 2024.12.19 |
| C++ 동적 메모리 관리 완벽 가이드: new/delete부터 스마트 포인터까지 (0) | 2024.12.19 |
| C++ 포인터와 참조 완벽 정리: 차이점부터 메모리 구조, 활용법까지 (0) | 2024.12.18 |