관리 메뉴

Kim's Programming

C언어 - 구조체(Structure)(2/2) 본문

Programming/C

C언어 - 구조체(Structure)(2/2)

Programmer. 2015. 8. 21. 19:31
구조체 대입


구조체가 배열과의 차이점을 꼽자는 것은 대입이 가능하다는 것입니다. 다음 코드는 초기화된 구조체 Person1의 멤버들을 Person2에 그대로 대입합니다.

1
2
3
tag_Person Person1={"홍길동",29,176.43};
tag_Person Person2;
Person2=Person1;
cs

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

1
memcpy(&Person2,&Person1,sizeof(Person1))
cs

Person1번지에서 부터 sizeof(Person1)만큼의 바이트를 Person2번지로 복사하는 것입니다. 따라서 구조체가 아무리 크더라도 대입만 하면 모든 멤버값을 한꺼번에 복사할 수 있으며 복사속도도 비교적 빠릅니다. 대입을 했으니 좌우변이 같아지는게 당연한것은 아닙니다. 배열의 경우는 불가하기 때문입니다. 다음 코드는 실행이 되지 않습니다.

1
2
3
int array1[5]={1,2,3,4,5};
int array2[5];
array2=array1;
cs

배열의 이름은 시작 번지를 가리키는 포인터 상수이기 때문에 좌변값(Ivalue)이 아니며 대입식의 좌변에 놓일 수는 없습니다. 배열은 루프를 이용한 대입만 가능합니다. 하지만 구조체에 대해서는 특별하게 대입을 허용하는데 컴파일러가 구조체의 이름을 좌변값으로 인정하기 떄문입니다. 구조체가 클 경우 복사시간이 오래 걸리는 막대한 비용이 드는데도 불구하고 중급 언어인 C성능 저하를 감수해 가면서 까지 대입을 허용하기 때문에 특별하다고 합니다. 구조체는 대입 가능하기 때문에 함수의 인수나 리턴값으로 사용할 수 있습니다. 함수 호출시 형식 인수가 실 인수로 전달되는 과정은 일종의 대입 연산이기 때문에 구조체 그 자체를 인수로 사용할 수 있는 것입니다. 배열은 대입이 안 되기 때문에 배열 자체를 인수로 전달할 수는 없고 배열을 가리키는 포인터를 전달해야 하는 것과는 구분됩니다. 다음 소스는 구조체를 인수로 전달받아서 멤버들을 출력하는 소스입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
 
struct tag_Person
{
    char Name[10];
    int Age;
    double Height;
};
 
void Output(tag_Person f)
{
    printf("이름 = %s, 나이 = %d, 키=%.1f\n", f.Name, f.Age, f.Height);
}
 
void main()
{
    tag_Person Person = { "홍길동"26174.23 };
    Output(Person);
}
cs

Output 함수의 인수 목록에 tag_Person타입의 형식인수 f가 선언 되어 있고 main 함수에서 Output을 호출할 때 Friend 구조체 자체를 전달했습니다. 함수 호출과정에서 실인수 Friend는 형식인수 f로 복사되며 Output은 형식인수 f로 부터 Person의 모든 멤버값을 읽을 수 있습니다. 구조체를 함수의 인수로 전달할 수 있다는 것은 굉장히 편리한 기능입니다. 마치 정수나 실수를 사용하듯이 이 변수 자체를 그대로 전달할 수 있기 때문입니다. 하지만 실제로는 구조체가 커지면 인수전달에 그만큼 시간, 메모리가 많이 소모되기 때문에 직접 사용하지는 않고 포인터를 사용합니다. 포인터를 사용하는 경우는 다음과 같이 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
void Output(tag_Person *pf)
{
    printf("이름 = %s, 나이 = %d, 키=%.1f\n", pf->Name, pf->Age, pf->Height);
}
 
void main()
{
    tag_Person Person = { "홍길동"26174.23 };
    Output(&Person);
}
cs

Output 함수가 tag_Person *형의 pf를 전달받도록 했으며 함수 내부에서는 멤버 연산자 대신 포인터 멤버 연산자를 사용했습니다. main 함수에서 Output을 호출할 때는 구조체 자체를 전달하지 않고 대신 구조체의 번지 &Person으로 전달을 했습니다. 구조체 자체를 전달하느냐 아님 구조체를 가리키는 포인터를 전달하여 간접적으로 구조체를 참조하도록 하느냐의 차이가 있는데 결과는 동일합니다. 하지만 몇 가지 차이가 있습니다. 우선 포인터를 통해 참조 호출을 했으므로 함수 내부에서 구조체를 변경할 수 있습니다. 형식 인수가 실인수의 사본이 아니라 번지를 알고 있으므로 ->연산자로 실인수 자체를 읽고 쓸 수 있습니다. 그리고 성능상으로도 확연한 차이가 있는데 두말할 필요없이 포인터를 전달하는 방식이 더 빠릅니다. 구조체는 수십 바이트 이고 커지면 수백 바이트까지도 가는데 포인터는 4바이트뿐이기 떄문입니다. 다음은 구조체를 리턴하는 소스입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
 
struct tag_Person
{
    char Name[10];
    int Age;
    double Height;
};
tag_Person Getperson()
{
    tag_Person t;
    strcpy(t.Name, "홍길동");
    t.Age = 24;
    t.Height = 178.21;
 
    return t;
}
 
void main()
{
    tag_Person Person;
    Person = Getperson();
    printf("이름 = %s, 나이 = %d, 키= %.2f\n", Person.Name, Person.Age, Person.Height);
    
}
cs

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
tag_Person *Getperson()
{
    tag_Person t;
    strcpy(t.Name, "홍길동");
    t.Age = 24;
    t.Height = 178.21;
 
    return &t;
}
 
void main()
{
    tag_Person *Person;
    Person = Getperson();
    printf("이름 = %s, 나이 = %d, 키= %.2f\n", Person->Name, Person->Age, Person->Height);
    
}
cs

이 소스를 컴파일하게 되면 "지역 변수 또는 임시 변수의 주소를 반환하고 있습니다." 라는 경고가 뜨고 실행시키면 제대로 표시가 되지 않습니다. Getperson함수는 지역변수 t의 멤버에 값을 대입한 후 그 포인터를 리턴하며 main에서는 이 포인터를 Person으로 받았습니다. 여기까지만 보면 Person은 Getperson함수가 초기화 해놓은 t구조체의 번지를 가지고 있으며 이 번지에는 구조체의 정보가 들어있기도 합니다. 그러나 printf함수를 호출하는 순간 이 번지의 내용이 파괴되어 버리는데 printf 호출을 위해 스택에 저장된 값이 파괴되기 때문입니다. Person 포인터가 가리키고 있는 스택상의 번지는 리턴직후에만 유효하며 다른 함수를 호출하는 즉시 파괴되는 성질을 가지고 있습니다. 그래서 지역변수로 선언된 구조체(다른 변수도 마찬가지)의 번지를 리턴하는 것은 옳지 않습니다. 값은 임시 사본이 리턴되므로 상관은 없지만 포인터는 간접적으로 대상체를 참조하므로 대상체가 사라지면 무효해집니다. 마약 Getperson함수에서 지역변수가 t가 아닌 malloc이나 new로 동적 할당한 구조체의 번지를 리턴한다면 이 경우는 가능합니다. 동적으로 할당된 메모리는 일부러 파괴하지 않는 한 그 내용을 계속 가지고 있기 때문입니다.


깊은 복사


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<malloc.h>
struct tag_Person
{
    char *pName;
    int Age;
    double Height;
};
void main()
{
    tag_Person Albert = { NULL, 80154.};
    tag_Person Kim;
 
    Albert.pName = (char *)malloc(32);
    strcpy(Albert.pName, "알버트 아인슈타인"); 
    
    Kim = Albert;
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", Kim.pName, Kim.Age, Kim.Height);
    free(Albert.pName);
    free(Kim.pName);
}
cs

tag_Person 구조체에 문자형 포인터 멤버 pName이 포함되어 있는데 포인터는 정적 배열에 비해 가변 길이 문자열을 다룰 수 있는 반면 정보를 저장하기 전에 동적할당을 해야 하는 번거로움이 있습니다. main에서 Albert라는 이름으로 구조체 변수를 선언하고 pName에 32자 길이를 할당한 후 이 메모리에 이름을 복사해 넣었습니다. 이름 문자열이 구조체에 포함되어 있지는 않지만 동적으로 할당된 메모리의 번지를 멤버로 가지고 있으므로 이 번지를 읽으면 이름 문자열을 얻을 수 있습니다. 어쨋든 Albert라는 구조체로부터 아인슈타인에 대한 모든 정보를 읽거나 쓸 수 있는 것입니다. 이 상태에서 Kim이라는 같은 타입의 구조체에 Albert를 대입했는데 이렇게 되면 Kim은 Albert와 동일한 정보가 출력된다는 것을 확인 할 수 있습니다만 메모리가 굉장히 불안한 상태라는 것을 알 수 있습니다. 두 구조체의 변수의 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를 깊은 복사하여 사본을 작성하려면 다음과 같이 작성해야합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<malloc.h>
struct tag_Person
{
    char *pName;
    int Age;
    double Height;
};
void main()
{
    tag_Person Albert = { NULL, 80154.};
    tag_Person Kim;
 
    Albert.pName = (char *)malloc(32);
    strcpy(Albert.pName, "알버트 아인슈타인"); 
    
    Kim = Albert;
    Kim.pName = (char *)malloc(strlen(Albert.pName) + 1);
    strcpy(Kim.pName, Albert.pName);
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", Kim.pName, Kim.Age, Kim.Height);
 
    strcpy(Albert.pName, "아이작 뉴튼");
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", Kim.pName, Kim.Age, Kim.Height);
    free(Albert.pName);
    free(Kim.pName);
}
cs

일단 모든 멤버를 복사하되 포인터 멤버에 대해서는 원본의 길이만큼 별도로 메모리를 할당하고 원본의 내용만 복사했습니다. 이렇게 되면 두 변수가 완전히 독립된 메모리를 가지므로 종속성이 사라지며 한쪽의 내용을 변경해도 반대쪽은 전혀 영향을 받지 않습니다. 또한 각 변수가 개별적으로 메모리를 해제해도 아무런 문제가 없습니다. 포인터가 포함된 구조체를 다룰때는 각별한 주의가 필요합니다. 특히 C++에서 객체끼리 대입할 때 이런 문제가 흔히 나타납니다.



'Programming > C' 카테고리의 다른 글

C언어 - 포인터 고급(2/4)  (0) 2015.09.05
C언어 - 포인터 고급(1/4)  (0) 2015.08.31
C언어 - 구조체(Structure)(1/2)  (1) 2015.08.20
C언어 - 포인터(Pointer)(3/3)  (0) 2015.08.19
C언어 - 포인터(Pointer)(2/3)  (1) 2015.08.18