C++에서 클래스 간 관계를 구축하는 핵심 메커니즘인 상속을 다각도로 살펴본다. 접근 제어의 변화, 생성자 호출 순서, 그리고 다이아몬드 상속 문제 해결까지 실제 코드 중심으로 파헤친다.
상속 vs 파생: 개념적 구분
두 용어는 종종 혼용되지만 미묘한 차이가 있다. 상속은 기반 클래스의 특성을 물려받는 행위 자체를 강조하고, 파생은 물려은 바탕 위에 새로운 기능을 확장·발전시키는 과정을 의미한다. 실무에서는 두 용어를 구분하기보다 "파생 클래스가 기반 클래스를 상속한다"는 식으로 자연스럽게 사용한다.
접근 지정자 변환 규칙
상속 방식에 따라 기반 클래스 멤버의 접근 레이 변환된다.
| 상속 방식 | public 멤버 | protected 멤버 | private 멤버 |
|---|---|---|---|
| public | public | protected | 접근 불가 |
| protected | protected | protected | 접근 불가 |
| private | private | private | 접근 불가 |
핵심 제약: private 멤버는 어떤 상속 방식으로도 접근할 수 없으며, 연쇄적인 private 상속은 멤버를 완전히 무력화시킨다.
접근 지정자 변환 실증
기반 클래스 정의:
class Entity {
public:
int x_coord;
protected:
int y_coord;
private:
int z_coord; // 외부 및 파생 클래스 모두 접근 불가
};
public 상속을 받는 경우:
class Avatar : public Entity {
public:
void display() {
std::cout << x_coord; // OK: public 유지
std::cout << y_coord; // OK: protected로 접근 가능
// std::cout << z_coord; // 컴파일 오류
}
};
private 상속을 받는 경우:
class Shadow : private Entity {
public:
void reveal() {
std::cout << x_coord; // OK: but Shadow 내부에서만 private
std::cout << y_coord; // OK: but Shadow 내부에서만 private
// Shadow를 상속받는 클래스는 x_coord, y_coord 접근 불가
}
};
생성자 위임 패턴
기반 클래스 속성은 protected로 선언하여 상속받은 클래스가 직접 조작할 수 있게 하는 것이 권장된다.
class Character {
public:
Character() = default;
Character(int lvl, std::string id) : level(lvl), identifier(id) {}
protected:
int level;
std::string identifier;
};
class Hero : public Character {
public:
Hero() = default;
Hero(int base_lvl, std::string base_id, int hero_pwr, std::string hero_title)
: Character(base_lvl, base_id) // 기반 생성자 명시 호출
, power(hero_pwr)
, title(hero_title) {}
void show_profile() const {
std::cout << "레벨: " << level
<< ", ID: " << identifier << "\n";
std::cout << "전투력: " << power
<< ", 칭호: " << title << "\n";
}
private:
int power;
std::string title;
};
연쇄 단일 상속
단일 상속을 여러 단계로 연결하는 구조다. 각 파생 클래스는 직계 부모의 생성자만 호출하면 된다.
class Tier1 {
public:
explicit Tier1(int v) : value_t1(v) {}
protected:
int value_t1;
};
class Tier2 : public Tier1 {
public:
Tier2(int v1, int v2) : Tier1(v1), value_t2(v2) {}
protected:
int value_t2;
};
class Tier3 : public Tier2 {
public:
Tier3(int v1, int v2, int v3) : Tier2(v1, v2), value_t3(v3) {}
void output() const {
std::cout << value_t1 << value_t2 << value_t3 << "\n";
}
protected:
int value_t3;
};
다중 상속 구현
두 개 이상의 기반 클래스를 동시에 상속한다. 각 기반 클래스의 생성자를 초기화 리스트에서 개별 호출한다.
class Engine {
public:
explicit Engine(int hp) : horsepower(hp) {}
protected:
int horsepower;
};
class Chassis {
public:
explicit Chassis(int wt) : weight(wt) {}
protected:
int weight;
};
class Vehicle : public Engine, public Chassis {
public:
Vehicle(int hp, int wt, std::string nm)
: Engine(hp), Chassis(wt), name(nm) {}
void specs() const {
std::cout << name << ": "
<< horsepower << "마력, "
<< weight << "kg\n";
}
private:
std::string name;
};
가상 상속: 다이아몬드 문제 해결
공통 조상을 여러 경로로 상속받을 때 발생하는 멤버 중복을 방지한다. 중간 계에서 virtual 키워드를 사용하고, 최종 파생 클래스에서 공통 조상의 생성자를 직접 호출해야 한다.
class Root {
public:
Root() = default;
explicit Root(int r) : root_val(r) {
std::cout << "Root 생성\n";
}
protected:
int root_val;
};
class BranchX : virtual public Root {
public:
BranchX() = default;
BranchX(int r, int x) : Root(r), x_val(x) {
std::cout << "BranchX 생성\n";
}
protected:
int x_val;
};
class BranchY : virtual public Root {
public:
BranchY() = default;
BranchY(int r, int y) : Root(r), y_val(y) {
std::cout << "BranchY 생성\n";
}
protected:
int y_val;
};
class Leaf : public BranchX, public BranchY {
public:
// 핵심: 가상 상속 시 Root 생성자를 직접 호출해야 함
Leaf(int r, int x, int y, int l)
: Root(r) // 공통 조상 직접 초기화
, BranchX(r, x) // 가상 상속된 BranchX
, BranchY(r, y) // 가상 상속된 BranchY
, leaf_val(l) {
std::cout << "Leaf 생성\n";
}
void display() const {
std::cout << "root: " << root_val
<< ", x: " << x_val
<< ", y: " << y_val
<< ", leaf: " << leaf_val << "\n";
}
private:
int leaf_val;
};
생성 순서 확인:
Leaf node(10, 20, 30, 40);
// 출력: Root 생성 → BranchX 생성 → BranchY 생성 → Leaf 생성
// Root는 단 한 번만 생성되며, BranchX와 BranchY의 Root(r) 호출은 무시된다
상속 순서와 생성자 호출
다중 상속에서 초기화 리스트의 순서와 무관하게, 선언된 상속 순서대로 기반 클래스가 생성된다.
// class Leaf : public BranchX, public BranchY
// 생성 순서: Root → BranchX → BranchY → Leaf
// class Leaf : public BranchY, public BranchX
// 생성 순서: Root → BranchY → BranchX → Leaf
이는 가상 상속 여부와 관계없이 적용되며, 프로그램의 예 가능성을 위해 상속 선언 순서를 논리적으로 배치해야 함을 의미한다.