C++ < 상속, 다형성 >

2026. 3. 6. 14:16TIL

상속이란?

여러 클래스의 공통적인 속성을 하나의 기본클래스에 정의하고, 파생클래스가 이를 호출해서 쓰는 방식으로 코드 중복을 줄이고 구조를 효율적으로 하는 기법이다.

함수를 사용해 코드의 재사용성을 높이는 것과 비슷하다.

 

기본클래스(부모클래스) - 파생클래스(자식클래스)라는 용어로 사용하며 기본클래스의 속성을 파생클래스가 전달받아 사용할 때 상속이라는 용어를 사용한다.

 

예를 들어 자동차, 트럭, 자전거가 속도, 색상이라는 공통 속성을 가져 기본 클래스에 정의하고, 각각 상속받아 사용하고 대표적으로 트럭은 "물건을 싣는 것"이라는 속성을 추가해 사용한다.

 

이 상속에서 사용하는 접근 제어자가 바로 'protected'이다. 기본적으로 private와 같이 선언한 클래스 내에서만 사용가능하나 상속받은 클래스에서도 사용가능하다.

#include <iostream>
using namespace std;

class Animal
{
public:
    void speak()
    {
        cout << "동물이 소리를 냅니다." << endl;
    }
};

class Dog : public Animal
{
public:
    void bark()
    {
        cout << "멍멍!" << endl;
    }
};

int main()
{
    Dog d;

    d.speak();  // 부모 클래스 함수
    d.bark();   // 자식 클래스 함수

    return 0;
}

클래스의 기본 형태는 이렇다.

부모클래스는 Animal이고, 자식클래스는 Dog이다.

 

이렇게 자식클래스는 자기의 함수 + 부모클래스의 함수를 둘 다 사용할 수 있다.


상속에서 생성자

상속 관계에서 생성자는 부모클래스의 생성자가 먼저 실행되고, 그 다음에 자식 클래스의 생성자가 실행된다.

 

상속에서 생성자가 실행되는 순거는 다음 순서로 호출된다.

더보기

부모 클래스 생성자

자식 클래스 생성자

이 이유는 자식 클래스 안에는 부모클래스의 멤버도 포함되어 있기 때문에 부모 부분이 먼저 생성되어야 자식 객체가 완성된다.


자식 클래스의 생성자는 멤버 초기화 리스트를 이용해 부모 생성자를 호출할 수 있다.

자식클래스(매개변수) : 부모클래스(값)
{
}

이 형태는 "자식 객체 생성 전에 부모 생성자를 먼저 실행해라" 라는 뜻이다.

 

#include <iostream>
using namespace std;

class Animal
{
public:
    Animal(int age)
    {
        cout << "Animal 생성자 호출, age: " << age << endl;
    }
};

class Dog : public Animal
{
public:
    Dog(int age) : Animal(age)
    {
        cout << "Dog 생성자 호출" << endl;
    }
};

int main()
{
    Dog d(5);
}

간단한 예제로 이를 들어보겠다.

이는 실행결과가

Animal 생성자 호출, age: 5
Dog 생성자 호출

이렇게 출력된다.

 

Dog 객체 생성 → Animal 생성자 호출 → Dog 생성자 호출 

순서로 진행되어 부모클래스의 생성자의 내용이 출력되고, 그 후에 자식클래스의 생성자 내용이 출력되는 것이다.

 

상속에서 생성자는

1. 객체가 생성되면 부모 생성자가 먼저 실행된다.

2. 그 다음 자식 생성자가 실행된다.

3. 자식 생성자는 초기화 리스트를 통해 부모 생성자를 호출할 수 있다.

 

로 정리할 수 있겠다.

 


다형성

다형성(Polymorphism)이란, 같은 인터페이스(함수 이름)를 사용하지만 객체의 실제 타입에 따라 서로 다른 동작을 수행하도록 하는 객체지향 개념을 말한다.

 

즉, 하나의 부모 클래스 타입으로 여러 자식 객체를 다룰 수 있도록 만들어 코드의 확장성과 유지보수를 용이하게 해준다.

 

다형성은 왜 필요한가?

예를 들면 동물들을 예시로 들어보자.

동물들은 각자 울음소리가 다르다. 이 동물들의 울음소리를 별도의 함수로 관리한다면 새로운 동물이 추가될 때마다 울음소리의 코드를 다 다르게 늘려야한다.

 

이렇게 되면 클래스와 함수가 계속 증가하고 관리가 어려워진다.

void print(Lion lion)
{
	lion.bark();
}

void print(Wolf wolf)
{
	wolf.bark();
}

이런식으로 함수를 계속 생성해야만 한다.

이렇게 같은 이름의 함수이지만 매개변수가 다른 함수를 함수 오버로딩이라고 한다.

 

그래서 공통 개념인 Animal을 부모 클래스로 지정해 선언하고 각 동물은 이를 상속받아서 구현하도록 설계한다.

 

다형성의 구조

다형성은 다음과 같은 구조로 구현할 수 있다.

class Animal
{
public:
    virtual void makeSound()
    {
        cout << "Animal makes a sound." << endl;
    }
};

class Dog : public Animal
{
public:
    void makeSound()
    {
        cout << "Dog barks." << endl;
    }
};

class Cat : public Animal
{
public:
    void makeSound()
    {
        cout << "Cat meows." << endl;
    }
};

여기서 Animal 클래스에서 virtual void makeSound()를 확인할 수 있는데, virtual이 가상함수를 의미한다. 이 함수를 통해 자식 클래스에서 함수를 다시 정의할 수 있도록 해준다.

 

여기서 virtual을 사용하지 않는다면 어떻게 될까?

 

class Animal
{
public:
    void makeSound()
    {
        cout << "Animal makes a sound." << endl;
    }
};
Animal* myAnimal = &myDog;
myAnimal->makeSound();

이렇게 사용해서 실행하게 되면 결과는

 

Animal makes a sound.

생각했던 Dog의 클래스는 출력되지 않을 것이다.

왜냐하면 컴파일 시점에 부모 함수가 호출되도록 결정되기 때문이다.

 

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

부모 클래스에서 구현을 하지 않고 자식 클래스에서 각각 구현을 하기 때문에 부모 클래스에서는 굳이 구현을 안하고 싶을 수도 있다. 

 

동물은 소리를 낸다 라는 개념은 있지만 Animal 자체의 소리는 정의하기 어렵기 때문에 부모 클래스에서는 구현하기 애매할 때가 있는데 이럴 때 사용하는 것이 순수 가상 함수이다.

 

아까 전에 사용했던 virtual 함수의 끝에 = 0을 붙이면 순수 가상 함수가 된다.

class Animal
{
public:
    virtual void makeSound() = 0;
};

 

이렇게 하나 이상의 순수 가상 함수를 가지고 있는 클래스를 "추상 클래스"라고 부른다.

 

이 추상 클래스의 특징은

  • 직접 객체를 생성할 수 없다. (인스턴스화 할 수 없다.)
  • 반드시 자식 클래스에 순수 가상 함수를 구현해야 한다.
class Dog : public Animal
{
public:
    void makeSound()
    {
        cout << "Dog barks." << endl;
    }
};

 

다형성의 장점

  1. 코드 확장성이 좋아진다 : 새로운 동물이 추가되어도 기존 코드를 거의 수정할 필요가 없다.
  2. 공통 인터페이스를 제공한다. : 모든 동물이 makeSound() 함수를 가지도록 강제할 수 있다.
  3. 코드 관리가 쉬워진다 : 하나의 함수나 포인터로 여러 객체를 처리할 수 있다.

 

C++에서 다형성

C++에서 다형성은 보통 포인터나 참조를 사용해야 제대로 된 다형성이 동작한다.

 

객체를 그냥 전달하게 되면 어떻게 될까?

 

void print(Animal animal)
{
    animal.bark();
}

print(lion);

이런식으로 호출하게 되면 문제가 생긴다.

 

lion 객체의 구조는 다음과 같다

Animal 부분
+ Lion 부분

그런데 함수 매개변수가

 

Animal anim

이면

 

Animal 부분만 복사가 된다.

즉, Lion의 정보가 잘려나가게 된다.

 

이것을 객체 슬라이싱이라고 하는데 이렇게 되면

lion bark()가 아니라 Animal bark()가 실행될 수 있다.

 

따라서 지금 코드처럼 포인터를 사용하면

 

void print(Animal* animal)
{
    animal->bark();
}

Animal* → lion
Animal* → wolf
Animal* → dog

객체를 복사하지 않고 객체의 주소만 전달한다.

객체를 복사하지 않고 객체의 주소만 전달한다.

그래서 실제 객체 타입을 유지하게 된다.

 

포인터 대신 참조(&)를 사용하면

void print(Animal& animal)
{
    animal.bark();
}

print(lion);
print(wolf);
print(dog);

이런식으로 사용할 수 있는데, 이렇게 사용하면

 

객체를 복사하지 않고 원본을 그대로 사용한다.

 

그렇기 때문에 다형성이 정상적으로 동작한다.

 

보통은 참조를 많이 사용한다.

그 이유는

 

  • 포인터보다 안전하다.
  • nullptr 문제가 없다.
  • 객체 복사가 없다.

 

정리하면

  • 다형성은 같은 함수 이름으로 서로 다른 동작을 수행하는 객체지향 개념이다.
  • virtual 키워드를 사용하면 자식 클래스의 함수가 실행된다.
  • 부모 클래스 포인터로 여러 자식 객체를 처리할 수 있다.
  • =0을 사용하면 순수 가상 함수가 되며, 이를 포함한 클래스는 추상 클래스가 된다.