본문 바로가기

Programming Languages/C++

[클래스와 객체]-생성자의 종류-복사 생성자

복사 생성자(copy constructor)란 같은 클래스의 객체를 복사하여 객체를 만드는 생성자이다. 만일 복사 생성자를 명시적으로 선언하지 않으면 컴파일러는 원본 객체의 멤버들을 그대로 복사하여 객체를 정의하는 복사 생성자를 자동으로 만든다. 예를 들어 [소스코드 4-6]의 클래스 CounterM의 객체를 다음과 같이 정의하였다고 하자.

CounterM cnt4(99); // ① 0부터 99까지 카운트하는 계수기 객체

CounterM cnt5(cnt4); // ② cnt4를 복사한 계수기 객체 cnt5 정의

CounterM cnt6 = cnt4; // ③ cnt4를 복사한 계수기 객체 cnt6 정의

객체 cnt4는 데이터 멤버 maxValue와 value가 각각 99와 0인 상태로 정의될 것이다. 그런데 CounterM에는 ②와 같이 CounterM 클래스의 객체가 인수인 생성자는 명시적으로 선언하지 않는다. 그러나 컴파일러가 데이터 멤버를 1:1로 복사해 주는 복사 생성자를 자동적으로 만들어 놓기 때문에 ②와 같은 문장이 가능하며, cnt5 역시 maxValue는 99, value는 0이라는 값으로 초기화된다. ③의 문장은 대입이 아닌 초기화 문장이며, 이 경우에도 복사 생성자가 동작한다.

여기에서 문장③은 다음 문장과는 다르다는 점을 주의하여야 한다.

CounterM cnt7(99);
cnt7 = cnt4; // ④ 에러! 이 문장은 초기화 문장이 아닌 대입 연산자임

문장④는 대입 연산자가 적용되는 문장이다. 컴파일러는 ③과 같은 초기화와 ④와 같은 대입을 다른 방법으로 취급한다. const 데이터는 초기화는 가능하지만 대입은 할 수 없다. CounterM 클래스는 const 데이터 멤버를 포함하고 있기 때문에 ④와 같은 대입은 할 수 없다. 

복사 생성자는 프로그래머가 명시적으로 복사 생성자를 정의할 수도 있다. 이를테면 CounterM 클래스에 다음과 같이 복사 생성자를 정의할 수 있다.

class CounterM {
   ......
   CounterM(const CounterM& c)
        : maxValuec.maxValue, valuec.value
   ......
};

주의할 점은 형식 매개변수가 그 클래스의 참조형이어야 한다는 것이다. 형식 매개변수가 참조형이 아니라면 값 호출 방식으로 인수를 전달하게 되며, 이렇게 되면 현재 정의하고 있는 복사 생성자가 필요하게 되는 모순이 발생하기 때문이다.

이상의 내용으로 보면 복사 생성자를 명시적으로 선언할 이유가 있는지 의문이 들겠지만 문제는 객체에 필요한 메모리를 동적으로 할당받고 반납하도록 되어 있는 클래스에서 발생할 수 있따. 다음의 VecF 클래스는 지정된 개수의 값을 저장하는 벡터(1차원 배열 형태로 표현됨)를 저장하며, 벡터 사이의 덧샘을 할 수 있는 클래스이다.

VecF 클래스
벡터 객체를 만들 수 있는 VecF 클래스를 정의하고자 한다. VecF 객체는 저장할 수 있는 float 값의 수를 인수로 지정하여 생성되며, 저장할 값의 배열이 제공될 경우 그 값으로 초기화한다. 인수로 전달된 VecF 객체와 덧셈한 결과를 반환할 수 있으며, 객체에 저장된 벡터를 출력할 수 있다.

|표 4-6| VecF 클래스의 메소드

메소드 비고
VecF(int d, float* a) 생성자
~VecF() 소멸자
VecF add(const VecF& fv) 벡터의 덧셈을 한다.
void print() 벡터를 출력한다.

|표 4-7| VecF 클래스의 속성

속성 비고
int n 벡트의 크기를 저장한다.
float *arr 벡터의 저장공간 포인터

소스코드 4-11 : VecF.h

#ifndef VEC_F_H_INCLUDRD
#define VEC_F_H_INCLUDRD
#include <iostream>
#include <cstring>
using namespace std;

class VecF {
	int n;
	float* arr;
public:
	VecF(int d, float* a = nullptr) : n{ d } { // n개의 float 값을 저장할 수 있는 공간을 동적으로 할당하여 객체를 생성한다.
		arr = new float[d];
		if (a) memcpy(arr, a, sizeof(float) * n); // memcpy함수를 이용하여 arr에 복사한다.
	}
	~VecF() { // 소멸자로 할당된 메모리를 반납한다.
		delete[] arr;
	}
	VecF add(const VecF& fv) const {
		VecF tmp(n); // 벡터의 덧셈 결과를 저장할 임시 객체
		for (int i = 0; i < n; i++) {
			tmp.arr[i] = arr[i] + fv.arr[i];
		}
			return tmp; // 덧셈 결과를 반환함
	}
	void print() const {
		cout << "[ ";
		for (int i = 0; i < n; i++) {
			cout << arr[i] << " ";
		}
			cout << "]";
	}
};

#endif // !VEC_F_H_INCLUDRD

클래스에는 2개의 데이터 멤버를 선언하였다. n은 벡터에 저장할 float형 값의 수이고, arr는 값을 저장할 공간을 가리키는 포인터이다. 

소스코드 4-12 : VFMain.cpp

#include <iostream>
using namespace std;
#include "VecF.h"

int main() {
	float a[3] = { 1,2,3 };
	VecF v1(3, a); // 1,2,3을 저장하는 벡터
	VecF v2(v1); // v1을 복사하여 v2를 만듦
	v1.print();
	cout << endl;
	v2.print();
	cout << endl;
	return 0;
}

의 코드 결과는 프로그램이 정상적으로 종료하지 못하고 에러가 발생하게 된다.

그 이유는 [소스코드 4-12]에서 3개의 float값을 저장할 공간을 할당하고, 매개변수로 전달된 배열의 값 1,2,3을 저장하여 v1이 생성된다. 그 다음 v1을 복사하여 v2를 만든다. 그런데 컴파일러가 묵시적으로 만드는 복사 생성자는 데이터 멤버를 그대로 복사하는 것 이외에 다른 처리는 하지 않았으므로, v2의 arr는 v1의 arr를 그대로 복사한다. 이때 arr는 포인터이므로 이 복사에 의해 v2의 arr는 v1의 arr가 가리키는 공간을 함께 가리키는 것으로 처리된다. 이러한 복사를 얕은 복사(shallow copy)라고 한다. 이 상태에서 print를 호출하여 출력하면 데이터를 올바르게 출력하며, 이상이 있는 것으보 보이지 않는다. 

문제는 객체의 소멸자가 동작할 때 발생한다. v1, v2는 별개의 객체처럼 보이지만, 사실은 데이터를 저장하는 공간을 함께 가리키므로 완전히 별개의 객체라고 할 수 없다. main() 함수의 종료시점이 되면 지역변수인 객체 v1과 v2가 제거되며, 이때 소멸자가 동작한다. 소멸자에서는 arr가 가리키는 공간을 반납한다. 만일 v1이 먼제 제거되는 대상이라면 v1의 arr가 가리키는 공간이 반납된 후 v1이 제거된다. 그런다음 v2의 arr가 가리키는 공간을 반납하려고 하지만 그 공간은 이미 반납이 되어 더 이상 이 프로그램의 영역이 아니게 된다. 그러므로 이 반납 명령은 거부되며, 그 결과 에러가 발생하는 것이다.

이 문제를 해결하는 방법은 클래스 VecF에 복사 생성자를 명시적으로 선언하되, arr가 가리키는 공간을 별도로 할당하고 내용을 복사하게 하는 것이다. 이러한 복사를 깊은 복사(deep copy)라고 한다.

#ifndef VEC_F_H_INCLUDRD
#define VEC_F_H_INCLUDRD
#include <iostream>
#include <cstring>
using namespace std;

class VecF {
	int n;
	float* arr;
public:
	VecF(int d, float* a = nullptr) : n{ d } {
		arr = new float[d];
		if (a) memcpy(arr, a, sizeof(float) * n);
	}
	VecF(const VecF& fv) : n{ fv.n } { // 복사되는 객체의 이름을 저장하기 위한 공간을 별도로 할당받아 이름을 복사하도록 복사 생성자 선언
		arr = new float[n];
		memcpy(arr, fv.arr, sizeof(float) * n);
	}
	~VecF() {
		delete[] arr;
	}
	VecF add(const VecF& fv) const {
		VecF tmp(n); 
		for (int i = 0; i < n; i++) {
			tmp.arr[i] = arr[i] + fv.arr[i];
		}
			return tmp; 
	}
	void print() const {
		cout << "[ ";
		for (int i = 0; i < n; i++) {
			cout << arr[i] << " ";
		}
			cout << "]";
	}
};

#endif // !VEC_F_H_INCLUDRD

그 결과 v1과 v2는 완전히 별개의 객체가 되어, v1이 소멸되더라도 v2는 온전한 상태로 남아 있어 문제가 되지 않는다.