2022.03.01 구조체

2022. 3. 1. 21:20IoT Embeded 강의

구조체

구조체는 데이터 타입을 한 세트로 묶어 놓은 것이라고 생각하면 좋습니다. 우리는 char, short, int, long, double 과 같은 자료형에 대해서 배웠는데 이를 기본 데이터 타입이라고 합니다. 그렇다면 이런 기본 데이터 타입들을 조합하여 새로운 데이터 타입을 만들수도 있을 것입니다. 왜 기본 자료형을 조합해야 하는 일이 필요할까요? 

 

회원 관리 프로그램을 만든다고 했을 때 고객의 정보에 들어가는 자료형이 단순히 한가지의 자료형은 아닐 것입니다. 고객의 이름은 char형으로 이루어진 배열일 것이며, 고객의 전화번호, 주소, 계좌번호 등 다양한 정수형, 실수형 자료들이 요구될 것입니다. 이렇게 한 명의 고객이 갖고 있는 정보들을 같은 자료형끼리만 묶어서 관리하는 방법도 있겠지만 고객 이름만 검색하면 관련 정보들이 바로 출력될 수 있도록 프로그래밍을 하는 것이 편리할 것입니다. 이와 같이 한 고객 안에 다양한 자료형들을 조합하여 관리할 수 있도록 하는 것이 구조체의 역할입니다. 

 

먼저 구조체는 여러 기본 데이터 타입들을 블럭 조립하듯 한 곳에 모아 새로운 자료형을 만드는 것이라고 생각하는게 좋습니다. 구조체를 만들고 구조체 변수를 선언해보면서 자세히 설명하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

//구조체 정의
struct MyStruct
{
    char letter;
    int number1;
    double number2;
};
 
int main()
{
    struct MyStruct x1; //구조체 변수 선언
    x1.letter = 'a'; //구조체 멤버변수 letter 초기화
    x1.number1 = 123; //구조체 멤버변수 number1 초기화
    x1.number2 = 3.141592; //구조체 멤버변수 number2 초기화

    printf("%c | %d | %lf", x1.letter, x1.number1, x1.number2);
    return 0;
}
 
cs

구조체 정의

먼저 구조체를 정의하는 법을 살펴보겠습니다. 3~8번 줄에서와 같이 'struct' 키워드'MyStruct'라는 사용자가 지정하는 식별자(구조체 이름)를 쓴 뒤 중괄호 안에 사용할 변수들을 자료형과 함께 선언해 줍니다. 문자를 담을 변수인 letter은 char형, 정수형 변수 number1은 int형, 실수형 변수 number2는 double형으로 구조체 안에서 선언해줍니다. 그러면 MyStruct라는 새로운 자료형이 만들어진 것입니다. 구조체 정의는 메인 함수 밖에서 작성해주며 중괄호 마지막 부분에 세미콜론을 붙여주는 것을 기억해야 합니다.

 

구조체 변수 선언 및 초기화

새로운 자료형을 만들었으면 그것으로 변수를 선언합니다. 12번 줄 메인함수 내에서처럼 키워드와 식별자를 써준 뒤 변수 이름을 붙여서 세미콜론으로 마무리합니다. 기본 자료형으로 변수를 선언하는 것과 같습니다. 

 

초기화는 13~15번줄입니다. 이는 선언과 초기화를 별개로 하는 방법입니다. 선언 후 초기화를 할 때에는 구조체 변수인 x1에 멤버 참조 연산자 ' . '을 붙여서 구조체 안에 각각의 변수들의 이름을 적은 뒤 자료형에 맞게 초기화 해줍니다. 이 때 구조체 안에 있는 각각의 변수들을 멤버 변수라고 합니다.

 

구조체 변수 선언과 초기화를 동시에 하는 방법은 중괄호를 사용하여 멤버 변수의 순서에 맞게 초기화 값을 입력하는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
struct MyStruct
{
    char letter;
    int number1;
    double number2;
};
 
int main()
{
    struct MyStruct x1 = {'a'1233.141592};
    printf("%c | %d | %lf", x1.letter, x1.number1, x1.number2);
    return 0;
}
 
cs

 

멤버 참조 연산자 ( . )

구조체 안에는 기본 자료형인 변수들이 여러 개 존재할 수 있습니다. 만약 구조체 안에 데이터를 입력한다면 제가 구조체 변수이름만 사용하여 대입연산자를 쓴 뒤 여러 데이터들을 한번에 집어 넣을수는 없을 것입니다. 각각의 멤버 변수의 자료형에 맞게 데이터를 입력해야 하는데 이 때 사용하는 것이 멤버 참조 연산자입니다. 

 

멤버 참조 연산자는 구조체 안의 멤버 변수에 접근하는 기능을 가진 연산자입니다. 연산자이기 때문에 우선순위도 존재하다는 것을 기억해두시기 바랍니다. 멤버 참조 연산자를 사용하면 소스 코드 작성 시 편리한 기능이 있습니다. 구조체 변수 이름을 입력한 뒤 . 만 입력해주면 그 안의 멤버 변수 리스트를 IDE에서 알아서 띄어줍니다. 덕분에 아무리 멤버 변수가 많다고 하더라도 자신이 어디에 어떻게 접근하고 있는지 알기 쉽습니다.

 

구조체 변수 메모리와 멤버 변수의 정렬

구조체를 정의하고 변수를 선언했을 때 메모리 안에서 어떻게 그림이 그려질지 설명하겠습니다. 먼저 다음과 같은 구조체가 정의된 후 선언됐다고 가정하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
 
struct MyStruct
{
    char letter;
    int number1;
    double number2;
    char array[5];
};
 
int main()
{
    struct MyStruct x1= {'a'1233.141592"Good"};
    printf("%c | %d | %lf | %s", x1.letter, x1.number1, x1.number2,x1.array);
printf("%d", sizeof(struct MyStruct));
    return 0;
}
 
cs

 

그러면 메모리에서는 아래와 같이 구조체 변수 x1이 공간을 차지하게 됩니다.

구조체 변수x1 한개가 선언되면 구조체 자료형안에 멤버 변수의 자료형들의 크기에 맞게 메모리 크기를 할당하게 됩니다. 이 때 구조체 안의 기본 자료형을 살펴보면 char형 1byte , int형 4byte, double형 8byte, char형 배열[5] 5byte 들을 모두 합해 18byte의 자료형 크기를 가질 것 같지만 실제는 24byte를 갖습니다. 왜냐하면 컴파일러에서 Struct member alignment 옵션이 있어서 구조체의 멤버 변수를 단위에 맞게 정렬하기 때문입니다. 1,2,4,8,16 byte 중 하나를 선택할 수 있는데 보통 4byte로 설정되어 있습니다. 컴퓨터가 데이터를 보통 4byte의 단위로 처리하기 때문입니다. 위의 그림에서 자료형들을 순서대로 정렬하면 회색칸 만큼의 빈 공간이 정렬 옵션에 의해 생기게 될 것입니다. 그래서 구조체 멤버 변수를 선언하는 순서에도 신경을 써야합니다. 만약 구조체 변수를 포인터 변수로 설정하면 이와 같은 구조체 자료형의 크기를 아는 것이 포인터 연산을 할 때 중요할 것입니다.

 

메모리 주소는 일반적으로 메모리 공간 1byte마다 한개 씩 존재합니다. 그림에서는 한 칸입니다. 그렇다면 구조체 변수x1의 주소는 1번이 됩니다. 이는 구조체 멤버 변수 x1.letter의 주소와 동일한 주소일 것입니다. 각각의 멤버 변수도 주소값을 갖고 있습니다. 각 멤버 변수의 주소값을 출력해보면 메모리 공간을 어떤 방식으로 차지하고 있는지 알 수 있습니다. 

 

구조체의 배열

구조체를 한번 정의하면 기본 자료형이 할 수 있는 일은 대부분 할 수 있습니다. 구조체로 변수를 선언한 것처럼 구조체로 배열을 선언할 수 있습니다. 구조체 배열 선언과 초기화, 그리고 구조체 배열 선언 시 멤버변수로의 접근 방법을 아래 코드를 보면서 설명하겠습니다.

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
#include <stdio.h>
 
struct People
{
    char name[10];
    char phone[20];
    char address[20];
    int age;
    float height;
};
 
int main()
{
    struct People human[4= {
        {"김민수""010-1111-1111""서울시"20178.5},
        {"최길동","010-2222-2222","경기도"22167.4},
        {"이진아","010-3333-3333","전라도"25158.2},
        {"박지은","010-4444-4444","경상도"19182.4}
    };
    printf("이름\t전화번호\t주소\t나이\t키\n");
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human->name,human->phone,human->address,human->age,human->height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[0].name, human[0].phone, human[0].address,human[0].age,human[0].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[1].name, human[1].phone, human[1].address,human[1].age,human[1].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[2].name, human[2].phone, human[2].address,human[2].age,human[2].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[3].name, human[3].phone, human[3].address,human[3].age,human[3].height);
    
    return 0;
}
 
 
cs

먼저 구조체 People 를 정의하여 안에 이름, 전화번호, 주소 문자열을 담을 배열을 3개 넣었습니다. 그리고 나이와 키는 각각 정수형 실수형으로 선언했습니다. 3~10번줄

 

이렇게 만들어진 구조체를 사용하여 크기가 4인 배열 human을 선언하고 초기화 했습니다. 초기화를 할 때에는 중괄호를 중첩하여 사용합니다. human 인덱스 순서대로 중괄호를 두번 중첩하여 순서대로 값을 입력합니다. 멤버 변수의 순서도 유의하면서 입력합니다. 또한 멤버 변수로 배열들이 있는데 문자열 입력 시 이중 따옴표안에 문자열을 넣어서 초기화 해주는 것도 기억해야 합니다. 아래 그림은 위의 구조체 배열이 차지하는 공간을 표현한 그림입니다. 14~19번줄

이 후에 출력함수에서 멤버 참조 연산자를 사용하여 멤버 변수에 접근하여 출력할 것입니다. 형식지정자는 멤버 변수의 자료형에 맞게 지정해줍니다. 여기서 한 가지 유의해야할 점이 있습니다. 구조체 변수에서는 멤버 참조 연산자를 사용했으며 21번줄의 구조체 배열의 배열 이름을 사용 시에는 간접 멤버 연산자(->)를 사용했습니다. 이 둘을 구분하는 방법은  주소가 아닌 일반 데이터에는 ( . ) 을 , 주소(포인터) 에는 (->) 을 사용하는 것입니다. 배열 이름은 배열의 첫번째 인덱스의 주소값을 데이터로 갖고 있으므로 간접 멤버 연산자를 사용하여 멤버 변수에 접근합니다. 이는 구조체 포인터에서 다시 설명하겠습니다. 한가지 더 기억해야 할점은 멤버 변수로 접근한 문장 자체는 주소가 아닌 데이터로 취급되므로 scanf함수에서 사용할 때에는 주소연산자 &를 사용해야 한다는 점입니다. 

 

21번줄과 22번 줄의 출력결과는 같습니다. 왜냐하면 human은 배열 이름으로 첫번째 인덱스의 주소를 담고 있습니다. 그 주소에서 멤버 변수를 참조했으니 인덱스를 사용한 변수 human[0]에서 변수로서 멤버 변수에 접근한 결과와 같은 것입니다. 배열은 브래킷 연산자를 사용 시 일반 데이터로 취급됩니다. 22~25번줄은 구조체 배열을 브래킷 연산자를 사용하여 모든 인덱스에서 멤버 변수로 접근한 상황입니다. 출력 결과는 아래와 같습니다.

 

구조체 포인터

자료형이 있다면 그 자료형에 맞는 포인터도 만들 수 있을 것입니다. 구조체 포인터는 일반 포인터와 같습니다. 구조체가 저장된 메모리 주소값을 저장하며 크기도 운영체제에 따라 4, 8 byte입니다.

 

포인터 연산자를 사용할 때도 비슷합니다. 구조체 변수에 &를 붙이면 주소값을 얻을 수 있고, 구조체 포인터에 *를 붙이면 데이터를 얻을 수 있습니다. 또한 구조체 포인터에 []으로 포인터 연산(주소 이동)을 할 수 있는데 이 때는 구조체의 자료형의 크기를 인지하고 계산해야 합니다. 사용자가 정의한 구조체 자료형의 크기가 24byte라면 포인터 연산 +1은 24byte 만큼 메모리 공간을 건너뛰는 것과 같습니다.

 

구조체 포인터 변수를 선언한 뒤 위에서 사용한 배열 이름을 대입하여 데이터를 출력해보도록 하겠습니다.

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
 
struct People
{
    char name[10];
    char phone[20];
    char address[20];
    int age;
    float height;
};
 
int main()
{
    struct People human[4= {
        {"김민수""010-1111-1111""서울시"20178.5},
        {"최길동","010-2222-2222","경기도"22167.4},
        {"이진아","010-3333-3333","전라도"25158.2},
        {"박지은","010-4444-4444","경상도"19182.4}
    };
    struct People* ptr = human;
    printf("이름\t전화번호\t주소\t나이\t키\n");
    //구조체 배열에 인덱스를 사용한 뒤 멤버 참조 연산자 사용하여 멤버 변수 접근
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[0].name, human[0].phone, human[0].address,human[0].age,human[0].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[1].name, human[1].phone, human[1].address,human[1].age,human[1].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[2].name, human[2].phone, human[2].address,human[2].age,human[2].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n",human[3].name, human[3].phone, human[3].address,human[3].age,human[3].height);
    printf("\n");
    //구조체 배열 이름(=포인터,주소)에 포인터 연산과 간접 멤버 연산자를 사용하여 멤버 변수 접근 
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", human->name, human->phone, human->address, human->age, human->height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (human + 1)->name, (human + 1)->phone, (human + 1)->address, (human + 1)->age, (human + 1)->height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (human + 2)->name, (human + 2)->phone, (human + 2)->address, (human + 2)->age, (human + 2)->height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (human + 3)->name, (human + 3)->phone, (human + 3)->address, (human + 3)->age, (human + 3)->height);
    printf("\n");
 
    //구조체 포인터에 브래킷 연산자를 사용하여 변수 취급한 뒤 멤버 참조 연산자를 사용하여 멤버 변수 접근
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (*ptr).name, (* ptr).phone, (* ptr).address, (* ptr).age, (* ptr).height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (*(ptr+1)).name, (* (ptr+1)).phone, (*(ptr+1)).address, (* (ptr+1)).age, (* (ptr+1)).height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (*(ptr+2)).name, (* (ptr+2)).phone, (*(ptr+2)).address, (* (ptr+2)).age, (* (ptr+2)).height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (*(ptr+3)).name, (* (ptr+3)).phone, (*(ptr+3)).address, (* (ptr+3)).age, (* (ptr+3)).height);
    printf("\n");
 
    //구조체 포인터에 브래킷 연산자를 사용하여 변수 취급한 뒤 멤버 참조 연산자를 사용하여 멤버 변수 접근
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", ptr[0].name, ptr[0].phone, ptr[0].address, ptr[0].age, ptr[0].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", ptr[1].name, ptr[1].phone, ptr[1].address, ptr[1].age, ptr[1].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", ptr[2].name, ptr[2].phone, ptr[2].address, ptr[2].age, ptr[2].height);
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", ptr[3].name, ptr[3].phone, ptr[3].address, ptr[3].age, ptr[3].height);
    printf("\n");
 
    //구조체 배열 이름을 구조체 포인터에 대입한 뒤 포인터 연산과 간접 멤버 연산자를 사용하여 멤버 변수 접근
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", ptr->name,ptr->phone,ptr->address,ptr->age,ptr->height );
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (ptr+1)->name,(ptr+1)->phone,(ptr+1)->address,(ptr+1)->age,(ptr+1)->height );
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (ptr+2)->name,(ptr+2)->phone,(ptr+2)->address,(ptr+2)->age,(ptr+2)->height );
    printf("%s\t%s\t%s\t%d\t%.2lfcm\n", (ptr+3)->name,(ptr+3)->phone,(ptr+3)->address,(ptr+3)->age,(ptr+3)->height );
 
    return 0;
}
 
 
cs
 

구조체 포인터 변수 선언 및 초기화

구조체 변수 선언은 20번줄에서 처럼 구조체 자료형을 쓴 뒤 *과 포인터 변수 이름을 써주면 됩니다. 다음으로 가르킬 주소값을 대입하면 됩니다. 여기서는 human 배열의 주소값을 대입하도록 했습니다. 

 

간접 멤버 연산자 ( -> )

포인터, 즉 주소값을 이용해 멤버 변수에 접근하기 위해서는 간접 멤버 연산자를 사용합니다. 구조체 변수에서는 멤버 변수에 접근하기 위해 멤버 참조 연산자( . )를 사용한 것과는 다릅니다. 구조체 배열 이름, 포인터는 주소값을 저장하고 있기 때문에 28~38번 줄에서는 간접 멤버 연산자를 사용합니다. 22~26번 줄은 배열에 브래킷 연산자를 사용했으므로 주소값이 아닌 배열 인덱스에 담긴 실제 데이터 값을 의미하므로 변수의 접근 방식인 멤버 참조 연산자를 사용합니다. 

 

간접멤버 연산자는 사실 참조 연산자( * )와 멤버 참조 연산자( . )를 합해둔 기능과 같습니다. 다만, 참조 연산자보다 멤버 참조 연산자의 우선 순위가 높아 소괄호로 참조 연산자를 먼저 사용하게 해야하는 번거로움이 있어 이를 피하기 위해 만든 연산자입니다. 

 

구조체 배열, 포인터를 이용한 멤버 변수 접근 및 출력

위의 소스 코드에서 멤버 참조 방법들을 쓰는 방법들이 모두 다르지만 출력 결과는 같습니다. 배열 이름도 결국 포인터이기 때문에 배열 이름과 포인터의 쓰임새가 같은 것을 확인할 수 있습니다. 위의 예시에서 출력 결과를 데이터가 아닌 주소값을 받아 출력해보시는 것도 구조체와 멤버 참조를 이해하는데 도움이 됩니다.

 

'IoT Embeded 강의' 카테고리의 다른 글

2022.02.28 유틸리티 함수, 수학 함수  (0) 2022.03.01
동적 메모리 할당  (0) 2022.02.28
포인터 / 포인터 선언 / 초기화 / 연산 / 문자열  (0) 2022.02.28
자료형과 해석  (0) 2022.02.24
배열과 문자열  (0) 2022.02.14