구조체, 공용체, 함수
구조체, 구조체의 필요성
-
구조체
- 데이터의 집합
- 여러 자료형을 가진 변수들을 하나의 패키지로 만들어 놓은 것
- 주소를 전달하지 않는 한 값형
-
구조체의 필요성
- 사람은 세상을 바라볼 때 물체 단위로 봄
- 구조체를 사용하면 실수도 막을 수 있다
- 같은 형의 데이터 여러 개를 매개변수로 받을 때 순서가 바뀌면 컴파일러가 실수를 찾을 방법이 없음
실수를 줄이려면 원자성을 보장하는 연산(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++;
- 세 코드 모두 같은 의미!
구조체 매개변수 베스트 프랙티스
- 값으로 전달 vs 주소로 전달
- 구조체의 경우 데이터 크기가 클 수도 있음
- 포인터에 const 포인터를 붙이면 원본도 못바꾸니 안전
- 구조체 매개변수 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);
- 보통 assert()를 사용해서 크기를 확인
어쩔 수 없이 패딩이 생길 거라면 구조체에 패딩을 명시적으로 넣기도 함
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*를 사용하면 어떤 포인터도 받을 수 있는 함수 탄생
- 단, 다음과 같은 경우 다른 포인터로 캐스팅 또는 대입해서 써야 함
- 역참조
- 포인터 산술 연산
'프로그래머 > C, C++' 카테고리의 다른 글
[면접 대비] C를 사용한 해시 맵 구현 (0) | 2020.11.29 |
---|---|
[면접 대비] C를 사용한 linked list 구현 (0) | 2020.11.29 |
[포프 tv 복습] C 자료구조 기초 (0) | 2020.11.29 |
[포프 tv 복습] 레지스터, 스택 & 힙, 동적 메모리. 다중 포인터 (0) | 2020.11.28 |
[포프 tv 복습] 가변 인자 함수, 올바른 오류 처리 방법 (0) | 2020.11.26 |