윤성우 열혈 C++ 프로그래밍

임성준·2022년 8월 7일
0

복사 생성자

C++에서는 두 가지 초기화 방식을 동시에 지원하고 있다.

int num1 = 20; 
int num2(num1);

그렇다면, 클래스도 대입 연산을 통한 복사(초기화)가 가능할까?

class SoSimple
{
private :
	int num1; 
    int num2; 
public : 
	SoSimple(int n1, int n2) : num1(n1), num(n2)
    { }
}

int main()
{
	SoSimple sim1(15,20);
    SoSimple sim2 = sim1; -(*)
    return 0;
}

(*)행의 코드를 보면 sim2 객체를 새로 생성해서, 객체 sim1과 sim2 간의 멤버 대 멤버 복사가 일어난다고 예상해 볼 수 있고, 실제로 그렇다.


객체를 선언하게 되면 생성자의 호출을 동반하게 되는데, (*)행에서의 생성자 호출은 어떻게 되며, 어떠한 과정을 거쳐서 생성이 되는걸까?

SoSimple sim2(sim1);
- SoSimple형 객체를 생성한다.
- 객체의 이름은 sim2로 정한다.
- sim1을 인자로 받을 수 있는 생성자의 호출을 통해서 객체생성을 완료한다., 위의 객체생성문에서 호출하고자 하는 생성자는 다음과 같이 *SoSimple 객체
를 인자로 받을 수 있는 생성자이다.

SoSimpe(SoSimple &copy)
{
     ...
}

SoSimple sim2 = sim1; 과 같은 대입연산자를 통한 복사는 묵시적 변환이 
되어서 객체가 생성이된다. 그러나 다음과 같이 키워드 explicit를 사용하면 
묵시적으로 변환이 일어나지 않는다. 생성자도 동일하게 적용된다.

explicit SoSimple(const SoSimple &copy) 
	: num1(copy.num1), num2(copy.num2)
{ }
복사 생성자 
- 호출되는 시점이 일반 생성자와 차이가 있다.

SoSimple(const SoSimple &copy)
	: num1(copy.num1), num2(copy.num2)
{
	...
}
멤버 대 멤버의 복사에 사용되는 원본의 변경을 막기 위해 키워드 const를 
삽입하는게 좋다.

디폴트 생성자

"복사 생성자를 정의하지 않으면, 멤버 대 멤버의 복사를 진행하는 디폴트 복사 생성자가 자동으로 삽입된다."


'깊은 복사'와 '얕은 복사'

디폴트 복사 생성자를 통한 복사는 '얕은 복사'라고 하는데, 이는 멤버변수가 힙의 메모리 공간을 참조하는 경우에 문제가 된다.

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()
{
	Person man1("Lee dong woo", 29);
    Person man2 = man1; -(*)
    return 0; 
}

디폴트 복사 생성자는 멤버 대 멤버를 단순히 복사하므로, 다음의 구조를 띠게 된다.

따라서, 만약 man2의 객체가 소멸된다고 하면 참조하고 있는 주소도 소멸이 되므로 man1객체에서가 가리키는 주소가 없어지게 된다.

'깊은 복사'를 위한 복사 생성자의 정의

깊은 복사는 value값의 전달로 객체 소멸 과정에서의 문제가 발생하지 않는다. 멤버뿐만 아니라, 포인터로 참조하는 대상까지 깊게 복사한다는 뜻으로 정해진 이름이다.

Person(const Person& copy) : age(copy.age)
{
	name = new char[strlen(copy.name)+1];
    strcpy(name, copy.name);
}

위의 복사 생성자가 하는 일은 다음과 같다. 
- 멤버변수 age의 멤버 대 멤버 복사
- 메모리 공간 할당 후 문자열 복사, 그리고 할당된 메모리 주소 값에 멤버 name
  저장


복사 생성자의 호출시점

호출되는 시점은 크게 세가지로 구분할 수 있다.
1. 기존에 생성된 객체를 이용해서 새로운 객체를 초기화하는 경우
2. call-by-value 방식의 함수호출 과정에서 객체를 인자로 전달하는 경우
3. 객체를 반환하되, 참조형으로 반환하지 않는 경우
이들은 "객체를 새로 생성해야 한다. 단, 생성과 동시에 동일한 자료형의 객체로 초기화해야 한다." 라는 공통점을 지닌다.

메모리 공간의 할당과 초기화가 동시에 일어나는 상황.
1) Sosimple obj1 = obj2;
2) SimpleFuncObj(obj);
3) SoSimple SimpleFuncObj(SoSimple ob)
   { 
   	 return ob; //반환하는 순간 메모리 공간이 할당되면서 동시에 초기화!
   } 
// 함수가 값을 반환하면, 별도의 메모리 공간이 할당되고, 
// 이 공간에 반환 값이 저장된다. 

위의 코드에서 SimpleFuncObj 함수가 호출되는 순간, 매개변수로 선언된 ob 객체가 생성되고(ob를 위한 메모리 공간이 할당되고), 이는 인자로 전달된 obj객체로 초기화된다.
return문이 실행되는 순간은, SoSimple 객체를 위한 메모리 공간이 할당되고, 이 공간에 할당된 객체는 반환되는 객체 ob의 내용으로 초기화 된다.
객체를 반환하게 되면, '임시객체'라는 것이 생성되고, 이 객체의 복사 생성자가 호출되면서 return문에 명시된 객체가 인자로 전달된다. 즉, 최종적으로 반환되는 객체는 새롭게 생성되는 임시객체이다. 따라서 함수호출이 완료되고 나면, 지역적으로 선언된 객체는 ob는 소멸되고 obj 객체와 임시객체만 남는다.

객체의 소멸(함수의 반환 시)

임시객체가 생성된 위치에는 임시객체의 참조 값이 반환된다.

Temporary(200).ShowTempInfo();
(임시객체의 참조 값).ShowTempInfo();

이렇듯, '참조 값'이 반환되기 때문에 다음과 같은 문장의 구성도 가능하다.
const Temporary &ref = Temporary(300);
  • 임시객체는 다음 행으로 넘어가면 바로 소멸되어 버린다.
  • 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
profile
일단 적어봅니다.

0개의 댓글