Core Programming/C for Systems Engineering

C언어 구조체와 공용체 완벽 정리: 메모리 구조 차이점 비교부터 포인터 활용까지

임베디드 친구 2024. 12. 13. 13:41
반응형

이전 글에서 변수, 포인터, 그리고 함수까지 배우면서 우리는 C 언어로 프로그램을 움직이는 핵심 뼈대를 모두 갖추었습니다. 하지만 현실 세계의 복잡한 데이터를 다루다 보면 한 가지 난관에 부딪힙니다. 예를 들어 하나의 '학생'이나 '로봇 센서' 데이터를 표현하고 싶은데, 이름은 문자열, 나이는 정수, 키는 실수형으로 데이터 타입이 제각각 다르기 때문입니다. 이처럼 서로 다른 종류의 변수들을 보기 좋게 묶어 나만의 새로운 데이터 타입을 만들 수 있게 해주는 도구가 바로 구조체(Structure)와 공용체(Union)입니다. 비슷해 보이지만 속을 들여다보면 메모리를 쓰는 방식이 완전히 반대인 두 문법의 본질을 알기 쉽게 풀어드리겠습니다.

Generated by Gemini AI.

📌 핵심 요약 3줄

  • 연관 데이터의 커스텀 패키징: 구조체(struct)를 사용하면 서로 다른 자료형의 변수들을 하나의 의미 있는 단위로 묶어 관리 효율성을 극대화할 수 있습니다.
  • 메모리를 공유하는 공용체: 공용체(union)는 모든 멤버 변수가 동일한 메모리 시작 주소를 공유하므로, 한 번에 하나의 데이터만 사용할 때 메모리를 극적으로 절약합니다.
  • 화살표 연산자(->)의 본질: 구조체나 공용체를 포인터 주소 방식으로 접근할 때는 온점(.) 대신 화살표 연산자를 사용해 동적 메모리를 제어합니다.

1. 구조체(Structure): 데이터를 하나의 단위로 묶기

구조체는 여러 가지 자료형을 하나의 바구니에 담아두는 서랍장과 같습니다. 서랍 안의 변수들은 각각 독립된 공간을 차지합니다.

💻 기본 구조체 정의와 초기화 예제

C
 
#include <stdio.h>

// 사람의 인적사항을 담는 구조체를 정의합니다.
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    // 구조체 변수를 선언하고 초기화합니다.
    struct Person person1 = {"홍길동", 30, 175.5};

    // 온점(.) 연산자를 사용해 내부 멤버에 접근합니다.
    printf("이름: %s\n", person1.name);
    printf("나이: %d 세\n", person1.age);
    printf("키: %.1f cm\n", person1.height);

    return 0;
}

2. 구조체 포인터와 동적 할당 활용

실무에서는 구조체 변수를 통째로 넘기기보다 포인터 주소만 넘겨 작업하는 경우가 대부분입니다. 이때 포인터를 통해 구조체 멤버에 접근하려면 화살표 연산자(->)를 사용합니다.

C
 
#include <stdio.h>
#include <stdlib.h>

struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    // 구조체 크기만큼 힙 메모리에 자리를 빌려옵니다.
    struct Person *personPtr = (struct Person *)malloc(sizeof(struct Person));

    if (personPtr == NULL) {
        printf("⚠️ 메모리 할당에 실패했습니다.\n");
        return 1;
    }

    // 포인터로 접근할 때는 화살표(->) 연산자를 사용합니다.
    printf("이름을 입력하세요: ");
    scanf("%s", personPtr->name);
    printf("나이를 입력하세요: ");
    scanf("%d", &personPtr->age);
    printf("키를 입력하세요: ");
    scanf("%f", &personPtr->height);

    // 데이터 출력
    printf("\n--- 입력된 정보 ---\n");
    printf("이름: %s\n나이: %d\n키: %.1f cm\n", personPtr->name, personPtr->age, personPtr->height);

    // 다 쓴 동적 할당 메모리는 반드시 반납합니다.
    free(personPtr);
    return 0;
}

3. 공용체(Union): 메모리 단 한 칸을 나눠 쓰기

공용체는 구조체와 문법적으로 매우 비슷하지만, 내부 동작은 완전히 다릅니다. 모든 멤버 변수가 같은 메모리 주소에서 시작하여 공간을 공유합니다. 즉, 안방 하나를 두고 정수, 실수, 문자열이 돌아가면서 방을 차지하는 구조입니다.

💻 공용체 센서 데이터 처리 예제

다양한 형태(정수형, 실수형, 문자열형)로 들어오는 임베디드 장비의 센서 데이터를 하나의 공용체 공간에서 효율적으로 스위칭하는 예제입니다.

C
 
#include <stdio.h>
#include <string.h>

// 데이터 형태가 바뀔 때마다 메모리를 새로 파지 않고 공유합니다.
union SensorData {
    int intValue;
    float floatValue;
    char strValue[20];
};

void printSensorData(union SensorData data, int type) {
    switch (type) {
        case 1:
            printf("정수형 데이터: %d\n", data.intValue);
            break;
        case 2:
            printf("실수형 데이터: %.2f\n", data.floatValue);
            break;
        case 3:
            printf("문자열 데이터: %s\n", data.strValue);
            break;
        default:
            printf("알 수 없는 데이터 타입입니다.\n");
    }
}

int main() {
    union SensorData data;

    // 1. 정수 데이터 저장
    data.intValue = 42;
    printSensorData(data, 1);

    // 2. 실수 데이터 저장 (이 순간 기존 정수 데이터는 지워지고 덮어씌워집니다)
    data.floatValue = 3.14;
    printSensorData(data, 2);

    // 3. 문자열 데이터 저장 (이 순간 실수 데이터는 파괴됩니다)
    strcpy(data.strValue, "Sensor_A");
    printSensorData(data, 3);

    return 0;
}

4. 구조체와 공용체의 메모리 핵심 차이점 총정리

두 개념의 차이를 직관적으로 비교해 보겠습니다. 똑같이 정수 하나와 실수 하나를 멤버로 가져도 컴퓨터가 할당하는 주소 방식 체계가 다릅니다.

비교 특징 구조체 (struct) 공용체 (union)
메모리 할당 방식 모든 멤버 변수가 각각 독자적인 메모리 방을 가집니다. 모든 멤버 변수가 하나의 첫 시작 주소를 공유합니다.
최종 메모리 크기 기본적으로 모든 멤버 변수 크기의 합산입니다. 멤버 변수 중 가장 크기가 큰 변수의 크기를 따릅니다.
동시 데이터 유지 변수 안의 모든 멤버 값을 동시에 언제든 읽고 쓸 수 있습니다. 오직 가장 마지막에 갱신한 멤버 변수 한 개만 유효합니다.
주요 사용 목적 연관된 다양한 특성의 데이터를 한 묶음으로 설계할 때 사용합니다. 메모리가 극도로 제한된 환경에서 버퍼 공간을 절약할 때 사용합니다.

💡 C 언어 사용자 정의 자료형을 위한 개발 팁 (Tip)

  1. 타입 정의 키워드 typedef를 적극 활용하세요: 구조체를 선언할 때마다 매번 struct Person person1;처럼 앞에 struct를 붙이는 것은 꽤 번거롭습니다. 정의할 때 typedef struct { ... } Person; 형태로 적어주면 다음부터 일반 원시 자료형처럼 Person person1; 기호만 써서 깔끔하게 선언할 수 있습니다.
  2. 함수 인자로 구조체를 넘길 때는 무조건 주소(포인터)로 넘기세요: 50바이트짜리 구조체를 일반 값(Call by Value) 형태로 함수에 넘기면 매번 50바이트의 복사 연산이 일어나 스택 메모리가 낭비됩니다. 단 4바이트 또는 8바이트 크기의 주소값만 넘기는 구조체 포인터(Call by Reference) 방식을 쓰는 것이 성능 면에서 압도적으로 유리합니다.
  3. 공용체는 데이터 변환(Type Punning) 툴로도 쓰입니다: 하드웨어 통신을 할 때 4바이트짜리 float 실수 데이터를 1바이트짜리 데이터 4개로 쪼개서 전송해야 할 때가 있습니다. 이때 float와 char arr[4]를 공용체로 묶어두면, 실수를 대입하자마자 별도의 형 변환 연산 없이도 각 바이트의 16진수 원본 비트 열을 곧바로 추출해 낼 수 있어 편리합니다.

⚠️ 초보자가 흔히 하는 실수 (Mistakes)

  • 구조체 패딩(Padding)으로 인한 크기 예측 오류: char형(1바이트) 하나와 int형(4바이트) 하나를 가진 구조체의 크기를 계산하면 당연히 5바이트가 나올 것 같지만, sizeof를 찍어보면 8바이트가 나옵니다. 32비트/64비트 CPU가 데이터를 고속으로 읽기 위해 4바이트 단위로 메모리 정렬을 맞추면서 중간에 빈 공간(더미 바이트)을 끼워 넣기 때문입니다. 네트워크 패킷 전송 시 크기가 어긋날 수 있으므로 주의해야 합니다.
  • 공용체 멤버의 동시 사용 처리 오류: 공용체에 data.intValue = 100;을 넣은 직후에 data.floatValue = 5.5;를 대입하고 나서 다시 data.intValue를 출력하면 전혀 엉뚱한 깨진 숫자가 나옵니다. 이전 정수 데이터 비트 위에 실수의 비트 열이 덮어써 졌기 때문입니다. 공용체는 절대로 두 멤버의 값을 동시에 유지할 수 없음을 망각하면 안 됩니다.
  • 구조체 정의 끝에 세미콜론 ; 누락: 처음 문법을 접할 때 가장 빈번하게 발생하는 오타입니다. 함수 블록 {} 끝에는 세미콜론이 붙지 않지만, 구조체와 공용체는 선언문이기 때문에 중괄호가 닫히는 시점 뒤에 반드시 문장의 끝을 알리는 세미콜론 ;을 명시해 주어야 컴파일 에러가 나지 않습니다.

🔚 맺음말

이번 포스팅에서는 흩어져 있던 다양한 데이터 타입의 변수들을 하나의 유기적인 생명체로 묶어주는 구조체와, 좁은 메모리를 영리하게 쪼개 쓰는 공용체의 설계 메커니즘을 살펴보았습니다. 이 두 도구를 자유자재로 다룰 수 있게 되면 비로소 현실 세계의 객체와 하드웨어 레지스터 제어 장치들을 C 언어 코드로 모델링하는 진정한 설계자의 길로 들어서게 됩니다.

오늘 학습한 구조체 배정 방식에 대해 궁금한 점이 있다면 언제든 소프트웨어 공장 댓글 창에 질문을 남겨주세요!

반응형