C++ 상속에서 생성자 호출 규칙

2024-07-27

기본 클래스 생성자 우선 호출:

파생 클래스 객체를 생성하면 먼저 기본 클래스 생성자가 호출됩니다. 즉, 파생 클래스의 생성자 코드가 실행되기 전에 기본 클래스의 생성자가 실행되어 기본 클래스 멤버 변수를 초기화합니다.

명시적 호출 vs. 암묵적 호출:

  • 명시적 호출: 파생 클래스 생성자에서 base_class_constructor(arg_list)와 같은 형식으로 기본 클래스 생성자를 명시적으로 호출할 수 있습니다. 이를 통해 기본 클래스의 특정 생성자를 선택하여 사용할 수 있습니다.
  • 암묵적 호출: 명시적 호출이 없으면 파생 클래스 생성자는 기본 클래스의 기본 생성자를 암묵적으로 호출합니다. 기본 클래스에 기본 생성자가 없으면 컴파일 에러가 발생합니다.

생성자 초기화 목록 사용:

파생 클래스 생성자에서 : base_class(arg_list) 형식의 생성자 초기화 목록을 사용하여 기본 클래스 생성자에 인수를 전달할 수 있습니다. 이는 명시적 호출과 동일한 효과를 가지며, 코드를 보다 간결하게 작성할 수 있도록 합니다.

다중 상속:

다중 상속에서 여러 기본 클래스가 있는 경우, 생성자 호출 순서는 상속 선언 순서에 따라 결정됩니다. 즉, 먼저 선언된 기본 클래스의 생성자부터 차례대로 호출됩니다.

예제:

class Base {
public:
  Base() { std::cout << "기본 클래스 생성자" << std::endl; }
  Base(int x) { std::cout << "기본 클래스 생성자(int)" << std::endl; }
};

class Derived : public Base {
public:
  Derived() : Base(10) { std::cout << "파생 클래스 생성자" << std::endl; }
};

int main() {
  Derived d;
}

위 코드에서 출력 결과는 다음과 같습니다.

기본 클래스 생성자(int)
파생 클래스 생성자



C++ 상속 생성자 호출 예제 코드

Base 클래스:

class Base {
public:
  Base() { std::cout << "기본 클래스 생성자 호출" << std::endl; }
  Base(int x) { std::cout << "기본 클래스 생성자(int) 호출, x = " << x << std::endl; }

  void info() {
    std::cout << "기본 클래스 정보" << std::endl;
  }
};

Derived 클래스:

class Derived : public Base {
public:
  Derived() : Base(100) { std::cout << "파생 클래스 생성자 호출" << std::endl; }
  Derived(int x, int y) : Base(x), member_var(y) { std::cout << "파생 클래스 생성자(int, int) 호출, y = " << y << std::endl; }

  int member_var;

  void info() override {
    std::cout << "파생 클래스 정보" << std::endl;
  }
};

메인 함수:

int main() {
  // 기본 클래스 생성자 호출
  Base b;
  b.info();

  // 파생 클래스 기본 생성자 호출
  Derived d;
  d.info();

  // 파생 클래스 생성자(int, int) 호출
  Derived d2(20, 30);
  d2.info();

  return 0;
}

출력 결과:

기본 클래스 생성자 호출
기본 클래스 정보
파생 클래스 생성자 호출
파생 클래스 정보
파생 클래스 생성자(int, int) 호출, y = 30
파생 클래스 정보

설명:

  • Base 클래스는 기본 생성자(Base())와 인수를 받는 생성자(Base(int x))를 가지고 있습니다.
  • Derived 클래스는 Base 클래스를 상속받고, 기본 생성자(Derived())와 두 개의 인수를 받는 생성자(Derived(int x, int y))를 가지고 있습니다.
  • Derived 클래스의 기본 생성자는 Base(100)을 사용하여 기본 클래스 생성자를 명시적으로 호출합니다.
  • Derived(int x, int y) 생성자는 생성자 초기화 목록을 사용하여 Base(x)를 사용하여 기본 클래스 생성자를 호출하고, member_var 멤버 변수를 초기화합니다.
  • main 함수에서 각 생성자를 호출하고, 각 객체의 info() 메서드를 호출합니다.



C++ 상속에서 생성자 호출 대체 방법

가상 기본 클래스 사용:

  • 가상 기본 클래스를 사용하면 파생 클래스에서 기본 클래스 생성자 호출을 제어할 수 있습니다.
  • 가상 기본 클래스는 virtual 키워드를 사용하여 기본 클래스를 선언합니다.
  • 파생 클래스에서 가상 기본 클래스의 생성자를 호출하려면 this->Base::Base(...)와 같은 형식으로 명시적으로 호출해야 합니다.
class Base {
public:
  virtual ~Base() = 0; // 가상 소멸자 선언
};

class Derived : public Base {
public:
  Derived() { }
  ~Derived() override { } // 가상 소멸자 재정의

  void callBaseConstructor() {
    this->Base::Base(); // 명시적 호출
  }
};
  • 추상 기본 클래스를 사용하면 기본 클래스에서 생성자를 직접 정의할 수 없으므로 파생 클래스에서 반드시 생성자를 정의해야 합니다.
  • 파생 클래스는 추상 기본 클래스의 생성자를 호출해야 하며, 명시적 또는 암묵적 호출이 가능합니다.
class Base {
public:
  virtual void info() = 0; // 추상 메서드 선언
  abstract:
};

class Derived : public Base {
public:
  Derived() { }

  void info() override {
    std::cout << "파생 클래스 정보" << std::endl;
  }
};

템플릿 메타프로그래밍 사용:

  • 고급적인 방법으로는 템플릿 메타프로그래밍을 사용하여 생성자 호출을 제어할 수 있습니다.
  • 템플릿 메타프로그래밍은 컴파일 타임에 코드를 생성하는 기술입니다.
  • 이 방법은 복잡하고 숙련된 프로그래머에게만 적합하며, 일반적인 상황에서는 rarely 사용됩니다.

주의 사항:

  • 기본 클래스 생성자 호출을 대체할 때는 주의해야 합니다. 부주의하게 대체하면 객체가 제대로 초기화되지 않아 오류가 발생할 수 있습니다.
  • 가상 기본 클래스 또는 추상 기본 클래스를 사용하는 경우, 파생 클래스에서 반드시 가상 메서드와 추상 메서드를 재정의해야 합니다.
  • 템플릿 메타프로그래밍은 복잡한 기술이며, 잘못 사용하면 코드를 이해하기 어렵게 만들 수 있습니다.

c++ inheritance constructor



C++에서 switch 문에서 변수를 선언할 수 없는 이유

이것에는 몇 가지 중요한 이유가 있습니다.1. 스택 프레임 관리:C++에서 함수나 블록을 호출할 때마다 메모리 스택에 프레임이 생성됩니다. 이 프레임에는 해당 함수 또는 블록에서 사용되는 변수와 임시 데이터가 저장됩니다...


C++에서의 "Strict Aliasing Rule" 란 무엇일까요?

이 규칙은 다음과 같은 상황에 적용됩니다.서로 다른 기본 유형을 가진 포인터: int* 포인터와 char* 포인터는 서로 다른 유형으로 간주되므로 별칭이 허용되지 않습니다.const 또는 volatile 키워드가 달라지는 포인터: const int* 포인터와 int* 포인터는 서로 다른 유형으로 간주되므로 별칭이 허용되지 않습니다...


C++에서 스마트 포인터란 무엇이며 언제 사용해야 할까요?

1. 자동 메모리 해제:스마트 포인터는 소멸자를 통해 자동으로 메모리를 해제하기 때문에 메모리 누수를 방지하는 데 도움이 됩니다. 일반 포인터를 사용하는 경우 프로그래머가 직접 메모리를 해제해야 하기 때문에 누수가 발생하기 쉽습니다...


C++ 및 C 언어에서 구조체 크기 계산: sizeof 연산자의 비밀

1. 메모리 정렬:컴파일러는 메모리 접근 속도를 최적화하기 위해 데이터를 특정 방식으로 정렬합니다. 이는 구조체 멤버의 배치에도 영향을 미칩니다.예를 들어, 다음 구조체를 살펴보겠습니다.int는 일반적으로 4바이트...


C# 자동 속성에 초기값을 할당하는 방법

1. 생성자 사용:생성자는 객체가 생성될 때 자동으로 호출되는 메서드입니다. 생성자를 사용하여 속성에 초기값을 할당할 수 있습니다. 예를 들어 다음과 같은 코드는 Person 클래스를 정의하고 Name 속성에 초기값 "John Doe"를 할당합니다...



c++ inheritance constructor

C/C++ 프로그래밍에서 #include <filename>과 #include "filename"의 차이점

1. #include <filename>각 컴파일러마다 정의된 표준 헤더 파일을 포함하는 데 사용됩니다.<filename> 안에 작성된 파일 이름은 컴파일러가 미리 정의된 경로 목록에서 검색됩니다. 이 목록은 일반적으로 운영 체제 및 컴파일러에 따라 다릅니다


C++에서의 일반 캐스트, 정적 캐스트, 동적 캐스트 비교: 포인터 캐스팅 심층 분석

일반 캐스트는 C++에서 가장 강력한 캐스팅 유형으로, 다양한 형식 변환을 수행할 수 있습니다. 하지만 다른 캐스팅 유형에 비해 안전성이 낮고 오류 가능성이 높다는 단점이 있습니다. 일반 캐스트는 다음과 같은 용도로 사용됩니다


C++/C에서 비트 조작: 특정 비트 설정, 해제, 토글하기

C++와 C 프로그래밍에서 비트 조작은 저수준 시스템 프로그래밍이나 효율적인 알고리즘 구현에 필수적인 기술입니다. 특히, 특정 비트를 설정, 해제, 또는 토글하는 작업은 하드웨어 제어, 데이터 압축, 암호화 등 다양한 분야에서 활용됩니다


C++에서 클래스와 구조체 사용 시점

1. 기본 접근 지정자:구조체: 기본적으로 모든 멤버가 public으로 접근 가능합니다. 즉, 외부 코드에서 쉽게 변경될 수 있습니다.클래스: 기본적으로 모든 멤버가 private으로 접근 제한됩니다. 외부 코드에서 직접 액세스를 제한하고 데이터 은닉을 통해 코드 보안을 강화합니다


C++에서 포인터 변수와 참조 변수의 차이점

1. 선언:포인터 변수: 변수 이름 뒤에 * (별표)를 사용하여 선언합니다.참조 변수: 변수 이름 뒤에 & (앰퍼샌드)를 사용하여 선언합니다.2. 초기화:포인터 변수: 선언 시 nullptr로 초기화하거나 다른 메모리 위치의 주소로 초기화해야 합니다