공부/C++

상속, 파생 클래스, 객체 생성 / 소멸 순서, 오버라이드, 가상 함수, 동적 바인딩, 가상 소멸자, 순수 가상 함수, 추상 클래스, 업 캐스팅, 다운 캐스팅, 객체지향 프로그래밍 4대 특성

월러비 2025. 6. 24. 19:15

짧은 설명

  • 생성자는 한 객체에서 하나 이상 한번에 호출할 수 있다.
  • 부모자식간에 생성자를 명시적으로 호출할 수 있다.
    • 한 객체 선언 시 여러 생성자를 한번에 호출할 수 있다.
  • 부모의 오버로딩 생성자에서 기본 생성자를 호출한다면?
    • 자식 생성자에서 기본 생성자를 호출해야한다.
  • 자식 멤버에서 부모의 객체가 멤버변수로 있다면?
    • 부모의 기본 생성자가 선언되어있어야한다.
  • 호출 순서
    1. 부모 클래스 생성자 - 따로 지정 안하면 기본 생성자 호출
    2. 부모 멤버 초기화

상속

  • 부모클래스의 멤버를 물려받는 것이다.
  • 자식 클래스는 부모 클래스의 private에 접근할 수 없다.
  • 간접 참조 : 포인터, 레퍼런스
    • 변수들이다.
  • 업캐스팅 / 다운캐스팅 : 변수들의 형변환이다.
  • 절차 지향 : 순서대로 작업 처리
    • ~한다. → 함수의 목적
    • 함수들의 집합 처리 프로그래밍을 의미한다.
  • 객체 지향 : 주체 우선 처리
    • ~한다가 아니라 ‘누가’ 뭘 한다 처럼 주체가 중요한 프로그래밍이다.
    • 클래스들의 상호작용이다.

상속 기본

  • 상속은 기존 클래스의 특성을 물려받아 새로운 클래스를 만드는 객체지향 프로그래밍의 핵심 개념이다.
  • 코드 재사용성 향상: 공통 기능을 기본 클래스에 정의
  • 계층적 관계 표현: 현실 세계의 계층 구조를 프로그램으로 모델링
    • 계층 관계 : 파일시스템처럼 부모안에 자식안의 자식같이 계층이 나눠진것이다.
  • 유지보수 용이: 공통 기능 수정 시 한 곳만 수정

파생 클래스

  • 파생 클래스(Derived Class): 상속을 받는 클래스, 자식 클래스라고도 함
    • 기본 클래스(Base Class): 상속을 제공하는 클래스, 부모 클래스라고도 함
class 파생클래스명 : 접근지정자 상속받을 기본클래스명 {
    // 파생 클래스의 멤버들
};

class Animal {
public:
    void Breathe() { cout << "숨을 쉰다." << endl; }
    int age;
};

class Dog : public Animal {
public:
    void Walk() { cout << "걷는다." << endl; }
};

class Sparrow : public Animal {
public:
    void Fly() { cout << "난다." << endl; }
};

int main() {
    Dog d;
    d.age = 7;
    d.Breathe();  // 상속받은 멤버 함수
    d.Walk();     // 자신의 멤버 함수
    cout << d.age << endl;

    Sparrow s;
    s.age = 2;
    s.Breathe();  // 상속받은 멤버 함수
    s.Fly();      // 자신의 멤버 함수
    cout << s.age << endl;
}

  • Animal 클래스는 모든 동물의 공통 특성인 Breathe() 메서드와 age 속성을 가짐
  • Dog 클래스는 Animal을 상속받아 Walk() 메서드를 추가
  • Sparrow 클래스는 Animal을 상속받아 Fly() 메서드를 추가
  • 파생 클래스 객체는 기본 클래스의 모든 public 멤버에 접근 가능

생성자 호출 순서

  • 상속 관계에서 객체 생성 시 생성자 호출 순서:
  1. 기본 클래스 생성자가 먼저 호출
  2. 파생 클래스 생성자가 나중에 호출
class Base {
public:
    Base() {
        cout << "Base()" << endl; // ------- 1
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived()" << endl; // -------- 2
    }
};

int main() {
    Derived d;
}

객체 생성 / 소멸 순서

class Ice {
public:
    Ice() { cout << "Ice()" << endl; }
    ~Ice() { cout << "~Ice()" << endl; }
};

class Pat {
public:
    Pat() { cout << "Pat()" << endl; }
    ~Pat() { cout << "~Pat()" << endl; }
};

class Bingsoo {
public:
    Bingsoo() { cout << "Bingsoo()" << endl; }
    ~Bingsoo() { cout << "~Bingsoo()" << endl; }

private:
    Ice ice;
};

class PatBingsoo : public Bingsoo {
public:
    PatBingsoo() { cout << "PatBingsoo()" << endl; }
    ~PatBingsoo() { cout << "~PatBingsoo()" << endl; }

private:
    Pat pat;
};

int main() {
    cout << "===== 1 =====" << endl;
    PatBingsoo* p = new PatBingsoo;
    cout << "===== 2 =====" << endl;
    delete p;
    cout << "===== 3 =====" << endl;
}
===== 1 =====
Ice()
Bingsoo()
Pat()
PatBingsoo()
===== 2 =====
~PatBingsoo()
~Pat()
~Bingsoo()
~Ice()
===== 3 =====
  • 생성 순서: 기본 클래스 멤버 → 기본 클래스 → 파생 클래스 멤버 → 파생 클래스
  • 소멸 순서: 파생 클래스 → 파생 클래스 멤버 → 기본 클래스 → 기본 클래스 멤버

상속이 필요한 경우

코드 중복 제거

  • 개선 전
  • class Image { public: operator string() //string 형변환 오버로드 { return "사진"; } }; class TextMessage { public: TextMessage(int sendTime, string sendName, string text) { this->sendTime = sendTime; this->sendName = sendName; this->text = text; } int GetSendTime() const { return sendTime; } string GetSendName() const { return sendName; } string GetText() const { return text; } private: int sendTime; // 중복 string sendName; // 중복 string text; }; class ImageMessage { public: ImageMessage(int sendTime, string sendName, Image* image) { this->sendTime = sendTime; // 중복 this->sendName = sendName; // 중복 this->image = image; } int GetSendTime() const { return sendTime; } // 중복 string GetSendName() const { return sendName; } // 중복 Image* GetImage() const { return image; } private: int sendTime; // 중복 string sendName; // 중복 Image* image; }; int main() { Image* dogImage = new Image; TextMessage* hello = new TextMessage(10, "두들", "안녕"); ImageMessage* dog = new ImageMessage(20, "두들", dogImage); cout << "보낸 시간 : " << hello->GetSendTime() << endl; cout << "보낸 사람 : " << hello->GetSendName() << endl; cout << " 내 용 : " << hello->GetText() << endl; cout << endl; cout << "보낸 시간 : " << dog->GetSendTime() << endl; cout << "보낸 사람 : " << dog->GetSendName() << endl; cout << " 내 용 : " << (string)*dog->GetImage() << endl; cout << endl; delete dogImage; delete hello; delete dog; }
  • 상속을 사용하지 않으면 여러 클래스에서 동일한 코드를 반복해야 함
  • 개선 후
  • class Image { public: operator string() { return "사진"; } }; class Message { public: Message(int sendTime, string sendName) { this->sendTime = sendTime; this->sendName = sendName; } int GetSendTime() const { return sendTime; } string GetSendName() const { return sendName; } private: // 자식 클래스에서도 이 멤버들에 직접적인 접근 불가 int sendTime; string sendName; }; class TextMessage : public Message { public: TextMessage(int sendTime, string sendName, string text) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출, 자기 바로 위에만 가능. this->text = text; } string GetText() const { return text; } private: string text; }; class ImageMessage : public Message { public: ImageMessage(int sendTime, string sendName, Image* image) : Message(sendTime, sendName) { // 부모 클래스 생성자 호출 this->image = image; } Image* GetImage() const { return image; } private: Image* image; }; int main() { Image* dogImage = new Image; TextMessage* hello = new TextMessage(10, "두들", "안녕"); ImageMessage* dog = new ImageMessage(20, "두들", dogImage); cout << "보낸 시간 : " << hello->GetSendTime() << endl; cout << "보낸 사람 : " << hello->GetSendName() << endl; cout << " 내 용 : " << hello->GetText() << endl; cout << endl; cout << "보낸 시간 : " << dog->GetSendTime() << endl; cout << "보낸 사람 : " << dog->GetSendName() << endl; cout << " 내 용 : " << (string)*dog->GetImage() << endl; cout << endl; delete dogImage; delete hello; delete dog; }
  • 공통 멤버 변수와 메서드를 Message 클래스에 통합
  • 코드 중복 제거
  • 유지보수가 용이해짐

오버라이드

  • 상속 관계 : 부모자식간에 모든 객체를 참조할 수 있다.
    • A / B : A / C : B
    • A* or A& → A객체에 참조 가능 , B객체에 참조 가능 , C객체에 참조 가능
    • 간접 참조하고 있는 주소를 통해 해당 클래스의 함수를 호출할 수 있다.
  • 파생 클래스에서 기본 클래스와 동일한 이름의 멤버를 정의하면 기본 클래스의 멤버가 가려짐
  • 부모 자식에 같은 이름의 멤버 함수가 있다면, 멤버 하이딩 또는 오버라이딩이 발생한다.
    • 상속관계에서 동일한 이름의 함수가 있을때 참조하고 있는 주소의 함수가 호출된다.
    • 멤버 하이딩 : 부모와 같은 이름의 함수이지만, 부모가 가상 함수가 아닌 함수의 호출이다.
      • 정적 바인딩이다.
      • 포인터나 참조 타입이 아닌 객체의 데이터형 기준으로 호출된다.
    • 오버 라이딩 : 부모가 가상 함수를 붙였을 때 같은 이름의 함수를 호출하는 것이다.
      • 저장하는 객체의 데이터형과 무관하게 실행 당시에 참조하고 있는 주소의 데이터형의 함수가 호출된다.
class Base {
public:
    int a = 10;
};

class Derived : public Base {
public:
    int a = 20;  // Base의 a를 가림 / 멤버 은닉 / 멤버 하이딩
};

int main() {
    Base b;
    Derived d;

    cout << b.a << endl;  // 10 출력
    cout << d.a << endl;  // 20 출력 (Derived의 a)
}

  • 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
  • 가려진 기본 클래스의 멤버에 접근할 때 유용
  • class Base { public: void Print() { cout << "From Base!" << endl; } }; class Derived : public Base { public: void Print() override { // Base의 Print()를 오버라이드 cout << "From Derived!" << endl; } }; int main() { Derived d; d.Print(); // Derived의 Print() 호출 d.Base::Print(); // 명시적으로 Base의 Print() 호출 d.Derived::Print(); // 명시적으로 Derived의 Print() 호출 }
  • 객체.클래스명::멤버: 특정 클래스의 멤버를 명시적으로 호출
  • 가려진 기본 클래스의 멤버에 접근할 때 유용

가상 함수 / 오버라이드 / 동적 바인딩

  • 가상 함수 : 멤버함수 앞에 virtual을 선언한다.
    • 동작에는 차이가 없다.
    • 차이 : 자식 함수에 같은 이름이 있다면 ‘오버라이드’ 되는 것을 의미한다.
    • 필요에 의해서 재정의를 안해도 된다.
      • 부모에 정의된 기능대로 작동하게 된다.
  • 오버 라이딩 : 동적 바인딩에 의해 호출되는것이다. ( = 재정의)
    • 오버 로딩 : 정적 바인딩에 의해 호출되는 것이다.
    • 정적 바인딩 : 컴파일 전에 참조를 결정하는 것이다.
  • 바인딩 : 함수를 호출할때 같은 이름의 함수가 여러개일때 어떤 함수를 호출할지 참조를 확인하는 기능이다.
  • 동적 바인딩 : 실행 중에 호출할 떄 참조를 확인하여 어떤 함수를 호출할 지 정하는 것이다.
  • 부모 클래스의 간접 참조로 자기 자식 객체도 참조할 수 있다.
    • 부모* = 자식 객체 주소
class Weapon {
public:
    Weapon(int power) : power(power) {
        cout << "Weapon(int)" << endl;
    }

    virtual void Use() {  // 가상 함수!!
        cout << "Weapon::Use()" << endl;
    }

protected:
    int power;
};

class Sword : public Weapon {
public:
    Sword(int power) : Weapon(power) {
        cout << "Sword(int)" << endl;
    }

    void Use() overrride {  // 오버라이드
        cout << "Sword::Use()" << endl;
        swing();
    }

private:
    void swing() {
        cout << "Swing sword." << endl;
    }
};

class Magic : public Weapon {
public:
    Magic(int power, int manaCost) : Weapon(power), manaCost(manaCost) {
        cout << "Magic(int, int)" << endl;
    }

    void Use() overrride {  // 오버라이드
        cout << "Magic::Use()" << endl;
        cast();
    }

private:
    void cast() {
        cout << "Cast magic." << endl;
    }

    int manaCost;
};

int main() {
    Sword mySword(10);
    Magic myMagic(15, 7);

    Weapon* currentWeapon; //현재 Weapon, Sword, Magic을 참조할 수 있다.

    currentWeapon = &mySword; //Sword 자식 간접참조 / 동적 바인딩을 위한 참조 할당이다.
    currentWeapon->Use();  // 동적 바인딩에 의해 Sword::Use() 호출
}
  • virtual 키워드로 선언
  • 기본 클래스 포인터로 파생 클래스 객체를 가리킬 때, 실제 객체의 타입에 따라 호출될 함수가 결정됨
  • 동적 바인딩(Dynamic Binding): 실행 시간에 호출할 함수가 결정됨

가상 소멸자

  • 소멸자 앞에 virtual을 붙인것이다.
    • 앞으로 소멸자는 전부 가상 소멸자로 선언해라
    • 부모 소멸자가 호출되면 자식의 소멸자가 호출이 안되는 경우가 있기 때문이다.
  • 객체의 소멸자가 호출될때 부모의 소멸자까지 역순으로 호출된다.
    • 그럼 왜 자식 소멸자는 호출이 안된다는 걸까?
      • 이유 : 참조를 하면 부모클래스* 변수 = &자식클래스 변수; 이렇게 포인터 변수로 자식클래스 참조를 받았을때 같은 이름이기에 소멸자는 해당 데이터형의 소멸자만 호출하게 된다.
      • virtual을 붙였을 떄는? : 소멸자의 호출 순서는 자식 → 부모이기에 자식 소멸자 호출 → 부모 소멸자 호출 순서로 진행하게 된다.
class Image {
public:
    operator string() {
        return "사진";
    }
};

class Message {
public:
    Message(int sendTime, string sendName) {
        this->sendTime = sendTime;
        this->sendName = sendName;
    }
    virtual ~Message() {}  // 가상 소멸자

    int GetSendTime() const { return sendTime; }
    string GetSendName() const { return sendName; }
    virtual string GetContent() const { return ""; }

private:
    int sendTime;
    string sendName;
};

class TextMessage : public Message {
public:
    TextMessage(int sendTime, string sendName, string text)
        : Message(sendTime, sendName) {
        this->text = text;
    }

    string GetContent() const { return text; }  // 오버라이드

private:
    string text;
};

class ImageMessage : public Message {
public:
    ImageMessage(int sendTime, string sendName, Image* image)
        : Message(sendTime, sendName) {
        this->image = image;
    }

    string GetContent() const { return (string)*image; }  // 오버라이드

private:
    Image* image;
};

void printMessage(Message* m) {
    cout << "보낸 시간 : " << m->GetSendTime() << endl;
    cout << "보낸 사람 : " << m->GetSendName() << endl;
    cout << "  내 용   : " << m->GetContent() << endl;  // 동적 바인딩
    cout << endl;
}

int main() {
    Image* dogImage = new Image;
    TextMessage* hello = new TextMessage(10, "두들", "안녕");
    ImageMessage* dog = new ImageMessage(20, "두들", dogImage);

    printMessage(hello);
    printMessage(dog);

    delete dogImage;
    delete hello;
    delete dog;
}
  • 기본 클래스 포인터로 파생 클래스 객체를 삭제할 때 올바른 소멸자가 호출되도록 보장
    • 실행 시 참조되어있는 주소(클래스)의 소멸자가 호출된다.
  • 가상 함수를 가진 클래스는 항상 가상 소멸자를 가져야 함

참조를 사용한 다형성

class Image {
public:
    operator string() {
        return "사진";
    }
};

class Message {
public:
    Message(int sendTime, string sendName) {
        this->sendTime = sendTime;
        this->sendName = sendName;
    }
    virtual ~Message() {}  // 가상 소멸자

    int GetSendTime() const { return sendTime; }
    string GetSendName() const { return sendName; }
    virtual string GetContent() const { return ""; }

private:
    int sendTime;
    string sendName;
};

class TextMessage : public Message {
public:
    TextMessage(int sendTime, string sendName, string text)
        : Message(sendTime, sendName) {
        this->text = text;
    }

    string GetContent() const { return text; }  // 오버라이드

private:
    string text;
};

class ImageMessage : public Message {
public:
    ImageMessage(int sendTime, string sendName, Image* image)
        : Message(sendTime, sendName) {
        this->image = image;
    }

    string GetContent() const { return (string)*image; }  // 오버라이드

private:
    Image* image;
};

void printMessage(const Message& m) {  // 참조로 받기
    cout << "보낸 시간 : " << m.GetSendTime() << endl;
    cout << "보낸 사람 : " << m.GetSendName() << endl;
    cout << "  내 용   : " << m.GetContent() << endl;
    cout << endl;
}

int main() {
    Image* dogImage = new Image;
    Message* messages[] = {
        new TextMessage(10, "두들", "안녕"),
        new ImageMessage(20, "두들", dogImage),
        new TextMessage(30, "두들", "잘가")
    };

    // 범위 기반 for문
    for (Message* m : messages) {
		    //실제로 참조하고 있는 클래스에 따라 다른 함수가 호출된다.
        printMessage(*m);
    }

    // 배열의 각 칸마다 동적 할당된 객체를 가리키는 포인터가 들어있으므로 각각 삭제
    for (Message* m : messages) {
        delete m;
    }
    delete dogImage;
}

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

  • 순수 가상 함수 : 구현은 없고 선언만 있는 가상 함수다.
  • 추상 클래스 : 순수 가상 함수를 하나 이상 선언되어있는 클래스다.
const double PI = 3.141592653589793;

class Shape {
public:
    virtual ~Shape() {}
    virtual double GetArea() const = 0;      // 순수 가상 함수
    virtual void Resize(double factor) = 0;  // 순수 가상 함수
};

**class** Circle **: public Shap**e {
public:
    Circle(double r) : r(r) {}

    double GetArea() const {
        return r * r * PI;
    }

    void Resize(double factor) {
        r *= factor;
    }

private:
    double r;
};

class Rectangle : public Shape {
public:
    Rectangle(double a, double b) : a(a), b(b) {}

    double GetArea() const {
        return a * b;
    }

    void Resize(double factor) {
        a *= factor;
        b *= factor;
    }

private:
    double a, b;
};

int main() {
    // Shape는 추상 클래스이므로 객체를 만들 수 없음
    // Shape s;  // 오류!

    // Shape를 가리키는 포인터 배열로 여러 도형을 한꺼번에 관리
    Shape* shapes[] = { new Circle(1), new Rectangle(1, 2) };

    for (Shape* s : shapes) {
        s->Resize(2);  // 각 도형을 2배씩 확대
    }

    for (Shape* s : shapes) {
        cout << s->GetArea() << endl;  // 각 도형의 넓이를 출력
    }

    for (Shape* s : shapes) {
        delete s;
    }
}

  • 순수 가상 함수: = 0으로 선언된 가상 함수
    • 반드시 상속받은 자식 클래스에서 재정의를 해야한다.
    • 일반 가상함수는 필요에 의해서 재정의를 해도 되고, 안해도 된다.
  • 추상 클래스: 하나 이상의 순수 가상 함수를 가진 클래스
  • 추상 클래스는 인스턴스(객체)를 생성할 수 없음
    • 상속 받아서 부모클래스 로만 사용하라는 의도다.
  • 파생 클래스는 모든 순수 가상 함수를 구현해야 함

배열과 상속 - 잘못된 사용

struct Animal {
    float xpos = 1;
    float ypos = 2;
};

struct FlyingAnimal : public Animal {
    float zpos = 3;
};

void printAnimals(Animal* a, int n) {  // Animal *a는 Animal a[]와 동일
    for (int i = 0; i < n; i++) {
        cout << "(" << a[i].xpos << ", " << a[i].ypos << ")" << endl;
        //a[i] == *(a + i) -> 8바이트씩 커지는 것이다.
    }
}

int main() {
    FlyingAnimal* arr = new FlyingAnimal[5];
    printAnimals(arr, 5);  // 문제 발생!
    delete[] arr;
}

  • Animal과 FlyingAnimal의 크기가 다름
  • 배열 인덱싱 시 잘못된 메모리 접근 발생

해결법

struct Animal {
    float xpos = 1;
    float ypos = 2;
    virtual ~Animal() {}
};

struct FlyingAnimal : public Animal {
    float zpos = 3;
};

void printAnimals(Animal** a, int n) {  // 포인터의 포인터 사용
    for (int i = 0; i < n; i++) {
        cout << "(" << a[i]->xpos << ", " << a[i]->ypos << ")" << endl;
    }
}

int main() {
    Animal** a = new Animal*[5];
    for (int i = 0; i < 5; i++) {
        a[i] = new FlyingAnimal;//자식으로 캐스팅히는 다운 캐스팅이다.
    }

    printAnimals(a, 5);

    for (int i = 0; i < 5; i++) {
        delete a[i];  // 각 객체 삭제
    }
    delete[] a;       // 포인터 배열 삭제
}
  • 객체 배열 대신 포인터 배열 사용
  • 각 포인터가 개별 객체를 가리키도록 함

상속 관계 형변환

  • 포인터, 레퍼런스의 형변환을 의미한다.

업 캐스팅

  • 자식에서 부모로 올라가는 캐스팅
  • 파생 클래스 포인터를 기본 클래스 포인터로 변환
  • 암시적 변환: 컴파일러가 자동으로 처리
  • 항상 안전: 파생 클래스는 기본 클래스의 모든 멤버를 포함

다운 캐스팅

  • 부모에서 자식으로 내려가는 캐스팅
  • 기본 클래스 포인터를 파생 클래스 포인터로 변환
struct Base {
    int a = 1;
    virtual ~Base() {}
};
struct Drv1 : Base {
    float x = 3.14;
};
struct Drv2 : Base {
    int y = 3;
};

int main() {
    Base* b = new Drv1; //자식 -> 부모 : 업 캐스팅 (이런 경우는 부작용 없다.)
    //Drv1* d1 = static_cast<Drv1*>(b);  // 올바른 다운캐스팅
		Drv1* d1 = (Drv1*)b;  // 올바른 다운캐스팅

    cout << d1->x << endl;
    delete b;
}
  • static_cast는 컴파일 타임에 타입 검사만 수행
  • 실제 객체 타입과 캐스팅 타입이 다르면 런타임 오류 발생 가능

객체지향 프로그래밍 4대 특성

캡슐화

  • 데이터와 함수를 하나의 단위로 묶음
  • 정보 은닉을 통한 내부 구현 보호
    • 정보 은닉 : 접근 지정자를 의미한다.

상속성

  • 기존 클래스의 특성을 물려받아 새로운 클래스 생성
  • 코드 재사용성 향상

다형성

  • 하나의 인터페이스로 여러 구현체 사용
  • 가상 함수와 오버라이딩으로 구현

추상화

  • 복잡한 시스템을 단순한 개념으로 표현
  • 추상 클래스와 인터페이스로 구현