[VEDA] 11일차

나우히즈·2025년 3월 31일

VEDA

목록 보기
10/16

지난 시간 요약

📌 상속의 개념

  • 객체지향의 핵심 특징 중 하나.
  • 자식 클래스가 부모 클래스의 멤버(변수, 함수 등)를 물려받아 사용.
  • 코드 재사용성과 유지보수성을 높일 수 있음.
  • 자식은 부모로 대체 가능해야 하며, 다형성을 구현하기 위해선 부모의 인터페이스를 변경 없이 상속하는 것이 중요.

📌 상속과 접근 지정자/접근 제한자

  • 접근 지정자 (public, protected, private)
    → 클래스 내부 멤버(변수, 함수)의 접근 범위를 정함.

    •	public: 누구나 접근 가능
    •	protected: 자식 클래스에서 접근 가능, 외부에서는 불가
    •	private: 자식도 접근 불가, 내부에서만 사용 가능

    → 부모 클래스의 멤버들을 자식 클래스에 어떤 접근 수준으로 물려줄지를 결정

📌 생성자와 초기화 순서

  • 자식 객체 생성 시 → 부모 생성자가 먼저 호출되고 → 자식 생성자 호출
  • 소멸은 그 역순으로 진행
  • 부모의 멤버를 초기화하려면 초기화 리스트를 통해 부모 생성자를 호출하는 것이 바람직

📌 초기화 리스트와 접근 제한

  • 부모 클래스의 멤버가 protected일 경우
    → 자식 클래스에서 직접 접근 가능

  • 부모 클래스의 멤버가 private일 경우
    → 자식 클래스에서는 직접 접근 불가
    → 초기화 리스트에서 부모 생성자를 호출해 초기화해야 함


다형성과 오버라이딩

📌 1. 다형성의 개념

  • 하나의 인터페이스(부모 타입)여러 실제 객체(자식 타입)를 다룰 수 있게 하는 객체지향의 핵심 개념
  • C++에서는 가상 함수(virtual)를 통해 런타임 다형성을 구현함

📌 2. 오버라이딩(Overriding)

  • 부모 클래스에 선언된 함수를 자식 클래스에서 재정의
  • 반드시 함수 원형(이름, 매개변수, 반환형)을 동일하게 유지해야 함
  • 함수 내용만 다르게 구현하여, 같은 인터페이스에 다른 동작을 부여
class Shape {
public:
    virtual void show();  // 가상 함수
};

class Triangle : public Shape {
public:
    void show() override;  // 오버라이딩
};

📌 3. 업캐스팅과 다형성

  • 자식 객체를 부모 클래스의 포인터 또는 참조로 다루는 것 → 업캐스팅
  • 이때 함수 호출은 객체의 실제 타입에 따라 결정되려면,
    부모의 함수가 virtual로 선언되어 있어야 함
Triangle t;
Shape &s = t;     // 업캐스팅
s.show();         // 🔸 Triangle::show() 호출 (다형성 발동: virtual 필요)
  • 만약 show()가 virtual이 아니면 → Shape::show()만 호출됨

📌 4. 포인터로 업캐스팅한 경우도 동일

Triangle t;
Shape *s = &t;
s->show();         // 🔸 virtual 없으면 Shape::show() 호출
  • 참조(&)든 포인터(*)든, 업캐스팅 후에 다형성을 원한다면 virtual이 반드시 필요

📌 5. 다운캐스팅의 위험성

  • 부모 클래스 포인터를 자식 타입으로 캐스팅하는 것
  • 컴파일러는 허용하지만, 실제 객체가 자식이 아닐 경우 정의되지 않은 동작(UB) 발생
  • C++에서는 static_cast 또는 dynamic_cast로 명시적으로 수행
Shape* s = new Shape;
Triangle* t = static_cast<Triangle*>(s);  // ❌ 위험함 (메모리 구조 다름)

❗ Shape는 Triangle이 가진 base, height 멤버를 갖지 않으므로 잘못된 메모리 접근 발생 가능

📌 6. virtual 함수 관련 정리

  • virtual로 선언된 부모 함수는 자식에서 자동으로 가상 함수로 간주됨
  • 하지만 코드 명확성과 실수 방지를 위해 override 키워드 사용 권장
class Shape {
public:
    virtual void show();
};

class Triangle : public Shape {
public:
    void show() override;  // 명시적으로 override
};

8장.

✅ virtual 함수 호출의 메모리 동작 원리

✅ 핵심 개념 두 가지

  1. V-Table (Virtual Table)
    → 클래스마다 존재하는 가상 함수 주소들의 배열
  2. VPTR (Virtual Pointer)
    → 객체마다 가지고 있는 V-Table을 가리키는 포인터

✅ 가상함수 동작 흐름

  1. 클래스에 virtual 함수가 하나라도 선언되면,
  2. 컴파일러는 해당 클래스 전용의 V-Table을 자동으로 생성합니다.
  3. 객체가 생성될 때, VPTR이라는 숨겨진 멤버가 객체에 추가되고,
  4. VPTR은 클래스의 V-Table을 가리킵니다.
  5. virtual 함수 호출 시 → VPTR → V-Table → 해당 함수 주소를 따라가서 호출합니다.

✅ 함수 호출 시 실제 동작(중요)

Animal* a = new Dog();
a->speak();
  1. a는 Animal* 타입이지만, 실제 객체는 Dog
  2. a의 VPTR은 Dog의 V-Table을 가리킴
  3. speak() 호출 시 컴파일러는 VPTR → V-Table → speak 주소로 이동
  4. 결과적으로 Dog::speak()가 호출됨

👉 이게 바로 동적 바인딩(dynamic dispatch)입니다.

✅ 정적 바인딩과의 차이

  • 동적 바인딩은 실행 시점에 결정됨. (정적바인딩은 컴파일타임에 결정)
  • V-Table 참조하여 함수를 호출. (정적바인딩은 직접 주소로 호출)

좋습니다! 😊
이번엔 C++의 객체지향에서 매우 중요한 두 개념:
순수 가상 함수(Pure Virtual Function)가상 소멸자(Virtual Destructor)에 대해
개념 → 사용 목적 → 문법 → 예제 → 실무에서의 필요성까지 자세히 알려드릴게요!

✅ 순수 가상 함수 (Pure Virtual Function)

✅ 개념

순수 가상 함수는 자식 클래스가 반드시 재정의해야 하는 가상 함수입니다.
이 함수가 하나라도 포함된 클래스는 추상 클래스(abstract class)가 됩니다.

✅ 문법

virtual 반환형 함수이름(매개변수) = 0;

예:

class Shape {
public:
    virtual void draw() = 0;  // 순수 가상 함수
};

✅ 특징

  • 함수 정의 없음 : 함수 본문 없이 = 0으로 끝남
  • 자식에서 반드시 오버라이딩 : 안 하면 컴파일 에러 발생
  • 추상 클래스 : 순수 가상 함수가 1개 이상 있으면 인스턴스 생성 불가
  • 인터페이스 역할 : 인터페이스처럼 “기능 명세서” 역할 수행

✅ 사용 예제

class Shape {
public:
    virtual void draw() = 0;  // 순수 가상 함수
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "원을 그립니다.\n";
    }
};

Shape* s = new Circle();
s->draw();  // 출력: 원을 그립니다.

✅ 언제 쓰나?

  • 여러 자식 클래스에게 동일한 인터페이스를 강제하고 싶을 때
  • 설계상 “이 클래스는 직접 쓰지 않고, 상속용으로만 쓴다”는 걸 표현하고 싶을 때
  • 다형성을 위한 기반 클래스 역할

✅ 2. 가상 소멸자 (Virtual Destructor)

✅ 개념

부모 클래스의 소멸자를 virtual로 선언하면,
자식 객체를 부모 포인터로 삭제할 때 자식의 소멸자도 제대로 호출됩니다.

✅ 왜 필요할까?

C++에서는 소멸자가 virtual이 아니면,
부모 포인터로 자식을 delete할 때 자식의 소멸자가 호출되지 않습니다.

→ 자식에서 할당한 리소스 누수 발생 가능

✅ 예제

class Shape {
public:
    virtual ~Shape() {
        std::cout << "Shape 소멸자\n";
    }
};

class Triangle : public Shape {
public:
    ~Triangle() {
        std::cout << "Triangle 소멸자\n";
    }
};

int main() {
    Shape* s = new Triangle();
    delete s;  // ✅ 자식 소멸자 먼저, 부모 소멸자 순서로 호출됨
}

🔸 출력

Triangle 소멸자  
Shape 소멸자

❌ 만약 Shape의 소멸자가 virtual이 아니라면?

  • delete s; 호출 시 Shape::~Shape()만 실행
  • Triangle::~Triangle()는 호출되지 않음 → 메모리 누수 위험

0개의 댓글