복사생성자
class
{
private:
int num1
int num2;
public:
SoSimple(int n1, int n2) : num1(n1), num2(n2)
{}
void ShowSimpleData()
{
cout << num1 << endl;
cout << num2 << endl;
}
};
----------------------------------------------------------------------
int main(void)
{
SoSimple sim1(15, 20);
/*-*-*-*-*--*--*-*-*-*/
SoSimple sim2 = sim1;
/*-*-*-*-*-*-*-*-*-*-*/
si2.ShowSimpleData();
return 0;
}
묵시적 생성자 호출이 일어나서 생성자를 호출할 수 있게 된다.
SoSimple sim2 = sim1;
대입연산의 의미처럼 실제 멤버 대 멤버의 복사가 일어남
코드를 자세히 보면 SoSimlpe 객체에 복사 생성자를 호출하는
T (T &obj)
복사생성자 구조가 없는 것을 볼 수 있다.
복사생성자 역시 정의되지 않는다면 Default 복사 생성자가 생성되어 처리하게 해준다.
그런 이유로 객체에 복사생성자가 명시되지 않는 객체를 초기화하려고 객체를 전달하거나 대입을해도 에러를 발생시키지 않는다.
이번엔 복사생성자가 있는 구조의 예를 통해서 살펴보자
class
{
private:
int num1
int num2;
public:
SoSimple(int n1, int n2) : num1(n1), num2(n2)
{}
/*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/
SoSimple(SoSimple ©) : num1(copy.n1), num2(coopy.n2)
{
cout << "Called SoSimple(SoSimple ©)" << endl;
}
/*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/
void ShowSimpleData()
{
cout << num1 << endl;
cout << num2 << endl;
}
};
------------------------------------------------------------------------------
int main(void)
{
SoSimple sim1(15, 20);
cout << "생성 및 초기화 직전" << endl;
/*-*-*-*-*--*--*-*-*-*/
SoSimple sim2 = sim1; // SoSimple sim(sim1); 으로 변환!
/*-*-*-*-*-*-*-*-*-*-*/
cout << "생성 및 초기화 직후" << endl;
si2.ShowSimpleData();
return 0;
}
여기서 조금 더 팁을 쓰자면 복사 생성자 사용시 인자에 const 를 사용하는 것이 좋다
non-const 로 설정해서 객체를 받을 경우
1. cosnt 객체를 받을 수(복사할 수) 없다
2. 받은 객체를 실수로 수정할 경우 참조로 연결된 원본 객체의 값이 의도치않게 변경될 수 있다.
이 두가지 경우를 방지하기 위해서라도 안전하게 복사생성자 인자에 const 를 사용하는 것을 C++ 에선 지향하고 있다
explicit 키워드
explicit 은 객체 인스턴스의 대입연산(=) 을 통한 초기화 과정에서 발생할 수 있는
묵시적 생성자 호출(암시적 형변환 또는 복사생성자)을 차단하는 키워드로써,
이는 컴파일러가 자동으로 수행하는 암시적 변환 경로를 차단하여 명시적인 객체 생성만을 허용함으로써
코드의 의도와 안전성을 높여준다
즉, explicit 은 객체 인스턴스 생성(초기화: init) 일 때만 적용되는 키워드로 (대입: assign 일 때는 미적용)
같은 타입의 객체로의 초기화시 호출되는 복사 생성자의 묵시적 호출을 차단하고,
다른 타입의 객체로의 초기화시 호출되는 변환 생성자(형변환)의 암시적 형변환을 차단하는 키워드이다.
디폴트 복사 생성자의 문제점
class Person
{
private:
char *name;
int age;
public:
Person(char *myname, int myage)
{
int len=strlen(myname) + 1;
name = new char[len]; // 동적할당
strcpy(name, myname);
age = myage;
}
...
~Person()
{
delete []name; // 동적할당 해제
cout << "called destructor!" << endl;
}
};
-------------------------------------------------------------
int main(void)
{
Person man1("Lee dong woo", 29);
Person man2 = man1;
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
---------------------------------------------------------------
// 결과
이름: Lee dong woo
나이: 29
이름: Lee dong woo
나이: 29
called destructor!
이 구조는 객체 소멸시 문제가 되는 구조로 얕은 복사이다.
출력 또한 한 번 만 발생한 것을 토대로 두 번째 객체 소멸시 소멸자가 제대로 동작하지 못했다는 반증이 될 수 있다.
Persont(const Person& copy) : age(copy.age)
{
name = new char[strlen(copy.name) + 1];
strcpy(name, copy.name);
}
그렇기 때문에 위의 코드와 같이 깊은 복사가 발생할 수 있게
복사생성자를 직접 정의해주는 것이 조금 더 좋은 방향이 될 수 있다.
복사 생성자의 호출시점
복사 생성자의 호출 case 의 확인 1
class SoSimple
{
private:
int num;
public:
SoSimple(int n) : num(n)
{}
SoSimple(const SoSimple& copy) : num(copy.num)
{
cout << "Called SoSimple(const SoSimple& copy)" << endl;
}
void ShowData()
{
cout << "num: " << num << endl;
}
};
-------------------------------------------------------------------------------
void SimpleFuncObj(SoSimple ob)
{
ob.ShowData();
}
int main(void)
{
SoSimple obj(7);
cout << "함수 호출 전" << endl;
SimpleFuncObj(obj);
cout << "함수 호출 후" << endl;
return 0;
}
-------------------------------------------------------------------------------
// 결과
함수 호출 전
Called SOSimple(const SoSimple& copy)
num: 7
함수 호출 후
복사 생성자의 호출 case 의 확인 2
class SoSimple
{
private:
int num;
public:
SoSimple(int n) : num(n)
{}
SoSimple(const SoSimple& copy) : num(copy.num)
{
cout << "Called SoSimple(const SoSimple& copy)" << endl;
}
SoSimple& AddNum(int n)
{
Num += n;
return *this;
}
void ShowDataq()
{
cout << "num: " << num << endl;
}
};
-------------------------------------------------------------------------
SoSimple SimpleFuncObj(SoSimple ob)
{
cout << "return 이전" << endl;
return ob;
}
int main(void)
{
SiSimple obj(7);
SimpleFuncObj(obj).AddNum(30).ShowData();
obj.ShowData();
return 0;
}
---------------------------------------------------------------------------
// 결과
Called SoSimple(const SoSimple& copy)
return 이전
Called SoSimple(const SoSimple& copy)
num: 37
num: 7
메인의
SimpleFuncObj(obj).AddNum(30).ShowData();
이 부분은
생성된 obj 의 AddNum 이나 ShowData 가 아닌
SimpleFuncObj 에서 ob 를 통해 반환되는 중에 복사생성자를 통해 생성된 Sosimple 객체의 멤버 함수로써 실행이 된 것
(반환받지 않아도 임시객체로써 존재하기 때문에 유효한 동작)
반환할 떄 만들어진 객체의 소멸 시점
class Temporary
{
private:
int num;
public:
Temporary(int n) : num(n)
{
cout << "create obj: " << num << endl;
}
~Temporary()
{
cout << "destory obj: " << num << endl;
}
void ShowTempInfo()
{
cout << "My num is " << num << endl;
}
};
--------------------------------------------------------------------------
int main(void)
{
Temporary(100);
cout << "********** after make!" << endl << endl;
Temporary(200).ShowTempInfo(300);
cout << "********** after make!" << endl << endl;
const Temporary &ref = Temporary(300);
cout << "********** end of main!" << endl << endl;
return 0;
}
----------------------------------------------------------------------------
// 결과
create obj: 100
destroy obj: 100
********** after make!
create obj: 200
My num is 200
destroy obj: 200
********** after make!
create obj: 30
********** end of main!
destroy obj: 300
흔히
int num = 3 + 4;
이 코드에서 3 과 4 는 상수값인 걸 우리는 이해를 하고 있을 것이다
위의 본문 코드에서
const Temporary &ref = Temporary(300);
const 참조로 선언 한 변수에 임시 객체가 할당이 가능한 이유 또한
Temporary(300) 이 상수 객체로써 사용되기 때문이라는 것을 알 수 있는 대목이다
이 때, 임시 객체는 실제 스택 프레임에 메모리 공간을 할당 받는데 보통은 코드 라인이 끝나면 곧바로 소멸된다
하지만 const 참조 또는 rvalue 참조에 바인딩 되면 해당 참조가 유지되는 동안 똑같이 유지되며 사라지지 않고,
스택 프레임에도 여전히 참조가 사라질 때까지 유지된다.
'C++ > 윤성우의 열혈 프로그래밍 C++' 카테고리의 다른 글
윤성우의 열형 C++ chpt.10 (2) | 2025.07.25 |
---|---|
윤성우의 열혈 C++ chpt9. (4) | 2025.07.21 |
윤성우의 열혈 C++ chpt7. (0) | 2025.06.26 |
윤성우의 열혈 C++ chpt6. (2) | 2025.06.08 |
윤성우의 열혈 C++ chpt5. (0) | 2025.05.16 |