카테고리 없음

김민찬 12주차 수업 후 과제

content9349 2025. 11. 19. 14:12

유니티에서 상속은 다음과 같은 방식으로 활용됩니다:

부모 클래스(Enemy)자식 클래스(FastEnemy, StrongEnemy 등)
공통 기능을 정의 고유한 기능을 추가하거나 수정
재사용성 증가 확장성 증가
유지보수성 향상 동작을 다양하게 커스터마이즈

 

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

1. C#에서의 상속

: 기호 사용
virtual, override 키워드로 메서드 재정의

 
public class Animal { public string name; public virtual void Speak() { Console.WriteLine("Animal sound"); } } public class Dog : Animal { public override void Speak() { Console.WriteLine("Woof!"); } }

2. Java에서의 상속

extends 키워드 사용
메서드 재정의 시 @Override

 
class Animal { String name; void speak() { System.out.println("Animal sound"); } } class Dog extends Animal { @Override void speak() { System.out.println("Woof!"); } }

3. C++에서의 상속

: 뒤에 접근 지정자와 함께 부모 클래스 지정
virtual 메서드를 자식에서 override

 
#include <iostream> using namespace std; class Animal { public: string name; virtual void speak() { cout << "Animal sound" << endl; } }; class Dog : public Animal { public: void speak() override { cout << "Woof!" << endl; } };

4. Python에서의 상속

부모 클래스 이름을 괄호 안에 표기
메서드 재정의는 같은 이름으로 함수 작성

 
class Animal: def __init__(self, name): self.name = name def speak(self): print("Animal sound") class Dog(Animal): def speak(self): print("Woof!")

5. JavaScript(ES6)에서의 상속

class + extends
부모 생성자 호출 시 super() 필요

 
class Animal { constructor(name) { this.name = name; } speak() { console.log("Animal sound"); } } class Dog extends Animal { speak() { console.log("Woof!"); } }

언어별 상속 문법 요약

언어상속 키워드메서드 재정의 방법
C# : virtual + override
Java extends @Override
C++ : + 접근자 virtual + override
Python class Child(Parent) 같은 이름으로 재정의
JavaScript extends 같은 이름으로 재정의, 필요 시 super()

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

  1. #include <iostream>: C++ 표준 라이브러리의 iostream 헤더 파일을 포함합니다. 이 파일은 std::cout과 같은 입출력 기능을 사용하기 위해 필요합니다.
  2. using std::cout;: std::cout을 사용할 때마다 std:: 접두어를 붙이지 않고 cout이라고만 쓸 수 있게 해줍니다.
  3. class A: '기본 클래스' 또는 '부모 클래스'인 A를 정의합니다.
    • private:: 이 안에 선언된 a1()과 a2() 메서드는 class A 내부에서만 직접 호출할 수 있습니다. main 함수나 class B 같은 외부에서는 직접 접근할 수 없습니다.
    • public:: 이 안에 선언된 b1(), b2(), b3(), b4() 메서드는 class A 외부에서도 자유롭게 호출할 수 있습니다.
  4. class B : public A: class B를 정의합니다. 여기서 : public A는 B가 A를 공개적으로(public) 상속받는다는 것을 의미합니다.
    • 이것은 B가 A의 모든 public  protected 멤버를 자신의 멤버처럼 사용할 수 있게 됩니다.
    • A의 public 멤버 (b1, b2, b3, b4)는 B의 public 멤버가 됩니다.
    • A의 private 멤버 (a1, a2)는 B에게 상속되지만, B 내부에서도 직접 접근할 수 없고 여전히 A 클래스 내부에서만 접근 가능합니다. (마치 DNA는 물려받지만 조상만 알고 쓰는 비법 같은 것이죠.)
    • B 클래스 자체에는 아무런 추가 멤버가 정의되어 있지 않지만, A의 공개된 기능들을 모두 물려받았기 때문에 '확장된 A'의 역할을 합니다.
  5. int main(): C++ 프로그램의 실행이 시작되는 메인 함수입니다.
    • A aa;: A 클래스의 객체 aa를 생성합니다. aa는 A의 public 메서드들(b1, b2, b3, b4)을 사용할 수 있습니다.
    • B bb;: B 클래스의 객체 bb를 생성합니다. bb는 B가 상속받은 A의 public 메서드들(b1, b2, b3, b4)을 사용할 수 있습니다.
    • bb.b2();: bb 객체를 통해 b2() 메서드를 호출합니다. b2()는 A의 public 메서드이며, B가 A를 공개 상속했으므로 bb를 통해서도 접근 가능합니다. 따라서 A의 b2()가 실행되어 "b2"가 출력됩니다.
    • aa.b1();: aa 객체를 통해 b1() 메서드를 호출합니다. b1()는 A의 public 메서드이므로 aa를 통해 직접 호출 가능합니다. 따라서 A의 b1()이 실행되어 "b1"이 출력됩니다.
    • std::cout << "Hello World\n";: 화면에 "Hello World"를 출력하고 줄을 바꿉니다.
    • return 0;: 프로그램이 오류 없이 성공적으로 종료되었음을 나타냅니다.

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

 

public이 상속되더라도 부모의 private는 상속이 안된다.
거의 안씀(자식한테만 허용)

 

첫 번째 소스 코드 설명

cpp

#include <iostream>
using std::cout;
using std::endl;

class A
{
    int x = 1; // In-class member initializers
    //생성자보다 먼저 실행됨
public:
    void setX(int i) { x = i; }
    int getX() { return x ; }
};

int main()
{
    A a1; // 디폴트 생성자 호출, 눈에 안보이는 A(){}
    cout << a1.getX() << endl;
    return 0;
}

설명:

  1. int x = 1; (클래스 내 초기화)
    • 클래스 A의 멤버 변수 x가 선언될 때, = 1이라는 초기값이 직접 할당되어 있어요. 이걸 **"인클래스 멤버 초기화(In-class member initializer)"**라고 부른단다.
    • 이 방식은 C++11 표준부터 추가된 기능으로, 객체가 생성될 때 생성자가 따로 x를 초기화하지 않으면 이 기본값(1)으로 자동 초기화돼.
  2. A a1; (객체 생성)
    • main 함수에서 A 타입의 객체 a1을 생성하고 있어.
    • 클래스 A에는 개발자가 직접 만든 생성자가 하나도 없지? 이럴 때는 C++ 컴파일러가 자동으로 아무 기능도 하지 않는 "기본 생성자(Default Constructor)"를 만들어 호출해 줘. (A(){}와 비슷하다고 생각하면 돼.)
    • 이 자동 생성된 기본 생성자는 x를 특별히 초기화하지 않으므로, 위에서 설정한 인클래스 멤버 초기화 값인 1이 a1.x에 적용된답니다.
  3. cout << a1.getX() << endl; (출력)
    • a1 객체의 getX() 메서드를 호출해서 x의 값을 가져와 출력해.
    • 따라서 1이 출력될 거야.

결과:

 
1

두 번째 소스 코드 설명

cpp

#include <iostream>
using std::cout;
using std::endl;

class A
{
    int x = 1; // 먼저 실행
public:
    A() { x = 2; } // A():x(2){} // 나중에 실행
    void setX(int i) { x = i; }
    int getX() { return x ; }
};

int main()
{
    A a1;
    cout << a1.getX() << endl;
    return 0;
}

설명:

  1. int x = 1; (클래스 내 초기화)
    • 첫 번째 코드와 마찬가지로, x에는 인클래스 멤버 초기화로 1이 할당되어 있어. 객체 생성 과정에서 가장 먼저 실행되는 초기화 단계라고 생각하면 돼.
  2. A() { x = 2; } (사용자 정의 생성자)
    • 이번에는 개발자가 직접 A()라는 기본 생성자를 만들었지? 이 생성자는 호출되면 x의 값을 2로 설정하는 명령을 가지고 있어.
    • 객체가 생성될 때, 인클래스 멤버 초기화(x = 1)가 먼저 실행되고, 그 다음에 이 생성자의 몸체({ }) 안에 있는 코드(x = 2;)가 실행된단다. 
    • 따라서 x는 1로 초기화된 후, 다시 2로 변경되는 과정을 거치게 돼.
    • (참고로 A():x(2){}처럼 "생성자 초기화 리스트"를 사용하면 x=1을 건너뛰고 바로 x=2로 효율적으로 초기화된단다!) 
  3. A a1; (객체 생성)
    • main 함수에서 A 타입의 객체 a1을 생성해.
    • 이때는 컴파일러가 만든 기본 생성자가 아니라, 네가 직접 만든 A() { x = 2; } 이 생성자가 호출돼.
    • 결과적으로 a1.x는 2가 돼.
  4. cout << a1.getX() << endl; (출력)
    • a1 객체의 getX() 메서드를 호출해서 x의 값을 가져와 출력해.
    • 따라서 2가 출력될 거야.

결과:

 
2

핵심 정리!

  • 인클래스 멤버 초기화 (int x = 1;): 생성자가 아무런 명시적인 초기화를 하지 않을 때, 멤버 변수에 기본값을 제공해. 객체 생성 과정에서 가장 먼저 실행돼.
  • 생성자의 몸체 (A() { x = 2; }): 생성자가 호출될 때 실행되는 코드 블록이야. 인클래스 멤버 초기화 다음에 실행되므로, 같은 멤버를 초기화하면 생성자 몸체 안의 코드가 인클래스 초기화 값을 덮어쓴다는 점이 중요해!

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

private과 protected의 공통점과 차이점

1. 공통점

  • 클래스 외부 직접 접근 금지: private과 protected로 선언된 멤버(변수나 함수)는 클래스 인스턴스를 통해 직접 접근할 수 없어요. 즉, MyClass obj; obj.private_member = 5;  obj.protected_member = 10;과 같은 코드는 컴파일 에러를 일으킨답니다. 이 점은 두 접근 지정자의 핵심적인 공통점입니다. 
  • 캡슐화 (Encapsulation) 지원: 둘 다 클래스 내부 구현을 숨기고 외부로부터의 불필요한 접근을 막아서, 코드의 안정성을 높이고 유지보수를 쉽게 하는 캡슐화 원칙을 지키는 데 사용됩니다.

2. 차이점

가장 중요한 차이점은 바로 **'상속받은 클래스(파생 클래스/자식 클래스)에서의 접근 허용 여부'**예요.

특성private 접근 제한자protected 접근 제한자
정의 선언된 해당 클래스 내부에서만 접근 가능. 선언된 해당 클래스 내부와 이 클래스를 상속받은 파생 클래스 내부에서 접근 가능. 
상속 후 파생 클래스에서 접근 불가. (상속은 되지만, 사용할 수 없음) 파생 클래스에서 접근 가능.
목적 완벽하게 숨겨서 클래스 내부에서만 사용하려는 목적. 클래스 내부에서 사용하고, 상속을 통해 기능을 확장할 자식 클래스에게는 접근을 허용하려는 목적. 
활용 클래스 내부 로직 처리용 변수, 헬퍼 함수 등. 상속받은 클래스들이 공통적으로 접근해야 할 데이터나 기능.

예시로 이해하기:

Person이라는 클래스가 있고, Student가 Person을 상속받는다고 해보자.

cpp

class Person {
private:
    std::string ssn; // 주민등록번호: 완벽하게 숨겨야 함
protected:
    int family_secret_code; // 가족들끼리만 공유하는 비밀 코드: 자식에게는 알려줄 수 있음
public:
    std::string name; // 이름: 누구나 알 수 있음
};

class Student : public Person {
public:
    void displayInfo() {
        // cout << ssn; // 에러! private 멤버는 자식 클래스도 접근 불가
        std::cout << family_secret_code; // 가능! protected 멤버는 자식 클래스 접근 가능
        std::cout << name; // 가능! public 멤버는 누구나 접근 가능
    }
};

int main() {
    Person p;
    // p.ssn = "12345"; // 에러! private 접근 불가
    // p.family_secret_code = 123; // 에러! protected도 클래스 외부에서는 접근 불가
    p.name = "홍길동"; // 가능! public은 접근 가능

    Student s;
    // s.ssn = "12345"; // 에러! 여전히 private 접근 불가
    // s.family_secret_code = 123; // 에러! protected도 클래스 외부에서는 접근 불가
    s.name = "이순신"; // 가능! public은 접근 가능
    
    s.displayInfo(); // Student 클래스 내부에서 protected와 public 멤버에 접근
    return 0;
}

이처럼 private은 해당 클래스 울타리 밖으로는 한 발짝도 나가지 못하게 철저히 막아버리는 것이고, protected는 자기 울타리 밖으로는 못 나가게 하지만, 자식들(상속받은 클래스)에게는 접근을 허용해주는 개념이라고 생각하면 이해하기 쉬울 거야. 

 

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

protected 멤버 변수를 사용하는 주요 경우

  1. 부모 클래스와 자식 클래스가 공유해야 하는 핵심 내부 데이터
    • 부모 클래스가 정의하는 어떤 데이터가 부모 클래스 자신에게도 필요하고, 이 데이터를 바탕으로 자식 클래스들이 각자의 방식으로 동작해야 할 때 protected로 선언해요.
    • 이 데이터는 외부에 직접 노출되면 안 되지만, 자식 클래스들이 그 데이터를 사용하여 기능을 구현하거나 수정해야 할 때 유용해요.
    예시: Shape (도형) 클래스와 Circle, Rectangle (원, 사각형) 클래스위 예시에서 x와 y는 protected이므로 main 함수에서 myCircle.x = 100;처럼 직접 접근할 수 없어. 하지만 Circle 클래스 내에서는 x와 y에 접근하여 자신의 draw() 함수를 구현하는 데 사용할 수 있지. 
  2. #include <iostream> class Shape { protected: // Shape와 그 자식 클래스들만 접근 가능 int x, y; // 도형의 위치 좌표 // 모든 도형은 위치를 가지고 있지만, 외부에서 마음대로 이 위치를 변경하지 못하게 하고 싶어. // 하지만 자식 도형들(원, 사각형)은 자신의 위치를 기반으로 움직이거나 그려져야 하니까 접근이 필요해. public: Shape(int _x = 0, int _y = 0) : x(_x), y(_y) {} void move(int dx, int dy) { // 위치를 변경하는 공개 메서드 x += dx; y += dy; std::cout << "도형이 (" << x << ", " << y << ")로 이동했습니다.\n"; } // 자식 클래스들이 구현할 함수 (다형성) virtual void draw() = 0; // 순수 가상 함수 (하위 클래스에서 반드시 구현해야 함) }; class Circle : public Shape { private: int radius; public: Circle(int _x, int _y, int r) : Shape(_x, _y), radius(r) {} void draw() override { std::cout << "원을 (" << x << ", " << y << ")에 그립니다. 반지름: " << radius << std::endl; // 여기에서 'x'와 'y'는 protected 멤버지만, Circle은 Shape의 자식이므로 접근 가능합니다. } }; int main() { Circle myCircle(10, 20, 5); myCircle.draw(); // 원을 (10, 20)에 그립니다. 반지름: 5 myCircle.move(5, 5); // 도형이 (15, 25)로 이동했습니다. myCircle.draw(); // 원을 (15, 25)에 그립니다. 반지름: 5 // myCircle.x = 100; // 에러! protected 멤버는 main 함수에서 직접 접근 불가 return 0; }
  3. 캡슐화를 유지하면서 상속 계층의 유연성을 확보할 때
    • 어떤 멤버 변수가 private이라면 자식 클래스조차도 접근할 수 없게 되어, 자식 클래스가 부모의 기능을 확장하거나 변형하기 어렵게 만들 수 있어요.
    • 이런 경우 protected를 사용하면 부모 클래스는 내부 데이터를 외부로부터 안전하게 보호하면서, 자식 클래스에게는 그 데이터를 다룰 수 있는 권한을 주어 유연하게 설계할 수 있게 돼요.
    • protected는 상속을 통해 관계가 형성된 클래스들에게만 문을 열어주는 것이므로, 여전히 캡슐화 원칙을 지키면서 상속의 이점을 최대한 활용하는 방법이라고 할 수 있어.

protected 사용 시 고려 사항

  • 남용은 피하는 것이 좋아요: protected를 너무 많이 사용하면 private의 강력한 캡슐화 장점이 희석될 수 있고, 자식 클래스들이 부모 클래스의 내부 구현에 너무 깊이 의존하게 되어 코드 변경 시 복잡도가 증가할 수 있어요.
  • protected 메서드 활용: 데이터 멤버보다는 protected **메서드(함수)**를 통해 데이터를 조작하는 방식을 더 권장하기도 해요. 이렇게 하면 자식 클래스가 데이터를 직접 변경하기보다는, 부모가 제공하는 안전한 메서드를 통해 간접적으로 접근하게 할 수 있어 더 안전한 캡슐화를 유지할 수 있답니다. 

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

현재 소스 코드 설명 (문제점 포함)

이 코드는 Animal이라는 부모 클래스를 만들고, Dog라는 자식 클래스가 Animal을 상속받아 더 구체적인 기능을 추가하는 상속 관계를 보여줍니다.

  1. class Animal (부모 클래스)
    • private: int age;: 동물의 나이를 저장하는 변수입니다. private으로 선언되었기 때문에 Animal 클래스 내부에서만 직접 접근할 수 있습니다.
    • public:: 외부에서 접근 가능한 멤버들입니다.
      • Animal(int a): Animal 객체를 생성할 때 호출되는 생성자입니다. age를 전달받은 a 값으로 초기화합니다.
      • int getAge(): age 값을 반환하는 Getter 메서드입니다. private age에 외부에서 안전하게 접근할 수 있도록 해줍니다.
      • void setAge(int a): age 값을 설정하는 Setter 메서드입니다. private age를 외부에서 안전하게 변경할 수 있도록 해줍니다.
      • void eat() const: 동물이 먹는 행동을 출력하는 메서드입니다. const는 이 메서드가 Animal 객체의 상태(멤버 변수)를 변경하지 않는다는 것을 의미합니다.
  2. class Dog : public Animal (자식 클래스)
    • class Dog : public Animal: Dog 클래스가 Animal 클래스를 public으로 상속받는다는 것을 의미합니다. 이는 Dog가 Animal의 모든 public 멤버(메서드)를 물려받아 사용할 수 있다는 뜻이에요.
    • private: std::string name;: 강아지의 이름을 저장하는 변수입니다. private이라 Dog 클래스 내부에서만 직접 접근 가능해요.
    • public::
      • Dog(std::string n, int a) : Animal(a): Dog 객체를 생성할 때 호출되는 생성자입니다.
        • : Animal(a)는 Animal 부모 클래스의 생성자를 호출하여 age 부분을 초기화하도록 지시합니다. (자식이 태어날 때 부모의 유전 정보(나이)도 함께 전달하는 것이죠!)
        • name = n;은 Dog 클래스 고유의 멤버인 name을 전달받은 n 값으로 초기화합니다.
      • std::string getName(): name 값을 반환하는 Getter 메서드입니다.
      • void setName(std::string n): name 값을 설정하는 Setter 메서드입니다.
      • void bark() const: 강아지가 짖는 행동을 출력하는 Dog 고유의 메서드입니다.
  3. int main() (프로그램 실행 부분)
    • Animal ani(1);: Animal 객체 ani를 생성하고 생성자에 1을 전달하여 나이를 1로 초기화합니다.
    • ani.eat();: ani 객체의 eat() 메서드를 호출하여 "동물이 먹어요."를 출력합니다.
    • std::cout << "동물 나이: " << ani.getAge() << std::endl;: ani 객체의 나이(1)를 출력합니다.
    • Dog coco("코코", 3);: Dog 객체 coco를 생성하고 이름은 "코코", 나이는 3으로 초기화합니다.
    • coco.eat();: coco 객체가 Animal로부터 상속받은 eat() 메서드를 호출하여 "동물이 먹어요."를 출력합니다.
    • coco.bark();: coco 객체의 고유 메서드인 bark()를 호출하여 "코코가 멍멍해요."를 출력합니다.
    • std::cout << coco.getName() << "의 나이는 " << coco.getAge() << "살입니다.\n";: coco의 이름("코코")과 Animal로부터 상속받은 getAge() 메서드를 통해 나이(3)를 출력합니다.