13-2-다.구조체 배열

배열의 요소가 될 수 있는 타입에는 제한이 없다. 따라서 구조체도 배열의 요소가 될 수 있으며 구조체 배열을 정의하는 것이 가능하다. tag_Friend 타입의 구조체 변수 10개를 모아서 주소록을 만들고 싶다면 다음과 같이 선언한다. 크기 10의 정수형 배열을 선언하는 것과 형식상 다를 바가 전혀 없다.

 

tag_Friend arJuso[10];

 

구조체를 구성하는 멤버는 타입이 모두 다르지만 배열을 구성하는 요소는 모두 구조체라는 같은 타입이므로 배열이 될 수 있다. 위 선언문에서 arJuso의 정체는 타입이 다른 변수의 집합인 tag_Friend라는 같은 타입의 구조체 집합이라고 할 수 있으며 메모리에는 다음과 같이 생성될 것이다.

배열속에 구조체가 있고 구조체 속에는 멤버들이 있다. 이 상태에서 특정 구조체의 멤버를 읽고 싶다면 첨자 연산자와 멤버 연산자를 같이 사용한다. 예를 들어 세 번째 요소의 나이값에 30을 대입하고 싶다면 다음과 같이 한다.

 

arJuso[2].Age=30;

 

첨자 연산자 [ ]와 멤버 연산자 .은 둘 다 1순위이고 왼쪽 우선의 결합 순서를 가지므로 arJuso[2]가 먼저 연산되어 배열에서 2번째 구조체를 찾는다. 다음으로 멤버 연산자에 의해 2번째 구조체의 Age 멤버를 찾을 것이다. arJuso.Age[2]=30; 이 아님을 유의하자. 그림에서 진하게 표시되어 있는 Age멤버에 30이라는 정수값이 대입된다.

구조체가 배열의 요소인 경우를 살펴 봤는데 이번에는 반대의 경우를 보자. 구조체가 배열의 요소가 될 수 있는 것과 마찬가지로 배열도 구조체의 멤버가 될 수 있는데 tag_Friend 구조체의 Name멤버가 바로 문자형 배열로 선언되어 있다. 구조체에 속한 배열의 한 요소를 참조하고 싶을 때도 멤버 연산자와 첨자 연산자를 같이 사용하면 된다. 다음 코드는 Friend 구조체의 멤버인 Name 배열의 첫 번째 요소에 문자 상수 'K'를 대입한다.

 

tag_Friend Friend;

Friend.Name[0]='K';

 

이번에는 멤버 연산자가 먼저 실행되어 Friend 구조체의 Name 멤버를 먼저 찾으며 다음으로 첨자 연산자가 실행되어 Name 배열의 첫 번째 요소를 참조하게 된다. 다음은 조금 더 복잡한 형태를 보자. arJuso 배열에 구조체가 있고 이 구조체에 Name 배열이 있는데 배열에 속한 구조체에 속한 배열의 한 요소를 읽고 싶다면 다음과 같이 코드를 작성한다.

 

arJuso[1].Name[2]='J';

 

주소록의 두 번째 사람 이름의 세 번째 문자에 'J'를 대입하는 연산문이다. 구조체와 배열의 포함관계는 이처럼 항상 가능하며 순서나 깊이의 제한도 없기 때문에 때로는 무척 복잡해 보일 수도 있다. 그러나 순서대로 [ ] 연산자와 . 연산자만 사용하면 참조하고자 하는 대상을 찾는 것은 그리 어렵지 않을 것이다.

그렇다면 구조체와 배열의 이 복잡한 관계에 포인터를 추가해서 생각해 보기로 하자. 포인터가 가리킬 수 있는 타입에도 제한이 없기 때문에 포인터가 구조체나 배열을 가리킬 수 있고 구조체 배열을 가리킬 수도 있다. 다음은 구조체 포인터 배열이다.

 

tag_Friend *pJuso[10];

 

pJuso는 일단은 크기 10의 배열이되 tag_Friend형의 구조체를 가리킬 수 있는 포인터를 배열 요소로 가진다. 이 배열의 각 요소인 pJuso[0], pJuso[1], pJuso[2] 등은 tag_Friend형의 구조체를 가리킬 수 있으며 이런 동일한 타입의 포인터 변수 10개가 pJuso라는 이름의 배열로 선언되어 있는 것이다. 이 배열이 메모리에 생성된 모양을 그려 보면 다음과 같다.

이 상태에서 pJuso 배열에 저장된 포인터가 가리키는 구조체의 한 멤버를 참조하고 싶다면 다음과 같이 한다.

 

pJuso[3]->Age=40;

 

pJuso 배열의 네 번째 요소가 가리키는 곳에 저장된 tag_Friend 구조체의 Age 멤버에 40을 대입하는 문장이다. 그림에서 진하게 표시되어 있는 Age 멤버에 40이라는 정수값이 대입된다. ->연산자의 정의에 의해 이 문장은 (*pJuso[3]).Age=40;과 동일하며 [ ] 연산자의 정의에 의해 이 문장은 (**(pJuso+3)).Age와도 동일하다. 아무래도 포인터 연산자를 직접 쓴 문장보다는 [ ], -> 연산자를 사용한 문장이 더 읽기 쉽다.

[ ] 연산자가 제일 먼저 실행되어 pJuso 배열의 네 번째 요소값을 먼저 읽는데 이 값은 tag_Friend 구조체를 가리키는 포인터이다. 이 포인터가 가리키는 곳에 저장되어 있는 tag_Friend 구조체를 먼저 읽고 이 구조체의 멤버인 Age를 참조했다. 구조체 배열에서 멤버를 읽을 때는 멤버 연산자 .을 사용했는데 구조체 포인터 배열의 경우에는 포인터 멤버 연산자 ->를 사용한다는 것만 다르다.

무척 복잡해 보이겠지만 배열, 포인터, 구조체를 착실하게 공부해왔다면 이들 연산자의 사용법은 지극히 자연스럽게 이해될 것이다. 각 타입이 중첩된 순서대로 참조하고 싶은 변수에 도달할 때까지 적절한 연산자를 사용하면 된다. 사실 초보자에게는 다소 헷갈리는 면이 있기는 하지만 연산자의 동작과 사용 방법에 일관성이 있기 때문에 조금만 연습해 보면 금방 익숙해질 수 있다.

다음 예제는 지금까지 설명한 코드가 제대로 동작하는지 테스트해 보기 위해 작성했는데 문법의 적법성만 테스트하며 별다른 출력은 없다. 이 예제가 제대로 컴파일되는지 확인해 보고 다양한 방법으로 구조체와 배열과 포인터를 사용하는 실습을 해 보기 바란다.

 

: StructArrayPointer

#include <Turboc.h>

 

void main()

{

     struct tag_Friend {

          char Name[10];

          int Age;

          double Height;

     };

 

     // 구조체 배열 사용예

     tag_Friend arJuso[10];

     arJuso[5].Age=30;

 

     // 구조체에 속한 배열 사용예

     tag_Friend Friend;

     Friend.Name[0]='K';

 

     // 배열에 속한 구조체에 속한 배열 사용예

     arJuso[1].Name[2]='J';

 

     // 구조체 포인터 배열 사용예

     tag_Friend *pJuso[10];

     int i;

     for (i=0;i<10;i++) {

          pJuso[i]=(tag_Friend *)malloc(sizeof(tag_Friend));

     }

 

     pJuso[3]->Age=40;

 

     for (i=0;i<10;i++) {

          free(pJuso[i]);

     }

}

 

예제에서는 구조체 포인터 배열 pJuso를 초기화하기 위해 malloc 함수를 사용하여 동적으로 할당했다. pJuso 배열의 모든 요소가 tag_Friend 구조체를 가리키도록 초기화해야 하는데 동적 할당이 가장 쉬운 방법이기 때문이다. 동적할당된 구조체는 프로그램을 종료하기 전에 반드시 해제해야 한다.