C 언어 | 구조체 선언 | 구조체 struct

int 형과 float 형 등 다양한 데이터 유형을 결합하여 하나의 변수로하는 구조에 대해 설명한다. 구조체를 응용하여, 여러 값의 조합으로 이루어진 복잡한 정보를 효율적으로 관리할 수 있다.

여러 형태로 이루어진 구조

배열은 어느 형태의 변수를 여러개를 모은 집합이었다. 따라서 C 언어 단순형을 모은 집합적인 요소를 합성체라고 부르고 있다. 여기서는 설명하는 구조체도 배열과 같은 간단한 형식을 모은 합성체이다.

배열은 지정된 형을 여러개를 모은 합성체이지만, 구조체는 다른 타입의 변수를 하나로 묶은 형태와 같은 것이다. 이는 논리적으로 연결되는 여러 정보를 정리하기 위해서, 유효하고 매우 중요한 기능이다. 대규모 시스템 개발에서 구조체는 없어서는 안될 존재이다.

논리적으로 연결하는 정보에는 예를 들어, 그래픽에서 좌표와 크기는 중요한 요소이다. 좌표는 x 좌표와 y 좌표의 두 숫자 정보로 구성하고, 크기도 폭과 높이의 두 가지 정보로 구성된다. 이러한 따로따로 다룰 수도수도 있지만, 현대 프로그래밍에서의 설계 사상에는 어긋난다. 이처럼 사람이 봤을 때 연결이 있는 정보는 하나로 정리하는 것이 바람직하다. 이렇게하면 프로그래머가 보다 확실하게 스마트 정보를 함수와 시스템 사이에서 전달시킬 수 있다.

이 때, 구조체가 가지고 있는 각 변수를 멤버라고 한다. 배열과 같은 요소라고 불러도 개념적으로 틀린 것은 아니지만, 그다지 그런 표현이 사용하지는 않는다.

구조체 선언은 struct 키워드를 이용하여 다음과 같은 형식을 갖는다.

구조체 선언

struct 태그명 {
 형식 멤버명1;
  형식 멤버명2;
  ...
} 구조체변수목록;

태그명에는 이 구조체의 이름을 지정한다. 이것은 구조체 변수를 선언할 때에 구조체를 참조하는데 사용된다. 구조체의 내부에서는 형식 및 멤버명을 지정하여, 구조체의 멤버를 선언한다. 멤버 선언은 일반 변수 선언과 기본적으로 비슷하지만, 초기화되지 않는다. 구조체 변수 목록에는 그 구조체의 변수 이름을 지정하여 구조체 변수를 정의한다. 이 구조체 변수 목록에 변수명을 선언함으로써 실제 메모리가 할당된다. 좌표 정보를 저장하기위한 구조체 변수, 예를 들어 다음과 같이 될 것이다.

struct Point {
 int x;
  int y;
} pt ;

이 때, Point 구조체는 int 형의 멤버 x와 y를 보유하고 있다고 생각할 수 있다. 이 구조체의 선언은 동시에 구조체 변수 pt를 정의하고 있다. 구조체 변수 목록은 생략할 수 있으며, 이 경우는 구조의 형식만이 선언 된 메모리가 할당되지 않는다. 구조체 변수를 임의의 장소에서 선언하려면 태그 이름을 이용하여 다음과 같이 설명한다.

태그에 의한 구조체 선언

struct 태그명 변수명;

struct 키워드는 이 변수가 지정한 태그의 구조체 변수임을 명시하기 위해 필요하며, 생략할 수 없다. 참고로 구조체 변수는 선언된 구조체의 실체라는 생각에서 인스턴스라고 부르기도 한다. 구조체는 인스턴스를 생성하기 위해, 컴파일러에 통지하기 위한 정보에 불과하다는 것을 이해한다.

생성한 구조체 변수의 멤버에 액세스하려면 구조체 변수명과 멤버명 사이에 마침표(.)를 지정한다. 이를 멤버 액세스 연산자라고 한다. 멤버 액세스 연산자는 구조체 변수를 왼쪽 피연산자에 오른쪽 피연산자 지정한 변수의 멤버를 오른쪽 피연산자에 지정하는 2항 연산자이다.

멤버 액서스 연산자

구조체변수명.멤버명

예를 들어, 이전의 Point 구조체의 변수 pt의 x 멤버값 10을 대입하려 한다면, pt.x = 10과 같이 작성한다.

코드1

#include <stdio.h>

int main() {
  struct Point {
    int x , y;
  } pt ;

  pt.x = 100;
 pt.y = 50;

  printf("pt.x = %d : pt.y = %d\n" , pt.x , pt.y);
 return 0;
}

이 프로그램은 좌표를 관리하는 Point 구조체를 선언하고, Point 구조체 형의 pt 변수를 만들고 있다. 멤버 액세스 연산자(.)를 사용하여 각 멤버에 값을 대입하여 printf() 함수에서 각 멤버의 값을 표시하고 있다.

구조체는 배열과 마찬가지로 합성체임을 설명하였다. 합성체는 이니셜 라이저를 사용하여 초기화할 수 있기 때문에 코드1과 같이 정적인 값으로 초기값을 할당한다면, 이니셜 라이저를 사용하는 것이 효과적이다. 즉 변수 선언시에 다음과 같이 초기화할 수 있다.

struct Point {
  int x , y;
} pt = { 100 , 50 } ;

초기화 방법은 배열과 동일하게 처음부터 순서대로 할당된 멤버의 값을 나타낸다. 초기화 목록의 값이 멤버 수보다 적은 경우, 나머지는 0으로 초기화된다.

또한, Point 구조체는 그 특징으로 재사용성이 높고, 어떤 함수에서도 자유롭게 접근할 수 있어야 한다. 일시적으로 구조체의 경우는 코드1과 같은 작성법이 권장되지만, 프로그램의 많은 곳에서 사용할 수 있는 재사용성이 높은 구조체는 글로벌 영역에 작성하고 필요할 때에 인스턴스를 만들 수 있도록 설계해야 한다.

코드2

#include <stdio.h>

struct Point {
 int x;
  int y;
};

int main() {
  struct Point pt = { 100 , 50 };
 printf("pt.x = %d : pt.y = %d\n" , pt.x , pt.y);
 return 0;
}

코드2는 Point 구조체를 선언할 때에 변수를 생성하지 않는다. 구조체의 인스턴스는 main() 함수에서 struct 키워드와 태그명을 사용하여 작성하고 있는 것에 주목한다. 대부분의 경우, 구조체는 이렇게 이용된다. 또한 초기값이 정해져 있다면, 이니셜 라이저를 사용하여 초기화할 수 있다.

코드2에서 만든 Point 구조체처럼, 구조체 자체의 선언은 새로운 형태 정보이며, 그 인스턴스는 필요할 때에 개발자가 정의하여 사용한다. 구조체에는 여러 단순 형식을 합성하여 새로운 형태를 만들어 버리는 것이라고 생각해도 좋을 것이다. 인스턴스가 생성될 때까지 메모리는 확보되지 않는다는 것과 인스턴스는 여러개를 만드는 것이 가능하다는 것을 이해한다.

다른 구조체 변수는 인스턴스가 다르기 때문에 멤버의 값을 공유할 수 없다. 구조체는 생성된 인스턴스마다 메모리를 할당한다. 구조체가 사용자 정의의 새로운 형정보이라는 것은 이 때문이다. 한번 만든 구조체(태그명이 정해져 있다면)는 여러 번 인스턴스를 만들고, 그 구조를 이용할 수 있는 것이다.

코드3

#include <stdio.h>

struct Point {
  int x;
  int y;
};

int main() {
  struct Point pt1 = { 100 , 50 } , pt2 = { 200 , 100 };

  printf("pt1 : x = %d , y = %d\n" , pt1.x , pt1.y);
 printf("pt2 : x = %d , y = %d\n" , pt2.x , pt2.y);

 return 0;
}

코드3에는 2개의 구조체 변수 pt1과 pt2를 정의하고 있다. 이들은 각각 독립적인 메모리 공간을 가지고 있기 때문에 멤버가 공유되지 않는다. pt1의 x 멤버의 값을 변경해도 pt2에는 물리적으로 관계가 없기 때문에 pt2 멤버의 값이 변화하는 것은 아니다. 그림1은 코드3의 두개의 구조체 변수의 메모리 구조를 간단히 나타낸다.

그림1 - 구조체의 인스턴스

구조체의 인스턴스

이렇게 구조체의 인스턴스는 멤버에 값을 저장하기 위한 메모리 영역을 개별적으로 보유하고 있다. pt1.x과 pt2.x의 메모리 주소는 다르기 때문에 실제 관련이 없다.

익명의 구조체

태그명을 생략하고 이름없는 구조체를 만들 수 있다. 다만, 태그명이 없는 경우는 구조체의 선언부 이외의 인스턴스를 만들 수 없기 때문에, 이 기법이 채용되는 것은 어느 일부에서 사용되는 구조의 경우뿐이다. 개발자와 설계자가 프로그램의 간편성을 위해 일시적으로 필요하다고 판단할 때에 사용하는 구조체으로 사용한다.

#include <stdio.h>

int main() {
  struct {
    int x;
    int y;
  } pt = { 100 , 35 };

  printf("pt.x = % d: pt.y = %d\n" , pt.x , pt.y);
 return 0;
}

코드4의 main() 함수 내에서 선언된 구조체는 태그명을 생략하고 있기 때문에 무명이다. 이 구조체의 인스턴스는 이 프로그램에서 pt 변수 뿐이며, 새로운 인스턴스를 만들 수 없다.

구조체 대입

모두가 같은 형태의 구조체 변수라면 구조체 변수를 다른 구조체 변수에 할당할 수 있다. 이것은 순전히 인스턴스를 복제하는 것이며, 구조체 변수가 보유하고 있는 멤버의 값도 똑같이 복사된다.

구조체 변수1 = 구조체 변수2;

이렇게 하면 위의 “구조체 변수1"은 “구조체 변수2"와 동일한 정보를 보유하게 된다. 이와 같은 대입을 실시하는 것으로, 간단하게 구조체 변수의 복사를 할 수 있다.

코드6

#include <stdio.h>

struct Point {
 int x;
  int y;
};

int main() {
  struct Point pt1 = { 200 , 100 } , pt2 = { 0 };

 printf("pt2.x = %d : pt2.y = %d\n" , pt2.x , pt2.y);
 pt2 = pt1;
  printf("pt2.x = %d : pt2.y = %d\n" , pt2.x , pt2.y);
 return 0; 
}

코드6은 Point 형의 pt1 변수와 pt2 변수를 선언하고 각각을 초기화한다. 이 때 pt2 변수는 0으로 초기화하고 있기 때문에 최초의 printf() 문에서 x 멤버도 y 멤버도 0이라는 값을 표시한다. 그러나, 다음에 pt2 = pt1라고하는 대입식으로 pt1 변수를 pt2 변수에 할당하고 있다. 그 결과 pt1 변수의 멤버가 그대로 pt2 변수에 복사되었기 때문에, 두 번째 printf() 문장에서는 pt1 변수와 같은 값을 표시하는 것이다.

이 특성은 함수의 인수 또는 반환 값으로 구조체를 전달할 때에도 적용할 수 있다. 인수로 구조체 변수를 전달하면 대입과 마찬가지로 인스턴스의 내용을 그대로 복사된다.

코드7

#include <stdio.h>

struct Point {
 int x;
  int y;
};

struct Point SizeToPoint(struct Point offsetPoint , int width , int height) {
 struct Point pt;
  pt.x = offsetPoint.x + width;
 pt.y = offsetPoint.y + height;
  return pt;
}

int main() {
 struct Point location = { 100 , 100 };
  struct Point target = SizeToPoint(location , 200 , 40);

 printf(
   "Rectangle\n\t"
   "Left = %d : Top = %d\n\t"
    "Right = %d : Bottom = %d\n" ,
   location.x , location.y , target.x , target.y
 );
  return 0; 
}

코드7은 어떤 좌표를 원점으로 지정된 폭과 높이의 좌표를 구하는 SizeToPoint() 함수를 만들고, 이 함수와 구조체 변수를 전달하고 있다. SizeToPoint() 함수는 offsetPoint에 지정된 좌표를 원점으로 폭 width와 높이 height에서 새로운 좌표를 생성하고 이를 반환한다. 이 함수를 이용하면 폭과 높이를 바탕으로 사각형의 오른쪽 아래 모서리의 좌표를 얻을 수 있다. 실행 결과의 Left와 Top는 사각형 왼쪽 위 모서리의 좌표 Right와 Bottom은 우측 하단의 좌표를 나타낸다. 프로그램에서는 오른쪽 아래 모서리의 좌표를 얻기 위해 SizeToPoint() 함수를 이용하고 있다.

구조체의 대입은 모든 멤버를 통째로 복사하는 방법으로 메모리 사용량과 CPU에 부하가 발생하는 점에주의해야 한다. 값의 복제가 아니라, 단순히 함수에 구조체의 값을 전달하는 것이 목적이라면 함수를 호출 할 때마다 구조체를 통째로 복사하는 방법보다 변수에 대한 포인터를 전달하는 것이 분명히 효율적이다. 이 방법은 “구조체에 대한 포인터"에서 명확하게 설명한다.

프로그램을 작성하고 종종 함수가 반환하는 합성체에 있어서, 합성체의 전체 요소에는 관심이 없고 특정 요소만이 식에서 필요한 것을 확인하는 경우가 있다. 예를 들어, 코드7의 SizeToPoint() 함수가 반환하는 Point 구조체의 정보 중에 x 또는 y 멤버의 값이 특정 표현식에서 한번만 필요한 경우이다. 이러한 경우 코드8와 같은 표기법을 사용하면 스마트하게 된다.

코드8

#include <stdio.h>

struct Point {
 int x;
  int y;
};

struct Point SizeToPoint(struct Point offsetPoint , int width , int height) {
 struct Point pt;
  pt.x = offsetPoint.x + width;
 pt.y = offsetPoint.y + height;
  return pt;
}

int main() {
 struct Point location = { 100 , 100 };
  printf(
   "Left = %d : Right = %d\n" ,
   location.x , SizeToPoint(location , 200 , 40).x
 );
  return 0;
}

주목해야 하는 것은 printf() 함수의 인수로 지정하고 있는 SizeToPoint(location, 200, 40).x라는 식이다. 마치 SizeToPoint() 함수 자체가 구조체 변수인 것처럼 작성하고 있다. 그러나, 이 작성법은 유효하다.

함수을 식에서 지정한 경우, 그 식에 있어서 함수는 반환값 형식의 값으로 해석할 수 있다. 식을 계산할 때에 함수가 실행되고, 제어가 돌아오면 반환 값이 식에서 사용되는 것이다. 코드8에 printf() 함수는 세번째 인수가 판단될 때, SizeToPoint() 함수가 호출된다. SizeToPoint() 함수의 반환 값은 Point 형이며, 이 함수는 식에서 Point 형이라고 생각할 수 있다. 따라서 SizeToPoint(...).x는 SizeToPoint() 함수가 반환하는 Point 형 인스턴스의 x멤버를 참조한다는 것을 나타낸다.