13-3-다.깊은 복사

구조체끼리 대입이 가능하다는 것은 문법적으로 대입이 허용된다는 얘기이다. 그러나 실제로는 대입에 의해 예상치 못한 문제가 발생하는 경우도 있다. 구조체 멤버 중에 포인터가 있고 이 포인터가 구조체 외부의 메모리를 가리키고 있다거나 또는 비슷한 방식으로 외부의 어떤 대상을 참조하고 있다면 단순히 복사만 해서는 사본을 만들 수 없다. 예를 들어 구조체 멤버 중에 유일한 값을 가져야 하는 ID가 있다면 이 경우도 문제가 된다. 어떤 문제가 있는지 포인터 멤버를 가진 구조체의 예를 보도록 하자.

 

: ShallowCopy

#include <Turboc.h>

 

struct tag_Friend {

     char *pName;

     int Age;

     double Height;

};

 

void main()

{

     tag_Friend Albert={NULL,80,165.0};

     tag_Friend Kim;

 

     Albert.pName=(char *)malloc(32);

     strcpy(Albert.pName,"알버트 아인슈타인");

 

     Kim=Albert;

     printf("이름=%s, 나이=%d, 키=%.1f\n",Kim.pName,Kim.Age,Kim.Height);

 

     strcpy(Albert.pName,"아이작 뉴튼");

     printf("이름=%s, 나이=%d, 키=%.1f\n",Kim.pName,Kim.Age,Kim.Height);

     free(Albert.pName);

     free(Kim.pName);

}

 

tag_Friend 구조체에 문자형 포인터 멤버 pName이 포함되어 있는데 포인터는 정적 배열에 비해 가변 길이 문자열을 다룰 수 있는 장점이 있는 반면 정보를 저장하기 전에 동적 할당을 해야 하는 번거로움이 있다. main에서 Albert라는 이름으로 구조체 변수를 선언하고 pName에 32자 길이를 할당한 후 이 메모리에 이름을 복사해 넣었다. 이때 Albert가 메모리에 생성된 모양은 다음과 같다.

이름 문자열이 구조체에 포함되어 있지는 않지만 동적으로 할당된 메모리의 번지를 멤버로 가지고 있으므로 이 번지를 읽으면 이름 문자열을 얻을 수 있다. 어쨌든 Albert 구조체로부터 아인슈타인에 대한 모든 정보를 읽거나 쓸 수 있는 것이다. 이 상태에서 Kim이라는 같은 타입의 구조체에 Albert를 대입했는데 이렇게 되면 Kim은 Albert의 모든 멤버를 그대로 대입받으며 똑같은 정보를 가지게 된다. 대입 직후에 Kim의 멤버를 출력해 보면 과연 똑같은 정보가 출력된다는 것을 확인할 수 있다.

그러나 제대로 대입된 것 같지만 메모리 내부를 들여다 보면 굉장히 불안한 상태라는 것을 알 수 있는데 두 구조체 변수의 pName 번지가 똑같은 곳을 가리키고 있는 것이다. 대입 연산자로 대입했으므로 번지까지도 그대로 대입되었다. 대입하는 시점에서 두 변수는 똑같은 정보를 가지기는 하지만 동적 할당된 메모리를 공유하고 있기 때문에 잠재적인 문제가 발생할 소지를 많이 가지고 있다. 이 때의 메모리 상황은 다음과 같다.

우선 두 변수 중 한쪽의 pName을 바꾸면 양쪽이 모두 영향을 받는다는 점이 문제다. Kim이 Albert의 사본으로 생성되었는데 Albert의 pName을 변경하면 Kim의 이름도 같이 변경될 수밖에 없으며 반대의 경우도 마찬가지다. 대입에 의해 두 변수가 일시적으로 같은 상태가 되기는 했지만 서로 종속적인 관계가 되었으므로 완전한 사본이라 할 수 없다. 정수형 변수 i의 값을 j=i로 대입하여 사본 j를 만들었다면 j가 어떻게 되더라도 i는 영향을 받지 말아야 한다.

또 다른 문제점은 두 변수가 파괴될 때 메모리를 이중으로 해제할 위험이 있다는 것이다. Albert는 자신의 멤버 pName이 동적으로 할당되었으므로 파괴되기 전에 이 메모리를 해제하려고 할 것이다. 이렇게 되면 Kim의 pName도 같이 해제되어 버려 Kim은 정보를 잃어 버리게 되고 또한 Kim이 pName을 해제할 때는 이미 해제된 메모리를 이중으로 해제하게 되므로 이상 동작을 하게 된다.

이처럼 대입 연산자로 단순 대입하여 구조체의 사본을 만드는 것을 얕은 복사(Shallow Copy)라고 한다. 구조체의 멤버들이 정수나 실수 따위의 단순 타입만 있을 경우는 얕은 복사만으로도 완전한 사본을 만들 수 있지만 포인터가 포함되어 있을 경우는 대입에 의해 똑같은 번지를 가리키는 문제점이 있다. 포인터에 대해서는 별도의 메모리를 할당한 후 내용을 복사해야 두 변수가 완전한 독립성을 가지게 된다.

 이런 식으로 포인터 멤버에 대해서는 번지를 바로 대입하지 않고 필요한 길이만큼 따로 할당한 후 원본의 내용만 취하는 복사를 깊은 복사(Deep Copy)라고 한다. 원본의 멤버뿐만 아니라 멤버가 가리키는 곳의 내용까지도 같이 복사하는 좀 더 복잡한 복사 방법이다. 위 예제에서 Albert를 깊은 복사하여 사본을 작성하려면 다음 두 줄을 삽입해야 한다.

 

     Kim=Albert;

     Kim.pName=(char *)malloc(strlen(Albert.pName)+1);

     strcpy(Kim.pName,Albert.pName);

     printf("이름=%s, 나이=%d, 키=%.1f\n",Kim.pName,Kim.Age,Kim.Height);

 

일단 모든 멤버를 복사하되 포인터 멤버에 대해서는 원본의 길이만큼 별도로 메모리를 할당하고 원본의 내용만 복사했다. 이렇게 되면 두 변수가 완전히 독립된 메모리를 가지므로 종속성이 사라지며 한쪽의 내용을 변경해도 반대쪽은 전혀 영향을 받지 않는다. 또한 각 변수가 개별적으로 메모리를 해제해도 아무런 문제가 없다.

포인터가 포함된 구조체를 다룰 때는 각별한 주의가 필요하다. 특히 C++에서 객체끼리 대입할 때 이런 문제가 흔히 나타난다.