유니티에서 상속은 다음과 같은 방식으로 활용됩니다:
| 공통 기능을 정의 | 고유한 기능을 추가하거나 수정 |
| 재사용성 증가 | 확장성 증가 |
| 유지보수성 향상 | 동작을 다양하게 커스터마이즈 |
============================================================================================
1. C#에서의 상속
: 기호 사용
virtual, override 키워드로 메서드 재정의
2. Java에서의 상속
extends 키워드 사용
메서드 재정의 시 @Override
3. C++에서의 상속
: 뒤에 접근 지정자와 함께 부모 클래스 지정
virtual 메서드를 자식에서 override
4. Python에서의 상속
부모 클래스 이름을 괄호 안에 표기
메서드 재정의는 같은 이름으로 함수 작성
5. JavaScript(ES6)에서의 상속
class + extends
부모 생성자 호출 시 super() 필요
언어별 상속 문법 요약
| C# | : | virtual + override |
| Java | extends | @Override |
| C++ | : + 접근자 | virtual + override |
| Python | class Child(Parent) | 같은 이름으로 재정의 |
| JavaScript | extends | 같은 이름으로 재정의, 필요 시 super() |
============================================================================================

- #include <iostream>: C++ 표준 라이브러리의 iostream 헤더 파일을 포함합니다. 이 파일은 std::cout과 같은 입출력 기능을 사용하기 위해 필요합니다.
- using std::cout;: std::cout을 사용할 때마다 std:: 접두어를 붙이지 않고 cout이라고만 쓸 수 있게 해줍니다.
- class A: '기본 클래스' 또는 '부모 클래스'인 A를 정의합니다.
- private:: 이 안에 선언된 a1()과 a2() 메서드는 class A 내부에서만 직접 호출할 수 있습니다. main 함수나 class B 같은 외부에서는 직접 접근할 수 없습니다.
- public:: 이 안에 선언된 b1(), b2(), b3(), b4() 메서드는 class A 외부에서도 자유롭게 호출할 수 있습니다.
- 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'의 역할을 합니다.
- 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;: 프로그램이 오류 없이 성공적으로 종료되었음을 나타냅니다.
============================================================================================












첫 번째 소스 코드 설명
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;
}
설명:
- int x = 1; (클래스 내 초기화)
- 클래스 A의 멤버 변수 x가 선언될 때, = 1이라는 초기값이 직접 할당되어 있어요. 이걸 **"인클래스 멤버 초기화(In-class member initializer)"**라고 부른단다.
- 이 방식은 C++11 표준부터 추가된 기능으로, 객체가 생성될 때 생성자가 따로 x를 초기화하지 않으면 이 기본값(1)으로 자동 초기화돼.
- A a1; (객체 생성)
- main 함수에서 A 타입의 객체 a1을 생성하고 있어.
- 클래스 A에는 개발자가 직접 만든 생성자가 하나도 없지? 이럴 때는 C++ 컴파일러가 자동으로 아무 기능도 하지 않는 "기본 생성자(Default Constructor)"를 만들어 호출해 줘. (A(){}와 비슷하다고 생각하면 돼.)
- 이 자동 생성된 기본 생성자는 x를 특별히 초기화하지 않으므로, 위에서 설정한 인클래스 멤버 초기화 값인 1이 a1.x에 적용된답니다.
- 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;
}
설명:
- int x = 1; (클래스 내 초기화)
- 첫 번째 코드와 마찬가지로, x에는 인클래스 멤버 초기화로 1이 할당되어 있어. 객체 생성 과정에서 가장 먼저 실행되는 초기화 단계라고 생각하면 돼.
- A() { x = 2; } (사용자 정의 생성자)
- 이번에는 개발자가 직접 A()라는 기본 생성자를 만들었지? 이 생성자는 호출되면 x의 값을 2로 설정하는 명령을 가지고 있어.
- 객체가 생성될 때, 인클래스 멤버 초기화(x = 1)가 먼저 실행되고, 그 다음에 이 생성자의 몸체({ }) 안에 있는 코드(x = 2;)가 실행된단다.
- 따라서 x는 1로 초기화된 후, 다시 2로 변경되는 과정을 거치게 돼.
- (참고로 A():x(2){}처럼 "생성자 초기화 리스트"를 사용하면 x=1을 건너뛰고 바로 x=2로 효율적으로 초기화된단다!)
- A a1; (객체 생성)
- main 함수에서 A 타입의 객체 a1을 생성해.
- 이때는 컴파일러가 만든 기본 생성자가 아니라, 네가 직접 만든 A() { x = 2; } 이 생성자가 호출돼.
- 결과적으로 a1.x는 2가 돼.
- 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 멤버 변수를 사용하는 주요 경우
- 부모 클래스와 자식 클래스가 공유해야 하는 핵심 내부 데이터
- 부모 클래스가 정의하는 어떤 데이터가 부모 클래스 자신에게도 필요하고, 이 데이터를 바탕으로 자식 클래스들이 각자의 방식으로 동작해야 할 때 protected로 선언해요.
- 이 데이터는 외부에 직접 노출되면 안 되지만, 자식 클래스들이 그 데이터를 사용하여 기능을 구현하거나 수정해야 할 때 유용해요.
- #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; }
- 캡슐화를 유지하면서 상속 계층의 유연성을 확보할 때
- 어떤 멤버 변수가 private이라면 자식 클래스조차도 접근할 수 없게 되어, 자식 클래스가 부모의 기능을 확장하거나 변형하기 어렵게 만들 수 있어요.
- 이런 경우 protected를 사용하면 부모 클래스는 내부 데이터를 외부로부터 안전하게 보호하면서, 자식 클래스에게는 그 데이터를 다룰 수 있는 권한을 주어 유연하게 설계할 수 있게 돼요.
- protected는 상속을 통해 관계가 형성된 클래스들에게만 문을 열어주는 것이므로, 여전히 캡슐화 원칙을 지키면서 상속의 이점을 최대한 활용하는 방법이라고 할 수 있어.
protected 사용 시 고려 사항
- 남용은 피하는 것이 좋아요: protected를 너무 많이 사용하면 private의 강력한 캡슐화 장점이 희석될 수 있고, 자식 클래스들이 부모 클래스의 내부 구현에 너무 깊이 의존하게 되어 코드 변경 시 복잡도가 증가할 수 있어요.
- protected 메서드 활용: 데이터 멤버보다는 protected **메서드(함수)**를 통해 데이터를 조작하는 방식을 더 권장하기도 해요. 이렇게 하면 자식 클래스가 데이터를 직접 변경하기보다는, 부모가 제공하는 안전한 메서드를 통해 간접적으로 접근하게 할 수 있어 더 안전한 캡슐화를 유지할 수 있답니다.
============================================================================================




현재 소스 코드 설명 (문제점 포함)
이 코드는 Animal이라는 부모 클래스를 만들고, Dog라는 자식 클래스가 Animal을 상속받아 더 구체적인 기능을 추가하는 상속 관계를 보여줍니다.
- 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 객체의 상태(멤버 변수)를 변경하지 않는다는 것을 의미합니다.
- 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 고유의 메서드입니다.
- Dog(std::string n, int a) : Animal(a): Dog 객체를 생성할 때 호출되는 생성자입니다.
- 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)를 출력합니다.