관리 메뉴

Kim's Programming

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

Programming/C

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

Programmer. 2015. 8. 20. 23:19

구조체


구조체는 C의 데이터 타입 중에서 크기가 게일 큽니다. 정수나 실수 또는 문장열 등의 단순한 형태로 나타낼 수 없는 복잡한 데이터를 표현할 때 구조체를 사용합니다. 표준 함수 중 구조체를 사용하는 함수가 있으며 윈도우에서도 구조체가 흔히 사용됩니다. 구조체(Structure)를 문장화하여 정의를 내리면 "타입이 다른 변수들의 집합" 이라고 할 수 있으며 더 간단하게 표현하게되면 이종 변수 집합이라고 할 수 있습니다. 배열이 타입이 같은 변수들의 집합인 것에 비해 구조체는 다른 타입을 가지는 변수들을 하나의 이름으로 묶어둔 것입니다. 한 사람의 신상정보를 표현하고자 한다면 다음과 같은 변수들이 필요할 것입니다.

1
2
3
char Name[10];
int Age;
double Height;
cs

Name은 사람 이름을 위한 문자열, Age는 나이를 위한 정수형 변수, Height는 키를 입력하기 위한 실수형 변수로 각각의 정보들을 기억합니다. 이런 변수들은 타입이 서로 다르기 때문에 배열로 이 변수들을 묶을 수는 없습니다. 이런 관련성 있는 정보들을 하나로 묶어서 선언하고자 할 때 사용하는 타입이 바로 구조체입니다.


관련정보를 하나의 구조체로 묶어서 선언하면 양이 많은 정보를 전달하거나 리턴받을 수도 있습니다. 구조체를 선언할 때는 예약어 struct를 사용합니다. 신상정보를 기억하는 세 개의 변수 Friend라는 이름의 구조체로 묶어서 표현하고 싶다면 다음과 같이 선언합니다.

1
2
3
4
5
6
struct
{
    char Name[10];
    int Age;
    double Height;
}Friend;
cs

struct {} 블록안에 구조체에 포함되는 변수들의 목록을 순서대로 선언하고난 다음 구조체 변수의 이름을 끝에 적으면 됩니다. "구조체 변수를 선언하는 기본 형식은 "stuct { 멤버목록 } 변수명;"입니다. 구조체 선언문 뒤쪽에도 세미콜론을 꼭 써넣어야합니다. Name, Age, Height는 멤버(Member)라고 하고 struct블록 안에 일반 변수를 선언할 때와 같은 방법으로 선언하면 됩니다. 구조체의 멤버가 될 수 있는 타입에는 제한이 없습니다. 안에 멤버를 생성하는것은 종류, 개수도 상관이 없으며 구조체가 다른 구조체에 멤버로 들어 갈 수까지도 있습니다. 하지만 구조체 선언이란 것은 구조체의 모양을 컴파일러에게 알리는 것 뿐이기 때문에 static, register 같은 기억 부류를 지정한다든가 초기값을 줄 수 는 없습니다. 구조체는 타입이 다른 변수들의 집합이지만 모든 멤버 타입이 반드시 달라야 하는 것은 아닙니다. 다음과 같이 타입이 같은 변수들도 하나의 구조체로 묶을 수 있으며 멤버를 하나만 가지는 구조체를 선언하는 것도 가능합니다.

1
2
3
4
struct
{
    int x,y;
}Point;
cs

이 구조체는 좌표를 구현하는데 좌표는 x축 y축 2개의 정수값으로 구성되게 됩니다. 두 멤버의 타입이 같으므로 int Point[2];라고도 선언이 가능합니다. 그러나 배열을 사용하면 어느 쪽이 x 값이고 어느쪽이 y값인지 각 점자의 의미를 직접 지정해야하고 실수할 위험이 많지만 구조체로 선언하게 되면 멤버에 이름이 부여되기 떄문에 사용하기 쉽고 읽기도 쉽습니다.


구조체 태그


구조체 변수가 딱 하나만 필요하다면 앞에서 예를 든 방법대로 구조체 변수를 바로 선언할 수는 있지만 태그를 먼저 정의하고 이 태그로 구조체 변수를 선언하는 것이 더 일반적입니다. 구조체 태그는 열거형의 태그와 마찬가지로 타입에 대해 이름을 붙이는 것입니다. 태그를 사용하여 구조체를 정의하는 형식은 다음과 같습니다.


struct 태그명 { 멤버 목록 };


키워드 struct다음에 태그의 이름을 주고 멤버 목록을 나열합니다. 태그도 일종의 명칭이므로 명칭 규칙에만 맞다면 자유롭게 붙일 수 있는데 관습적으로는 구조체 태그는 tag_라는 접두어를 붙이는 경우가 많습니다. 다음은 사람의 신상 명세를 구조체 타입을 tag_person 으로 선언한 경우입니다.

1
2
3
4
5
6
7
struct tag_Person
{
    char Name[10];
    int Age;
    double Height;
};
 
cs

태그 선언문도 일종의 문장이기 떄문에 마지막에 세미콜론은 존재합니다. 태그선언 문은 컴파일러에게 구조체의 모양이 어떻냐를 등록할 뿐이지 태그를 위해 메모리를 할당한다거나 변수를 생성하는 것은 아니며 중복 선언해도 상관은 없습니다. 태그 선언에 의해 컴파일러는 tag_Friend라는 태그가 Name, Age, Height를 멤버로 가지는 구조체라는 것을 기억할 것입니다. 태그를 한 번 등록해놓으면 이 태그로 구조체 변수를 여러번 선언 할 수 있습니다.

1
2
struct tag_Person person;
tag_Person person;
cs

cs

구형 C 컴파일러는 태그를 사용할 때 구조체 태그라는 것을 명확하게 알리기 위해 struct라는 키워드를 태그 앞에 붙여야 하나 c++에서는 태그가 하나의 타입으로 인정되기 때문에 struct없이 태그명만으로 구조체를 선언할 수 있습니다. 하지만 최근의 c++컴파일러들은 struct를 붙이지 않고 부를 수 있지만 소스파일의 확장자는 반드시 .cpp로 하여 C++문법으로 컴파일 해야합니다 새로운 타입을 정의하는 typedef문을 이용하면 태그를 정의하는 것과 동일한 효과를 낼 수 있습니다. 다음 선언문은 PersonType이라는 새로운 타입을 정의합니다.

1
2
3
4
5
6
typedef struct
{
    char Name[10];
    int Age;
    double Height;
}PersonType;
cs

이 선언에 의해 컴파일러는 Name, Age, Height를 멤버로 가지는 구조체 타입을 PersonType이라는 이름으로 새롭게 정의 합니다. 이 정의에 의해 PersonType은 int, double, char 등과 다른 완전히 동일한 자격을 가지는 사용자 정의 타입으로 인정됩니다. typedef로 정의한 타입은 태그와는 문법적으로 다른 존재이지만 c++에서는 동일하게 취급되므로 태그로 변수를 선언하듯이 사용자 정의 타입으로도 변수를 정의 할 수 있습니다. 구조체 변수를 바로 선어할 수도 있고 태그나 사용자 타입을 먼저 정의한 후 간접적으로 구조체 변수를 선언 할 수도 있습니다. 그래서 다음 세 선언문에 의해 선언되는 Person 변수는 전부 동일합니다.

1
2
3
4
5
6
struct
{
    char Name[10];
    int Age;
    double Height;
}Person;
cs
1
2
3
4
5
6
7
struct tag_Person
{
    char Name[10];
    int Age;
    double Height;
};
tag_Person person;
cs
1
2
3
4
5
6
7
typedef struct
{
    char Name[10];
    int Age;
    double Height;
}PersonType;
PersonType person;    
cs

언뜻 보이기에는 변수를 바로 선언하는것이 편해보이지만 타입을 먼저 정의하면 여러가지 편리한 점이 있습니다.


    1. 타입이 정의되면 이 타입으로 같은 형의 변수를 여러번 선언할 수 있습니다. tag_Person이라는 태그에 이 구조체의 모양이 이미 저장되어 있으므로 이런 구조체 변수가 필요하면 언제든지 태그로부터 변수를 선언하기만 하면 됩니다.

      1
      2
      tag_Person ABC;
      tag_Person person;
      cs

      만약 태그를 정의할 수 없고 변수를 바로 선언하는 것만 가능하다면 구조체 변수가 필요할 때마다 멤버 목록을 일일이 나열해야 하므로 무척 불편할것입니다.

    2. 이 타입으로부터 파생되는 유도형 변수를 선언할 수 있습니다. 예를 들어 tag_Person형의 구조체를 가리키는 포인터 변수를 선언하고 싶다거나 이런 구조체 여러 개를 모아 배열을 구성하고 싶다면 다음과 같이 선언합니다.

      1
      2
      tag_Person *pPerson;
      tag_Person array_Person[100];
      cs

      포인터나 배열은 타입으로부터 유도되는 것이지 변수로부터 유도되는 것이 아니므로 이런 변수를 선언하려면 반드시 타입이 먼저 정의되어 있어야 합니다. 또한 구조체가 다른 구조체를 포함한다거나 할 때도 타입이 필요합니다.

    3. 구조체를 함수의 인수나 리턴값으로도 사용이 가능합니다. 예를 들어서 사람들의 신상을 출력하는 OutFriend라는 함수를 만들어야한다고 하면 이름 , 나이, 키 등의 정보를 따로따로 넘기고 싶지 않으면 구조체 포인터로 넘겨야합니다. 그렇게 된다면 다음과 같이 할 수 있습니다.

      1
      2
      void Output(tag_Person *pPerson){}
      void Output(tag_Person array_Person){}
      cs

      함수의 인수 목록에 tag_person형의 변수 또는 포인터를 넘기도록 선언 했는데 태그가 없다면 "이렇게 생긴 모양의 구조체를 전달"이라는 선언 자체가 불가능 합니다. 컴파일러가 구조체의 모양을 먼저 알아야 함수의 인수나 리턴값으로 구조체를 사용할 수 있습니다.

이런 이유들로 구조체가 필요할 때는 태그나 사용자 정의 타입을 먼저 정의하고 태그로부터 변수를 선언하는 것이 좋습니다. 추가로 다음과 같은 형태의 소스도 가능합니다.

1
2
3
4
5
6
struct tag_person
{
    char Name[10];
    int Age;
    double Height;
} Person;
cs

태그도 정의하면서 변수도 같이 선언하는 형식인데 두 개의 문장을 하나로 합친 것 외에는 차이가 없습니다.


멤버 연산자


같은 타입의 변수 집합인 배열을 참조할 때는 []연산자를 사용합니다. []연산자 안에 참조하고자 하는 배열의 요소의 번호인 첨자만 적으면 배열 요소를 읽거나 쓸 수 있습니다.

1
2
int ar[5];
ar[4]=5;
cs

배열 요소를 참조하는 방식이 단순한 이유는 배열을 구성하는 모든 요소의 크기가 일정, 인접해있기 떄문입니다.그래서 단순히 "몇 번쨰 것" 이라는 순서만 지정하면 첨자 연산(포인터와 정수의 덧셈, 그리고 *연산자의 합작 연산)에 의해 원하는 배열 요소를 신속하고 정확하게 찾을 수 있습니다. 그러나 구조체의 멤버는 타입이 제각각 다르며 크기도 다릅니다. 그렇기 때문에 구조체에 속한 멤버를 읽을 때는 순서값을 수용할 수 없고 별도의 연산자와 멤버의 이름을 사용하여야 합니다.

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

main함수 이전에 tag_Person이라는 이름으로 구조체 태그를 먼저 선언했는데 열거형이나 구조체 같은 사용자 정의형 타입은 가급적이면 main함수 이전에 선언해야 모든 함수에서 이 타임을 사용할 수 있습니다. main함수 안에 구조체 태그를 선언할 수도 있지만 이 구조체는 main함수 안에서만 사용할 수 있는 지역 타입이 되어 버립니다. 태그 정의는 실제로 변수를 생성하는 것도 아니므로 가급적이면 앞쪽에 선언하여 컴파일러가 먼저 알 수 있도록 하는 것이 좋습니다. main함수에서는 태크로 부터 Person이라는 이름의 구조체 변수를 선언 했습니다. Person은 Name, Age, Height등의 변수를 가지는데 이 멤버들은 읽고 쓸 때는 멤버 연산자를 사용합니다. age멤버에 30을 대입하여면 Person.Age=30이라는 대입문을 사용합니다.(나머지 멤버들에서도 같습니다.)

구조체 멤버들은 크기가 제각각이기 때문에 배열처럼 단순한 곱셈으로 멤버의 위치를 찾을 수 없으며 구조체 시작 번지로부터 멤버까지의 거리인 오프셋(offset)을 더해 멤버를 읽습니다. 오프셋은 구조체의 시작 번지에서 멤버까지의 거리인데 이 값은 자기보다 앞에 있는 멤버들의 크기의 총합과 같습니다. 첫 번째 멤버인 Name의 오프셋은 0이며  Age멤버는 Name의 크기만큼을 더한 오프셋 10을 가지게 되며 그 다음 멤버인 Height는 Age와 Name을 더한 14가 됩니다. 이 상태에서 멤버 연산자로 Height를 읽으면 이 연산자는 구조체의 시작 번지에서 오프셋을 더해 멤버의 시작번지를 찾고 이 번지에서 멤버의 타입만큼 값을 읽습니다. Height의 오프셋은 14이므로 (만약 구조체 시작 번지가 100이라면) 100+14=114번지에 시작되게 되며 이 멤버가 double형으로 정의되어 있으므로 이 번지에서 부터 8바이트를 읽어주면 됩니다.


컴파일러는 구조체가 선언될 때 각 멤버의 오프셋과 타입을 기억해 둡니다. 그리고 멤버를 참조하는 문장을 만나면 구조체의 시작 번지에서 오프셋을 더한만큼 이동한 후 이 위치에서 멤버의 타입 길이만큼 값을 읽도록 코드를 생성할 것입니다. 이런 동작을 하는 연산자가 바로 .연산자 입니다. 배열의 요소와 마찬가지로 멤버는 구조체에 소속되어 있을 뿐이지 일반 변수와 완전히 같은 자격을 가지게 됩니다. Person의 멤버 Age는 정수형으로 선언되어 있으므로 정수값을 기억하며 정수형 변수가 사용될 수 있는 곳 어디든지 대신해서 사용할 수 있습니다. 다만 구조체에 속해 있기 떄문에 멤버 연산자로 어느 구조체에 속해 있는지만 밝혀주어야 합니다. 구조체 멤버의 통용 범위는 구조체 내부로 국한되며 구조체 없이 멤버 홀로 사용될 수는 없습니다. 그래서 멤버를 참조할 때는 반드시 앞에 소속을 밝혀야 하는데 이는 소속이 다르면 멤버의 이름이 같아도 상관없다는 이야기 입니다.


포인터 멤버 연산자


구조체에 대해서도 배열과 포인터를 선언할 수 있습니다. 다음 소스는 tag_Person형 구조체 포인터를 선언 및 초기화합니다.

1
2
3
tag_Person Person;
tag_Person *pPerson;
pPerson=&Person;
cs

Person 구조체를 먼저 생성하고 이 구조체를 가리킬 수 있는 pPerson 포인터 변수를 선언한 후 여기에 Person의 번지를 대입했습니다. 이 상태에서 pPerson 포인터가 가리키는 구조체의 멤버를 참조하고 싶을 때는 다음과 같이 합니다.

1
(*pPerson).Age=40;
cs

pPerson이 Person을 가리키고 있으므로 *연산자로 이 포인터가 가리키는 곳을 참조하면 곧 Person이 됩니다. (*pPerson)은 곧 Person과 같으며 따라서 위 연산문은 Person.Age=40과 같은 역할을 한다 볼 수 있습니다. 이 문장에서는 괄호를 절대 생략할 수 없는데 왜냐하면 포인터 연산자는 2순위이고 멤버연산자는 최우선 순위인 1위이기 때문입니다. *pPerson.Age라고 쓰면 컴파일러는 이를 *(pPerson.Age)로 해석하여 멤버연산자가 먼저 실행되어 에러로 처리됩니다. 멤버 연산자의 좌변은 반드시 구조체여야 하는데 pPerson는 구조체가 아니라 구조체를 가리키는 포인터이기 때문입니다. 그래서 *연산자가 먼져 실행 될 수 있도록 괄호는 반드씨 써줘야합니다. 하지만 괄호로 반드시 싸줘야 하며 보기에도 좋지도 안습니다. 그래서 이럴 때 대신 사용 가능한 ->라는 연산자가 있습니다. 모양이 화살표와 비슷하다고 하여 화살 연산자(Arrow Operator)라고 부르거나 Arrow라고 부릅니다. 포인터 멤버 ->연산자는 좌변에 구조체 포인터, 우변에 멤버 이름을 취하며 포인터가 가리키는 번지에 저장된 구조체의 멤버를 읽는 연산을 합니다. 그렇기 떄문에 (*pPerson).Age라고 되있는것이 화살표 연산자를 이용하면 pPerson->age가 됩니다. ->연산자라는 것을 문장으로 바꾸면 "~번지의 구조체 멤버~"라고 볼 수 있습니다. 간단하게 한문장으로 정의하자면 다음과 같이 됩니다.


p가 구조체를 가리키는 포인터이고 m이 멤버일 때

(*p).m은 p->m과 같다.


다음 소스를 보겠습니다.
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>
struct tag_Person
{
    char Name[10];
    int Age;
    double Height;
};
 
void main()
{
    tag_Person Person;
    tag_Person *pPerson;
    pPerson = &Person;
 
    strcpy(pPerson->Name, "홍동길");
    pPerson->Age = 40;
    pPerson->Height = 151.87;
 
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", pPerson->Name, pPerson->Age, pPerson->Height);
 
    strcpy(Person.Name, "홍길동");
    Person.Age = 40;
    Person.Height=151.98;
    
    printf("이름 = %s, 나이 = %d, 키 = %.2f\n", Person.Name, Person.Age, Person.Height);
}
cs
Person의 멤버를 직접 읽는 대신 pPerson 포인터로 간접적으로 멤버를 참조 했으며 멤버 연산자 . 대신 포인터 멤버 연산자 ->를 사용했습니다. 비교를 위해서 . 연산자를 사용하는것과 -> 연산자를 사용하는거 둘다 두었습니다.

구조체 배열


배열의 요소가 될 수 있는 타입에는 제한이 없습니다. 따라서 구조체도 배열의 요소가 될 수 있으며 구조체 배열을 정의하는 것이 가능합니다. tag_Person 타입의 구조체 변수 10개를 모아서 주소록을 만들고 싶다면 다음과 같이 선언하면 됩니다.

1
tag_Person ar_Address[10]
cs
구조체를 구성하는 멤버는 타입이 모두 다르지만 배열을 구성하는 요소는 모두 구조체라는 같은 타입이므로 배열이 될 수 있습니다. 위 선언문에서 ar_Address의 정체는 다른 변수의 집합인 tag_Person이라는 같은 타입의 구조체 집합이라고 할 수 있으며 메모리에는 다음과 같이 생성될 것입니다. tag_Person ar_Address[3]일 때의 배열 모습입니다.

ar_Address[0]

Name

Age

Height

ar_Address[1]

Name

Age

Height

ar_Address[2]

Name 

Age

Height

(진한 글씨 들어있는 칸은 이름을 표기하기위하여 만든 부분입니다.)


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

1
ar_Address[2].Age=40;
cs

첨자연산자 []와 멤버 연산자 . 는 둘 다 1숭위이고 왼쪽 우선의 결합 순서를 가지므로 ar_Address[2]가 먼저 연산되어 배열에서 2번째 구조체를 찾습니다. 다음으로 멤버 연산자에 의해 2번째 구조체의 Age멤버를 찾을 것입니다. 구조체가 배열의 요소인 경우말고 그 반대의 경우도 살펴보겠습니다. 구조체가 배열의 요소가 될 수 있는 것과 마찬가지로 배열도 구조체의 멤버가 될 수 있는데 tag_Person 구조체의 Name멤버가 문자형 배열로 선언되어 있습니다. 구조체에 속한 배열의 한 요소를 참조하고 싶을 때도 멤버 연산자와 첨자 연산자를 같이 사용하면 됩니다. 다음 소스는 Person 구조체의 멤버인 Name 배열의 첫번째 요소에 문자 상수 'K'를 대입하는 소스입니다.

1
2
tag_Person Person;
Person.Name[0]='K';
cs

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

1
ar_Address[1].Name[3]='L';
cs

주소록의 두번째 사람의 4번째 문자에 'L'을 대입하는 연산문 입니다. 구조체와 배열의 포함관계는 이처럼 항상 가능하며 순서나 깊이의 제한도 없기 때문에 때로는 무척 복잡해 보이기도 합니다. 그러나 순서대로 []연산자와 .연산자만 사용하면 참조하자는 대상을 찾기는 그리 어렵지는 않습니다. 그렇다면 구조체와 배열의 이 복잡한 관계에 포인터를 추가해서 생각해보겠습니다. 포인터가 가리킬 수 있는 타입에도 제한이 없기 떄문에 포인터가 구조체나 배열을 가리킬 수 있고 구조체 배열을 가리킬 수도 있습니다. 다음은 구조체 포인터 배열입니다.

1
tag_Person *ar_Address[10];
cs

ar_Address는 일단은 크기 10의 배열이되 tag_Address형 구조체를 가리킬 수 있는 포인터를 배열 요소로 가집니다. 이 배열의 각 요소인 ar_Address[0], ar_Address[1], ar_Address[2] 등은 tag_Person형의 구조체를 가리킬 수 있으며 이런 동일한 타입의 포인터 변수 10개가 ar_Address라는 이름의 배열로 선언되어 있는 것입니다. 이렇게 된 상태에서 ar_Address 배열에 저장된 포인터가 가리키는 구조체의 한 멤버를 참조하고 싶다면 다음과 같이 하면 됩니다.

1
ar_Address[3]->Age=50;
cs

ar_Address 배열의 네 번째 요소가 가리키는 곳에 저장된 tag_Person 구조체의 Age멤버에 50을 대입하는 문장입니다. ->연산자의 정의에 의해 이 문장은 (*ar_Address[3]).Age=50;과 동일하며 [ ]연산자의 정의에 의해 이 문장은 (**(ar_Address+3)).Age와도 동일합니다. 아무래도 포인터 연산자를 직접 쓴 문장보다는 [ ], ->연산자를 사용한 문장이 더 읽기 쉽습니다. 다른 소스를 이용하여 보겠습니다.

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
29
30
31
32
33
34
35
36
37
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<malloc.h>
 
void main()
{
    struct tag_Person
    {
        char Name[10];
        int Age;
        double Height;
    };
    //구조체 배열 사용 예
    tag_Person ar_Address[10];
    ar_Address[5].Age = 30;
 
    //구조체에 속한 배열 사용예
    tag_Person Person;
    Person.Name[0= 'L';
 
    //배열에 속한 구조체에 속한 배열 사용예
    ar_Address[1].Name[3= 'J';
 
    //구조체 포인터 배열 사용예
    tag_Person *pPerson[10];
    for (int i = 0; i < 10; i++)
    {
        pPerson[i] = (tag_Person *)malloc(sizeof(tag_Person));
    }
 
    pPerson[3]->Age = 50;
    for (int i = 0; i < 10; i++)
    {
        free(pPerson[i]);
    }
}
cs

여기서는 구조체 포인터 배열 ar_Address를 초기화하기 위해 malloc함수를 사용하여 동적으로 할당했습니다. ar_Address 배열의 모든 요소가 tag_Person 구조체를 가리키도록 초기화해야 하는데 동적 할당이 가장 쉬운 방법이기 때문입니다. 그래서 프로그램 종료 전에 해제를 하였습니다.


중첩 구조체


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

1
2
3
4
5
6
7
8
9
10
11
12
struct tag_A
{
    int i;
    char ch;
};
struct tag_B
{
    double d;
    tag_A A;
};
 
tag_B B;
cs

tag_A타입에는 정수형 멤버 i와 문자형 멤버 ch가 선언되어 있고 tag_B 타입에는 실수형 멤버 d와 tag_A가 포함되어 있습니다. 이 상태에서 tag_B 타입의 B 구조체를 선언하면 메모리상에 다음과 같이 생성될 것입니다. B구조체 안에 tag_A 타입의 구조체 A가 멤버로 포함되어 있으며 A안에는 또 i와 ch가 포함되어 있습니다. 이 상태에서 중첩된 구조체 내의 멤버를 참조할 때는 멤버 연산자를 두 번 연거푸 사용하는데 i값을 읽고 싶다면 B.A.i라고 읽으면 됩니다. 이 연산문을 말로 바꾸면 "B에 속한 A에 속한 i"라는 뜻입니다. A는 구조체 변수 이지만 B의 입장에서 보면 자신의 멤버이므로 B와 A사이에 멤버 연산자가 필요하고 또 i는 A의 멤버이므로 A와 i사이에서도 멤버 연산자가 필요합니다.


구조체는 하나의 복잡한 실체에 대한 정보들을 저장하는데 이 정보들이 좀 더 큰 정보의 일부가 되는 경우는 아주 흔합니다. 예를 들어 주문 정보와 주문자에 대한 정보의 집합이며 도서 정보속에는 저자, 출판사 등에 대한 정보가 포함될 수 있습니다. 이런 큰 정보를 다룰 때 구조체끼리 중첩시킬 수 있으며 때로는 이중 삼중으로 중첩되기도 합니다. 구조체를 아무리 중첩시킨다고 해도 포함관계에 따라 멤버 연산자만 적절히 사용하면 중첩된 구조체의 멤버도 얼마든지 참조할 수 있습니다. C는 구조체끼리의 중첩에 대해 별다른 제한을 두지 않으므로 여러 겹으로 구조체를 중첩시키는 것이 가능합니다. 그러나 다음과 같이 자기 자신을 포함하는 구조체는 선언할 수 없습니다.

1
2
3
4
5
struct tag_A
{
    int i;
    tag_A A
};
cs

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

1
2
3
4
5
6
7
8
9
10
struct tag_A
{
    int i;
    tag_B B;
};
struct tag_B
{
    double d;
    tag_A A;
};
cs

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

1
2
3
4
5
struct tag_A
{
    int i;
    tag_A *pA;
};
cs

자기 자신을 멤버로 포함할 수는 없지만 자신과 같은 타입의 구조체에 대한 포인터를 멤버로 가지는 것은 가능합니다. 왜냐하면 포인터는 자신이 가리키는 대상이 무엇이든간에 크기가 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
28
29
30
31
32
33
34
#include<stdio.h>
 
void main()
{
    //회원 한명의 신상
    struct tag_Info
    {
        char Name[10];
        int Age;
        double Height;
    };
 
    //동아리에 대한 정보
    struct tag_Circle
    {
        char Name[16];
        int MemNum;
        tag_Info 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;
    //pCircle이 가리키는 동아리의 3번째 회원의 이름중 2번째 문자
    pCircle->Member[3].Name[2= 'M';
}
cs


중첩 구조체


구조체 멤버는 일반 변수와 완전히 동일합니다. 그래서 멤버에 값을 대입할 때도 일반 변수와 마찬가지로 대입 연산자를 사용하는데 Friend.Age=30이라고 대입하면 됩니다. 이런식으로 구조체의 각 멤버에 원하는 값을 대입할 수 있는데 멤버가 아주 많다면 일일이 대입하기 무척 귀찮을 것입니다. 그래서 구조체를 선언함과 동시에 멤버의 값을 초기화시킬 수 있는 방법이 제공됩니다. 구조체를 초기화 하는 방법은 배열과 거의 비슷합니다. 선언시에 =구분자와 { } 괄호를 쓰고 괄호 안에 멤버의 초기값을 나열하면 됩니다. 다음은 tag_Person형의 구조체 person을 선언하면서 이 구조체를 초기화하는 예입니다.

1
tag_Person Person={"홍길동" , 20 181.34};
cs

Name 멤버에 "홍길동" 문자열이 복사되고 Age에는 20이 대입되며 Heoght는 181.34가 될 것입니다. 초기값이 없는 멤버는 자동으로 0으로 초기화됩니다. 배열의 경우와 다른 점이라면 초기값이 대응되는 멤버의 타입과 같아야 한다는 점입니다. Age는 정수형이므로 정수, 상수를 대입해야 하며 Height는 실수형이므로 실수 상수를 대입해야합니다. 또한 Name은 문자 배열이므로 문자열 상수를 주어야 하는데 초기화 문자열이 Name 멤버의 길이보다 더 커서는 안 됩니다. 구조체 배열을 초기화하는 방법도 배열의 초기화 방법과 동일합니다. { } 중괄호 안에 각 배열 요소가 되는 구조체의 초기값을 콤마로 끊어서 나열하면 됩니다. 다음 소스는 크기 10의 Person 배열을 선언하고 이 중 앞쪽 다섯 개의 요소를 초기화 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
 
struct tag_Person
{
    char Name[10];
    int Age;
    double Height;
};
 
void main()
{
    tag_Person Person[10]=
    {
        { "홍길동"30170 },
        { "길동홍"40167.32},
        { "동홍길"23190.32},
        { "이영애"21176.45}
    };
 
    printf("세 번째 사람 정보 : 이름 = %s, 나이 = %d, 키=%.2f\n", Person[2].Name, Person[2].Age, Person[2].Height);
}
cs

초기식에 나타나는 순서대로 배열을 구성하는 구조체가 초기화될 것입니다. Person[0]에 김은철의 정보가 들어가고 Person[0]에 홍길동의 정보가 들어가고 Person[1]에 길동홍의 정보로 초기화 됩니다. 만약 배열 크기보다 더 많은 초기식이 있으면 에러로 처리되며 초기식이 부족하면 나머지 요소는 모두 0으로 초기화 하는데 이 점도 배열과 동일합니다. 이 예제를 실행해 보면 세 번쨰 구조체의 정보가 출력됩니다. 구조체의 초기화 방법은 배열과 비슷하므로 특별히 새로울 것이 없습니다. 마지막 행의 여분 콤마는 구조체의 초기값의 순서를 바꾸거나 개수를 늘릴 떄의 편의를 위해 삽입된 것이며 컴파일러는 이 여분 콤마를 무시합니다. 단순히 중괄호 안에 초기화시킬 값을 나열하기만 되므로 기술이라고 할 만한 것도 아닙니다. 하지만 초기화는 오래 걸리므로 지역보단 전역이 좋고 컴파일러는 구조체 초기식을 대입문으로 바꿔서 기록하기 때문에 하기 때문에 구조체를 초기화 하는것은 굉장히 많은 시간이 걸립니다.


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

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