Skip to content

C++ OOP 핵심 — 클래스·상속·다형성·가상함수

개념역할
캡슐화(Encapsulation)데이터와 함수를 클래스로 묶고, 접근을 제한
상속(Inheritance)기존 클래스를 확장해 코드 재사용
다형성(Polymorphism)같은 인터페이스로 다른 동작 수행
추상화(Abstraction)구현을 숨기고 인터페이스만 노출

C++에서 classstruct의 유일한 문법적 차이는 기본 접근 제어자입니다.

키워드기본 접근 제어자기본 상속 방식
classprivateprivate
structpublicpublic
// struct: 데이터 묶음, 멤버 기본 public
struct Point
{
float X;
float Y;
float Z;
};
// class: 동작을 포함하는 객체, 멤버 기본 private
class Player
{
public:
Player(const std::string& name, int health)
: m_Name(name), m_Health(health) {}
const std::string& GetName() const { return m_Name; }
int GetHealth() const { return m_Health; }
void TakeDamage(int amount);
private:
std::string m_Name;
int m_Health;
};
class BankAccount
{
public:
// 외부에서 자유롭게 접근 가능
BankAccount(const std::string& owner, double balance)
: m_Owner(owner), m_Balance(balance) {}
void Deposit(double amount)
{
if (amount > 0)
{
m_Balance += amount;
LogTransaction("Deposit", amount);
}
}
bool Withdraw(double amount)
{
if (amount > 0 && m_Balance >= amount)
{
m_Balance -= amount;
LogTransaction("Withdraw", amount);
return true;
}
return false;
}
double GetBalance() const { return m_Balance; }
protected:
// 이 클래스와 파생 클래스에서만 접근 가능
void LogTransaction(const std::string& type, double amount)
{
std::cout << "[" << type << "] " << amount << " (balance: " << m_Balance << ")\n";
}
private:
// 이 클래스 내부에서만 접근 가능
std::string m_Owner;
double m_Balance;
};
#include <iostream>
#include <string>
class Resource
{
public:
// 기본 생성자
Resource() : m_Id(0), m_Name("default")
{
std::cout << "Resource(): " << m_Name << "\n";
}
// 매개변수 생성자
explicit Resource(int id, const std::string& name)
: m_Id(id), m_Name(name)
{
std::cout << "Resource(id, name): " << m_Name << "\n";
}
// 복사 생성자
Resource(const Resource& other)
: m_Id(other.m_Id), m_Name(other.m_Name + "_copy")
{
std::cout << "Resource(copy): " << m_Name << "\n";
}
// 소멸자
~Resource()
{
std::cout << "~Resource(): " << m_Name << "\n";
}
int GetId() const { return m_Id; }
const std::string& GetName() const { return m_Name; }
private:
int m_Id;
std::string m_Name;
};
int main()
{
Resource r1(1, "Texture"); // 매개변수 생성자
Resource r2(r1); // 복사 생성자
Resource r3; // 기본 생성자
return 0; // ~Resource() 역순으로 3번 호출
}

#include <string>
#include <iostream>
class Animal
{
public:
Animal(const std::string& name, int age)
: m_Name(name), m_Age(age) {}
virtual ~Animal() = default;
void Breathe() const
{
std::cout << m_Name << " breathes.\n";
}
const std::string& GetName() const { return m_Name; }
int GetAge() const { return m_Age; }
protected:
std::string m_Name;
int m_Age;
};
// public 상속: Animal의 public → Dog의 public, protected → protected
class Dog : public Animal
{
public:
Dog(const std::string& name, int age, const std::string& breed)
: Animal(name, age) // 부모 생성자 명시적 호출
, m_Breed(breed)
{
}
void Bark() const
{
// protected 멤버 접근 가능
std::cout << m_Name << " (" << m_Breed << ") barks!\n";
}
private:
std::string m_Breed;
};
int main()
{
Dog dog("Buddy", 3, "Labrador");
dog.Breathe(); // Animal의 public 함수 상속
dog.Bark(); // Dog의 고유 함수
std::cout << "Age: " << dog.GetAge() << "\n";
return 0;
}
public 상속 : 부모의 public → 자식 public, protected → protected
protected 상속 : 부모의 public/protected → 자식 protected
private 상속 : 부모의 public/protected → 자식 private

3.1 가상함수 없이는 다형성이 없다

Section titled “3.1 가상함수 없이는 다형성이 없다”
#include <iostream>
class Shape
{
public:
// 가상함수 아님 → 정적 바인딩
void Draw() const
{
std::cout << "Shape::Draw()\n";
}
};
class Circle : public Shape
{
public:
void Draw() const // 오버라이드처럼 보이지만...
{
std::cout << "Circle::Draw()\n";
}
};
int main()
{
Circle c;
Shape* ptr = &c;
ptr->Draw(); // Shape::Draw() 출력 ← 기대한 Circle::Draw()가 아님!
return 0;
}
#include <iostream>
#include <memory>
#include <vector>
class Shape
{
public:
// virtual 선언: 동적 바인딩 활성화
virtual void Draw() const
{
std::cout << "Shape::Draw()\n";
}
virtual float Area() const { return 0.f; }
// 기반 클래스 소멸자는 반드시 virtual
virtual ~Shape() = default;
};
class Circle : public Shape
{
public:
explicit Circle(float radius) : m_Radius(radius) {}
// override 키워드: 가상함수 오버라이드임을 명시 (컴파일러 검증)
void Draw() const override
{
std::cout << "Circle::Draw() r=" << m_Radius << "\n";
}
float Area() const override
{
return 3.14159f * m_Radius * m_Radius;
}
private:
float m_Radius;
};
class Rectangle : public Shape
{
public:
Rectangle(float w, float h) : m_Width(w), m_Height(h) {}
void Draw() const override
{
std::cout << "Rectangle::Draw() " << m_Width << "x" << m_Height << "\n";
}
float Area() const override { return m_Width * m_Height; }
private:
float m_Width;
float m_Height;
};
int main()
{
// 기반 클래스 포인터로 파생 클래스 객체를 다형성으로 사용
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.f));
shapes.push_back(std::make_unique<Rectangle>(4.f, 3.f));
shapes.push_back(std::make_unique<Circle>(2.f));
for (const auto& shape : shapes)
{
shape->Draw(); // 동적 바인딩
std::cout << "Area: " << shape->Area() << "\n";
}
return 0;
}

virtual 함수가 있는 클래스는 컴파일러가 **vtable(가상 함수 테이블)**을 생성합니다.

[Shape vtable] [Circle vtable]
+------------------+ +--------------------+
| Shape::Draw ptr | | Circle::Draw ptr |
| Shape::Area ptr | | Circle::Area ptr |
+------------------+ +--------------------+
Circle 객체:
+-------------------+
| vptr ─────────────────► Circle vtable
+-------------------+
| m_Radius |
+-------------------+
Shape* ptr = &circleObj;
ptr->Draw();
→ ptr의 vptr가 가리키는 테이블에서 Draw 찾음
→ Circle::Draw 호출 (동적 바인딩)
  • virtual 함수 하나라도 있으면 객체에 vptr(포인터 크기) 오버헤드 추가
  • 가상 함수 호출은 포인터 역참조 1회 추가 비용 (일반적으로 무시 가능)

4. 순수 가상함수와 추상 클래스

Section titled “4. 순수 가상함수와 추상 클래스”
#include <iostream>
#include <string>
// 추상 클래스: 순수 가상함수를 하나 이상 포함
class ISerializable
{
public:
virtual ~ISerializable() = default;
// 순수 가상함수: = 0 으로 선언, 파생 클래스에서 반드시 구현
virtual std::string Serialize() const = 0;
virtual void Deserialize(const std::string& data) = 0;
// 추상 클래스도 일반(비순수) 멤버 함수를 가질 수 있음
void SaveToFile(const std::string& path) const
{
std::string data = Serialize(); // 다형성 호출
// 파일 저장 로직...
std::cout << "Saved to " << path << ": " << data << "\n";
}
};
class PlayerData : public ISerializable
{
public:
PlayerData(const std::string& name, int score)
: m_Name(name), m_Score(score) {}
// 순수 가상함수 구현 (필수)
std::string Serialize() const override
{
return m_Name + ":" + std::to_string(m_Score);
}
void Deserialize(const std::string& data) override
{
size_t pos = data.find(':');
if (pos != std::string::npos)
{
m_Name = data.substr(0, pos);
m_Score = std::stoi(data.substr(pos + 1));
}
}
private:
std::string m_Name;
int m_Score;
};
int main()
{
// ISerializable obj; // 컴파일 에러: 추상 클래스는 인스턴스 생성 불가
PlayerData pd("Alice", 1500);
pd.SaveToFile("save.dat"); // Saved to save.dat: Alice:1500
return 0;
}

4.2 인터페이스 패턴 (순수 가상함수만 포함)

Section titled “4.2 인터페이스 패턴 (순수 가상함수만 포함)”
// C++에는 interface 키워드가 없음
// 관례상 순수 가상함수만 포함한 추상 클래스를 인터페이스로 사용
class IDamageable
{
public:
virtual ~IDamageable() = default;
virtual void TakeDamage(float amount) = 0;
virtual bool IsAlive() const = 0;
};
class IMovable
{
public:
virtual ~IMovable() = default;
virtual void MoveTo(float x, float y, float z) = 0;
virtual void Stop() = 0;
};
// 다중 상속으로 여러 인터페이스 구현
class Enemy : public IDamageable, public IMovable
{
public:
Enemy() : m_Health(100.f) {}
void TakeDamage(float amount) override
{
m_Health -= amount;
if (m_Health < 0.f) m_Health = 0.f;
std::cout << "Enemy HP: " << m_Health << "\n";
}
bool IsAlive() const override { return m_Health > 0.f; }
void MoveTo(float x, float y, float z) override
{
std::cout << "Enemy moves to (" << x << ", " << y << ", " << z << ")\n";
}
void Stop() override
{
std::cout << "Enemy stops\n";
}
private:
float m_Health;
};

class Base
{
public:
virtual void Update(float deltaTime) {}
virtual ~Base() = default;
};
class Derived : public Base
{
public:
// override 없이: 오타가 있어도 경고 없이 새 함수로 처리됨
void update(float deltaTime) // 소문자 u → Base::Update를 오버라이드하지 못함
{
}
// override 있음: 서명 불일치 시 컴파일 에러
// void update(float deltaTime) override // 컴파일 에러: Base에 해당 가상함수 없음
void Update(float deltaTime) override // OK
{
std::cout << "Derived::Update\n";
}
};

5.2 final — 상속/오버라이드 금지

Section titled “5.2 final — 상속/오버라이드 금지”
// final 클래스: 더 이상 상속 불가
class Singleton final
{
public:
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
private:
Singleton() = default;
};
// class ExtendedSingleton : public Singleton {}; // 컴파일 에러
class Animal
{
public:
virtual void Speak() = 0;
virtual ~Animal() = default;
};
class Cat : public Animal
{
public:
// final 함수: 이 클래스의 파생에서 재정의 불가
void Speak() override final
{
std::cout << "Meow\n";
}
};
class SpecialCat : public Cat
{
public:
// void Speak() override {} // 컴파일 에러: Cat::Speak는 final
};

6. 실전 패턴 — 컴포넌트 기반 캐릭터

Section titled “6. 실전 패턴 — 컴포넌트 기반 캐릭터”
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// 컴포넌트 기반 클래스 (추상)
class Component
{
public:
virtual ~Component() = default;
virtual void Update(float dt) = 0;
virtual std::string GetName() const = 0;
};
// 체력 컴포넌트
class HealthComponent : public Component
{
public:
explicit HealthComponent(float maxHp) : m_MaxHp(maxHp), m_Hp(maxHp) {}
void Update(float dt) override {} // 체력은 틱 업데이트 불필요
std::string GetName() const override { return "HealthComponent"; }
void TakeDamage(float amount)
{
m_Hp = std::max(0.f, m_Hp - amount);
std::cout << "HP: " << m_Hp << "/" << m_MaxHp << "\n";
}
bool IsAlive() const { return m_Hp > 0.f; }
private:
float m_MaxHp;
float m_Hp;
};
// 이동 컴포넌트
class MovementComponent : public Component
{
public:
explicit MovementComponent(float speed) : m_Speed(speed) {}
void Update(float dt) override
{
// 이동 처리 (예시)
}
std::string GetName() const override { return "MovementComponent"; }
float GetSpeed() const { return m_Speed; }
private:
float m_Speed;
};
// 캐릭터: 컴포넌트들을 조합
class Character
{
public:
explicit Character(const std::string& name) : m_Name(name) {}
template<typename T, typename... Args>
T* AddComponent(Args&&... args)
{
auto comp = std::make_unique<T>(std::forward<Args>(args)...);
T* ptr = comp.get();
m_Components.push_back(std::move(comp));
return ptr;
}
void Update(float dt)
{
for (auto& comp : m_Components)
{
comp->Update(dt);
}
}
const std::string& GetName() const { return m_Name; }
private:
std::string m_Name;
std::vector<std::unique_ptr<Component>> m_Components;
};
int main()
{
Character player("Hero");
auto* health = player.AddComponent<HealthComponent>(200.f);
auto* movement = player.AddComponent<MovementComponent>(300.f);
health->TakeDamage(50.f);
std::cout << "Speed: " << movement->GetSpeed() << "\n";
std::cout << "Alive: " << (health->IsAlive() ? "Yes" : "No") << "\n";
return 0;
}

개념핵심 요약
class vs struct기본 접근 제어자만 다름. 동작 포함 시 class 사용 권장
가상함수virtual 선언으로 동적 바인딩 활성화, vtable 사용
순수 가상함수= 0 선언, 추상 클래스 생성, 파생 클래스에서 구현 강제
override서명 불일치를 컴파일 타임에 잡아줌, 항상 붙이는 것 권장
final상속 또는 오버라이드를 금지할 때 사용
기반 클래스 소멸자반드시 virtual 선언 (다형성 삭제 시 파생 소멸자 호출 보장)