C++ 객체지향 프로그래밍 완벽 가이드: 클래스, 상속, 가상 함수
📌 학습 목표
- 객체지향 핵심 개념(클래스, 상속, 다형성) 완벽 이해
- 가상 함수와 vtable 메커니즘 심층 분석
- 순수 가상 함수, 추상 클래스, 다이아몬드 상속 문제 해결 방법 학습
- 상속과 컴포지션의 적절한 선택 기준 습득
📝 개념 정리
1. 클래스 (Class)
핵심 원리:
- 객체지향 프로그래밍의 기본 단위
- 데이터(멤버 변수)와 동작(멤버 함수)을 하나로 묶는 사용자 정의 타입
- 캡슐화를 통한 데이터 은닉과 인터페이스 제공
접근 지정자:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Student {
private: // 외부에서 접근 불가
int studentId;
string name;
protected: // 상속받은 클래스에서만 접근 가능
float gpa;
public: // 어디서든 접근 가능
// 생성자
Student(int id, const string& n) : studentId(id), name(n), gpa(0.0f) {}
// 접근자 메서드 (Getter)
int getId() const { return studentId; }
string getName() const { return name; }
// 설정자 메서드 (Setter)
void setGpa(float newGpa) {
if (newGpa >= 0.0f && newGpa <= 4.0f) {
gpa = newGpa;
}
}
// 소멸자
~Student() {
cout << "Student " << name << " destroyed" << endl;
}
};
생성자와 소멸자:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Resource {
private:
int* data;
size_t size;
public:
// 기본 생성자
Resource() : data(nullptr), size(0) {}
// 매개변수 생성자
Resource(size_t s) : size(s) {
data = new int[size];
cout << "Resource allocated: " << size << " integers" << endl;
}
// 복사 생성자 (깊은 복사)
Resource(const Resource& other) : size(other.size) {
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
cout << "Resource copied" << endl;
}
// 이동 생성자 (C++11)
Resource(Resource&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
cout << "Resource moved" << endl;
}
// 대입 연산자
Resource& operator=(const Resource& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}
return *this;
}
// 소멸자
~Resource() {
delete[] data;
cout << "Resource deallocated" << endl;
}
};
2. 상속 (Inheritance)
기본 상속:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Vehicle {
protected:
string brand;
int year;
public:
Vehicle(const string& b, int y) : brand(b), year(y) {}
virtual void start() {
cout << "Vehicle starting..." << endl;
}
virtual void displayInfo() {
cout << "Brand: " << brand << ", Year: " << year << endl;
}
virtual ~Vehicle() = default; // 가상 소멸자
};
class Car : public Vehicle {
private:
int numDoors;
public:
Car(const string& b, int y, int doors)
: Vehicle(b, y), numDoors(doors) {}
void start() override {
cout << "Car engine starting..." << endl;
}
void displayInfo() override {
Vehicle::displayInfo(); // 부모 클래스 메서드 호출
cout << "Doors: " << numDoors << endl;
}
void honk() { // Car만의 고유 메서드
cout << "Beep beep!" << endl;
}
};
다중 상속:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Flyable {
public:
virtual void fly() = 0;
virtual ~Flyable() = default;
};
class Swimmable {
public:
virtual void swim() = 0;
virtual ~Swimmable() = default;
};
class Duck : public Vehicle, public Flyable, public Swimmable {
public:
Duck() : Vehicle("Duck", 2023) {}
void start() override {
cout << "Duck waddles away..." << endl;
}
void fly() override {
cout << "Duck flying!" << endl;
}
void swim() override {
cout << "Duck swimming!" << endl;
}
};
3. 다형성 (Polymorphism)
컴파일 타임 다형성 (정적 다형성):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 함수 오버로딩
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
string add(const string& a, const string& b) {
return a + b;
}
};
// 템플릿
template<typename T>
class Container {
private:
T data;
public:
Container(const T& d) : data(d) {}
T getValue() const { return data; }
void setValue(const T& d) { data = d; }
};
런타임 다형성 (동적 다형성):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Shape {
public:
virtual double area() const = 0; // 순수 가상 함수
virtual double perimeter() const = 0;
virtual void draw() const {
cout << "Drawing a shape" << endl;
}
virtual ~Shape() = default;
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() const override {
return 3.14159 * radius * radius;
}
double perimeter() const override {
return 2 * 3.14159 * radius;
}
void draw() const override {
cout << "Drawing a circle with radius " << radius << endl;
}
};
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override {
return width * height;
}
double perimeter() const override {
return 2 * (width + height);
}
void draw() const override {
cout << "Drawing a rectangle " << width << "x" << height << endl;
}
};
4. 가상 함수와 vtable
vtable 동작 원리:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void nonVirtualFunc() { cout << "Base::nonVirtualFunc" << endl; }
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
// func2는 오버라이드하지 않음
void nonVirtualFunc() { cout << "Derived::nonVirtualFunc" << endl; }
};
// 메모리 구조 분석
void demonstrateVtable() {
Base* basePtr = new Derived();
// 가상 함수 호출 - vtable을 통해 실제 타입의 함수 호출
basePtr->func1(); // "Derived::func1" 출력
basePtr->func2(); // "Base::func2" 출력 (오버라이드 없음)
// 비가상 함수 호출 - 컴파일 타임에 결정
basePtr->nonVirtualFunc(); // "Base::nonVirtualFunc" 출력
delete basePtr;
}
가상 소멸자의 중요성:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Base {
public:
Base() { cout << "Base constructor" << endl; }
virtual ~Base() { cout << "Base destructor" << endl; } // 가상 소멸자
};
class Derived : public Base {
private:
int* data;
public:
Derived() : data(new int[100]) {
cout << "Derived constructor" << endl;
}
~Derived() {
delete[] data;
cout << "Derived destructor" << endl;
}
};
void demonstrateVirtualDestructor() {
Base* ptr = new Derived();
delete ptr; // 가상 소멸자 덕분에 Derived의 소멸자도 호출됨
// 출력: Derived destructor, Base destructor
}
💻 실무 활용 예제
1. 추상 클래스와 팩토리 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class Database {
public:
virtual void connect() = 0;
virtual void disconnect() = 0;
virtual void executeQuery(const string& query) = 0;
virtual ~Database() = default;
};
class MySQLDatabase : public Database {
public:
void connect() override {
cout << "Connecting to MySQL database..." << endl;
}
void disconnect() override {
cout << "Disconnecting from MySQL database..." << endl;
}
void executeQuery(const string& query) override {
cout << "Executing MySQL query: " << query << endl;
}
};
class PostgreSQLDatabase : public Database {
public:
void connect() override {
cout << "Connecting to PostgreSQL database..." << endl;
}
void disconnect() override {
cout << "Disconnecting from PostgreSQL database..." << endl;
}
void executeQuery(const string& query) override {
cout << "Executing PostgreSQL query: " << query << endl;
}
};
// 팩토리 클래스
class DatabaseFactory {
public:
static unique_ptr<Database> createDatabase(const string& type) {
if (type == "mysql") {
return make_unique<MySQLDatabase>();
} else if (type == "postgresql") {
return make_unique<PostgreSQLDatabase>();
}
return nullptr;
}
};
2. 다이아몬드 상속 문제 해결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Animal {
public:
virtual void eat() {
cout << "Animal eating..." << endl;
}
};
// 가상 상속으로 다이아몬드 문제 해결
class Mammal : virtual public Animal {
public:
void breathe() {
cout << "Mammal breathing air..." << endl;
}
};
class WingedAnimal : virtual public Animal {
public:
void fly() {
cout << "Flying..." << endl;
}
};
class Bat : public Mammal, public WingedAnimal {
public:
void eat() override {
cout << "Bat eating insects..." << endl;
}
void echolocate() {
cout << "Using echolocation..." << endl;
}
};
void demonstrateDiamondInheritance() {
Bat bat;
bat.eat(); // Bat의 eat() 호출
bat.breathe(); // Mammal의 breathe() 호출
bat.fly(); // WingedAnimal의 fly() 호출
bat.echolocate(); // Bat의 고유 메서드
// Animal이 한 번만 상속됨을 확인
Animal* animalPtr = &bat;
animalPtr->eat(); // Bat::eat() 호출
}
3. 컴포지션 vs 상속
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 상속 방식
class Engine {
public:
void start() { cout << "Engine starting..." << endl; }
void stop() { cout << "Engine stopping..." << endl; }
};
// 잘못된 상속 사용 (is-a 관계가 아님)
class BadCar : public Engine {
public:
void drive() {
start(); // Engine의 메서드 직접 호출
cout << "Car driving..." << endl;
}
};
// 올바른 컴포지션 사용 (has-a 관계)
class GoodCar {
private:
Engine engine; // 컴포지션
public:
void start() {
engine.start(); // Engine 객체의 메서드 위임
}
void stop() {
engine.stop();
}
void drive() {
start();
cout << "Car driving..." << endl;
}
};
🎯 연습 문제
1. 도형 클래스 계층 구조 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 추상 기본 클래스
class Shape {
public:
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
// 구체적인 도형 클래스들 구현
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// area(), perimeter(), draw() 구현
};
class Square : public Shape {
private:
double side;
public:
Square(double s) : side(s) {}
// area(), perimeter(), draw() 구현
};
class Triangle : public Shape {
private:
double a, b, c;
public:
Triangle(double a, double b, double c) : a(a), b(b), c(c) {}
// area(), perimeter(), draw() 구현
};
// 다형성 테스트 함수
void testPolymorphism() {
vector<unique_ptr<Shape>> shapes;
shapes.push_back(make_unique<Circle>(5.0));
shapes.push_back(make_unique<Square>(4.0));
shapes.push_back(make_unique<Triangle>(3.0, 4.0, 5.0));
for (const auto& shape : shapes) {
shape->draw();
cout << "Area: " << shape->area() << endl;
cout << "Perimeter: " << shape->perimeter() << endl;
cout << "---" << endl;
}
}
2. virtual vs non-virtual 함수 비교
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BaseClass {
public:
virtual void virtualFunc() {
cout << "Base::virtualFunc" << endl;
}
void nonVirtualFunc() {
cout << "Base::nonVirtualFunc" << endl;
}
};
class DerivedClass : public BaseClass {
public:
void virtualFunc() override {
cout << "Derived::virtualFunc" << endl;
}
void nonVirtualFunc() {
cout << "Derived::nonVirtualFunc" << endl;
}
};
void compareVirtualNonVirtual() {
BaseClass* ptr = new DerivedClass();
ptr->virtualFunc(); // "Derived::virtualFunc" - 런타임에 결정
ptr->nonVirtualFunc(); // "Base::nonVirtualFunc" - 컴파일타임에 결정
delete ptr;
}
3. override와 final 키워드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
virtual void func1() {}
virtual void func2() {}
virtual void func3() final {} // 더 이상 오버라이드 불가
};
class Derived : public Base {
public:
void func1() override {} // 올바른 오버라이드
void func2() override {} // 올바른 오버라이드
// void func3() override {} // 컴파일 에러! final 함수는 오버라이드 불가
};
class FinalClass final : public Base { // 더 이상 상속 불가
public:
void func1() override {}
};
// class CannotInherit : public FinalClass {}; // 컴파일 에러!
🔎 심화 학습
RTTI (Run-Time Type Information)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <typeinfo>
void demonstrateRTTI() {
Base* ptr = new Derived();
// typeid 연산자
cout << "Type: " << typeid(*ptr).name() << endl;
// dynamic_cast
Derived* derivedPtr = dynamic_cast<Derived*>(ptr);
if (derivedPtr) {
cout << "Successfully cast to Derived" << endl;
}
delete ptr;
}
가상 함수 성능 고려사항
1
2
3
4
5
6
7
8
9
10
11
12
13
// 가상 함수는 약간의 성능 오버헤드가 있음
// - 간접 함수 호출 (vtable 룩업)
// - 인라인 최적화 어려움
// - 추가 메모리 사용 (vptr)
class PerformanceCritical {
public:
// 성능이 중요한 경우 비가상 함수 고려
void fastFunction() { /* 인라인 가능 */ }
// 다형성이 필요한 경우에만 가상 함수 사용
virtual void polymorphicFunction() = 0;
};
🌐 외부 링크
💡 실무 적용 팁
-
상속 vs 컴포지션 선택 기준:
- “is-a” 관계: 상속 사용
- “has-a” 관계: 컴포지션 사용
- 코드 재사용만 목적이라면 컴포지션 고려
-
가상 함수 사용 가이드라인:
- 다형성이 필요한 경우에만 virtual 사용
- 기본 클래스의 소멸자는 항상 virtual로 선언
- 성능이 중요한 부분에서는 신중하게 고려
-
메모리 관리:
- 스마트 포인터 활용으로 자동 메모리 관리
- RAII 패턴으로 자원 관리
- 가상 소멸자로 올바른 소멸 보장
다음 학습 주제
- 고급 C++ 기능: 템플릿 메타프로그래밍, SFINAE, Concepts
- 디자인 패턴: 싱글톤, 팩토리, 옵저버, 전략 패턴
- C++20 모던 기능: 모듈, 코루틴, Ranges
- 메모리 관리: 스마트 포인터, 커스텀 할당자, 메모리 풀
🪞 회고 질문
- 객체지향의 4가지 특성을 실무 예시와 함께 설명할 수 있는가?
- vtable의 동작 원리를 메모리 구조와 함께 설명할 수 있는가?
- 상속과 컴포지션 중 어떤 것을 선택할지 판단 기준을 가지고 있는가?
- 가상 함수 사용 시 성능 고려사항을 인지하고 적절히 판단할 수 있는가?
This post is licensed under CC BY 4.0 by the author.