1학년 대학 수업/C++ 프로그래밍

12주차 복습 과제

hawon6691 2025. 11. 20. 16:32
728x90

물론이죠! Unity에서 상속을 사용하는 대표적인 예제는 공통된 동작을 여러 오브젝트에 적용할 때입니다. 예를 들어, 적(enemy) 캐릭터들이 공통적으로 움직이고 공격하는 기능을 공유하지만, 각각의 적은 고유한 행동을 추가로 가질 수 있다고 가정해볼게요.


🎮 예제: 적 캐릭터 상속 구조

1. Enemy 기본 클래스 (부모 클래스)

using UnityEngine;

public class Enemy : MonoBehaviour
{
    public float health = 100f;

    public virtual void Move()
    {
        Debug.Log("적이 움직입니다.");
    }

    public virtual void Attack()
    {
        Debug.Log("적이 공격합니다.");
    }

    public void TakeDamage(float damage)
    {
        health -= damage;
        Debug.Log($"적이 {damage}의 피해를 입었습니다. 남은 체력: {health}");

        if (health <= 0)
        {
            Die();
        }
    }

    protected void Die()
    {
        Debug.Log("적이 죽었습니다.");
        Destroy(gameObject);
    }
}


2. Zombie 클래스 (Enemy 상속)

public class Zombie : Enemy
{
    public override void Move()
    {
        Debug.Log("좀비가 느릿느릿 움직입니다.");
    }

    public override void Attack()
    {
        Debug.Log("좀비가 물어뜯습니다!");
    }
}


3. Robot 클래스 (Enemy 상속)

public class Robot : Enemy
{
    public override void Move()
    {
        Debug.Log("로봇이 바퀴로 빠르게 이동합니다.");
    }

    public override void Attack()
    {
        Debug.Log("로봇이 레이저를 발사합니다!");
    }
}


🧠 요점 정리

  • Enemy 클래스는 공통된 기능(이동, 공격, 피해 처리)을 정의합니다.
  • Zombie, Robot은 Enemy를 상속받아 고유한 방식으로 Move()와 Attack()을 오버라이드합니다.
  • Unity에서는 이 구조를 통해 다양한 적 캐릭터를 효율적으로 관리할 수 있습니다.

코드 예제 언어 정리

사용자가 여러 언어를 요청했으니, 아마 각 언어에 대해 간단한 코드와 설명을 제공해야 할 것 같아요. 6개 섹션: 소개 부분과 Java, C++, Python, C#, JavaScript ES6 코드를 포함해야겠네요. 각 언어마다 코드와 설명을 넣고, 중요한 부분은 굵은 글씨로 표시해서 쉽게 눈에 띄도록 해야겠어요. H3 헤딩을 사용해서 각 섹션을 구분하고, 너무 반복적이지 않게 간결하게 설명을 적을게요.

개요

아래 예제들은 모두 같은 개념을 보여줍니다: 공통된 기능을 부모 클래스에 정의하고 자식 클래스가 상속받아 재사용 및 재정의(오버라이드)하는 방식, 그리고 부모 타입으로 다형성(polymorphism)을 활용하는 방법입니다. 각 언어별로 간단한 Animal -> Dog 구조를 사용해 코드와 핵심 설명을 제시합니다.


Java

// Animal.java
public class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public void speak() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

// Dog.java
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
    @Override
    public void speak() {
        System.out.println(name + "가 멍멍 짓습니다.");
    }
}

// Main.java
public class Main {
    public static void main(String[] args) {
        Animal a = new Dog("바둑이");
        a.speak(); // 바둑이가 멍멍 짓습니다.
    }
}

  • 설명: Dog이 Animal을 상속하고 speak를 오버라이드합니다. super로 부모 생성자를 호출합니다. 부모 타입으로 참조하면 런타임에 자식의 오버라이드된 메서드가 호출됩니다(동적 바인딩).

C++

// animal.h
#include <iostream>
#include <string>

class Animal {
protected:
    std::string name;
public:
    Animal(const std::string& n) : name(n) {}
    virtual void speak() {
        std::cout << "동물이 소리를 냅니다." << std::endl;
    }
    virtual ~Animal() = default;
};

// dog.h
class Dog : public Animal {
public:
    Dog(const std::string& n) : Animal(n) {}
    void speak() override {
        std::cout << name << "가 멍멍 짓습니다." << std::endl;
    }
};

// main.cpp
int main() {
    Animal* a = new Dog("몽이");
    a->speak(); // 몽이가 멍멍 짓습니다.
    delete a;
}

  • 설명: C++는 메서드를 가상 함수(virtual)로 선언해야 런타임 다형성이 작동합니다. 소멸자는 보통 가상 소멸자로 만들어 메모리 누수를 방지합니다.

Python

# animal.py
class Animal:
    def __init__(self, name):
        self.name = name
    def speak(self):
        print("동물이 소리를 냅니다.")

# dog.py
class Dog(Animal):
    def speak(self):
        print(f"{self.name}가 멍멍 짓습니다.")

# main.py
a = Dog("초코")
a.speak()  # 초코가 멍멍 짓습니다.

  • 설명: Python은 모든 클래스가 기본적으로 상속 가능한 동적 언어입니다. 명시적 가상 키워드가 필요 없고, 메서드 재정의는 간단합니다. 런타임 타입에 따라 오버라이드된 메서드가 호출됩니다.

C#

// Animal.cs
public class Animal {
    protected string name;
    public Animal(string name) { this.name = name; }
    public virtual void Speak() {
        System.Console.WriteLine("동물이 소리를 냅니다.");
    }
}

// Dog.cs
public class Dog : Animal {
    public Dog(string name) : base(name) {}
    public override void Speak() {
        System.Console.WriteLine(name + "가 멍멍 짓습니다.");
    }
}

// Program.cs
class Program {
    static void Main() {
        Animal a = new Dog("구름");
        a.Speak(); // 구름가 멍멍 짓습니다.
    }
}

  • 설명: C#에서는 virtual(부모)과 override(자식)를 명시해야 합니다. base로 부모 생성자/멤버에 접근합니다. Unity의 C# 스크립트에서도 동일한 상속/오버라이드 개념을 사용합니다.

JavaScript ES6

// animal.js
class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    console.log("동물이 소리를 냅니다.");
  }
}

// dog.js
class Dog extends Animal {
  speak() {
    console.log(`${this.name}가 멍멍 짓습니다.`);
  }
}

// main.js
const a = new Dog("별이");
a.speak(); // 별이가 멍멍 짓습니다.

  • 설명: ES6 클래스 문법에서 extends로 상속하고 메서드를 재정의합니다. 프로토타입 기반이지만 문법은 다른 OOP 언어와 유사하게 작성됩니다.

핵심 포인트 비교

  • 생성자 호출: 대부분 언어는 부모 생성자를 호출하는 문법이 있음(Java: super, C#: base, C++: 초기화 리스트, Python/JS는 자동 또는 super() 사용).
  • 오버라이드 선언: Java/C#/C++은 명시적(예: @Override, override, virtual)이 권장/필수인 반면 Python/JS는 암시적으로 오버라이드 가능.
  • 다형성: 부모 타입으로 자식 인스턴스를 참조하면 런타임에 자식 메서드가 호출되는 동적 바인딩이 일반적으로 지원됨.
  • 소멸자/메모리 관리: C++는 소멸자를 주의해야 하고, 가상 소멸자가 필요함. 다른 언어는 가비지 컬렉션으로 관리.

다형성, 상속, 캡슐화, 추상화

캡슐화 - 상속

다형성 - 상속

추상화 - 상속

상속은 실제 부모와 자식 간의 상속과 비슷하다.

상속 시

private을 제외한 나머지 접근 지정자 protected와 public은 받습니다.

private - 개인의

protected - 상속

public - 공공의

그러니까 private은 오직 개인으로만. protected는 오직 상속으로만. public은 전부 사용이 가능하다는 말이다.

객체 지향 프로그래밍(Object-Oriented Programming)은 상속이 거의 다다.

 

 

부모와 자식 간을 A가 B의 is a 관계 이다.

 

 

보통 클래스를 상속할때 접근 지정자는 public으로 둔다. private은 요소를 아예 못쓰고 protected는 요소를 쓸 수는 있지만 출력을 못한다. 그래서 public으로 둔다. 그리고 부모 변수를 자식에게 넘길 때는 무조건 protected로 넘겨야 된다. private은 어차피 상속이 안되고 protected로 넘겨 받고 public으로 함수를 만들어 변수를 설정하는 것이 좋다.

 

 

 

저 int x를 protected나 public에 선언해야 한다.

 

 

이 그림처럼

private이든 public이든 그대로 값을 넘겨준다.

사용 방식도 똑같다.

대신 상속할 접근 지정자는 접근 지정자 기준이 제일 크다.

상속 접근 지정자 → 클래스 접근 지정자

부모의 있는 private 빼고 전부. 전부다 public, protected, private 은 공통으로 private은 못받음.

==================================

Private 상속 (Private Inheritance)

■ 코드 분석: 왜 이렇게 작성했는가?

이 코드는 private 상속의 특징과 접근 제어를 보여주기 위한 예제입니다.

========================================

  1. private A 상속을 사용한 이유 ========================================

class B : private A

설명:

  • B가 A를 '비공개적으로 상속'받음
  • 외부에서 B 객체로 A의 public 멤버에 직접 접근 불가능
  • 캡슐화: A의 기능을 B 내부에서만 사용하고, 외부에는 숨김

효과:

  • B의 사용자는 A의 존재를 알 필요 없음
  • B는 A의 구현을 내부적으로만 활용

========================================

  1. x가 private인 이유

int x; // private (기본값)

설명:

  • A 클래스의 데이터를 직접 접근 불가능하게 함
  • 오직 setX(), showX() 같은 public 메서드를 통해서만 접근 가능
  • 데이터 보호: 잘못된 값이 설정되지 않도록 제어

효과:

  • 데이터 무결성 보장
  • 클래스 내부 로직으로만 값 변경 가능

========================================

  1. setXY() 메서드를 정의한 이유

void setXY(int i, int j) { setX(i); // 부모의 메서드 사용 y = j; }

설명:

  • B에서 A의 setX()를 '감싸서(wrapping)' 호출
  • B의 사용자는 단순히 setXY()만 호출하면 됨
  • 부모 기능을 자식만의 방식으로 제공

효과:

  • 일관된 인터페이스: 사용자는 setXY() 하나만 알면 됨
  • 유연성: B는 내부적으로 A를 원하는 방식으로 활용

========================================

  1. showXY() 메서드를 정의한 이유

void showXY() { showX(); // 부모의 메서드 사용 cout << y << endl; }

설명:

  • x와 y를 함께 보여줌
  • 부모와 자식의 멤버를 일관된 방식으로 출력

효과:

  • B만의 출력 인터페이스 제공
  • 사용자는 B의 데이터를 B의 방식대로만 볼 수 있음

========================================

  1. main에서 이렇게만 사용한 이유

B bb; bb.setXY(1, 2); // O (가능) bb.showXY(); // O (가능)

// bb.setX(1); // X (불가능 - private 상속) // bb.showX(); // X (불가능 - private 상속)

설명:

  • B 클래스의 공개 인터페이스인 setXY(), showXY()만 사용
  • 부모의 메서드는 내부에서만 사용되고 외부에는 보이지 않음

효과:

  • 명확한 사용 패턴: B의 메서드만 호출
  • 깨끗한 API: 사용자가 복잡함을 모름

========================================

  1. 세 가지 상속 방식 비교

┌─────────────┬──────────────┬─────────────────────┐ │ 상속 방식 │ 부모 public │ 설명 │ ├─────────────┼──────────────┼─────────────────────┤ │ public 상속 │ 그대로 public│ "~는 ~의 종류이다" │ │ protected │ protected로 │ 자식만 상속 전달 │ │ private 상속│ 모두 private │ "~를 구현에 사용" │ └─────────────┴──────────────┴─────────────────────┘

========================================

이 코드의 핵심 목적

"private 상승을 통해 부모의 기능을 내부용으로만 사용하면서, 사용자에게는 자식만의 명확한 인터페이스를 제공하는 방식"

이것은 상속을 '구현 재사용 목적'으로 사용하는 고급 C++ 패턴입니다.

========================================

실제 예시: 자동차

class Engine { // 기본 클래스: 엔진 private: int power; public: void start() { ... } void stop() { ... } };

class Car : private Engine { // 자동차는 엔진을 가짐 public: void drive() { start(); // 내부에서만 엔진 사용 } void park() { stop(); // 내부에서만 엔진 사용 } };

int main() { Car car; car.drive(); // O (자동차의 인터페이스) // car.start(); // X (엔진은 내부 구현) }

설명:

  • Car는 Engine의 '종류'가 아니라 Engine을 '포함'함
  • 사용자는 drive(), park()만 알면 됨
  • Engine의 구현은 숨겨짐

 

생성자를 만드는 방법 중 하나이다.

근데 보통 생성자를 만들 때 따로 클래스 밖에서 정의하는 것을 추천한다.

그게 편하기도 하고 찾기도 쉽다.

 

 

private과 protected의 공통점은 외부에서 접근이 불가능하다.

다만 protected는 외부가 자식 클래스 안이면 접근이 가능하지만 상속 클래스 밖에서는 접근이 불가능하다.

 

 

더 자세히 말하자면 private과 protected의 차이는 이름의 뜻처럼 private은 오직 자기 자신만 사용/접근이 가능하다. protected는 자기와 상속된 객체 들만 사용/접근이 가능하다.

공통점

  • 역할: 둘 다 멤버(필드, 메서드, 중첩 타입 등)의 접근 범위를 결정하는 접근 지정자(access specifier)입니다.
  • 컴파일타임 제어: 접근 권한은 컴파일 시점에 검사되며 런타임 동작에는 직접적인 영향을 주지 않습니다.
  • 적용 대상: 클래스/구조체의 멤버와 상속 섹션에 사용할 수 있습니다.
  • 예외 규칙: friend(함수/클래스) 선언은 private와 protected 둘 다에 대해 예외적으로 접근을 허용합니다.

차이점

  • 외부(비멤버) 접근
    • private: 동일 클래스의 멤버와 friend 만 접근 가능. 클래스 외부의 코드(심지어 파생 클래스도) 접근 불가.
    • protected: 동일 클래스와 파생 클래스(그리고 friend)에서 접근 가능. 단, 외부 일반 코드에서는 접근 불가.
  • 상속 관점
    • protected 멤버는 파생 클래스에서 직접 사용/재정의 가능하므로 서브클래스가 내부 구현을 활용하도록 허용.
    • private 멤버는 파생 클래스에서 직접 접근 불가하므로 캡슐화가 더 강함(파생 클래스는 public/protected 인터페이스나 friend를 통해 간접 접근).
  • 인터페이스 노출
    • protected는 ‘확장용’으로 설계된 멤버(서브클래스가 사용할 API)를 나타내는 데 적절.
    • private는 내부 구현 세부를 숨기기 위해 사용.
  • C++ 특이 사항
    • 파생 클래스가 동일 네임의 private 멤버를 선언하면 이름 가림(name hiding)이 발생; private 멤버를 직접 재사용할 수 없음.
    • 클래스 외부에서의 접근 권한은 상속 방식(public/protected/private 상속)에 따라 달라질 수 있음(예: private 상속 시 base의 public/protected가 파생 클래스에서 private로 보일 수 있음).

간단한 예제

class Base {
private:
    int priv;       // Base 내부와 friend만 접근 가능
protected:
    int prot;       // Base와 파생 클래스에서 접근 가능
public:
    Base(): priv(1), prot(2) {}
};

class Derived : public Base {
public:
    void f() {
        // priv = 10;   // 컴파일 오류: Base::priv는 접근 불가
        prot = 20;      // 허용: protected는 파생에서 접근 가능
    }
};


메모리 할당 위치 (정리)

  • 인스턴스(비정적) 멤버 변수
    • 메모리 위치는 멤버의 접근 지정자(private/protected/public)와 무관합니다.
    • 객체가 어디에 생성되는지에 따라 달라집니다:
      • 지역 변수로 생성된 객체: 스택(stack) 영역
      • new로 할당된 객체: 힙(heap) 영역
      • 전역/정적 객체: 데이터 영역(data segment)의 정적 저장소
    • 예: MyClass a; → 스택, MyClass* p = new MyClass; → 힙, static MyClass s; → 데이터 영역.
  • 정적(static) 멤버 변수
    • 클래스의 static 멤버는 모든 인스턴스와 무관하게 프로그램 실행 동안 단일 저장소를 가지며 보통 데이터 섹션(데이터 영역)에 위치합니다.
    • 접근 지정자와 관계없이 위치는 동일합니다.
  • 멤버 함수(메서드)
    • 함수의 코드는 텍스트(코드) 섹션에 위치합니다. 접근 지정자는 호출 가능 여부만 결정합니다.
    • 인라인 함수라면 호출 시점에 코드가 인라인화되어 호출자 코드에 포함될 수 있습니다.
  • 가상 함수와 vtable/vptr
    • 가상 함수 테이블(vtable)은 보통 읽기 전용 데이터 영역(또는 컴파일러/플랫폼에 따라 다른 영역)에 저장됩니다.
    • 각 객체는 일반적으로 vptr(테이블을 가리키는 포인터)을 자신 내부(객체 메모리의 일부)로 가지고 있으며, vptr은 객체가 할당된 곳(스택/힙/데이터)에 함께 존재합니다.
    • 접근 지정자는 vtable의 위치에 영향을 주지 않습니다.
  • 참조형/포인터형 멤버
    • 포인터/참조 자체는 객체 내부(인스턴스의 일부)로 저장되며, 가리키는 대상이 스택/힙/데이터 어디에 있든 상관없습니다.

요약 (한눈에)

  • private와 protected는 모두 접근 제어자이며 런타임 메모리 위치에는 영향을 주지 않습니다.
  • 차이점은 누가 접근할 수 있느냐: protected는 파생 클래스 허용, private는 불허.
  • 멤버의 실제 메모리 위치는 그 멤버가 정적(static)인지/인스턴스인지와 객체가 어디에 할당되었는지(stack/heap/data)에 의해 결정됩니다.

 

좋아요! 이 그림은 C++에서 상속 관계에 있는 클래스의 생성자와 소멸자 실행 순서를 설명하는 슬라이드입니다. 객체지향 프로그래밍에서 매우 중요한 개념이죠. 아래에 핵심 내용을 정리해드릴게요.


🧩 핵심 개념: 상속에서 생성자와 소멸자의 실행 순서

🔹 생성자 실행 순서

  • **기본 클래스(부모 클래스)**의 생성자가 먼저 실행됩니다.
  • 그 다음에 **파생 클래스(자식 클래스)**의 생성자가 실행됩니다.

📌 이유: 부모 클래스의 멤버들이 먼저 초기화되어야 자식 클래스가 그 위에 자신의 멤버를 추가할 수 있기 때문입니다.

🧠 비유: 부모가 있어야 자식이 태어난다!


🔻 소멸자 실행 순서

  • **파생 클래스(자식 클래스)**의 소멸자가 먼저 실행됩니다.
  • 그 다음에 **기본 클래스(부모 클래스)**의 소멸자가 실행됩니다.

📌 이유: 자식 클래스의 자원부터 정리한 후, 부모 클래스의 자원을 정리해야 메모리 누수나 오류를 방지할 수 있기 때문입니다.

🧠 비유: 자식이 먼저 떠나고, 부모가 마지막에 정리한다.


🔄 전체 흐름 요약

객체 생성 시:
1. 부모 생성자 실행
2. 자식 생성자 실행

객체 소멸 시:
1. 자식 소멸자 실행
2. 부모 소멸자 실행


💡 실무에서 왜 중요할까?

  • 생성자에서 리소스를 할당하고, 소멸자에서 해제하는 경우가 많기 때문에 순서가 정확해야 안정적인 메모리 관리가 가능합니다.
  • 특히 **동적 메모리 할당(new/delete)**이나 파일/네트워크 핸들링을 할 때 이 순서를 잘 이해하고 있어야 합니다.
  • 가상 소멸자(virtual ~Base())를 선언하지 않으면 다형성 상황에서 부모 소멸자가 호출되지 않아 메모리 누수가 발생할 수 있습니다.

필요하시면 이 개념을 코드 예제나 다중 상속, 가상 상속까지 확장해서 설명드릴 수 있어요. 어떤 방향으로 더 깊이 들어가볼까요?

 

 

좋아요! 이 그림은 C++에서 파생 클래스(자식 클래스)의 생성자에서 기본 클래스(부모 클래스)의 생성자에 매개변수를 전달하는 방법을 설명하는 예제입니다. 아래에 핵심 내용을 정리해드릴게요.


🧩 핵심 개념: 생성자 초기화 리스트를 통한 상속 생성자 호출

🔹 코드 구조 요약

class A {
    int a;
public:
    A(int i) { ... }  // 기본 클래스 생성자
    ~A() { ... }      // 기본 클래스 소멸자
    void showA() { ... }
};

class B : public A {
    int b;
public:
    B(int i, int j) : A(i) { ... }  // 파생 클래스 생성자에서 A(i) 호출
    ~B() { ... }
    void showB() { ... }
};

  • B(int i, int j) : A(i) 부분이 핵심입니다.
    • B의 생성자가 호출될 때, 먼저 A(i)를 통해 부모 클래스의 생성자를 실행합니다.
    • 이 방식은 **생성자 초기화 리스트(initializer list)**라고 부릅니다.

🧪 실행 흐름

B bb(10, 20);

  • bb 객체가 생성될 때 다음 순서로 실행됩니다:
    1. A의 생성자 실행 → "A의 생성자" 출력
    2. B의 생성자 실행 → "B의 생성자" 출력
    3. bb.showA() → 10 출력
    4. bb.showB() → 20 출력
    5. bb 객체가 소멸될 때:
      • B의 소멸자 실행 → "B의 소멸자" 출력
      • A의 소멸자 실행 → "A의 소멸자" 출력

📌 출력 결과

A의 생성자
B의 생성자
10
20
B의 소멸자
A의 소멸자

  • 생성자는 부모 → 자식 순서로 실행되고,
  • 소멸자는 자식 → 부모 순서로 실행됩니다.

💡 왜 이렇게 동작할까?

  • 객체가 생성될 때 부모 클래스의 멤버도 초기화되어야 하므로 부모 생성자가 먼저 실행됩니다.
  • 객체가 소멸될 때는 자식부터 정리하고 부모를 정리해야 하므로 소멸자는 역순으로 실행됩니다.

✅ 실무 팁

  • 부모 클래스가 매개변수를 요구하는 생성자를 가질 경우, 자식 클래스 생성자에서 반드시 초기화 리스트를 통해 호출해야 합니다.
  • 초기화 리스트는 const 멤버, 참조 멤버, 멤버 객체 초기화에도 필수입니다.

필요하시면 이 구조를 기반으로 다중 상속, virtual 상속, 또는 생성자 오버로딩까지 확장해서 설명드릴 수 있어요. 어떤 방향으로 더 깊이 들어가볼까요?

 

 

다중 상속은 사실 단점이 명확하다.

다중 상속의 가장 명확한 단점은 ‘모호성(Ambiguity)’과 ‘다이아몬드 문제(Diamond Problem)’로 인한 설계 복잡성과 유지 보수 어려움이야.


🧨 다중 상속의 주요 단점

1. 모호성(Ambiguity)

  • 여러 부모 클래스에 동일한 이름의 멤버 함수나 변수 존재 시, 어떤 부모의 것을 사용할지 명확하지 않음.
  • 예: class A { void print(); }, class B { void print(); }, class C : public A, public B {} → C에서 print() 호출 시 컴파일러가 혼란.

2. 다이아몬드 문제(Diamond Problem)

  • 두 부모 클래스가 같은 조상 클래스를 상속받고, 자식 클래스가 그 두 부모를 상속할 때 조상 클래스가 중복 상속됨.
  • 결과적으로 중복된 멤버와 불필요한 메모리 낭비, 예상치 못한 동작 발생 가능.
  • 해결 방법: 가상 상속(virtual inheritance) 사용 → 하지만 복잡성 증가.

3. 유지 보수 어려움

  • 상속 구조가 복잡해질수록 코드 흐름 파악이 어려워지고, 버그 추적이 힘들어짐.
  • 특히 협업 시, 의도하지 않은 오버라이딩이나 충돌이 발생할 수 있음.

4. 객체 크기 증가 가능성

  • 중복된 멤버나 가상 상속 처리로 인해 객체의 메모리 크기가 커질 수 있음.
  • 이는 성능 저하로 이어질 수 있어.

5. 의도치 않은 동작

  • 부모 클래스의 동작이 자식 클래스에 예상치 못하게 영향을 줄 수 있음.
  • 특히 생성자/소멸자 호출 순서나 가상 함수 테이블(vtable) 충돌 등에서 문제가 발생할 수 있음.

6. 설계 복잡성 증가

  • 클래스 간 관계가 복잡해지면 설계 자체가 난해해지고, 유지보수나 확장성이 떨어짐.
  • 단일 책임 원칙(SRP)을 위반할 가능성도 높아짐.

🔍 실제 사례에서 나타나는 문제

  • class Liger : public Tiger, public Lion처럼 두 클래스가 Animal을 상속하고 있을 때, Liger는 Animal을 두 번 상속받게 되어 Animal의 멤버가 중복 존재.
  • 이 문제를 해결하려면 virtual public Animal로 가상 상속을 사용해야 하지만, 가상 상속은 또 다른 복잡성을 유발itlearner.co.kr.

✅ 요약

단점 설명

모호성 동일 이름 멤버 충돌로 컴파일러 혼란
다이아몬드 문제 조상 클래스 중복 상속으로 인한 충돌
유지 보수 어려움 구조 복잡성으로 디버깅·확장 어려움
객체 크기 증가 중복 멤버로 인한 메모리 낭비
의도치 않은 동작 부모의 동작이 자식에 예기치 않게 영향
설계 복잡성 클래스 관계가 난해해짐

결론: 다중 상속은 강력하지만, 신중하게 설계하지 않으면 코드가 복잡해지고 버그가 많아질 수 있어. 그래서 C++ 외의 많은 언어(Java, C#, Go 등)는 다중 상속을 지원하지 않거나 제한적으로만 허용해.

 

 

맞아요! 아주 직관적이고 좋은 비유예요 🚢🚗

C++에서 다중 상속 시 생성자와 소멸자의 실행 순서를 배와 자동차로 비유하면 이해가 훨씬 쉬워집니다:


🛳️ 비유: 배와 자동차

  • 배가 먼저 들어오고 → 차가 배에 들어간다
  • 기본 클래스의 생성자가 먼저 실행되고 → 파생 클래스의 생성자가 실행된다
  • 도착하면 → 차가 먼저 나가고 → 배가 나간다
  • 파생 클래스의 소멸자가 먼저 실행되고 → 기본 클래스의 소멸자가 역순으로 실행된다

🔄 코드와 비유 연결

class A1 { A1() {...} ~A1() {...} };
class A2 { A2() {...} ~A2() {...} };
class B : public A1, public A2 { B() {...} ~B() {...} };

  • 생성 순서: A1 → A2 → B
  • 배(A1, A2)가 먼저 도착하고 차(B)가 들어간다
  • 소멸 순서: B → A2 → A1
  • 차(B)가 먼저 나가고 배(A2, A1)가 나간다

 

물론이죠! 😊 이 그림은 C++에서의 상속(Inheritance) 개념을 설명하는 예제입니다. 객체지향 프로그래밍(OOP)의 핵심 개념 중 하나인 상속을 시각적으로 이해할 수 있도록 구성되어 있어요.


🧬 핵심 개념: 상속이란?

상속은 기존 클래스(부모 클래스)의 속성과 기능을 새로운 클래스(자식 클래스)가 물려받는 기능입니다. 이를 통해 코드 재사용과 구조적인 설계가 가능해집니다.


🐾 코드 설명

class Animal {
private:
    int age;
public:
    Animal(int a) { age = a; }
    int getAge() { return age; }
    void setAge(int a) { age = a; }
    void eat() const { std::cout << "동물이 먹어요.\\n"; }
};

  • Animal 클래스는 부모 클래스입니다.
  • age는 private 멤버로 외부에서 직접 접근할 수 없고, getAge()와 setAge()를 통해 접근합니다.
  • eat() 함수는 동물이 먹는 행동을 출력합니다.
class Dog : public Animal {
private:
    std::string name;
public:
    Dog(std::string n, int a) : Animal(a) { name = n; }
    std::string getName() { return name; }
    void setName(std::string n) { name = n; }
    void bark() const { std::cout << name << "가 멍멍해요.\\n"; }
};

  • Dog 클래스는 Animal 클래스를 public 상속자식 클래스입니다.
  • name이라는 고유 멤버를 추가했고, bark()라는 새로운 기능도 정의했습니다.
  • eat()과 getAge()는 Animal로부터 상속받아 사용할 수 있습니다.
int main() {
    Animal ani(1);
    ani.eat();
    std::cout << "동물 나이: " << ani.getAge() << std::endl;

    Dog coco("코코", 3);
    coco.eat(); // 상속받은 함수
    coco.bark();
    std::cout << coco.getName() << "의 나이는 " << coco.getAge() << "살입니다.\\n";
}

  • ani는 Animal 객체로, eat()과 getAge()를 사용합니다.
  • coco는 Dog 객체로, eat()은 상속받은 함수이고 bark()는 Dog만의 기능입니다.

📊 클래스 다이어그램 요약

클래스 멤버 변수 멤버 함수

Animal - age (private) + getAge(), + setAge(), + eat()
Dog - name (private) + getName(), + setName(), + bark()
  • Dog는 Animal을 상속받아 getAge()와 eat()을 사용할 수 있습니다.
  • Dog는 name이라는 고유 속성과 bark()라는 고유 기능을 추가합니다.

🎯 이 예제를 통해 알 수 있는 것

  • 상속을 통해 기존 클래스의 기능을 재사용할 수 있다.
  • 자식 클래스는 부모 클래스의 public, protected 멤버에 접근할 수 있다.
  • private 멤버는 직접 상속되지 않으며, 접근하려면 public/protected 함수를 통해야 한다.

728x90

'1학년 대학 수업 > C++ 프로그래밍' 카테고리의 다른 글

12주차 예습 과제  (0) 2025.11.16
11주차 복습 과제  (0) 2025.11.13
11주차 예습 과제  (0) 2025.11.08
10주차 복습 과제  (0) 2025.11.06
10주차 예습 과제  (0) 2025.11.05