본문 바로가기

프로그래머/C, C++

[포프 tv 복습] 구조체, 공용체, 함수 포인터

구조체, 공용체, 함수

구조체, 구조체의 필요성


  • 구조체

    • 데이터의 집합
    • 여러 자료형을 가진 변수들을 하나의 패키지로 만들어 놓은 것
    • 주소를 전달하지 않는 한 값형
  • 구조체의 필요성

    • 사람은 세상을 바라볼 때 물체 단위로 봄
    • 구조체를 사용하면 실수도 막을 수 있다
    • 같은 형의 데이터 여러 개를 매개변수로 받을 때 순서가 바뀌면 컴파일러가 실수를 찾을 방법이 없음

실수를 줄이려면 원자성을 보장하는 연산(atomic operation)을 사용하는 게 좋음


구조체의 선언 및 사용

세미콜론 잊지 말 것.

struct date{
    int year;
    int month;
    int day;
};

date란 구조체. 변수명은 date.

struct date date;

함수의 매개변수로도 사용 가능

int is_moday(struct date date);
  • 지역 변수 선언시 0으로 초기화 안 됨
  • 너무 장황한 구조체의 데이터형

typedef이란?, typedef 사용법

size_t, 보통 이런 식으로 되어있음

typedef unsigned int    size_t;

구조체에 typedef를 쓰면 다른 자료형처럼 간결한 변수 선언 가능

typedef struct date date_t;

date_t date;

더 간단히 쓸 수도 있다.

typedef struct date{
    int year;
    int month;
    int day;
} date_t;

typedef struct {
    int year;
    int month;
    int day;
} date_t;
  • 어느 방법을 써도 크게 상관 없음.
  • 참고로 C에서 _t로 끝나는 자료형은 보통 이렇게 typedef한 것.

enum도 마찬가지

enum game_role{
    GAME_ROLE_MID,
    GAME_ROLE_JUNGLE
};

enum game_role role = GAME_ROLE_MID;
enum game_role{
    GAME_ROLE_MID,
    GAME_ROLE_JUNGLE
};

typedef enum game_role game_role_t;

game_role_t role = GAME_ROLE_MID;

구조체 변수 초기화 하기

date_t date = {0, };
  • 배열 초기화 때 썻던 방법
  • 컴파일러에 따라 memset()으로 알아서 바꿔줌

구조체 매개변수

구조체 포인터에서 멤버의 값에 접근하기

void increase_year(date_t* date)
{
    (*date).year = (*date).year + 1;
}
  • () 필요한 이유?
    • 연산자 우선순위 때문

-> 연산자

(*date).year = (*date).year + 1;
date->year = date->year + 1;
date->year++;
  • 세 코드 모두 같은 의미!

구조체 매개변수 베스트 프랙티스

  1. 값으로 전달 vs 주소로 전달
    • 구조체의 경우 데이터 크기가 클 수도 있음
    • 포인터에 const 포인터를 붙이면 원본도 못바꾸니 안전
  2. 구조체 매개변수 vs 여러개의 개별 변수?
    • 보통 변수 많이 전달하는 대신 구조체 하나 전달
    • 이유는 다양
      • 실수 줄이기 위해
      • 성능 빠르게 하기 위해(주소전달)

함수 반환 값으로서의 구조체, 구조체 배열

  • 복사에 의한 반환
  • 구조체로 반환하면 실질적으로 여러 개의 값을 반환하는 격

얕은 복사, 깊은 복사

  • 실제 데이터가 아니라 주소를 복사하는 것을 얕은 복사라고 함
  • 깊은 복사는 대입만으로는 안 됨
    구조체 변수마다 독자적인 메모리 공간을 만들어주고 거기에 문자열을 복사해야 함
    • 동적 메모리

구조체 사용시 포인터 저장의 문제

포인터 변수 없는 name_t. 크기는 64.

enum{ NAME_LEN = 32 };
typedef struct{
    char firstname[NAME_LEN];
    char lastname[NAME_LEN];
} name_t;
  • 포인터만 없으면 된다

구조체를 다른 구조체의 멤버로 사용하기, 바이트 정렬

typedef user_info {
    unsigned int id;        // 4
    name_t name;            // 64
    unsigned short height;    // 4
    float weight;            // 4
    unsigned short age;        // 4
};
  • 어떤 시스템은 n바이트 배수인 시작 주소에서만 메모리를 접근 가능
  • x86 시스템은 4바이트 경계에서 읽어오는 게 효율적
  • 따라서 컴파일러가 알아서 각 멤버의 시작 위치를 그 경계에 맞춤
  • 그러기 위해 안쓰는 바이트를 덧붙임(padding)
  • 따라서 어떤 아키텍처에서 저장한 파일을 다른 아키텍처에서 읽으면 잘못 읽힐 수도 있음
typedef struct{
    unsigned int id;
    name_t name;
    unsigned short height;
    unsigned short age;
    float weight;
} user_info_t;    // sizeof(user_info_t) 76
  • 2개의 short형 변수가 4바이트로 합체
#pragma pack(push, 1)
typedef struct{
    unsigned int id;
    name_t name;
    unsigned short height;
    float weight;
    unsigned short age;
} user_info_t;
#pragma pack(pop)

구조체 베스트 프랙티스

  • 구조체를 파일이나 다른 데 저장해야 해서 바이트 크기가 정확히 맞아야 한다면?
    • 보통 assert()를 사용해서 크기를 확인
      #include <assert.h>
      assert(siszeof(user_info_t) == 76);

어쩔 수 없이 패딩이 생길 거라면 구조체에 패딩을 명시적으로 넣기도 함

typedef struct{
    unsigned int id;
    name_t name;
    unsigned char height;
    unsigned char age;
    char unused[2];
} user_info_t;
  • 특히 데이터의 전체 크기가 4바이트로 안 나눠 떨어질 때

비트필드

  • 비트 플래그 : bool 여럿을 효율적으로 저장
  • C에서 구조체를 사용하면 매우 간단히 비트 플래그를 구현 가능

크기는 1이다!

typedef struct{
    unsigned char b0 : 1;
    unsigned char b1 : 1;
    unsigned char b2 : 1;
    unsigned char b3 : 1;
    unsigned char b4 : 1;
    unsigned char b5 : 1;
    unsigned char b6 : 1;
    unsigned char b7 : 1;
} bitflags_t;

bitflags_t flags = {0, };
flags.b3 = 1;

이 방식은 멤버 함수들끼리 비교만 가능

int is_set  = (flags.b1 == 1)         // ok
int is_same = (flags.b1 == flags.b7) // ok
int is_all  = (flags == 0xFF)        // ok
int is_zero = (flags == 0)             // ok

포인터를 활용하면 할 수 있다.

char* val;
int is_zero;
bitflags_t flags = {0, };

flags.b3 = 1;

val = (char*)&flags;
is_zero = (*val == 0);
  • 이걸 좀 더 제대로 해주는 c언어 기능이 공용체

공용체

  • 똑같은 메모리 위치를 다른 변수로 접근하는 방법
  • 즉, 공용체 안에 있는 여러 변수들이 같은 메모리를 공유

val로도 bits로도 접근이 가능함

typedef union{
    unsigned char val;
    struct{
        unsigned char b0 : 1;
        unsigned char b1 : 1;
        unsigned char b2 : 1;
        unsigned char b3 : 1;
        unsigned char b4 : 1;
        unsigned char b5 : 1;
        unsigned char b6 : 1;
        unsigned char b7 : 1;
    } bits;
} bitflags_t;

int is_same;
int is_zero;
bitflags_t flags = {0, };

flags.bits.b1 = 1;
flags.bits.b4 = 1;

is_same = (flags.bits.b1 == flags.bits.b7);
is_zero = (flags.val == 0);
  • 동일한 메모리를 두 개의 서로 다른 자료형으로 접근. 그러나 그 메모리 안에 있는 값은 동일하다!

함수 포인터

  • 함수도 어디다 저장해 둔 뒤 매개변수로 전달할 수 없을까?
  • 어떤 함수를 호출할 때는 직접 함수명을 씀
  • 그러나 어셈블리어로는 그 함수의 주소로 점프
result = calculate(op1, op2, operator); // operator = add()

result = calculate(op1, op2, &operator); // operator = add()
  • 둘 다 가능
  • 그러나 보통 위의 방법을 더 많이 씀

매개변수로 전달되는 함수는...

  • 다음과 같은 내용이 있어야 함
    • 자기 자신이 받아야 하는 매개변수 목록
    • 자기 자신이 반환하는 자료형
double add(double x, double y)
{
    return x + y;
}
double (*func)(double, double) = add;
result = func(op1, op2);

double calculate(double, double, double (*)(double, double));
double calculate(double x, double y, double (*func)(double, double))
{
    return func(x, y);
}
result = calculate(op1, op2, add);

함수 포인터 선언

<반환형> (*<변수명>) (<매개변수 목록>);
  • 함수의 시작 주소를 저장하는 변수
  • 함수의 매개변수 목록과 반환형을 반드시 표기해야 함
int (*ops[])(int, int) = {add, sub, mul, div};
  • ops는 배열
    • 배열의 각 요소는 함수 포인터인데
      • 두 개의 int형 매개변수를 받고
      • int형을 반환
void (*bsd_signal(int, void (*)(int)))(int);
  • bsd_signal 함수의선언
    • 매개변수
      • int형
      • 함수 포인터 : void (*)(int)
    • 반환형
      • 함수 포인터 : void (*)(int)

배열의 포인터

int scores[3] = {80, 90, 100};
int (*p)[3] = &scores; // ok

int scores[5] = {11, 22, 33, 44, 55};
int (*p)[3] = &scores; // compile err

2차원 배열을 배개변수로 받을 때 쓸 수 있음

<자료형> (*<변수이름>)[<요수의 수>];
// void do_magic(int matrix[][10], size_t m)
// {
//     printf("m[1][2]: %d", matrix[1][2]);
// }

void do_magic(int (*matrix)[10], size_t m)
{
    printf("m[1][2]: %d", *(*(matrix + 1) + 2));
}

int main(void)
{
    int matrix[5][10] = {
        ...
    };

    do_magic(matrix, 5);
}

함수 포인터 예 : 퀵 정렬

void qsort(void *ptr, size_t count, size_t size, int (*comp)(const void*, const void*));

void*

  • 범용적 포인터
  • 어떤 포인터라도 여기에 대입 가능
    • 따라서 어떤 변수의 주소라도 곧바로 대입 가능
    • 매개변수 형으로 void*를 사용하면 어떤 포인터도 받을 수 있는 함수 탄생
  • 단, 다음과 같은 경우 다른 포인터로 캐스팅 또는 대입해서 써야 함
    • 역참조
    • 포인터 산술 연산