1. 상속과 클래스의 관계

1.1 상속의 의미

상속(Inheritance)

기존 클래스의 구성을 바탕으로 새로운 클래스를 정의하는 기능이다. 기존 클래스를 기본 클래스(Base Class), 이를 상속하는 클래스를 **파생 클래스(Derived Class)**라고 한다

기본 클래스에는 공통 상태와 동작을 두고, 파생 클래스에는 추가 상태와 동작을 정의한다

표현의미
기본 클래스, 부모 클래스, 상위 클래스상속의 기반이 되는 클래스
파생 클래스, 자식 클래스, 하위 클래스기본 클래스를 상속하는 클래스

파생 객체에는 기본 클래스에 해당하는 부분도 포함된다. 공통 상태와 동작은 기본 클래스에 두고, 파생 클래스에서는 필요한 기능을 확장한다

1.2 상속의 기본 문법

class Vehicle
{
public:
    void start() {}
};
 
class Car : public Vehicle
{
public:
    void openTrunk() {}
};
Car car;
car.start();
car.openTrunk();

Car는 Vehicle의 공개 기능을 이용하면서 자신의 기능을 추가한다. 반대로 Vehicle 객체가 Car의 추가 기능을 사용할 수 있는 것은 아니다

클래스 정의 끝에는 세미콜론을 붙인다. class Car : Vehicle처럼 상속 지정자를 생략하면 기본적으로 private 상속이므로, public 상속을 의도했다면 명시한다

1.3 is-a 관계와 has-a 관계

관계의미구현 방향
is-a자동차는 차량의 한 종류이다상속
has-a자동차는 엔진을 가진다객체를 멤버로 포함
class Engine {};
 
class Car : public Vehicle
{
private:
    Engine engine;
};

위 Car는 Vehicle을 상속하고, Engine 객체를 멤버로 가진다

public 상속은 파생 객체를 기본 클래스 객체가 필요한 곳에서도 사용할 수 있는 관계를 표현한다. 공통 코드가 있다는 이유만으로 모든 관계를 상속으로 만들지는 않는다

1.4 UML에서의 상속 표현

클래스 다이어그램에서 상속은 파생 클래스에서 기본 클래스를 향하는 빈 삼각형 화살표로 표현한다

기호접근 수준
+public
-private
#protected

UML의 화살표 방향과 C++ 선언의 class Car : public Vehicle을 연결해서 읽으면 된다






2. 상속과 멤버 접근 제어

2.1 세 가지 접근 지정자

기본 클래스 멤버기본 클래스 내부파생 클래스 내부에서 직접 접근외부 코드에서 직접 접근
public가능가능가능
protected가능가능불가능
private가능불가능불가능

위 표는 public 상속에서 자신의 멤버를 사용하는 일반적인 경우를 나타낸다. 파생 클래스가 임의의 기본 클래스 객체의 protected 멤버까지 직접 접근할 수 있다는 뜻은 아니다

class Vehicle
{
private:
    int weight;
 
protected:
    int speed;
 
public:
    Vehicle(int w) : weight(w), speed(0) {}
    int getWeight() const { return weight; }
};
 
class Car : public Vehicle
{
public:
    Car(int w) : Vehicle(w) {}
 
    void accelerate()
    {
        speed += 10;
        // weight += 10; // 기본 클래스의 private 멤버에 직접 접근 불가
    }
};

2.2 private 멤버와 상속

기본 클래스의 private 멤버는 파생 객체의 기본 클래스 부분에 존재하지만, 파생 클래스의 멤버 함수에서 직접 접근할 수 없다

Car car(1000);
int weight = car.getWeight();
// car.speed = 50; // 외부에서 protected 멤버에 직접 접근 불가

getWeight()는 Vehicle의 멤버 함수이므로 Vehicle::weight에 접근할 수 있다. 파생 객체를 통해 호출해도 이 권한은 유지된다

private 멤버가 필요하다면 기본 클래스가 제공하는 함수를 사용한다. 외부에서 사용할 필요가 없는 데이터까지 public으로 바꾸지는 않는다

따라서 “private 멤버는 상속되지 않으므로 파생 객체 안에도 없다”라고 이해하면 안 된다. 데이터가 객체 안에 존재하는지와 파생 클래스에서 직접 이름을 사용할 수 있는지는 서로 다른 문제이다

2.3 protected의 역할

protected는 외부 코드의 직접 접근은 제한하면서 파생 클래스에는 접근을 허용할 때 사용한다

다만 파생 클래스가 기본 클래스의 내부 데이터에 직접 의존하게 되므로, 모든 데이터를 protected로 두는 것이 항상 적절한 것은 아니다. 필요한 기능을 멤버 함수로 제공하는 방식도 사용할 수 있다






3. 상속 방식에 따른 접근 수준

3.1 멤버의 접근 지정자와 상속 지정자

class Derived : public Base
{
    // ...
};

Base 안의 public·protected·private는 멤버의 접근 수준이고, : public Base의 public은 상속 방식이다

상속 방식은 기본 클래스의 public·protected 멤버가 파생 클래스에서 어떤 접근 수준으로 취급되는지를 결정한다

3.2 public, protected, private 상속

기본 클래스의 멤버public 상속protected 상속private 상속
publicpublic으로 취급protected로 취급private로 취급
protectedprotected로 취급protected로 취급private로 취급
private직접 접근 불가직접 접근 불가직접 접근 불가

기본 클래스의 private 멤버가 파생 클래스의 private 멤버로 바뀌는 것은 아니다. 어떤 상속 방식을 사용하더라도 파생 클래스가 기본 클래스의 private 멤버에 직접 접근할 수는 없다

class Base
{
public:
    void f() {}
};
 
class PublicChild : public Base {};
class ProtectedChild : protected Base {};
class PrivateChild : private Base {};
PublicChild a;
a.f();
 
ProtectedChild b;
// b.f(); // 외부에서 접근 불가
 
PrivateChild c;
// c.f(); // 외부에서 접근 불가

3.3 여러 단계로 상속할 때

private 상속을 한 클래스 내부에서는 기본 클래스의 public·protected 멤버를 사용할 수 있다. 하지만 그 멤버를 private로 취급하므로 다음 파생 클래스는 이를 직접 사용할 수 없다

class Parent
{
public:
    void f() {}
};
 
class Child : private Parent
{
public:
    void call() { f(); }
};
 
class Grandchild : public Child
{
public:
    void test()
    {
        call();
        // f(); // Child의 private 상속 경로로는 직접 접근 불가
    }
};

Grandchild는 Child가 공개한 call()을 사용할 수 있다. 접근 여부는 최초 선언뿐 아니라 상속 경로 전체를 따라 판단한다






4. 함수 오버로딩, 이름 숨김과 재정의

4.1 오버로딩과 재정의의 구분

구분핵심
오버로딩(Overloading)같은 이름에 서로 다른 매개변수 목록을 가진 함수 제공
이름 숨김(Name Hiding)파생 클래스의 같은 이름 선언이 기본 클래스의 이름을 가림
가상 함수 재정의(Overriding)기본 클래스의 가상 함수에 대응하는 파생 클래스 구현 제공

매개변수가 다르면 항상 상속 관계 전체에서 자동으로 오버로딩되는 것은 아니다. 이름을 먼저 찾는 과정에서 기본 클래스의 함수가 가려질 수 있다

4.2 같은 이름의 함수와 이름 숨김

class Base
{
public:
    void print() {}
    void print(int n) {}
};
 
class Derived : public Base
{
public:
    void print(const char* text) {}
};
Derived d;
d.print("hello");
// d.print();  // Base의 print가 가려져 호출할 수 없음
d.Base::print();
d.Base::print(10);

Derived::print는 이름이 같은 기본 클래스의 오버로드들을 가린다. Base::를 명시하면 기본 클래스의 함수를 지정할 수 있다

기본 클래스의 오버로드도 함께 사용하려면 다음처럼 선언할 수 있다

class Derived : public Base
{
public:
    using Base::print;
    void print(const char* text) {}
};

using Base::print는 기본 클래스의 해당 이름을 파생 클래스의 이름 탐색에 포함한다

4.3 기본 클래스 기능을 활용한 함수 작성

class Vehicle
{
private:
    int weight;
 
public:
    Vehicle(int w) : weight(w) {}
    int getWeight() const { return weight; }
};
 
class Car : public Vehicle
{
private:
    int passengerWeight;
 
public:
    Car(int w, int p) : Vehicle(w), passengerWeight(p) {}
 
    int getWeight() const
    {
        return Vehicle::getWeight() + passengerWeight;
    }
};

Vehicle::getWeight()로 기본 클래스 함수를 지정한다. 여기에서 단순히 getWeight()를 호출하면 자기 자신을 다시 호출하므로 재귀 호출이 된다

Car car(1000, 150);
int total = car.getWeight();             // 1150
int body = car.Vehicle::getWeight();     // 1000
 
Vehicle& vehicle = car;
int baseWeight = vehicle.getWeight();    // 1000

Car 객체를 통해 getWeight를 호출하면 Car의 함수를 사용한다. 하지만 Vehicle 참조를 통해 호출하면 비가상 함수인 Vehicle의 함수를 사용한다

이 코드의 기본 클래스 함수는 virtual이 아니므로, 파생 클래스에 같은 이름과 매개변수의 함수를 작성했다고 해서 실행 중 다형성이 생기지는 않는다

같은 이름의 함수와 가상 함수 재정의

기본 함수가 virtual인지 확인해야 한다. 비가상 함수의 같은 이름 선언과 가상 함수의 재정의는 호출 결과가 다르며, 기본 클래스 포인터를 통한 동적 바인딩은 가상 함수의 동적 바인딩과 연결된다

4.4 가상 함수 재정의의 형태

class Base
{
public:
    virtual int value() const { return 1; }
};
 
class Derived : public Base
{
public:
    int value() const override { return 2; }
};

override는 C++11부터 사용할 수 있으며, 실제로 기본 클래스의 가상 함수를 재정의하는지 검사하게 한다. 매개변수나 함수 뒤의 const가 다르면 의도한 재정의가 되지 않을 수 있다






5. 상속과 생성자·소멸자

5.1 생성 순서

파생 객체를 생성하면 기본 클래스 부분을 먼저 생성한다

  1. 기본 클래스 생성
  2. 파생 클래스의 멤버 객체를 선언 순서대로 생성
  3. 파생 클래스 생성자 본문 실행
#include <iostream>
 
class Base
{
public:
    Base() { std::cout << "Base constructor\n"; }
    ~Base() { std::cout << "Base destructor\n"; }
};
 
class Derived : public Base
{
public:
    Derived() { std::cout << "Derived constructor\n"; }
    ~Derived() { std::cout << "Derived destructor\n"; }
};
 
int main()
{
    Derived d;
}

main의 지역 객체 d를 생성한 뒤 정상적으로 함수가 끝나면 다음 순서로 출력된다

Base constructor
Derived constructor
Derived destructor
Base destructor

5.2 기본 클래스 생성자의 지정

class Point
{
private:
    int x;
    int y;
 
public:
    Point(int px, int py) : x(px), y(py) {}
};
 
class Circle : public Point
{
private:
    double radius;
 
public:
    Circle(int x, int y, double r)
        : Point(x, y), radius(r)
    {
    }
};

Point(x, y)는 기본 클래스 부분을 초기화할 생성자를 지정한다. radius(r)는 파생 클래스의 멤버를 초기화한다

기본 클래스 생성자를 명시하지 않으면 기본 클래스의 기본 생성자를 사용하려 한다. 위 Point에는 기본 생성자가 없으므로 Point(x, y)를 생략하면 컴파일할 수 없다

5.3 생성자 본문과 초기화의 구분

기본 클래스 부분은 파생 클래스 생성자 본문에 진입하기 전에 이미 생성된다. 따라서 본문에서 기본 클래스 생성자를 나중에 호출하는 방식으로 초기화를 대신할 수 없다

Circle(int x, int y, double r)
    : Point(x, y), radius(r)
{
    // 이 시점에는 Point 부분과 radius의 초기화가 끝난 상태
}

기본 클래스의 private 멤버를 파생 생성자의 초기화 목록에서 직접 초기화하지 않는다. 기본 클래스의 생성자가 자기 멤버를 초기화하도록 인수를 전달한다

5.4 소멸 순서

정상적인 파생 객체의 소멸 순서는 생성의 역순이다

  1. 파생 클래스 소멸자 본문 실행
  2. 파생 클래스의 멤버 객체를 선언의 역순으로 소멸
  3. 기본 클래스 부분 소멸

파생 클래스 소멸자에서 기본 클래스 소멸자를 직접 호출할 필요는 없다

기본 클래스 포인터로 파생 객체를 delete하는 경우에는 별도로 가상 소멸자가 필요하다. 기본 클래스 포인터를 통한 삭제와 가상 소멸자의 관계는 다형성에서 다룬다






6. 상속 관계의 헤더 구성

6.1 기본 클래스 정의의 포함

파생 클래스를 정의하려면 기본 클래스의 정의를 확인할 수 있어야 한다

person.h

#ifndef PERSON_H
#define PERSON_H
 
class Person
{
private:
    int age;
 
public:
    Person(int a) : age(a) {}
    int getAge() const { return age; }
};
 
#endif

student.h

#ifndef STUDENT_H
#define STUDENT_H
 
#include "person.h"
 
class Student : public Person
{
private:
    int studentId;
 
public:
    Student(int age, int id) : Person(age), studentId(id) {}
};
 
#endif

class Person;과 같은 전방 선언만으로는 Person을 기본 클래스로 삼는 Student의 정의를 작성할 수 없다

6.2 헤더 가드와 중복 포함

#include "person.h"
#include "student.h"

student.h도 person.h를 포함하므로, 헤더 가드가 없다면 같은 번역 단위에 Person 정의가 중복될 수 있다

각 헤더에 서로 다른 가드 이름을 사용한다. 헤더 가드는 한 번역 단위에서 동일 헤더의 중복 처리를 방지하며, 여러 소스 파일에 들어간 일반 함수의 중복 정의까지 해결하지는 않는다






7. 다중 상속과 다이아몬드 문제

7.1 여러 기본 클래스의 상속

다중 상속(Multiple Inheritance)

하나의 클래스가 둘 이상의 클래스를 직접 상속하는 것이다

class Reader
{
public:
    void read() {}
};
 
class Writer
{
public:
    void write() {}
};
 
class IOFile : public Reader, public Writer
{
};
IOFile file;
file.read();
file.write();

각 기본 클래스 앞에 상속 지정자를 따로 작성한다. public Reader, Writer라고 쓰면 Writer에는 class의 기본값인 private 상속이 적용된다

7.2 생성자와 소멸자의 순서

class Combined : public First, public Second
{
public:
    Combined() : Second(), First() {}
};

위 형태에서는 초기화 목록에 적은 순서와 관계없이 기본 클래스 선언 순서인 First → Second로 생성된다

일반적인 비가상 다중 상속의 순서는 다음과 같다

단계생성소멸
기본 클래스상속 목록의 왼쪽부터상속 목록의 역순
멤버 객체선언 순서선언의 역순
해당 클래스 본문생성자 본문은 마지막소멸자 본문은 처음

Combined의 소멸자 본문을 실행한 뒤 멤버들을 소멸하고, Second와 First 부분을 차례로 소멸한다

7.3 같은 이름의 멤버와 모호성

서로 다른 기본 클래스가 같은 이름의 함수를 제공하면 어느 함수를 호출할지 모호할 수 있다

class Reader
{
public:
    void open() {}
};
 
class Writer
{
public:
    void open() {}
};
 
class IOFile : public Reader, public Writer {};
IOFile file;
// file.open(); // 어느 open인지 모호함
file.Reader::open();
file.Writer::open();

범위 지정 연산자로 경로를 선택할 수 있다

7.4 다이아몬드 구조

class File
{
public:
    int position = 0;
};
 
class ReadFile : public File {};
class WriteFile : public File {};
class IOFile : public ReadFile, public WriteFile {};

일반 상속에서는 IOFile 안에 ReadFile을 통한 File 부분과 WriteFile을 통한 File 부분이 각각 존재한다

IOFile file;
file.ReadFile::position = 10;
file.WriteFile::position = 20;

두 position은 서로 다른 데이터이다. ::로 경로를 지정하면 접근의 모호성은 해결할 수 있지만, 기본 클래스 부분이 두 개 존재하는 구조 자체는 바뀌지 않는다

7.5 가상 상속의 기본 형태

class File
{
public:
    int position = 0;
};
 
class ReadFile : virtual public File {};
class WriteFile : virtual public File {};
class IOFile : public ReadFile, public WriteFile {};

두 경로에서 File을 가상 상속하면 IOFile의 해당 가상 기본 클래스 부분을 공유한다

IOFile file;
file.position = 10;

가상 기본 클래스는 가장 파생된 클래스가 초기화한다. 기본 생성자가 없다면 가장 파생된 클래스의 초기화 목록에서 필요한 인수를 전달해야 한다

상속 선언의 virtual은 기본 클래스 부분의 공유에 관한 문법이다. 멤버 함수의 virtual과는 역할이 다르다

다중 상속은 여러 기능을 결합할 수 있지만 이름 충돌과 초기화 관계가 복잡해질 수 있으므로, 클래스 사이의 관계를 확인해서 사용한다