프로그래밍/cpp

복사 생성자 - 얕은복사 깊은복사 C++

콘파냐 2014. 3. 18. 21:30
반응형

C++에서 생성자의 주된 임무는 클래스 멤버 변수의 초기화 일 것이다. 이러한 생성자는 기본적으로 인수의 유무에 따라서 인수의 형태에 따라서 몇가지 이름을 붙인다. 기억해둘 생성자에는 디폴트 생성자, 복사 생성자가 있고 결국 생성자이고, 형태 또한 생성자의 형태에서 벗어나지 않는다.

그러면 왜 이렇게 구분해 놓은 것인가?

그러면 생성자의 형태를 살펴본 후 이야기하자.


아주 간단한 클래스와 생성자를 만들었다. 생성자의 기본적인 형태는 위 코드처럼 반환타입이 존재하지않고, 클래스의 이름과 똑같다. 그 외에는 일반적인 함수의 형태를 띠게된다. 생성자는 생성자라는 이름처럼 객체의 생성과 동시에 호출이 되게 되어있다.

myClass A; 는 myClass A = myClass(); 와 동일한 구문이다. 해석하자면 우변에 있는 생성자의 형태로 객체 A를 초기화하라는 이야기가 된다. 우리는 보통 왼편의 구문을 자주쓰지만, 오른편의 구문도 기억해놓자.

위 생성자의 경우는 인수가 없지만 인수가 있는 경우를 생각해보자.


인수를 받아 인수의 값으로 멤버를 초기화 하였다. 그리고 생성시에 구문을 살펴보면 인수가 없는 형태에서 인수의 값만 넣은 형태가 된다. 인수가 없는 경우와 인수가 있는 경우나 기본적인 형태는 같다.

myClass A(3); 과 myClass A = myClass(3); 는 같다.


단 인수가 없는경우 객체의 초기화에서 myClass A();는 에러가 생긴다. 보면 알겠지만, 함수의 선언과 모양이 같게된다. 그래서 인수가 없는 경우의 초기화시 주의하길 바란다.



처음에 언급했던 디폴트 생성자를 설명하자면 인수도 없고 내용도 없는 생성자다. 디폴트 생성자가 호출되는 조건은 클래스 내에서 아무런 생성자도 정의하지 않을 경우가 된다. 내용이 비었기 때문에 멤버 변수는 쓰레기 값을 가지고 있을 것이다.


여기까지는 생성자의 기본이다. 생성자의 형태는 어떤식으로든 문법만 맞으면 선언할 수 있고, 인수로 자신의 타입의 클래스객체를 가질 수도 있다. 특히 같은 클래스의 객체끼리의 대입을 위해서는 복사생성자라는 것이 필요한데, 우선 코드를 살펴보자.


결과는 4가 나오고 멤버 변수의 복사가 되었다. 그런데 왜 클래스의 멤버변수는 대입연산시 자동으로 복사가 되는 것일까? 위 코드에서 14~16라인을 보자. 얼핏 보기엔 관련이 없어보이지만, 이 부분이 복사생성자 부분이다. 주석처리를 하더라도 디폴트복사생성자가 자동으로 호출된다. 인수로 자신의 클래스타입의 객체 레퍼런스를 받는다. 이 생성자를 제대로 쓰려면 아래와 같이 코드를 작성해야 할 것이다


파란 땡땡이2줄과 붉은 땡땡땡이 한줄은 같다. 엄밀히 따지면 같지 않다. B=A;부분은 연산자 오버로딩 오버라이딩에 의해서 바꿀 수 있기 때문이다. 

불은 땡땡이의 표현은 우리가 처음에 이야기한 생성자 문법과 동일하다.  하지만 파란 땡땡이가 우리가 좀더 직관적으로 알기쉬운 표현이다. 때문에 복사생성자를 오버로딩 오버라이딩 한 경우, =연산자 오버로딩 오버라이딩도 같이 해줘야 할 것이다.


여기까지 알아봤으면 얕은 복사를 알아보자.

얕은복사는 멤버대 멤버 복사를 말한다. 인수와 내용이 없는 생성자가 디폴트 생성자라한다면, 같은 클래스 타입의 객체를 인수로 받아 멤버대 멤버 복사를 하는 생성자를 디폴트 복사 생성자라고 한다. 위 코드처럼 작성을 하지 않아도, 컴파일러가 알아서 디폴트 복사 생성자를 구현하기 때문에 멤버대 멤버 복사가 이루어진다. 이런 단순한 대입은 멤버변수가 포인터인 경우에도 그냥 대입이 이루어지기 때문에, 두 포인터변수가 같은 지점을 가리키는 얕은복사가 이루어진다. 때문에 포인터가 멤버 변수인 경우는 직접 디폴트 복사생성자를 오버로딩 오버라이딩 해주어야한다.

그럼 다음과 같이 포인터변수를 추가해보고 문제점을 파악해보자.




위 코드에서 빠졌지만, 동적할당된 name의 해제를 위해서소멸자 또한 오버로딩 오버라이딩 해야한다.

~myClass() {delete [] name; }     //추가

결과는 

4

spiderman이 나온다. 일부 컴파일러는 문제를 발생하지 않지만, 위 코드는 A,B 두 객체의 name변수가 힙에 할당된 동일한 메모리를 참조하게 된다. 이렇게 서로 다른 두 포인터가 동일한 메모리를 참조하게되면 소멸자의 호출시에 문제가 발생할 수가 있다.(컴파일러에 따라서 문제가 발생하지 않을 수도 있지만, 바람직하지 않다. 왜냐면 기본적으로 A와 B 객체는 서로 고유의 멤버변수를 가져야 하기 때문이다. 안그러면 A의 객체의 멤버변수의 내용을 수정할 때 B객체의 멤버변수도 덩달아 변하게 된다.)

아무튼 중요한 점은 각 객체가 가리키는 name의 내용은 같아도 메모리상의 위치는 따로따로 되도록 깊은 복사를 해야하는데....


그럼 깊은 복사를 하기위해선?

깊은 복사를 하기위해선 기본적으로 컴파일상에서 제공된 디폴트 복사생성자를 오버로딩 오버라이딩 해야한다.

다음은 디폴트 복사생성자를 오버로딩 오버라이딩을 한 코드다.


추가된 부분은 21,22라인이다. 우리가 생성자라는 개념에서 바라보면 동일한 작업을 한 것이다. 단지 상황에 따른 초기화의 방법이 다른 것일 뿐이다.

생성자, 디폴트생성자, 복사생성자, 디폴트 복사생성자, 모두 생성자기 때문에, 객체의 생성시에 호출되어야한다. 그리고 주된 임무는 객체의 멤버변수 초기화다. 그리고 멤버변수가 포인터인경우에 생성자에서 동적할당을 하는 것 처럼, 똑같이 객체의 복사(대입연산)을 할 때 디폴트 복사생성자 내에서도 동적할당이 이루어져야한다.

반응형