포인터라는 가장 어려운 관문을 통과하셨으니, 이제 남은 부분은 수월하게 지나가실 수 있을 것입니다.

사실 포인터가 어렵다 어렵다 하는것도 사람들의 인식빨이 아닌가 싶기도 하고??라고 말하며 포인터때문에 힘들었던 나날들을 생각한다...

 

이번 포스팅에서 다룰 내용은 구조체입니다.

어떤 사람의 주민등록증을 구현하고 싶으면 그 사람의 이름, 주민등록번호, 발급일자, 발급기관 등이 담길 공간이 필요합니다. 그런데 그것들이 다 따로 놀면 정리하기 어렵겠죠? 그래서 A의 주민등록증, B의 주민등록증 이렇게 묶어서 각종 자료를 저장하기 위해 나온 것이 바로 구조체입니다.

 

바로 본 내용으로 들어가시죠!

 

목차

     


    0. 구조체

    0.0. 구조체의 기초

    오늘 배울 내용 중에서 가장 핵심이 되는 구조체입니다.

    구조체연관성이 있는 서로 다른 개별적인 자료형의 변수들이 하나의 단위로 묶인 새로운 자료형을 뜻합니다.

    이처럼 구조체를 사용하면 관련된 변수들을 하나로 묶어서 사용할 수 있습니다.

     

    구조체의 멤버로 일반 변수 뿐만 아니라 배열이나 포인터를 선언할 수도 있습니다.

    참으로 많은 친구들을 담을 수 있죠.

     

    구조체를 정의하면 새로운 데이터형이 하나 만들어진다고 생각하시면 됩니다.

    하지만 구조체를 정의한다고 해서 구조체의 멤버가 메모리에 할당되는 것은 아닙니다.

    구조체형의 변수를 선언할 때 구조체의 멤버들이 메모리에 할당됩니다.

    즉, 껍데기를 만드는 것은 메모리를 사용하지 않지만, 그 껍데기를 이용해서 구조체를 하나 만드는 순간 메모리를 사용하게 됩니다.

    이 구조체를 통해 절차지향 언어인 C가 객체지향 언어의 느낌이 나게 쓰일 수 있게 된 것이죠! Bravo!

     

    구조체의 형식은 아래와 같습니다.

    struct를 쓰고 옆에 "주민등록증" 처럼 이름 쓰고 { } 안에 멤버들을 넣어주면 됩니다.

     


    0.1. 구조체의 크기

    구조체의 크기는 모든 멤버들의 크기의 합보다 크거나 같습니다.

    메모리 정렬 여부에 따라 패딩(padding)유무가 달라지고, 이 padding으로 인해 빈 공간이 날 수도 있습니다.

    만약 구조체의 크기를 구하고 싶다면 sizeof 연산자를 사용하실 수 있습니다.


    0.2. 구조체의 선언

    main함수, 혹은 다른 곳에서 구조체 변수를 사용하겠다고(만들겠다고) 선언할 때는 구조체 태그명 뿐만 아니라 사용을 위한 구조체 변수명을 적어주어야 합니다.

    구조체 변수명을 적지 않으면 그 구조체에 접근할 방법이 없으므로 컴파일러가 에러를 뿜뿜합니다.

     

    구조체 변수를 선언하면 구조체 변수가 가진 멤버들이 메모리에 선언된 순서로 할당됩니다.

     

    구조체를 정의하면서 동시에 선언할 수도 있습니다.

    구조체 맨 끝에 구조체 변수명을 적어주는 것이죠.

    이 때는 명시적으로 어떤 구조체인지 알 수 있으므로 태그명을 생략할 수도 있습니다.


    0.3. 구조체 변수의 사용

    구조체 멤버에 접근할 때는 멤버 접근 연산자인 .를 이용합니다.

    미리 만든 구조체 변수명에 멤버 접근 연산자 . 을 붙이고, 그 뒤에는 구조체 멤버의 이름을 쓰면 됩니다.

     

    만약 구조체 변수를 한번에 초기화하고 싶다면 구조체를 선언하고

    구조체 선언 = { } ; 

    을 작성한 후 { } 안에 구조체 멤버들의 초기값을 순서대로 나열하면 됩니다.

    만약 { } 안에 지정한 초기값이 멤버의 개수보다 부족하면 나머지 멤버들은 0으로 초기화가 됩니다.

     

    아래는 product 구조체의 정의와 사용 예시입니다.

    하나씩 뜯어보시면서 왜 이렇게 되었는지 생각하시면 이해하는 데에 도움이 되실 것이라고 믿습니다 :)

    /* Ex09_01.c */
    #include <stdio.h>
    #include <string.h>
    
    struct product {
    	char name[20];
    	int price;
    	int stock;
    
    };
    int main(void)
    {
    	struct product prd1;
    	struct product prd2 = { "생수2L", 9500, 20 };
    	prd1.price = 15000;
    	prd1.stock = 30;
    	strcpy_s(prd1.name, sizeof(prd1.name), "커피믹스");
    
    	printf("%s : %d원, 재고량 = %d\n",
    		prd1.name, prd1.price, prd1.stock);
    	printf("%s : %d원, 재고량 = %d\n",
    		prd2.name, prd2.price, prd2.stock);
    
    	return 0;
    }

     

    일부러 20을 뺐더니, 0으로 초기화가 되어있다.

     

    구조체는 구조체간의 초기화나 대입이 가능합니다. (물론 같은 구조체여야 한다!!)

    구조체간의 초기화는 멤버대 멤버로 1:1 복사가 됩니다.

    구조체간의 대입은 멤버대 멤버 대입이 됩니다.

    그냥 죄다 복사된다~ 라고만 알아두셔도 충분합니다.

     

    구조체 변수간에는 직접 비교 연산을 할 수 없습니다. 따라서 비교를 원한다면 구조체 변수끼리 멤버대 멤버로 비교하는것이 좋습니다.


    0.4. 구조체 배열

    같은 구조체형 변수를 여러 개 사용하기 위해서 구조체 배열을 쓸 수 있습니다.

    구조체 3개가 들어가는 배열을 만들기 위해서는 아래와 같은 방법으로 구조체 배열을 선언합니다.

     

    만드는 방법이 거의 같은 것처럼, 접근 방법도 동일합니다.

    인덱스를 이용해서 배열의 원소에 접근합니다.

     

    구조체 배열을 초기화하는 방식도 같습니다. { } 안에 초기값을 나열합니다.

     

    아래 사진은 구조체 배열을 이용한 상품 관리 프로그램입니다.

    입력값 5개를 받아서 출력 후 특정 재고 이하면 "재고부족! 주문필요!"가 뜨도록 만들었습니다.

    제가 직접 조금 바꾼 코드라 교재 내용과는 조금 다를 수 있는 점 양해 부탁드립니다!

    /* Ex09_04.c */
    #include <stdio.h>
    #include <string.h>
    
    #define MAX_PRODUCT 5
    
    struct product {
    	char name[20];
    	int price;
    	int stock;
    };
    
    int main(void)
    {
    	struct product prd[MAX_PRODUCT];
    	int i;
    
    	printf("%d명의 상품정보를 입력하세요.\n", MAX_PRODUCT);
    	for (i = 0; i < MAX_PRODUCT; i++)
    	{
    		printf("상품명: ");
    		scanf_s("%s", prd[i].name, sizeof(prd[i].name));
    		printf("가격, 재고량: ");
    		scanf_s("%d %d", &prd[i].price, &prd[i].stock);
    	}
    
    	printf("%-20s %8s %10s\n", "상품명","가격","재고량");
    	for (i = 0; i < MAX_PRODUCT; i++)
    	{
    		printf("%-20s %8d %10d", prd[i].name, prd[i].price, prd[i].stock);
    		if (prd[i].stock < 10)
    			printf(" ==> 재고부족! 주문필요!\n");
    		else
    			printf("\n");
    	}
    
    	return 0;
    
    }


    0.5. 구조체 포인터

    말 그대로 구조체 변수의 주소를 저장하는것이 구조체 포인터입니다.

     

    구조체 포인터로 구조체 멤버에 접근할 때에는 -> 연산자를 사용합니다.

    구조체 변수로 멤버에 접근할 때에는 . 연산자를 사용합니다.

     

    아래는 구조체 포인터의 사용 예입니다.

    맨 위는 구조체를 정의했고, 그 아래에는 구조체 포인터를 매개변수로 갖는 함수인 GetDistance를 선언했습니다.

    main 함수 내에서 GetDistance를 사용할 때 point형 구조체 주소 2개를 넣어줬고, main함수 아래에 있는 GetDistance의 구현부분을 돌아 결과값이 도출됩니다.

    25/26줄에 있는 ->부분만 잘 이해하실 수 있다면 크게 어렵지 않게 넘어가실 수 있을 겁니다.


    0.6. 비트필드

    구조체 안에서 구조체가 가진 멤버를 비트 수로 잘라서 사용할 때 쓰는 것이 바로 비트필드입니다.

    비트필드를 정의할 때는 unsigned 데이터형 멤버 이름 다음에 :를 쓰고 비트수를 적어주면 됩니다.

     

    메모리에 할당할 때는 첫 번째 멤버를 최하위 비트에서부터 할당합니다.

    비트필드의 멤버에 주어진 비트로 표현 가능한 범위 밖의 값을 저장하면 오버플로우가 발생합니다.

    지정 메모리를 초과했으니, 당연히 정상적으로 작동하지 않겠죠?

    멤버 변수에 접근하는 방법은 기존 구조체와 동일하게 . 입니다.

     

    비트필드를 정의할 때 일부러 중간 비트를 비워두고 멤버를 특정 비트에 할당할 수 있습니다.

    그럴 때는 멤버명을 비워두고 비트를 할당합니다.

     

    아래는 비트필드의 사용 예입니다.

    sec는 unsigned이므로 0부터 2^6-1인 63까지 표현이 가능한데 70은 63을 초과해버립니다.

    따라서 64로 넘어갈 때 0으로 넘어가버리고, 거기서부터 6이 커져서 6이 출력이 되는 모습입니다.

     


    1. 공용체

    공용체여러 멤버들이 메모리를 공유해서 사용하는 것이 핵심입니다!!

    형식은 struct와 동일합니다.

     

    다만 구조체와는 다르게 공용체 변수의 멤버들은 모두 같은 주소에 할당됩니다.

    공용체의 크기는 공용체의 멤버 중 가장 크기가 큰 멤버에 의해 결정됩니다.

    아래 예시는 long와 문자열이 모두 4의 크기를 가지고 있기 때문에

    dword 공용체는 4의 크기를 가집니다.

     

    공용체 변수를 초기화할 때는 첫 번째의 멤버의 초기값만 지정합니다. (어짜피 덮어씌워질테니까..)

     

    공용체 멤버에 접근할 때도 . 혹은 ->를 사용합니다.

    .은 일반 변수, ->는 포인터 변수에 사용합니다.

     

    위에서도 언급했지만 구조체와 공용체의 가장 큰 차이점은, 공용체의 멤버들은 동시에 사용되지 않는다는 것입니다.

    한 멤버를 사용하면 그 멤버가 모든 메모리를 점유하기 때문에 다른 멤버들은 정상적인 값을 가질 수 없습니다.

    아래 예시도 둘다 메모리를 20씩 잡아먹는데, union 구조체는 멤버가 필요로 하는 메모리의 최대값인 20의 메모리만 가지고 있게 됩니다. company_name을 쓰게 되면 school_name은 저장되지 않은 상태로 남아있게 되는 것이죠.

     

    union 구조의 경우 여러 멤버에 동시에 접근하지 않는 경우에 사용하며, 보통 임베디드 시스템에 주로 쓰이며 일반적으로 쓸 일은 거의 없습니다. 이런게 있구나 정도로만 넘어가시면 됩니다 :)

     


    2. 열거체

    열거체나열된 정수 값 중 하나를 갖는 정수형의 일종입니다.

    enum이라는 명령어로 사용할 수 있으며, 태그명을 작성하고 { } 안에는 열거상수가 들어가게 됩니다.

    각 열거 상수는 별도의 지정을 하지 않으면 0부터 순서대로 숫자가 매겨지고, =를 통한 할당으로 따로 숫자를 매길 수도 있습니다.

     

    열거체도 struct와 union처럼 사용자가 만들어낸 요소이기 때문에 일단 정의하고 나면 열거체형의 변수를 선언할 수 있습니다.

    이 열거체 변수에는 열거체 정의에 나열된 열거상수 중 하나를 저장하고 사용합니다.

     

    열거 상수만 정수형 상수로 정의할 수도 있습니다.

     

    아래는 열거체를 테스트한 코드입니다.

    열거 상수만 정의하기도 해보고, 일반적인 열거체 week을 만들어보기도 했습니다.

    열거체와 열거상수는 프로그램의 가독성을 향상시키는 기능을 합니다.(1,2.. 같은 숫자보다는 설명을 담은 문자열이 훨씬 많은 정보를 담을 수 있으니까!!)

    /* Ex09_04.c */
    #include <stdio.h>
    #include <string.h>
    
    #define MAX_PRODUCT 5
    
    enum {
    	red = 10, green = 20, blue = 30
    };
    
    enum week{ sun, mon, tue, wed, thu, fri, sat };
    
    int main(void)
    {
    	printf("%d\n", red); // 미리 지정한 red 출력 (10)
    	
    	enum week weekday; // weekday라는 열거체형 변수 선언
    	weekday = mon; // mon을 weekday에 할당 (사실상 1을 넣었다고 보면 된다)
    	printf("%d", weekday); // 1 출력
    
    	return 0;
    
    }

     

    이번엔 switch / case 문을 사용한 열거체의 사용 예입니다.

    weekday가 mon이면 월요일입니다를 출력하는 프로그램입니다.

     


    3. typedef

    typedef기존의 데이터형에 새로운 이름을 붙일 때 사용합니다.

     

    우리가 구조체를 한 번 정의하고 나면 선언할 때

    struct 태그명 변수명;

    까지 총 3단어를 적어야 합니다.

    하지만 미리 typedef를 이용해서 단축어를 정의해 놓으면 struct 태그명을 한 단어로 축약해서 사용할 수 있게 됩니다.

     

    당연히 typedef로 선언한 출임말을 쓰지 않고 원래대로 다 쓸 수도 있습니다.

     

    typedef는 프로그램의 이식성을 향상시킬 목적으로 사용합니다.

    구조체를 정의할 때 구조체 위에 미리 typedef를 정의해 놓으면 일일이 수정할 필요 없이 typedef로 지정해놓은 데이터형만 바꾸면 편리하게 관리 및 수정 작업을 할 수 있습니다.

     

    그리고 typedef는 프로그램의 가독성을 향상시킬 목적으로도 사용합니다.

    C 자체에서 지정해놓은 데이터형이 눈에 잘 들어오지 않는다면 아래와 같이 byte 등으로 짧고 명료하게 줄여서 사용할 수 있습니다.

     


    4. 함수의 활용 - 구조체의 전달

    구조체를 값으로 전달하게 되면 인자 전달 과정에서 구조체 변수가 복사되어서 전달됩니다.

    이것을 값에 의한 전달, 혹은 call by value라고 부릅니다.

     

    만약 구조체를 포인터로 전달하게 되면 구조체를 복사하지 않고 주소만 전달하기 때문에 불필요한 구조체의 복사를 막을 수 있습니다. (복사시간 단축)

    따라서 크기가 큰 구조체의 경우 포인터로 전달하는 것이 성능에서 이점을 가질 수 있습니다.

    이것을 call by pointer라고 부릅니다.

    (개인적으로는 포인터를 매개변수로 받는 것보다는 주소값을 매개변수로 받는 call by reference를 더 선호합니다. 주소값을 컨트롤할 수는 없어지지만, 일반적인 케이스에서 훨씬 명료하게 사용할 수 있거든요!! 이 부분은 차후에 C/C++ 카테고리에서 더 자세하게 포스팅을 작성하도록 하겠습니다!)

     

    구조체 앞에 const를 붙이면 구조체 내부의 요소들을 변경시킬 수 없게 됩니다.

    구조체가 함수 안에서 이용만 될 뿐 변경되지 않을 때는 혹시나 실수로라도 변경되지 않도록 const를 붙이는 것이 안전하겠죠?

     


    생각보다 너무나도 길어졌습니다. 내용이 엄청나게 많은 부분은 아니지만 이 부분은 차후 객체지향 프로그래밍 언어들에 있어서 가장 기초가 되는 내용이기 때문에 최대한 자세하게 설명하고자 노력했습니다.

    포인터 글에서도 비슷하게 길어졌던 것 같은데..?

     

    그래도 이제 끝이 보이고 있습니다. 어느새 9번째 글까지 달려왔네요..!!

    아마 다음 포스팅이 마지막이 될 것 같습니다! 마지막까지 열심히 달려봅시다!! 파이팅!!! :)

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기