# C언어 (10)

## 1\. 부모·자식 클래스 사이의 변환

상속 관계에서 부모 클래스와 자식 클래스 사이에는 **타입 변환 규칙** 이 존재한다. 이 규칙을 이해하는 것이 다형성의 출발점이다.

### 자식 → 부모 (업캐스팅)

자식 클래스 객체는 부모 클래스 타입으로 **자연스럽게 변환** 된다. 이것을 **업캐스팅(Upcasting)** 이라 한다.

```cpp
class Animal {
public:
    void breathe() {
        cout << "숨을 쉰다" << endl;
    }
};

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

int main() {
    Dog dog;
    Animal *pAnimal = &dog;   // 업캐스팅 — 자식을 부모 포인터로
    Animal &rAnimal = dog;    // 레퍼런스로도 가능

    pAnimal->breathe();  // OK
    // pAnimal->bark();  // 에러! — 부모 타입이므로 자식 고유 멤버 접근 불가

    return 0;
}
```

`Dog`은 `Animal`을 상속받았으므로 `Animal`이 가진 모든 것을 포함하고 있다. `Dog` 객체를 `Animal *`로 가리켜도 `Animal` 부분은 온전히 존재하기 때문에 안전하다. 다만 부모 타입 포인터로는 **부모가 가진 멤버만 접근** 할 수 있다. 자식 고유의 `bark()`는 호출할 수 없다.

업캐스팅은 **암시적(자동)** 으로 일어난다. 캐스팅 연산자를 쓸 필요가 없다.

### 부모 → 자식 (다운캐스팅)

반대로 부모 타입을 자식 타입으로 변환하는 것을 **다운캐스팅(Downcasting)** 이라 한다. 이것은 **위험할 수 있어서 명시적 캐스팅이 필요** 하다.

```cpp
Animal *pAnimal = new Dog();       // 업캐스팅
Dog *pDog = (Dog *)pAnimal;       // 다운캐스팅 — 명시적 캐스팅 필요
pDog->bark();                      // OK — 실제로 Dog 객체이므로 안전
```

위 경우는 `pAnimal`이 실제로 `Dog` 객체를 가리키고 있으므로 안전하다. 하지만 실제로 `Animal` 객체를 가리키는 포인터를 `Dog *`로 다운캐스팅하면 존재하지 않는 멤버에 접근하게 되어 프로그램이 비정상 동작한다.

```cpp
Animal *pAnimal = new Animal();
Dog *pDog = (Dog *)pAnimal;  // 위험! 실제로는 Animal 객체
pDog->bark();                 // 정의되지 않은 동작
```

안전한 다운캐스팅을 위해 C++에서는 `dynamic_cast`를 제공한다. 실제 타입이 맞지 않으면 포인터의 경우 `nullptr`을, 레퍼런스의 경우 예외를 반환한다.

```cpp
Animal *pAnimal = new Dog();
Dog *pDog = dynamic_cast<Dog *>(pAnimal);

if (pDog != nullptr) {
    pDog->bark();  // 안전하게 호출
} else {
    cout << "변환 실패" << endl;
}
```

`dynamic_cast`는 런타임에 실제 타입을 검사하므로, 부모 클래스에 **최소 하나의** `virtual` **함수** 가 있어야 사용할 수 있다.

### 객체 슬라이싱

업캐스팅을 포인터나 레퍼런스가 아닌 **값으로** 하면 문제가 생긴다.

```cpp
Dog dog;
Animal a = dog;  // 객체 슬라이싱 발생!
```

`Dog` 객체를 `Animal` 변수에 값으로 대입하면, `Dog`에만 있는 멤버가 잘려나간다. `Animal` 크기만큼만 복사되기 때문이다. 이것을 **객체 슬라이싱(Object Slicing)** 이라 한다. 그래서 다형성을 활용할 때는 반드시 **포인터 또는 레퍼런스** 를 사용해야 한다.

* * *

## 2\. 다형성

다형성(Polymorphism)은 **같은 인터페이스가 상황에 따라 다르게 동작하는 것** 이다. C++에서 다형성을 구현하는 두 가지 방법이 바로 **오버로딩** 과 **오버라이딩** 이다.

### 2-1. 오버로딩이 보여주는 다형성

오버로딩은 **같은 이름의 함수가 매개변수에 따라 다르게 동작** 하는 것이다. 함수 이름이라는 하나의 인터페이스 뒤에 여러 구현이 숨어있다.

```cpp
void print(int x) {
    cout << "정수: " << x << endl;
}

void print(double x) {
    cout << "실수: " << x << endl;
}

void print(string x) {
    cout << "문자열: " << x << endl;
}

int main() {
    print(42);          // 정수: 42
    print(3.14);        // 실수: 3.14
    print("Hello");     // 문자열: Hello
    return 0;
}
```

`print`라는 같은 이름을 호출하지만, 넘기는 인자의 타입에 따라 다른 함수가 실행된다. 이 결정은 **컴파일 타임** 에 이루어진다. 컴파일러가 인자의 타입을 보고 어떤 함수를 호출할지 미리 결정하기 때문이다.

### 2-2. 오버라이딩이 보여주는 다형성

오버라이딩은 **부모의 함수를 자식이 재정의하여, 실제 객체에 따라 다르게 동작** 하는 것이다.

```cpp
class Shape {
public:
    virtual void draw() {
        cout << "도형을 그린다" << endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "원을 그린다" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "사각형을 그린다" << endl;
    }
};

int main() {
    Shape *shapes[3];
    shapes[0] = new Shape();
    shapes[1] = new Circle();
    shapes[2] = new Rectangle();

    for (int i = 0; i < 3; i++) {
        shapes[i]->draw();  // 실제 객체에 따라 다른 함수 호출
    }
    // 출력:
    // 도형을 그린다
    // 원을 그린다
    // 사각형을 그린다

    for (int i = 0; i < 3; i++) {
        delete shapes[i];
    }
    return 0;
}
```

`shapes` 배열은 전부 `Shape *` 타입이지만, 실제 가리키는 객체가 `Circle`인지 `Rectangle`인지에 따라 다른 `draw()`가 호출된다. 이 결정은 **런타임** 에 이루어진다. 프로그램이 실행되면서 실제 객체의 타입을 확인하고 적절한 함수를 호출하기 때문이다.

오버로딩과 오버라이딩 모두 "같은 이름, 다른 동작"이라는 다형성의 본질을 보여주지만, 결정 시점이 다르다. 이 차이를 더 정확하게 설명하는 개념이 바로 **정적 결합** 과 **동적 결합** 이다.

* * *

## 3\. 동적 결합과 정적 결합

"결합(Binding)"이란 함수 호출 코드가 **실제로 실행될 함수의 주소와 연결되는 것** 을 말한다. 이 연결이 언제 이루어지느냐에 따라 정적 결합과 동적 결합으로 나뉜다.

### 3-1. 정적 결합 (Static Binding)

정적 결합은 **컴파일 타임** 에 호출할 함수가 결정되는 것이다. **Early Binding** 이라고도 한다.

일반 함수 호출, 오버로딩된 함수 호출, 그리고 `virtual`이 아닌 멤버함수 호출이 여기에 해당한다.

```cpp
class Animal {
public:
    void speak() {  // virtual 아님
        cout << "..." << endl;
    }
};

class Dog : public Animal {
public:
    void speak() {  // 재정의했지만 virtual이 아님
        cout << "멍멍!" << endl;
    }
};

int main() {
    Dog dog;
    Animal *p = &dog;  // 업캐스팅
    p->speak();         // "..." 출력 — Animal의 speak()이 호출됨!
    return 0;
}
```

`p`의 타입은 `Animal *`이다. `speak()`에 `virtual`이 없으므로, 컴파일러는 포인터의 타입만 보고 `Animal::speak()`를 호출하기로 **컴파일 시점에 결정** 한다. 실제로 `Dog` 객체를 가리키고 있어도 무관하다.

정적 결합은 빠르다. 컴파일 타임에 함수 주소가 확정되므로 런타임에 추가적인 탐색 비용이 없다.

### 3-2. 동적 결합 (Dynamic Binding)

동적 결합은 **런타임** 에 호출할 함수가 결정되는 것이다. **Late Binding** 이라고도 한다. `virtual` 키워드가 이것을 가능하게 한다.

```cpp
class Animal {
public:
    virtual void speak() {  // virtual 함수
        cout << "..." << endl;
    }
};

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

int main() {
    Dog dog;
    Animal *p = &dog;
    p->speak();  // "멍멍!" 출력 — Dog의 speak()이 호출됨!
    return 0;
}
```

`virtual`을 붙이면 컴파일러는 포인터의 타입이 아니라 **실제 객체의 타입** 을 기준으로 함수를 호출한다. `p`의 타입은 `Animal *`이지만 실제로 `Dog` 객체를 가리키고 있으므로 `Dog::speak()`가 호출된다.

이것이 가능한 이유는 **가상 함수 테이블(vtable)** 때문이다. `virtual` 함수가 있는 클래스는 내부적으로 vtable이라는 함수 포인터 배열을 가진다. 각 객체는 자신의 클래스에 맞는 vtable을 가리키는 포인터(vptr)를 숨겨진 멤버로 가지고 있다. 런타임에 `p->speak()`를 호출하면 vptr을 통해 vtable을 찾고, vtable에서 적절한 함수 주소를 가져와 호출한다.

```plaintext
Animal vtable                Dog vtable
┌──────────────┐            ┌──────────────┐
│ Animal::speak │            │ Dog::speak   │
└──────────────┘            └──────────────┘

Animal 객체                  Dog 객체
┌──────┐                    ┌──────┐
│ vptr ──→ Animal vtable    │ vptr ──→ Dog vtable
│ ...  │                    │ ...  │
└──────┘                    └──────┘
```

동적 결합은 vtable 탐색이라는 간접 비용이 있어서 정적 결합보다 약간 느리다. 하지만 이 비용은 매우 작고, 다형성이 주는 유연함에 비하면 무시할 수 있는 수준이다.

### 정적 결합과 동적 결합 비교

핵심 차이를 정리하면 이렇다.

**정적 결합** 은 컴파일 타임에 결정되고, `virtual` 없는 함수와 오버로딩에 적용된다. 포인터/레퍼런스의 **선언된 타입** 을 기준으로 함수를 선택한다.

**동적 결합** 은 런타임에 결정되고, `virtual` 함수와 오버라이딩에 적용된다. 포인터/레퍼런스가 가리키는 **실제 객체의 타입** 을 기준으로 함수를 선택한다.

다형성을 제대로 활용하려면 `virtual` 키워드가 필수다. `virtual` 없이 자식에서 같은 이름의 함수를 정의하면 재정의가 아니라 **함수 숨김(hiding)** 이 되어, 의도한 대로 동작하지 않는다.

### 가상 소멸자

동적 결합에서 중요한 실전 규칙이 하나 있다. 부모 클래스의 소멸자에는 **반드시** `virtual`**을 붙여야** 한다.

```cpp
class Animal {
public:
    virtual ~Animal() {  // 가상 소멸자
        cout << "Animal 소멸" << endl;
    }
};

class Dog : public Animal {
private:
    int *data;

public:
    Dog() {
        data = new int[100];
    }

    ~Dog() {
        delete[] data;
        cout << "Dog 소멸" << endl;
    }
};

int main() {
    Animal *p = new Dog();
    delete p;  // Dog 소멸자 → Animal 소멸자 순서로 호출
    return 0;
}
```

소멸자에 `virtual`이 없으면 `delete p`할 때 `Animal`의 소멸자만 호출되고 `Dog`의 소멸자는 호출되지 않는다. `Dog`이 동적 할당한 `data` 메모리가 해제되지 않아 **메모리 누수** 가 발생한다. `virtual`을 붙이면 실제 객체의 소멸자(`Dog`)가 먼저 호출되고, 이어서 부모의 소멸자(`Animal`)가 자동으로 호출된다.

상속을 사용할 가능성이 있는 클래스라면, 소멸자에 `virtual`을 붙이는 것을 습관으로 만들어야 한다.

* * *

## 4\. 추상 클래스

### 4-1. 추상 클래스란?

추상 클래스는 **순수 가상 함수(Pure Virtual Function)를 하나 이상 가진 클래스** 다. 직접 객체를 만들 수 없고, 반드시 자식 클래스에서 상속받아 사용해야 한다.

순수 가상 함수는 **본체(구현)가 없는 가상 함수** 다. 선언부 끝에 `= 0`을 붙여서 표시한다.

```cpp
class Shape {
public:
    virtual void draw() = 0;      // 순수 가상 함수
    virtual double area() = 0;    // 순수 가상 함수

    virtual ~Shape() {}
};
```

`draw()`와 `area()`에 `= 0`이 붙어있다. "이 함수는 여기서 구현하지 않겠다. 자식 클래스가 반드시 구현해라"는 뜻이다.

```cpp
// Shape s;  // 에러! 추상 클래스는 객체 생성 불가
```

추상 클래스는 직접 인스턴스화할 수 없다. "도형"이라는 것은 추상적인 개념이지, 실제로 존재하는 구체적인 형태가 아니기 때문이다. 실제 존재하는 것은 원, 사각형, 삼각형 같은 구체적인 도형이다.

### 4-2. 추상 클래스의 사용

자식 클래스에서 순수 가상 함수를 **전부 구현하면** 그 자식 클래스는 객체를 만들 수 있다.

```cpp
class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    void draw() override {
        cout << "원을 그린다 (반지름: " << radius << ")" << endl;
    }

    double area() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width, height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    void draw() override {
        cout << "사각형을 그린다 (" << width << " x " << height << ")" << endl;
    }

    double area() override {
        return width * height;
    }
};
```

만약 자식 클래스에서 순수 가상 함수를 하나라도 구현하지 않으면, 그 자식 클래스도 추상 클래스가 되어 객체를 만들 수 없다.

### 4-3. 추상 클래스의 힘

추상 클래스의 진짜 가치는 **부모 타입 포인터로 여러 자식 객체를 통일적으로 다룰 수 있다** 는 것이다.

```cpp
int main() {
    Shape *shapes[3];
    shapes[0] = new Circle(5.0);
    shapes[1] = new Rectangle(4.0, 6.0);
    shapes[2] = new Circle(3.0);

    double totalArea = 0;
    for (int i = 0; i < 3; i++) {
        shapes[i]->draw();
        totalArea += shapes[i]->area();
    }

    cout << "전체 면적: " << totalArea << endl;

    for (int i = 0; i < 3; i++) {
        delete shapes[i];
    }
    return 0;
}
```

`Shape *` 배열 하나로 `Circle`이든 `Rectangle`이든 상관없이 `draw()`와 `area()`를 호출할 수 있다. 새로운 도형(`Triangle`, `Ellipse` 등)을 추가하더라도 `Shape`을 상속받고 순수 가상 함수를 구현하기만 하면 된다. 기존 코드를 수정할 필요가 없다.

이것이 바로 추상 클래스가 **인터페이스 역할** 을 하는 것이다. "이 클래스를 상속받으면 반드시 이 함수들을 구현해야 한다"는 계약을 강제한다. JavaScript나 Java의 인터페이스와 비슷한 역할이다. 다만 C++에는 `interface` 키워드가 따로 없고, 순수 가상 함수만으로 이루어진 추상 클래스가 그 역할을 한다.

### 4-4. 일반 가상 함수와 순수 가상 함수의 차이

**일반 가상 함수(**`virtual void draw() { ... }`**)** 는 기본 구현이 있다. 자식이 재정의하지 않으면 부모의 구현이 사용된다. 부모 클래스의 객체를 만들 수 있다.

**순수 가상 함수(**`virtual void draw() = 0`**)** 는 기본 구현이 없다. 자식이 반드시 구현해야 한다. 부모 클래스는 추상 클래스가 되어 객체를 만들 수 없다.

기본 동작이 의미 있는 경우에는 일반 가상 함수를 쓰고, 자식마다 반드시 다르게 동작해야 하는 경우에는 순수 가상 함수를 쓴다.

* * *

## 마무리

부모·자식 사이의 타입 변환(업캐스팅/다운캐스팅)이 다형성의 토대가 되고, `virtual` 키워드가 동적 결합을 가능하게 하여 런타임 다형성을 실현한다. 추상 클래스는 이 다형성을 설계 수준에서 강제하는 도구다. "순수 가상 함수를 반드시 구현하라"는 계약을 통해, 서로 다른 자식 클래스들을 하나의 인터페이스로 통일적으로 다룰 수 있게 해준다. 이 개념들은 C++뿐 아니라 대부분의 객체지향 언어에서 핵심적으로 사용되므로, 확실히 이해해두면 다른 언어로 넘어갈 때도 큰 도움이 된다.
