13-3-나.구조체 대입

구조체가 배열과 다른 가장 큰 차이점은 대입이 가능하다는 점이다. 다음 코드는 초기화된 구조체 Friend1의 멤버들을 Friend2에 그대로 대입한다.

 

tag_Friend Friend1={"장달상", 19, 180.0 };

tag_Friend Friend2;

Friend2=Friend1;

 

i=j와 같이 정수형 변수끼리 대입하면 i가 j와 똑같은 값을 가지듯이 구조체를 대입하면 두 구조체의 모든 멤버는 같은 값을 가지게 될 것이다. 물론 대입 연산자의 좌, 우변은 동일한 타입의 구조체여야 한다. 구조체끼리의 대입 연산 동작은 구조체의 길이만큼 메모리 복사로 정의되어 있는데 Friend2=Friend1대입문은 다음 코드와 기능상 동일하다.

 

memcpy(&Friend2,&Friend1,sizeof(Friend1));

 

Friend1 번지에서부터 sizeof(Friend1) 바이트만큼 Friend2번지로 복사하는 것이다. 따라서 구조체가 아무리 크더라도 대입만 하면 모든 멤버값을 한꺼번에 복사할 수 있으며 복사 속도도 비교적 빠른 편이다. 대입을 했으니 좌우변이 똑같아지는 것은 당연한 게 아닌가라고 생각하겠지만 결코 당연하다고만 생각할 것이 아니다. 배열의 경우는 타입과 크기가 같더라도 대입 연산자로 사본을 만들 수 없다. 즉, 다음 코드는 컴파일되지 않는다.

 

int ar1[5]={1,2,3,4,5};

int ar2[5];

ar2=ar1;                                // 대입 자체가 안됨

 

배열의 이름은 시작 번지를 가리키는 포인터 상수이기 때문에 좌변값이 아니며 대입식의 좌변에 놓일 수 없다. 배열끼리 꼭 대입을 하려면 루프를 돌면서 배열 요소를 일일이 대입하는 수밖에 없다. 하지만 구조체에 대해서는 특별하게 대입을 허용하는데 컴파일러가 구조체의 이름을 좌변값으로 인정하기 때문이다. 구조체가 클 경우 복사 시간이 오래 걸리는 막대한 비용이 드는데도 불구하고 중급 언어인 C가 성능 저하를 감수해 가면서 대입을 허용하기 때문에 특별하다고 하는 것이다.

구조체는 대입 가능하기 때문에 함수의 인수나 리턴값으로 사용할 수 있다. 함수 호출시 형식 인수가 실인수로 전달되는 과정은 일종의 대입 연산이기 때문에 구조체 그 자체를 인수로 사용할 수 있는 것이다. 배열은 대입이 안되기 때문에 배열 자체를 인수로 전달할 수는 없고 배열을 가리키는 포인터를 전달해야 하는 것과는 구분된다. 다음 예제의 OutFriend 함수는 구조체를 인수로 전달받아 이 구조체의 모든 멤버를 출력한다.

 

: StructArg

#include <Turboc.h>

 

struct tag_Friend {

     char Name[10];

     int Age;

     double Height;

};

 

void OutFriend(tag_Friend f)

{

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

}

 

void main()

{

     tag_Friend Friend={"김상형", 30, 180.0 };

     OutFriend(Friend);

}

 

OutFriend 함수의 인수 목록에 tag_Friend 타입의 형식인수 f가 선언되어 있고 main 함수에서 OutFriend를 호출할 때 Friend 구조체 자체를 전달했다. 함수 호출 과정에서 실인수 Friend는 형식인수 f로 복사되며 OutFriend는 형식인수 f로부터 Friend의 모든 멤버값을 읽을 수 있다.

구조체를 함수의 인수로 전달할 수 있다는 것은 굉장히 편리한 기능이다. 마치 정수나 실수를 사용하듯이 변수 자체를 그대로 전달할 수 있기 때문이다. 그러나 실제로 구조체를 함수의 인수로 직접 사용하는 경우는 별로 없다. 구조체가 커지면 인수 전달에 그만큼 많은 시간을 필요로 하고 메모리도 많이 소모하기 때문에 구조체보다는 포인터를 사용하는 방법이 더 효율적이다. 이럴 때는 당연히 포인터를 사용해야 한다. 다음과 같이 수정해 보자.

 

void OutFriend(tag_Friend *pf)

{

     printf("이름=%s, 나이=%d, 키=%.1f\n",pf->Name,pf->Age,pf->Height);

}

 

void main()

{

     tag_Friend Friend={"김상형", 30, 180.0 };

     OutFriend(&Friend);

}

 

OutFriend 함수가 tag_Friend *형의 pf를 전달받도록 했으며 함수 내부에서는 멤버 연산자 대신 포인터 멤버 연산자를 사용했다. main 함수에서 OutFriend를 호출할 때는 구조체 자체를 전달하지 않고 대신 구조체의 번지 &Friend를 전달했다. 구조체 자체를 전달하느냐 아니면 구조체를 가리키는 포인터를 전달하여 간접적으로 구조체를 참조하도록 하느냐의 차이가 있는데 실행 결과는 일단 동일하다.

하지만 몇 가지 차이점이 존재하는데 우선 포인터를 통해 참조 호출을 했으므로 함수 내부에서 구조체를 변경할 수 있다. 형식 인수가 실인수의 사본이 아니라 번지를 알고 있으므로 -> 연산자로 실인수 자체를 읽고 쓸 수 있는 것이다. 그리고 성능상으로도 확연한 차이가 있는데 두말할 필요없이 포인터를 전달하는 방식이 훨씬 더 빠르다. 구조체는 보통 수십 바이트이고 커지면 수백 바이트 이상이 될 수 있지만 포인터는 기껏해야 4바이트밖에 안된다.

구조체를 통째로 복사하여 전달하는 데 걸리는 시간과 4바이트의 포인터를 전달하는 데 걸리는 시간은 비교해 보나 마나다. 그래서 구조체를 함수끼리 전달해야 할 필요가 있을 때는 보통 포인터를 사용한다. 단, 구조체가 아주 작다면 가령 10바이트 정도밖에 안된다면 이런 경우는 성능상의 불이익이 별로 없으므로 구조체를 그냥 넘기는 것이 더 편리하다.

구조체가 인수로 사용될 수 있는 것처럼 리턴값으로도 사용될 수 있다. 다음 예제는 구조체를 리턴하는 함수 GetFriend의 예이다.

 

: StructRet

#include <Turboc.h>

 

struct tag_Friend {

     char Name[10];

     int Age;

     double Height;

};

 

tag_Friend GetFriend()

{

     tag_Friend t;

 

     strcpy(t.Name,"아무개");

     t.Age=22;

     t.Height=177.7;

     return t;

}

 

void main()

{

     tag_Friend Friend;

     Friend=GetFriend();

     printf("이름=%s, 나이=%d, 키=%.1f\n",

          Friend.Name,Friend.Age,Friend.Height);

}

 

함수 내부에서 tag_Friend형의 구조체 지역변수 t를 선언한 후 이 구조체에 적당히 값을 채우고 지역변수 자체를 리턴했다. 지역변수는 함수가 종료될 때 사라지므로 이 변수를 리턴하는 것이 조금 이상하게 보이겠지만 이 경우는 안전하다. 왜냐하면 리턴되는 값은 지역변수 자체가 아니라 지역변수의 복사본이며 리턴되는 즉시 이 값을 다른 구조체가 대입받기 때문이다. 만약 대입을 받지 않으면 리턴된 구조체는 버려진다.

하지만 구조체 지역변수의 포인터를 리턴하는 것은 안된다. 다음과 같이 지역변수의 포인터를 리턴하도록 예제를 수정해 보자.

 

tag_Friend *GetFriend()

{

     tag_Friend t;

 

     strcpy(t.Name,"아무개");

     t.Age=22;

     t.Height=177.7;

     return &t;

}

 

void main()

{

     tag_Friend *pFriend;

     pFriend=GetFriend();

     printf("이름=%s, 나이=%d, 키=%.1f\n",

          pFriend->Name,pFriend->Age,pFriend->Height);

}

 

이 예제를 컴파일하면 곧 사라질 지역변수의 번지를 리턴했다는 경고가 발생할 뿐만 아니라 제대로 동작하지도 않는다. GetFriend 함수는 지역변수 t의 멤버에 값을 대입한 후 그 포인터를 리턴하며 main에서는 이 포인터를 pFriend로 대입받았다. 여기까지만 보면 pFriend는 GetFriend 함수가 초기화해 놓은 t구조체의 번지를 가지고 있으며 이 번지에는 과연 구조체의 정보가 들어 있기도 하다.

그러나 이 값을 출력하려고 printf를 호출하는 순간 이 번지의 내용이 파괴되어 버리는데 printf 호출을 위해 스택에 저장된 값이 파괴되기 때문이다. pFriend 포인터가 가리키고 있는 스택상의 번지는 리턴 직후에만 유효하며 다른 함수를 호출하는 즉시 파괴되는 성질을 가지고 있다. 그래서 지역변수로 선언된 구조체(다른 변수도 마찬가지이다)의 번지를 리턴하는 것은 옳지 않다.

값은 임시 사본이 리턴되므로 상관없지만 포인터는 간접적으로 대상체를 참조하므로 대상체가 사라지면 무효해진다. 만약 GetFriend 함수에서 지역변수 t가 아닌 malloc이나 new로 동적 할당한 구조체의 번지를 리턴한다면 이 경우는 가능하다. 동적으로 할당된 메모리는 일부러 파괴하지 않는 한 그 내용을 계속 보존하기 때문이다.