13-2-라.중첩 구조체

중첩 구조체란 다른 구조체를 멤버로 포함하는 구조체이다. 구조체의 멤버가 될 수 있는 타입에는 제한이 없으므로 구조체도 다른 구조체의 멤버가 될 수 있다. 다음이 중첩 구조체의 예이다.

 

struct tag_A {

     int i;

     char ch;

};

 

struct tag_B {

     double d;

     tag_A A;

};

 

tag_B B;

 

tag_A 타입에는 정수형 멤버 i와 문자형 멤버 ch가 선언되어 있고 tag_B 타입에는 실수형 멤버 d와 tag_A형 멤버 A가 포함되어 있다. 이 상태에서 tag_B 타입의 B 구조체를 선언하면 메모리상에 다음과 같이 생성될 것이다.

B 구조체 안에 tag_A 타입의 구조체 A가 멤버로 포함되어 있으며 A안에는 또 i와 ch가 포함되어 있다. 이 상태에서 중첩된 구조체 내의 멤버를 참조할 때는 멤버 연산자를 두 번 연거푸 사용하는데 i값을 읽고 싶다면 B.A.i라고 읽으면 된다. B.(A.i)라고 쓰면 좀 더 쉬워 보일지 모르겠는데 이 연산문을 말로 바꾸면 "B에 속한 A에 속한 i"라는 뜻이다. A는 구조체 변수이지만 B의 입장에서 보면 자신의 멤버이므로 B와 A 사이에 멤버 연산자가 필요하고 또 i는 A의 멤버이므로 A와 i 사이에도 멤버 연산자가 필요하다.

구조체는 하나의 복잡한 실체에 대한 정보들을 저장하는데 이 정보들이 좀 더 큰 정보의 일부가 되는 경우는 아주 흔하다. 예를 들어 주문 정보는 상품 정보와 주문자에 대한 정보의 집합이며 도서 정보 속에는 저자, 출판사 등에 대한 정보가 포함될 수 있다. 이런 큰 정보를 다룰 때 구조체끼리 중첩시킬 수 있으며 때로는 이중 삼중으로 중첩되기도 한다.

구조체를 아무리 중첩시킨다고 해도 포함관계에 따라 멤버 연산자만 적절히 사용하면 중첩된 구조체의 멤버도 얼마든지 참조할 수 있다. C는 구조체끼리의 중첩에 대해 별다른 제한을 두지 않으므로 여러 겹으로 구조체를 중첩시키는 것이 가능하다. 그러나 다음과 같이 자기 자신을 포함하는 구조체는 선언할 수 없다.

 

struct tag_A {

     int i;

     tag_A A;

};

 

tag_A 타입의 구조체 안에 tag_A 타입의 구조체가 포함되어 있는데 이것이 왜 안되는가는 어렵지 않게 이해될 것이다. 만약 이런 선언이 허용된다면 이 구조체의 크기는 무한대가 되어 버릴 것이며 시스템의 메모리를 몽땅 동원해도 tag_A형의 변수를 만들 수 없다. 다음과 같은 상호 중첩도 물론 안된다.

 

struct tag_A {

     int i;

     tag_B B;

};

 

struct tag_B {

     double d;

     tag_A A;

};

 

자신이 직접 자기를 포함하지는 않았지만 자신을 포함하는 다른 구조체를 포함하고 있으므로 결국 이것도 자기 중첩과 같아진다. 그러나 다음과 같은 중첩은 가능하다.

 

struct tag_A {

     int i;

     tag_A *pA;

};

 

자기 자신을 멤버로 포함할 수는 없지만 자신과 같은 타입의 구조체에 대한 포인터를 멤버로 가지는 것은 가능하다. 왜냐하면 포인터는 자신이 가리키는 대상이 무엇이든간에 크기가 4바이트로 고정되어 있으며 따라서 이 구조체의 크기는 무한대가 아니기 때문이다. 자신과 같은 타입의 포인터를 멤버로 가지는 이런 구조체를 자기 참조 구조체라고 하는데 연결 리스트나 트리 구성에 아주 요긴하게 사용되는 자료 구조이다.

배열과 구조체의 중첩 관계와 포인터에 대해 좀 더 연습해 보도록 하자. 다음 예제는 tag_Friend 구조체의 배열을 포함하고 있는 Circle 구조체를 선언한다. 그리고 이 구조체의 배열 arCircle과 포인터 pCircle로부터 여러 가지 멤버를 참조하는 방법을 보여주는데 이 예제도 대입만 해 볼 뿐 출력은 없다. 주석에 상세하게 기록해 두었으므로 어떤 멤버를 어떤 식으로 참조하는지 연습해 보자.

 

: StructAndArray

#include <Turboc.h>

 

void main()

{

     // 회원 한 명의 신상

     struct tag_Friend {

          char Name[10];          // 이름

          int Age;                  // 나이

          double Height;             // 키

     };

 

     // 동아리에 대한 정보

     struct tag_Circle {

          char Name[16];          // 동아리 이름

          int MemNum;                   // 회원수

          tag_Friend Member[50];    // 회원 목록

     };

 

     // 동아리 목록

     tag_Circle arCircle[10];

 

     // 동아리 목록을 가리키는 포인터

     tag_Circle *pCircle;

     pCircle=arCircle;

 

     // 4번째 동아리의 3번째 회원 나이

     arCircle[4].Member[3].Age=21;

     // pCircle이 가리키는 동아리의 3번째 회원의 나이

     pCircle->Member[3].Age=22;

     // pCircel이 가리키는 동아리의 3번째 회원의 이름 중 2번째 문자

     pCircle->Member[3].Name[2]='M';

}

 

구조체와 배열, 그리고 포인터가 메모리에 생성된 모양을 그림으로 그려 보면 다음과 같다.

여기까지 구조체와 배열을 같이 사용할 때 멤버를 참조하는 방법에 대해 알아보았는데 쉽다면 쉬운 내용이고 어렵다면 또 한없이 어려운 내용이기도 하다. 실전에서 이렇게까지 구조체와 배열을 중첩해서 사용하지도 않는데 왜 이렇게 복잡한 것까지 알아야 하느냐라고 물을지도 모르겠다. 물론 이런 복잡한 중첩의 예가 흔하지는 않지만 그렇다고 아주 드문 것도 아니다. 프로젝트가 조금만 커져도 구조체 배열이 필요한 경우가 많으므로 오히려 흔하다고 보는 것이 맞다.

현실의 문제는 상상 이상으로 복잡하기 때문에 구조체 배열 정도가 아니라 구조체를 포함한 구조체 포인터의 이차 배열 따위도 실제로 필요한 경우가 있다. 이런 복잡한 자료 구조를 사용해야 할 때 헷갈리지 않고 능수 능란하게 원하는 멤버를 관리하려면 여기서 논한 내용에 대해 별다른 거부감이 없어야 한다. 실전에서 A[n][m]->B->C.mem->D같은 문장을 만날 때 참조되는 값이 과연 무엇인지 곧바로 파악할 수 있어야 C를 좀 한다는 말을 할 수 있을 것이다.