C 프로그래밍 입문/구조체와 유니온 데이터

구조체와 유니온 데이터

+/-

구조체를 설명하기 위해, 성적표를 출력하는 프로그램을 작성하는 과정을 생각해 보자. 한 사람의 성적 데이터라는 것은 실제로 여러 정보를 포함한다. 성적표에 들어가는 정보는 학생의 이름, 학년, 반, 번호, 각 과목의 점수, 총점 그리고 평균 등이 될 것이다. 실제로 프로그램을 작성할때 이러한 성적표를 출력하는 프로그램을 만들때 한 반의 정보 전체를 넘겨 받아서 각 사람의 정보를 성적표로 출력해주는 프로그램을 만드는 것 보다. 한 사람의 정보를 받아서 프린터로 인쇄하는 프로그램을 만든 후 다시 그 프로그램으로 반 전체의 성적 리스트 중에 인쇄하려는 사람의 성적표 정보를 뽑아서 넘겨주는 프로그램 두 개를 만드는 편이 개념적으로도 이해하기 쉽고 나중에 프로그램을 변형해야 하더라도 좀 더 쉽게 작업을 할 수 있게 될 것이다. 이 경우, 정수를 저장할 수 있는 데이터 타입이 있는 것 처럼 한 사람의 정보를 저장할 수 있는 데이터 타입이 있다면 프로그램을 작성 할 때에도 편리 할 것인데, 이렇게 관계있는 데이터들을 하나로 묶어서 관리할 수 있도록 제공해주는 자료 구조가 바로 '구조체와 유니온'이다. 구조체와 유니온은 사용되는 개념에서 약간의 차이가 있지만 기본적인 개념의 시작은 동일하고, 유니온에는 구조체가 갖는 특성 위에 다른 특성이 부가 된다.[1]

구조체 (Structure)
+/-

C 스펙에 있는 구조체의 정의는 '동일하지 않은 데이터 타입을 갖는 일련의 데이터의 집합'이다. 그에 비해 배열의 정의는 '동일한 데이터 타입을 갖는 일련의 데이터의 집합' 이다. 두 정의에 들어있는 '일련의(sequential)'이란 단어의 의미는 '연달아 놓여있는'의 의미 이다. 위에서 배열의 정의에 관해 정확하게 이해 했다면 '연달아 놓여있는 데이터의 집합'이라는 말의 의미를 정확하게 이해할 수 있을 것이다.구조체는 배열과 마찬 가지로 데이터들이 연달아 놓여 있는데 배열과의 차이점 이라면 구성 요소가 되는 데이터의 타입이 다르다는 말이다. 여기에서 '변수'의 정의를 다시 한 번 기억해 보도록 하자. 변수는 '메모리 공간'이었고, 배열은 같은 종류의 변수가 여러개가 메모리상에 연달아 존재 한다. 배열의 특성상 같은 종류의 메모리가 여러개 연달아 놓여있기 때문에 배열 내 n 번째 메모리 공간을 사용하고 싶으면 '(n - 1) * 배열내 요소 하나의 크기'라는 간단한 계산식으로 사용하고자 하는 메모리 공간의 주소를 얻을 수 있기 때문에 배열에서는 인덱스(index)라는 개념을 이용해서 배열 멤버를 다룰 수 있었다. 그러나 구조체의 경우에는 '동일하지 않은 데이터 타입'이라는 전제 조건이 있기 때문에 구조체 내의 멤버를 다루기 위해 멤버가 위치하는 메모리내 위치를 계산하기 위해서는 위에서 배열내 멤버의 위치를 계산하는 방법을 사용할 수 없다. 그렇기 때문에 구조체는 인덱스가 아닌 멤버의 이름을 일일히 지정하고 그 이름을 사용하는 방법을 사용한다.

다음은 구조체를 사용하는 프로그램의 간단한 예 이다.

#include <stdio.h>
struct articles_type {
    int korean, english, math;
};

struct score_card_type {
    int year, grade, seq;
    struct articles_type scores;
    int total;
    double average;
};

int print_a_scorecard (struct score_card_type sc) {
    printf ("%2d 학년 %2d 반 %3d 번\n", sc.year, sc.grade, sc.seq);
    printf ("=========");
    printf ("국어: %4d    영어: %4d    수학: %4d\n", sc.scores.korean, sc.scores.english, sc.scores.math);
    printf ("총점: %5d   평균: %5.1f\n", sc.total, sc.average);

    return 0; 
}

int main (int argc, char *argv[]) {
   struct score_card_type a_student;

   /*  여기에 전체 데이터 베이스에서 한 학생의 성적을 추출해 내는 프로그램 코드가 있다고 가정 합니다.
       나중에 포인터를 배운 후에 이해될 수 있는 부분이기 때문에 이 내용은 잠시 비워둡니다. */

    print_a_scorecard (a_student);

    return 0;
}

구조체를 사용하기 위해서는 구조체의 형태를 먼저 정의하고, 정의된 형태의 구조체 타입의 변수를 만들어서 사용한다. 좀 다른 표현으로 설명하자면, 여러개의 데이터가 들어가는 새로운 변수의 타입을 하나 만든 다음에 그 타입의 변수를 만들어서 사용한다.[2] 배열의 경우 동일한 타입의 변수가 반복되는 것 이기 때문에 다로 그 내용에 대한 타입을 별도로 정의해 줄 필요가 없지만, 구조체의 경우에는 포함되는 데이터의 타입이 다르다는 전제가 있으므로 먼저 어떤 내용이 들어갈지를 정의해 주는 작업을 거쳐야 한다. 위의 코드를 보면 2번 라인에서 11번 라인까지의 프로그램 코드에 구조체의 구조를 정의 했다(혹은 struct articles_type 타입과 struct score_card_type 타입을 만들었다). 그리고 23번 라인에서 struct score_card_type 타입의 변수를 하나 만든 것 이다.

위 코드에서 주의할 점이 두 가지가 있는데, 첫째, 구조체의 끝에는 반드시 세미콜론(;)을 찍어 주어야 한다. 아직 설명되지 않았지만 구조체의 닫힘 중괄호(}) 다음에는 생략 가능한 내용이 있으며(실제로 위의 프로그램에서는 두 구조체 모두 생략했다), C 컴파일러는 그 생략가능한 내용이 생략된 것 인지 아닌지 확인할 방법이 없기 때문에 중괄호 다음에 더이상 내용이 없다면 반드시 세미콜론을 찍어서 문장이 더 이상 없다는 것을 표시해 주어야 하는 것 이다. 둘째, articles_type과 score_card_type은 변수 이름이 아니고 타입 이름의 일부 일 뿐이다. 많은 초보 C 프로그래머들이 쉽게 실수하는 내용이므로, 아무리 당연하다고 생각 되어도 헷갈리지 않도록 주의할 필요가 있다.

만들어진 구조체 변수내의 멤버를 액세스 하는 방법은 구별자인 마침표(.;dot, full stop)를 이용한다. 기본 형태는 '구조체변수.멤버변수'의 형태와 같이 구조체변수와 그 멤버이름 사이에 점을 찍음 으로서 두 변수가 포함 관계에 있음을 표시해 준다. 위 프로그램의 8번 라인을 보면 struct subjects_type scores 라는 구조체 멤버 변수를 볼 수 있을 것이다. 이 샘플 코드에서 볼 수 있듯이 일단 한번 만들어진 구조체는 그 자체로도 하나의 데이터 타입으로서 다루어 지므로 다른 구조체의 멤버 변수로 선언 될 수 있다. 구조체 변수 내의 구조체 멤버의 멤버를 액세스 할 때도 동일한 방법을 반복하여 사용해 주면 된다. 16번 라인을 보면 sc.scores.korean 라는 변수를 볼 수 있는데, 이는 'sc구조체 변수 내 멤버 구조체 변수인 scores의 멤버 변수 korean'이라는 의미로 액세스 하게 된다. 다차 배열에서 단순한 규칙을 이용해 다차 변수를 액세스 했듯이 다중 구조의 구조체(nested structure)를 액세스 할 때역시 기본 구조체 액세스 방법을 반복해 줌으로서 포함되어 있는 구조체 변수의 멤버 변수를 액세스 할 수 있다.

구조체 타입의 변수 역시 변수 임에는 변함이 없으므로 기본형의 데이터타입을 배열로 만드는 것과 같은 방법으로 구조체의 배열 역시 만들 수 있다. 다음 코드는 한반에 최대 60명까지 있을 수 있는 반 전체 성적표 데이터를 저장할 수 있는 배열의 선언이 된다.

 struct score_card_type a_class[60];

위와 같이 선언된 구조체 배열은 일반 배열과 동일하게 액세스 할 수 있으며, 배열내 n번째 구조체 변수의 멤버 변수를 액세스 하는 방법에 대해서는 해당 구조체 변수 배열이 메모리에 저장되어 있는 방식을 잠시 생각해 보면 쉽게 유추해 볼 수 있을 것이다. 전체 구조를 컨테이너라 가정한다면 가장 외부의 컨테이너는 배열이 된다. 그러므로 배열 컨테이너에 있는 구조체 변수 컨테이너 하나를 꺼낸 다음, 다시 구조체 안에 있는 내용물인 변수를 끄집어 내 주면 되는 것 이다.

구조체의 기본형은 다음과 같다:

struct struct_type_name {
    type variable;
    type variable;
         ...
} variable_name, variable_name, ..., variable_name;

위의 구조에서 struct_type_name이나 variable_name 목록중 하나는 생략이 가능하다. 그렇기 때문에 다음과 같은 형태로 선언하여 사용하는 것이 가능하다.

#include <stdio.h>
struct articles_type {
    int korean, english, math;
};

struct score_card_type {
    int year, grade, seq;
    struct articles_type scores;
    int total;
    double average;
} a_student;

int print_a_scorecard (struct score_card_type sc) {
    printf ("%2d 학년 %2d 반 %3d 번\n", sc.year, sc.grade, sc.seq);
    printf ("=========");
    printf ("국어: %4d    영어: %4d    수학: %4d\n", sc.scores.korean, sc.scores.english, sc.scores.math);
    printf ("총점: %5d   평균: %5.1f\n", sc.total, sc.average);

    return 0; 
}

int main (int argc, char *argv[]) {
   /*  여기에 전체 데이터 베이스에서 한 학생의 성적을 추출해 내는 프로그램 코드가 있다고 가정 합니다.
       나중에 포인터를 배운 후에 이해될 수 있는 부분이기 때문에 이 내용은 잠시 비워둡니다. */

    print_a_scorecard (a_student);

    return 0;
}

앞서 사용했던 코드에서 11번 라인이 수정되고 23번 라인이 제거 되었다. 위와 같이 타입을 선언함과 동시에 변수를 할당하는 것 역시 가능하다.

#include <stdio.h>
struct score_card_type {
    int year, grade, seq;
    struct articles_type {
        int korean, english, math;
    } scores;
    int total;
    double average;
} a_student;

int print_a_scorecard (struct score_card_type sc) {
    printf ("%2d 학년 %2d 반 %3d 번\n", sc.year, sc.grade, sc.seq);
    printf ("=========");
    printf ("국어: %4d    영어: %4d    수학: %4d\n", sc.scores.korean, sc.scores.english, sc.scores.math);
    printf ("총점: %5d   평균: %5.1f\n", sc.total, sc.average);

    return 0; 
}

int main (int argc, char *argv[]) {
   /*  여기에 전체 데이터 베이스에서 한 학생의 성적을 추출해 내는 프로그램 코드가 있다고 가정 합니다.
       나중에 포인터를 배운 후에 이해될 수 있는 부분이기 때문에 이 내용은 잠시 비워둡니다. */

    print_a_scorecard (a_student);

    return 0;
}

첫번째, 두번째 코드에서 struct articles_type은 struct score_card_type에서 단 한번 사용된다. 그런경우 위와 같이 타입 이름은 지정하지 않은 상태로 바로 해당 구조체를 사용하는 변수를 선언하여 사용하는 것도 가능하다. 위의 프로그램을 보면 앞서 프로그램의 2, 3, 4번 라인이 제거되고 structure score_card_type 구조체로 그 내용이 옮겨진 것을 볼 수 가 있을 것이다. 동일 형태의 구조체 타입이 다시 사용되지 않는다면 이와 같이 다른 구조체 내부에 구조체를 정의하고 구조체 타입의 이름을 생략한 상태로 바로 변수 이름을 지정하여 사용할 수 도 있다.

구조체의 초기화
+/-

구조체는 배열과 많은 다른 점이 있지만 동시에 유사한 점도 많다. 그중의 하나가 '일련의 데이터 모음'이라는 특성인데, 이 특성 덕분에 배열과 동일한 방법으로 구조체를 선언과 동시에 값을 초기화 할 수 있다. 앞서의 코드에서 사용되었던 구조체는 다음과 같이 초기화 될 수 있다.

struct score_card_type sdata = {
    1, 1, 12,
    {10, 10, 10},
    30,
    10.0
};

마찬가지로 다음과 같이 하여 일부분만 값을 할당하거나 모두 0으로 초기화 할 수 있다:

struct score_card_type sdata = { 1, 1, 12 };
struct score_card_type zdata = { 0 };

물론 일부분만 값을 할당한 경우에는 나머지는 모두 0으로 초기화 된다.


배열과 구조체의 차이점 중의 하나는 배열은 배열끼리 값의 할당이 안되지만 구조체는 구조체 끼리 값이 할당될 수 있다는 점이다. 다음의 코드를 보자:

char arr1[20] = "Hello World!";
char arr2[20];
arr2 = arr1; /* 여기에서 에러가 발생합니다. */

위의 코드는 컴파일 시에 에러가 발생한다. 에러 메시지는 컴파일러 마다 조금씩 다르겠지만 대충 '값을 옮겨 넣을 수 없다'는 의미 일 것이다. 그러나 다음의 코드는 문제없이 동작한다.

struct str_type {
    char message[20];
};
struct str_type msg1 = { "Hello World!" };
struct str_type msg2;
msg2 = msg1;
printf ("%s\n", msg2.message);

위의 프로그램은 에러없이 정상적으로 두 문자열이 복사가 된다. 이전에 문자열 배열을 다룰때 문자열 변수에는 나중에 값을 대입할 수 없기 때문에 strcpy()나 memcpy() 함수를 이용해서 복사해야 한다는 내용을 예를 들어가며 설명을 했었는데, 이 경우 실제 복사되는 데이터는 문자열 배열임에도 불구하고 깔끔하게 변수의 내용이 복사 되었다. 이런 차이를 있게한 위의 두 코드의 차이점이 무엇일까? 실제로 배열은 그 길이가 결정되어 있는 타입이 아니다. 그렇기 때문에 두 배열내 멤버의 타입이 동일 하다는 보장이 있다고 하더라도 두 배열의 길이가 동일 할 것이라는 보장이 없는데다가 배열의 길이를 실행시에 확인할 방법이 없다는 문제가 있다. 그에 반해 구조체는 먼저 타입을 '정의'하고 나서 해당 타입의 변수를 선언해서 사용하는 것 이기 때문에 두 구조체 변수의 크기가 항상 동일할 것 이라는 보장이 있다. 그렇기 때문에 구조체형 변수는 변수값들 간에 대입이 가능한 것이다.

참고:
  • 구조체간에 값의 대입이 가능하다고 해도 아래와 같이 '상수'를 대입하는 일은 할 수 없으며:
msg2 = { "Hello Another World!" };
  • C컴파일러가 구조체의 값을 일일히 복사하는 작업을 별도로 수행하기 때문에 컴파일된 프로그램의 코드가 커진다.
그렇기 때문에 구조체를 복사하는 경우에도 대입을 하는 방법 보다는 memcpy() 함수를 이용하여 복사하는 편이 프로그램 코드의 크기가 작을 수 있다.
반대로 일일히 메모리를 복사하는 코드를 컴파일러가 작성해 주기 때문에 memcpy() 함수를 이용하여 복사하는 것 보다 대입을 하여 구조체의 값을 복사하는 편이 속도면 에서는 더 빠를 수 있다.


구조체의 비트필드
+/-

C 구조체가 갖는 매력중의 하나는 메모리를 비트 단위로 잘라서 액세스 할 수 있다는 점이다. 이렇게 비트 단위로 분할해서 사용하는 구조체 멤버함수를 '비트필드'라고 부른다. 실제 비트필드는 일반 개인용 컴퓨터와 같이 풍부한 메모리를 제공하는 환경에서는 그다지 큰 매력이 없을 수 도 있으나, 작은 메모리 내에 프로그램과 데이터를 함께 밀어넣어야 하는 임베디드 환경이나 전송되는 속도가 상대적으로 느린 네트워크 환경에서 실제로 사용하지 않는 메모리 공간의 줄일 수 있다는 장점을 갖는다. 실제로 위의 예에서 한 학생의 정보를 저장하기 위해 사용된 데이터 공간의 크기는 대략 40바이트 정도 이다. 그러나 실제로 따져보면 그렇게 큰 메모리 공간을 필요로 하지 않는다. 예를 들자면, 학년은 외국의 학제를 따른다고 해도 12학년 까지 뿐 일 것 이므로 12 ≈ 2^4, 즉 4비트만 있으면 된다. 반의 갯수 역시 20반, 30반 이렇게 많은 학급이 있을 수 는 없으므로, 최대 30반으로 치더라도 30 ≈ 2^5, 5비트 정도로도 표현이 가능하다. 마지막으로 번호 역시 100번이 넘는 경우는 생각하기 어려우므로 100 ≈ 2^7, 약 7비트로 표현이 된다. 결국 16비트, 즉 2바이트로 최소 3바이트, 최대 12바이트를 대신 할 수 있게 된다. 최선의 경우를 생각한다 해도 학년, 반, 번을 표현하는데 한 명의 학생당 한 바이트를 절약하게 되는 셈인데 2011 예상 고 3 수험생의 수가 70만명대 인 것으로 예측되고 있는 상황이니, 이 학생들의 학년, 반, 번호만을 전송하는데에도 70만 바이트를 모의고사 볼 때 마다 더 전송해야 되는 셈이 된다.[3] 위에서 사용한 구조체를 비트 필드를 이용해 데이터를 압축하도록 재 구성하면 다음과 같다.

#include <sys/types.h>
struct score_card_type {
   uint16_t year:3, grade:5, seq:7;
   struct articles_type {
       uint32_t korean:7, english:7, math:7;
       uint32_t total:9;
   } scores;
   double average;
};

위의 구조체에서 사용된 uint16_t와 uint32_t는 네트워크 프로그래밍 시에 많이 사용되며 부호없는 16비트 정수와 부호없는 32비트 정수 타입을 의미한다. 이 타입들에 대한 정의는 sys/types.h 파일에 기록되어 있으므로, 이 타입들을 사용하기 위해서는 #include <system/types.h>를 이용해서 해당 파일을 사용하겠다고 지정해 주어야 한다.

위의 변수들을 보면 끝에 콜론(:)과 함께 숫자가 추가된 것을 볼 수가 있을 것이다.이 숫자가 몇 비트의 데이터를 사용하겠다는 의미로 사용된다는 것을 쉽게 눈치 챌 수 있을 것이다. 그리고, 변수 total은 메모리 공간을 더 줄이기 위해 articles_type의 정의 안으로 옮겼다. 이렇게 비트 단위로 줄인 데이터는 16바이트로, 처음의 40바이트의 절반 이하로 줄어들었다.

유니온 (Union)
+/-

유니온 구조는 '공용체'로 번역 되기도 하며, 문법적인 구조는 구조체와 동일하나 모든 멤버 변수들이 하나의 메모리 공간을 공유한다는 점이 다르다. 원래 유니온은 프로그램 코드의 다형성을 구현하기 위해 만들어진 것으로, 비슷한 동작을 하지만 동작의 대상이 되는 데이터의 타입이 여러 종류인 경우에 함수의 매개 변수로 많이 사용된다. 일단 다음의 코드를 보자, 분명한 차이점을 보이기 위해 아직 언급하지 않은 내용들이 많이 사용 되었지만 그다지 어렵지는 않을 것 이다.

#include <stdio.h>
union union_test_type {
        char smsg[20];
        long int limsg;
};

int check_diff(int target_type, union union_test_type a, union union_test_type b)
{
        switch(target_type) {
        case 0:
                return a.limsg - b.limsg;
                break;
        case 1:
                return strcmp(a.smsg, b.smsg);
                break;
        default:
                break;
        }
        return 0;
}

int main (int argc, char *argv[])
{
        union union_test_type a, b;

        printf("Size of union: %d\n", sizeof(union union_test_type));
        a.limsg = 10;
        b.limsg = 20;
        printf("Differental for integer: %d\n", check_diff(0, a, b));

        strcpy(a.smsg, "Hello World!");
        strcpy(b.smsg, "Hello C World!");
        printf("Differental for message: %d\n", check_diff(1, a, b));

        return 0;
}

객체지향 언어를 아는 사람이라면 그런 언어들에서 사용되는 함수나 연산자 오버로딩에 비하면 불편하기 짝이 없어 보이겠지만 나름대로 프로그래밍의 자유도를 높이는데 큰 역할을 해준다. 물론 함수의 매개변수를 보이드 타입을 이용해도 같은 효과를 얻을 수 는 있다.

먼저 사용된 변수와 C 예약어들을 간단하게 나마 설명해 보겠다. 먼저, sizeof() 연산자는 지정된 변수나 타입에 할당되어질 메모리 공간의 크기를 바이트단위로 알아내 주는 '연산자'이다. '함수'가 아닌 '연산자'이기 때문에 변수 뿐 아니라 변수의 타입도 사용될 수 있는 것이니 나중에 함수를 배울때 헷갈리지 않도록 주의해야 한다. switch-case구조는 switch 문에 지정된 변수의 값과 동일한 case 문에 해당되는 문장을 수행는 제어구조 이다. 위의 샘플을 예로 들자면 target_type변수의 값이 0이면 case 0: 아래에 있는 문장이 수행되고, 1이면 case 1: 아래에 있는 문장이 수행된다. 자세한 설명은 나중에 제어구조에서 하도록 할 것이다.

유니온을 이용하면 다음과 같은 구조체를 사용할 수 도 있다:

union a_register {
        uint32_t eax;
        uint16_t ax;
        struct {
                uint8_t al;
                unit8_t ah;
        };
}

인라인 어셈블러를 사용하는 경우에 가끔 사용되는 구조체로 실제 x86 계열의 레지스터와 같은 형태로 메모리가 배열되기 때문에 편리하게 사용될 수 있다. 위의 경우 구조체 변수를 eax로 액세스 하게되면 32비트 전체를, ax로 액세스 하게 되면 eax의 하위 16비트만을, al은 최하위 8비트를 그리고 ah는 차하위 8비트를 액세스 할 수 있도록 해준다.

주의:

위와 같은 방법을 사용하여 만든 프로그램은 -- 사실 인라인 어셈블러를 사용하는 시점부터 소스레벨의 호환성은 완전히 사라지게 된다. 왜냐 하면 첫째, 어셈블러는 당연히 CPU마다 조금씩 다르고, 같은 CPU라 해도 컴파일러 -- 정확하게는 컴파일러 내부에서 호출하게 되는 어셈블러에 따라 표기 방식이 달라질 수 있기 때문이다. 둘째, 앞서 정수형 데이터에서 언급되었던 엔디언 문제 때문에 예상했던 것 과는 다른 결과를 얻을 수 있다. 셋째, 컴파일러는 실행속도의 향상과 사용되는 메모리의 양을 줄이기 위해 여러가지 최적화 작업이 수행되는데, 구조체의 경우 구조체 멤버의 순서를 바꿈으로서 이러한 최적화를 수행하게 된다. 그러므로 위에 적은 방식으로 유니온을 사용하는 경우에는 소스레벨의 호환성을 포기 해야 한다.

참조:

네트워크 프로그래밍을 하는 경우 패킷의 헤더를 구조체로 정의하여 사용하는 경우가 있는데, 이 경우 최적화에 의해 구조체 멤버의 순서가 바뀌는 경우가 있다. x86 계열의 CPU의 경우 CPU의 특성 때문에 32비트 CPU의 경우 액세스 되는 변수의 메모리 시작 위치가 4의 배수가 되는 위치인 경우에 액세스 속도가 가장 빠르기 때문에 각 멤버 변수들의 액세스 위치를 맞추기 위해 구조체내 순서를 바꾸어 준다. (16비트 CPU의 경우 2의 배수가 되는, 즉 짝수 메모리인 경우 홀수 메모리 인 경우 보다 액세스 속도가 빠르다.)

이와같은 문제를 해결하기 위해 일반적으로 제안되는 해결방법은 메모리 공간을 더 차지 하는 변수를 구조체의 앞쪽에 두는 것 -- 즉 long long int, long int, int, short int, char의 순서로 구조체를 구성하는 것 이지만 항상 그것이 가능한 것은 아니기 때문에 구조체를 구성할 때 2의 배수 단위로 메모리가 배치 될 수 있도록 신경을 써 주면 된다. 예를 들어:

struct samp_stru {
        uint8_t a;
        uint16_t b;
        uint32_t c;
        uint8_t d;
}

위와같이 구조체가 배열된 경우 구조체 멤버 b, c의 경우 모두 홀수 메모리에서 시작되기 때문에 액세스 속도가 느려지기 때문에 최적화를 수행하는 경우 일반적으로 다음과 같이 최적화 된다:

struct samp_stru {
        uint32_t c;
        uint16_t b;
        uint8_t a;
        uint8_t d;
}

물론, 반드시 위의 순서로 최적화 되리라는 보장은 없다. 컴파일러에 따라, 컴파일되는 타겟 시스템에 따라 전혀 컴파일 되지 않을 수 도 있다. 그러나 네트워크 패킷을 만드는 경우라면 기대한 방법대로 데이터가 전송되도록 보장되도록 구조체를 구성해야 할 필요가 있다. 그렇기 때문에 처음부터 위와 같은 방식으로 구조체를 만들어도 되고, 순서를 반드시 지켜야 한다면 다음의 두 가지 방법중 하나를 선택해서 구조체를 디자인 해도 된다.

struct samp_stru {
        uint8_t a;
        uint8_t d;
        uint16_t b;
        uint32_t c;
}
struct samp_stru {
        uint16_t a;
        uint16_t b;
        uint32_t c;
        uint8_t d;
}

주석 및 참고자료

+/-
  1. 실제로 구조체와 유니온 이라는 개념은 객체지향 언어에서의 객체 개념이나 데이터의 다형성 개념의 뿌리가 되는 개념 이기도 합니다.
  2. 실제로 정수형, 실수형등의 데이터 타입을 기본형(base type)이라고 부르고 배열, 구조체, 유니온 등을 딜리버드 타입(delivered type;유도형 이라는 표현을 사용하는 경우도 있습니다.)혹은 사용자 정의 타입(user define type)이라고 부릅니다.
  3. 실제로 이런 데이터를 옮기는 것 때문에 늘어나는 데이터의 양 보다는 네트워크를 통해 전달하기 위해 사용되는 데이터 헤더에 사용되는 정보들을 비트 필드단위로 압축하지 않은 경우에 늘어나는 양이 훨씬 더 많습니다. 그렇기 때문에 실제 비트필드의 의의는 일반적인 프로그램 보다는 하위단에서 프로그래밍을 하는 경우에 있다고 할 수 있겠습니다.