Copy Constructor
◆ 복사 생성자의 상속
● 기본 클래스로부터 상속되지 않는다.
● (상속 전과 마찬가지로) 컴파일러가 자동 생성하지만, 필요한 경우 직접 구현해야 한다.
● 기본 클래스에서 구현한 복사 생성자 호출 가능하다.
복사 생성자도 생성자와 동일하다.
단지 인자가 const 참조자로 정해져 있는 것이다.
객체가 복사될 때 생성자가 호출하기 때문에 복사 생성자라고 표현한다.
* 강의 듣다가 추가 내용정리
복사 생성자는 기본적으로 일반 생성자의 한 종류이다.
복사 생성자 특징은
1. 같은 클래스의 다른 객체를 참조로 받아 새 객체를 초기화한다.
2. 매개변수가 반드시 같은 클래스의 const 참조여야 한다.
ClassName(const ClassName& other);
여기서 const 참조를 사용하는 이유는 원본 객체가 복사 과정에서 변경되지 않도록 보호하기 위함이다.
참조(&) 를 사용하는 이유는 큰 객체를 값으로 전달할 때 발생하는 불필요한 복사를 피하기 위함이다.
복사 생성자는 특별한 형태의 생성자이지만, 본질적으로는 객체를 초기화하는 생성자의 역할을 하고 있다.
기본 클래스로부터 상속되지 않기에 어떤 걸 호출해야 할지 정해주어야 하고,
마찬가지로 컴파일러가 자동 생성하지만,
복사 생성자 설명할 때,
생성자를 만들지 않아도 컴파일러는 기본 생성자를 하나 만들어준다.
복사 생성자도 마찬가지로 복사 생성자를 만들지 않아도
컴파일러가 기본 복사 생성자를 자동으로 만들어주는 걸 얕은 복사라고 했다.
얕은 복사는 다시 기억하고 있자.
* 얕은 복사(Shallow Copy) 란?
컴파일러가 자동으로 생성해 주는 기본 복사 생성자의 동작 방식을 말한다.
얕은 복사가 중요한 이유는 메모리 관리와 관련된 문제를 야기할 수 있기 때문이다.
얕은 복사의 특징
1. 멤버 변수들을 단순히 비트 단위로 그대로 복사한다.
2. 포인터 변수의 경우 포인터 값(메모리 주소)만 복사하고, 포인터가 가리키는 실제 데이터는 복사하지 않는다.
얕은 복사의 중요성
1. 동적 메모리를 관리하는 클래스에서는 얕은 복사가 메모리 관련 오류를 일으킬 수 있다.
2. 포인터나 자원 핸들을 포함한 클래스는 반드시 직접 복사 생성자를 정의해야 한다.
3. 직접 정의하지 않으면 컴파일러가 생성한 얕은 복사 버전이 사용된다.
얕은 복사와 관련된 특성들은 C++에서 자원 관리와 관련된 중요한 개념이다.
마찬가지로 컴파일러가 자동 생성하지만, 필요한 경우 직접 구현해야 한다.
언제 필요한지 기억해야 한다. 상속에서도 동일한 개념을 가지고 있다.
중요한 부분이다 기본 클래스에서도 구현한 복사 생성자를 직접 호출할 수 있다.
앞서 기본 클래스의 호출을 하는 걸 배운 것처럼 복사 생성자의 호출도 직접 할 수 있다.
고급 Part이다.★★★
◆ 유도 클래스의 복사 생성자
● 기본 클래스의 복사 생성자를 직접 호출 가능하다.
● Slice 과정을 거친다.
● (이동 생성자도 동일하게 동작한다.)
Derived::Derived(const Derived& other)
: Base{ other }, { Derived initialization list }
{
// code
}
* other 은 유도 클래스이지만, slice를 통해 기본 클래스의 복사 생성자에 인수로 넘겨줄 수 있다. 유도 클래스 is a 기본 클래스 관계를 준수하기 때문이다!
복사 생성자 만드는법은
Base(const Base& other) : value{ other.value } { } |
const 참조자에 같은 타입을 사용한다.
얕은 복사를 할 경우에는 other와 똑같은 데이터를 만들어라
Base 클래스가 가지고 있는
private:
int value;
를 활용하여
복사 생성자가 가진 새로운 value 값에다가
other의 value를 대입해서 넣는다.
유도 클래스에서 복사는 어떻게 할까?
Derived(const Derived &other) : Base{ other }, double_value { other.double_value } { } |
유도 클래스의 복사 생성자 방식은 동일하다.
유도 클래스인 class Derived : public Base에서
private:
int double_value;
값을 other.double_value로 대입하면 된다.
여기까지는 Derived(const Derived &other)
: double_value { other.double_value } { }
방식이고, 이제 기본 클래스의 생성자를 정해주어야 한다.
복사 생성자에서 기본 클래스에 있는 복사 생성자를 호출하면 된다.
복사 생성자를 호출하는 방식은
기본 클래스에다가 인자를 Base 타입의 객체를 넣어주어야 한다.
그렇기에
Derived(const Derived &other)
: Base { other }, double_value { other.double_value } { }
Base에 other로 인자로 그대로 넘겨주면 된다.
방금 배운 구문은 복사 생성자인 other를 기본 클래스에서 사용하는 멤버 변수에 대입하는 방식이다.
Derived initialization list 부분이다.
*조금 더 정리된 버전(ai claude)
♠ 복사 생성자의 기본 원리
1. 복사 생성자 형태 :
ClassName(const ClassName& other);
● const 참조자로 같은 타입의 객체를 받는다.
● 이는 원본 객체 보호와 불필요한 복사를 방지하기 위함이다.
2. 기본 클래스 복사 생성자 :
Base(const Base& other) : value {other.value} { }
● 멤버 초기화 리스트를 통해 other의 값들을 새 객체에 복사한다.
● 위 코드에서 value 멤버를 other.value로 초기화한다.
♠ 유도 클래스(파생 클래스)의 복사 생성자
1. 유도 클래스 복사 생성자 형태 :
Derived(const Derived &other)
: Base { other }, double_value { other.double_value } { }
2. 중요한 점 :
● 기본 클래스 부분 초기화 : Base {other}
☞ 기본 클래스의 복사 생성자를 호출한다.
☞ other는 Derived 타입이지만, 기본 클래스 복사 생성자에 전달될 때 자동으로 Base 부분만 사용됩니다. (업캐스팅)
※ 업캐스팅(Upcasting)
C++에서 객체지향 프로그래밍의 중요한 개념으로,
파생 클래스 (자식 클래스)의 객체를 기본 클래스 (부모 클래스)의
포인터나 참조로 다루는 것을 말한다.
● 파생 클래스 멤버 초기화 : double_value { other.double_value }
☞ 파생 클래스의 고유 멤버를 복사한다.
♠ 정리
1. 복사 생성자는 같은 클래스의 다른 객체를 참조로 받아 새 객체를 초기화한다.
2. 기본 클래스의 복사 생성자는 자신의 멤버만 복사한다.
3. 파생 클래스의 복사 생성자는 :
● 기본 클래스 복사 생성자를 호출하여 기본 클래스 부분을 복사하여
● 자신만의 멤버를 별도로 복사한다.
4. Base { other }에서 other는 Derived 타입이지만, 기본 클래스 (Base 부분)만 사용된다.
이 방식으로 복사가 계층적으로 이루어지며, 각 클래스는 자신의 멤버에 대한 복사를 책임진다.
※ 복사를 책임진다는 의미?
1. 책임 분리
: 각 클래스는 오직 자신이 직접 선언한 멤버 변수들만 복사한다.
- 기본 클래스는 자신의 멤버 변수들만 복사
- 파생 클래스는 자신만의 고유한 멤버 변수들만 복사
2. 계층적 복사
: 복잡한 객체의 복사는 계층적으로 이루어진다.
- 파생 클래스의 복사 생성자에는 기본 클래스의 복사 생성자를 호출한다.
- 기본 클래스의 복사 생성자는 기본 클래스 멤버들을 복사한다.
- 그 후 파생 클래스의 복사 생성자는 파생 클래스만의 멤버들을 복사한다.
Derived(const Derived &other)
: Base { other }, double_value { other.double_value } { }
● Base { other }는 기본 클래스 (Base)의 복사 생성자를 호출하여 value 멤버를 복사한다.
● double_value { other.double_value }는 파생 클래스 (Derived)가 직접 자신만의 멤버인 double_value를 복사한다.
이렇게 "책임을 진다"는 것은 각 클래스가 자신의 내부 구조에 대해서만 알고, 자신의 멤버만 복사하는 객체 지향 프로그래밍의 캡슐화 원칙을 따르는 것을 의미한다. 이는 코드 유지 보수성과 확장성을 높이는 중요한 설계 원칙이다.
다시 설명을 진행한다.
Derived라는 유도 클래스를 만들었다.
상속을 하게 되면은 Derived의 객체는 Derived 타입이기도 하지만, Base 타입이기도 한다.
other 도 Derived 타입이기도 하지만 Base 타입이기도 하다.
Base 클래스의 복사생성자를 호출하려면 Base 클래스의 인자를 넣어야 한다.
Base(const Base& other)
: value { other.value }
{ cout << "Base Copy constructor" << endl; }
그렇게 미리 정의를 해 놓았다.
그렇기 때문에 other를 인자로 넘겨줘도,
Base(const Base& other)
: value { other.value } { }가 호출이 된다.
int main()
{
Derived d1 {100 };
Derived d2{ d1 };
}
d2 가 세 번째로 설명하였던 복사 생성자가 된다.
기본 클래스에서 구현한 복사 생성자 호출 가능하다.
Derived d2에 중단점을 넣으면 다음과 같다.
const Derived &other의 인자가
: Base { other }의 인자로 넘겨져서 클래스 생성하였다.
이렇게 다른 객체를 받는 생성자를 호출하였으니
디버깅은 생성자 부분으로 넘어간 것이다.
헷갈릴 수 있는 부분은 다음과 같다.
Base(const Base& other) : value { other.value }
여기서 복사 생성자는 Base 타입을 받게 되어있는데
Derived(const Derived &other)
: Base { other }, double_value { other.double_value } {}
Derived 객체인 other를 넘겨주었는데도 될까?
답은 된다.
상속에서는 된다. 상속관계를 만들어주면 other는
Derived 타입이기도 하고 Base 타입이기도 하기 때문이다.
Base(const Base& other)
: value { other.value }에서
other에 100 이 들어가 있다.
새로 생성하는 복사 생성자에도 값이 100으로 복사되었다.
Derived(const Derived &other)
: Base { other }, double_value { other.double_value }
{ }
double_value {other.double_value} 부분을 보자면
100의 값은 d1에 들어가 있는 값이다.
double_value는 200 이기 때문에 200의 값이 복사된다.
디버깅을 해보면 d1과 d2는 값 복사가 잘 되었음을 확인할 수 있다.
만약에 복사 생성자를 호출할 때,
Derived(const Derived &other)
: double_value { other.double_value }
{ }
Base { other }를 빼고 선언을 안 하게 되면 어떻게 될까?
Base(const Base& other) : value { other.value }가 아닌
기본 생성자 Base() : value { 0 }으로 되기 때문에
d2의 Base value는 0 이 된다.
디테일하게 들어가서 확인하자
Derived d2 { d1 }; 를 실행하려고 한다면
Derived(const Derived &other)
: double_value { other.double_value }
{ }
복사 생성자 부분을 호출을 한다.
여기서 인자로 넘어온 other는
d1을 복사해서 d2를 만들려고 하는 거니깐,
d1 이 other이다.
other라는 인자로 넘겨져서 들어온 것이다.
value 에는 100
double_value 에는 200 이 들어가 있다.
그리고 따로 구분이 되어있다는 의미는 하단 그림으로 나타낼 수 있다.
other 객체의 상태를 알 수 있다.
Derived(const Derived &other)
: Base { other }, double_value { other.double_value }
{ }
Base 에다가 other를 넘겨주고 있다.
F11을 눌러서 함수를 더 들어가 보면
Base(const Base& other)
: value { other.value }
{ }
other를 확대시켜 보자
other의 모양이 바뀌었다.
value 만 존재하고 double_value는 존재하지 않는다.
이 형태가 강의 자료에서 언급한 Slice 과정이라는 것이다.
* slice 과정이란?
슬라이싱(slicing) 현상은 C++에서 특정 상황에 발생하는 데이터 손실 현상을 말한다.
슬라이싱이 발생하는 상황은 다음과 같다.
● 유도 클래스 (파생 클래스) 객체를 기본 클래스 (부모 클래스) 타입으로 복사할 때
● 이때 유도 클래스에서 추가된 멤버 변수나 재정의된 가상 함수 정보가 "잘려나가는(slice)" 현상이 발생한다.
class Base {
int baseValue;
public:
Base() : baseValue(0) {}
Base(int val) : baseValue(val) {}
};
class Derived : public Base {
int derivedValue;
public:
Derived() : Base(), derivedValue(0) {}
Derived(int base, int derived) : Base(base), derivedValue(derived) {}
};
Derived derived(10, 20);
Base base = derived; // 여기서 슬라이싱 발생
이 경우 derived의 derivedValue 부분이 잘려나가고 base 에는 baseValue 만 복사가 된다.
상속을 하게 되면 타입이 2개가 된다.
Base 타입과 Derived 타입도 된다.
Derived(const Derived &other)
: Base { other }, double_value { other.double_value }
{ }
이 other는 Base 이기도 하고, Derived 이기도 한다.
이게 왜 가능할까?
Derived 안에는 Base 가 항상 있기에 가능하다.
Derived는 Base 가 포함하도록 상속으로 정의했기 때문에 항상 Base 가 존재한다.
Derived 클래스 객체는 Base 클래스 객체를 무조건 가지고 있다.
그렇기 때문에 Base 객체만 필요하지만 Derived 객체도 사용할 수 있다.
어떻게 사용할 수 있냐면 Base 객체만 따로 빼서 사용할 수 있다.
무조건 Derived 클래스에는 Base 객체가 들어있기 때문이다.
Derived(const Derived &other)
: Base { other }, double_value { other.double_value }
{ }
Derived other 에는 Derived와 Base 객체가 모두 있다가,
Base other에서는 기존에 있는 객체 중 Base 객체만 쏙 빼서 인자로 넘어온다.
그걸 슬라이싱이라고 하고 , 가능한 문법이다.
가능하다는 의미가 타입이 2개이다.라고 처음에 설명하였다.
왜? Base 객체를 사용하는데 Derived 객체를 넣을 수 있다. 왜?
Derived 객체 안에는 Base 객체가 무조건 들어 있기 때문이다.
그 객체만 뽑아서 사용한다.
이 원리가 유도 클래스 호출할 때 사용 된다.
◆ 유도 클래스의 복사 생성자 예시
class Base {
private:
int value;
public:
...
Base(const Base& other) : value{ other.value } { cout << "Base Copy constructor" << endl; }
};
class Derived : public Base
{
private:
int double_value;
public:
...
Derived(const Derived &other)
: Base{ other }, double_value{ other.double_value } {
cout << "Derived Copy constructor" << endl;
}
~Derived() { cout << "Derived destructor" << endl; }
};
other 에는 어떤 데이터가 들어있을까?
앞에서 설명을 먼저 다 했으며
Derived(const Derived &other)
: Base { other }, double_value { other.double_value}
{}
이 문법이 가능하다.
복사 생성자에서는 이렇게 선언해줘야 한다.
중요한 Part이다.★★★★★
◆ 복사 생성자의 구현 가이드
● 유도 클래스에서 사용자가 복사 생성자를 구현하지 않은 경우,
☞ 컴파일러가 자동으로 생성하며, 기본 클래스의 복사 생성자를 호출한다.
● 유도 클래스에서 사용자가 복사 생성자를 구현한 경우,
☞ 명시하지 않으면 기본 클래스의 인자를 받지 않는 생성자를 호출한다.
☞ 기본 클래스를 위한 복사 / 이동 생성자를 명시적으로 호출해 주어야 한다.
● 따라서, 포인터형 멤버 변수를 가지고 있는 경우,
기본 클래스의 복사 / 이동 생성자를 호출하는 방법에 대해 반드시 숙지해 두어야 한다.
☞ 유도 클래스 멤버 변수에 대한 깊은 복사 고려한다.
유도 클래스에서 복사 생성자를 구현하지 않았을 경우
컴파일러는
Derived(const Derived &other)
: double_value{ }
{ }
이렇게 기본 클래스의 복사 생성자를 자동으로 호출한다.
사용자들이 어떤 걸 해줘야 하냐면
유도 클래스에서 복사 생성자를 구현하지 않을 경우 컴파일러가 자동으로 생성해 준다.
코드에서
/*
Derived(const Derived &other)
: Base { other }, double_value { other.double_value } {
cout << "Derived Copy constructor" << endl;
}
*/
이 부분을 주석처리 하더라도
int main()
{
Derived d1 { 100 };
Derived d2 { d1 };
}
d2 가 구현이 가능하다.
왜? 없으면 컴파일러가 자동으로 생성하기 때문이다.
그렇지만 사용자가 구현을 따로 하는 경우, 명시하지 않으면 기본 클래스의 인자를 받지 않는 생성자를 호출한다.
기본 클래스를 위한 복사 / 이동 생성자를 명시적으로 호출할 수 있다.
Derived(int x)
: Base { x }, double_value { x * 2 }
{ }
이렇게 명시해 주었다.
Derived(int x)
: double_value { x * 2 }
{ }
이렇게 호출하게 되면,
Base()
: value { 0 }
{ }
여기로 들어가는 걸 확인할 수 있다.
Derived(int x)
: double_value { x * 2 }
{ }
이렇게 호출하게 되면,
Base()
: value{ 0 }
{ }
여기로 들어가는 걸 확인할 수 있다.
기본클래스를 호출하지 않은 경우에는 컴파일러가 인자를 받지 않는 생성자로 들어가서 호출하기 때문이다.
복사생성자와 생성자와는 동일한 문법이다.
☞ 기본 클래스를 위한 복사 / 이동 생성자를 명시적으로 호출해 주어야 한다.
=> 사용자가 명시해 줄 수 있다. 타입이 Base 가 아니고 Derived 라도 상관없다. 슬라이싱 하여 집어넣어준다.
왜? 유도 클래스에는 기본 클래스가 항상 들어있기 때문에, 기본 클래스가 필요한 경우에도 유도 클래스 객체가 기본 클래스의 객체로 사용될 수 있기 때문이다.
문제는 멤버 변수로 포인트를 가지고 있을 때에는 복사 생성자를 주의해서 구현해야 한다.
예시로 클래스에 동적할당을 하는 멤버 변수
Base 에는
private:
int *value;
Derived 에는
private:
int * double_value;
복사 생성자를 어떻게 구현을 해야 하는지 직접 한번 해보는 게 좋을 방법이다.
Inheritance
◆ 상속의 정의 : 기본 클래스를 기반으로 새 클래스를 만드는 기능
◆ 유도 클래스 : 그렇게 만들어진 클래스를 유도 클래스라고 하며, 기본 클래스의 모든 것이 포함되어 있다.
◆ protected 멤버 : 상속에서 public은 그대로, private은 여전히 private 이므로, 정보를 숨기되 상속 계층의 하위로 데이터를 상속하고 싶을 때는 protected를 사용
◆ 상속에서의 생성자와 소멸자 : 기본 생성-유도 생성 / 유도 소멸 -기본 소멸 순서
◆ 기본 클래스의 생성자와의 관계 : 유도 클래스 (복사) 생성 시에, 기본 클래스를 어떻게 (복사) 생성할지를 초기화 리스트를 사용해 명시해 주어야 한다.
◆ 상속과 멤버 함수
유도 클래스를 생성하거나 복사 생성하거나 동일한 원칙이다.
기본 클래스 중에 어떤 생성자를 호출해서 생성할지 초기화 리스트를 사용해서 명시해줘야 한다.
명시를 안 해주면 인자가 없는 생성자로 바로 호출해 버리기 때문에
사용자가 원한다면 어떤 생성자를 호출할지 선택하여 호출해야 한다.
https://youtu.be/qkEdZxQn9 KM? list=PLMcUoebWMS1 nzhlx-NbD4 KBGEP1 UCUDF_
굉장히 오래 걸려서(일주일) 강의를 완독 하였다.
물론 나의 귀찮음이 연강을 멈췄지만, 짧은 강의이지만 긴 호흡으로 봐야 제대로 알 수 있는 부분이었다.
'C++ > C++ : Study' 카테고리의 다른 글
다형성(1) - 소개 (0) | 2025.04.27 |
---|---|
상속(8) - 멤버의 사용 (0) | 2025.04.17 |
상속(6) - 기본 클래스 생성자에 인수 전달 (0) | 2025.04.01 |
상속(5) - 생성자와 소멸자 (0) | 2025.03.25 |
상속(4) - protected 멤버 (0) | 2025.03.24 |