변수와 기본 타입
C에서 기본 타입을 고를 때 `int`와 `stdint.h`를 어디서 나눌지, `double`과 `float`, `const`, `unsigned`의 실전 선택 기준과 흔한 함정을 정리합니다.
int count = 100;
double ratio = 3.1415926535;
const int max_size = 256;
uint32_t flags = 0x000000FF;Category
Preparing references and filters for this topic. 이 주제의 레퍼런스와 필터를 준비하고 있습니다.
Category Reference
타입, 제어 흐름, 포인터, 메모리, 문자열 처리, 전처리, 디버깅까지 C의 핵심 흐름을 카드형 레퍼런스로 정리합니다.
Search titles, summaries, tags, and subcategories.
Showing 48 cards.
Subcategory
8 cards
C에서 기본 타입을 고를 때 `int`와 `stdint.h`를 어디서 나눌지, `double`과 `float`, `const`, `unsigned`의 실전 선택 기준과 흔한 함정을 정리합니다.
int count = 100;
double ratio = 3.1415926535;
const int max_size = 256;
uint32_t flags = 0x000000FF;C의 주요 연산자 그룹과 식 평가에서 자주 틀리는 지점을 함께 정리합니다. 정수 나눗셈, 증가 연산자, 단락 평가, 우선순위 함정까지 한 장에서 다시 볼 수 있게 구성합니다.
int q = a / b;
double x = (double)a / b;
if (ptr != NULL && *ptr > 0) { process(*ptr); }
if (x == 10) { /* 비교 */ }C 형변환은 문법보다도 결과가 언제 바뀌는지가 중요합니다. 정수 나눗셈, signed/unsigned 비교, 잘림과 승격을 중심으로 다시 찾기 쉽게 정리합니다.
int total = 7;
int count = 2;
double a = (double)total / count; // 3.5
double b = (double)(total / count); // 3.0이름이 보이는 범위와 값이 살아 있는 기간은 다른 개념입니다. `local`, `static`, `extern`을 헷갈릴 때 다시 찾기 쉽게 정리합니다.
static int file_only = 0;
void counter(void) {
static int calls = 0;
calls++;
}비트 연산은 플래그 조작과 마스크 작업에서 자주 다시 찾습니다. `&`, `|`, `^`, `~`, shift`를 암기보다 조작 패턴 중심으로 정리합니다.
flags |= BIT; // set
flags &= ~BIT; // clear
flags ^= BIT; // toggle
if (flags & BIT) { }`sizeof`는 크기 계산 도구이지만, 배열 decay와 함수 인자 경계에서 자주 오해됩니다. 배열 길이 계산과 `malloc(sizeof *ptr)` 패턴을 중심으로 정리합니다.
size_t count = sizeof arr / sizeof arr[0];
int *p = malloc(sizeof *p * n);`enum`은 상태 이름을 붙여 매직 넘버를 줄이고, `typedef`는 긴 타입 표기를 줄입니다. 둘을 같은 카드에서 다루되 역할은 섞지 않게 정리합니다.
typedef enum {
STATUS_OK,
STATUS_WARN,
STATUS_ERROR
} Status;`volatile`과 `restrict`는 둘 다 최적화와 관련 있지만 완전히 다른 문제를 해결합니다. 하드웨어/시그널 쪽의 `volatile`, aliasing 약속인 `restrict`를 분리해서 정리합니다.
volatile uint32_t *status = (volatile uint32_t *)0x40000000;
void copy_fast(int * restrict dst, const int * restrict src, int n);4 cards
`if/else if/else`와 삼항 연산자를 언제 고를지, 조건 순서와 분기 밀도가 어떻게 가독성을 바꾸는지 정리합니다.
if (ready) { start(); }
if (score >= 60) { pass(); }
else { fail(); }
if (score >= 90) { grade = 'A'; }
else if (score >= 80) { grade = 'B'; }
else { grade = 'F'; }`for`, `while`, `do-while` 중 무엇을 고를지, `break`와 `continue`를 어디까지 허용할지, 오프-바이-원과 무한 루프를 피하는 기준을 정리합니다.
for (int i = 0; i < len; i++) {
sum += scores[i];
}
while ((ch = getchar()) != '\n' && ch != EOF) {
/* 버퍼 비우기 */
}
do {
retry();
} while (!done);`switch`는 값 기반 분기에 적합하지만 `break` 하나만 빠져도 흐름이 무너집니다. fallthrough를 의도적으로 쓰는 경우와 실수하는 경우를 분리해서 정리합니다.
switch (command) {
case CMD_START:
start_engine();
break;
case CMD_STOP:
stop_engine();
break;
default:
fprintf(stderr, "unknown command\n");
break;
}C에서 `goto`는 아무 데나 쓰는 도구가 아니라, 여러 자원을 획득한 함수의 정리 경로를 한곳에 모으는 용도로 가장 실전적입니다. cleanup 레이블 패턴을 중심으로 정리합니다.
if (fp == NULL) goto cleanup;8 cards
함수 원형이 왜 필요한지, 값 전달과 포인터 전달을 어디서 나누는지, 반환값 설계와 출력 매개변수의 기본 기준을 정리합니다.
int max_of(int a, int b);
int best = max_of(left, right);
void log_message(const char *msg);
log_message("done");
void divide(int a, int b, int *q, int *r);
divide(10, 3, &q, &r);C 배열을 고정 길이 연속 메모리로 읽는 법, 같은 스코프에서만 길이를 안전하게 구하는 패턴, 함수 호출 시 배열 decay가 왜 중요한지 정리합니다.
int scores[5] = {90, 85, 100, 70, 88};
int len = (int)(sizeof(scores) / sizeof(scores[0]));
for (int i = 0; i < len; i++) {
printf("%d\n", scores[i]);
}다차원 배열은 결국 메모리에 줄줄이 이어진 행 우선 블록입니다. 함수 인자에서 왜 열 크기를 알아야 하는지와 동적 2D 배열 선택지를 중심으로 정리합니다.
int m[2][3] = {
{1, 2, 3},
{4, 5, 6}
};배열은 함수 경계를 넘는 순간 길이 정보를 잃습니다. 그래서 `arr + n`이나 `(ptr, len)` 패턴이 기본이 되며, `const`는 읽기 전용 의도를 드러내는 도구로 같이 봐야 합니다.
int sum_array(const int *arr, size_t n);재귀는 코드 모양보다 종료 조건과 호출 깊이가 핵심입니다. 언제 반복문보다 자연스럽고, 언제 스택 부담 때문에 피해야 하는지 중심으로 정리합니다.
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}배열 경계 문제는 대부분 `<`와 `<=` 하나에서 시작합니다. C는 경계를 검사해 주지 않기 때문에 off-by-one이 바로 메모리 오염으로 이어질 수 있다는 점에 초점을 둡니다.
for (size_t i = 0; i < n; i++) {
arr[i] = 0;
}`qsort`와 `bsearch`는 API 암기보다 비교 함수 규약이 더 중요합니다. 정렬 기준, 오버플로우 없는 비교, 정렬 후 검색 흐름을 한 번에 정리합니다.
int cmp_int(const void *a, const void *b) {
int ia = *(const int *)a;
int ib = *(const int *)b;
return (ia > ib) - (ia < ib);
}함수 포인터 선언을 읽는 법, 콜백으로 동작을 주입하는 패턴, dispatch table과 `qsort` 연결까지 함수 포인터를 다시 찾는 상황 중심으로 정리합니다.
int add(int a, int b) { return a + b; }
int (*fp)(int, int) = add;
printf("%d\n", fp(2, 3));9 cards
포인터를 주소 저장 문법이 아니라 원본 수정과 공유를 위한 도구로 읽는 법, `&`와 `*`, NULL 체크, 잘못된 포인터가 왜 위험한지 정리합니다.
int value = 10;
int *ptr = &value;
printf("%d\n", *ptr);
*ptr = 20;
printf("%d\n", value);배열이 언제 포인터처럼 동작하는지, 함수 인자에서 길이 정보가 왜 사라지는지, decay가 일어나지 않는 예외 상황을 정리합니다.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
printf("%d\n", p[2]);
printf("%d\n", *(p + 2));힙 할당이 왜 필요한지, `malloc`과 `free`를 소유권 관점에서 어떻게 읽어야 하는지, 누수와 use-after-free를 막는 기본 규칙을 정리합니다.
int n = 10;
int *arr = malloc(sizeof(int) * n);
if (arr == NULL) {
return 1;
}
free(arr);
arr = NULL;`ptr + 1`이 바이트가 아니라 원소 단위로 이동하는 이유, 배열 인덱싱과의 관계, 같은 배열 안에서만 안전하다는 제약을 정리합니다.
int data[] = {10, 20, 30, 40, 50};
int *ptr = data;
printf("%d\n", *ptr);
printf("%d\n", *(ptr + 1));
ptr++;`const`가 포인터 선언 어디에 붙느냐에 따라 무엇을 못 바꾸는지, 읽기 전용 API 설계에 왜 중요한지, 문자열 리터럴과 함께 어떻게 읽어야 하는지 정리합니다.
int value = 10;
const int *p1 = &value;
int *const p2 = &value;
const int *const p3 = &value;포인터가 가리키는 값이 아니라 포인터 변수 자체를 바꿔야 할 때 왜 `T **`가 필요한지, out parameter와 `argv` 해석까지 한 장에서 정리합니다.
int *data = NULL;
good_alloc(&data);`calloc`을 언제 `malloc` 대신 써야 하는지, `realloc`이 포인터를 이동시킬 수 있다는 점, 동적 배열 성장에서 왜 임시 변수 패턴이 필수인지 정리합니다.
int *arr = calloc(10, sizeof(int));
int *tmp = realloc(arr, 20 * sizeof(int));
if (tmp == NULL) {
free(arr);
return NULL;
}
arr = tmp;`memcpy`, `memmove`, `memset`를 언제 각각 써야 하는지, 겹치는 영역과 바이트 수 계산에서 왜 자주 깨지는지 정리합니다.
memcpy(dst, src, sizeof(src));
memmove(buf, buf + 1, len);
memset(dst, 0, sizeof(dst));C 메모리 버그를 증상보다 원인 패턴으로 추적하는 체크리스트, 버퍼 오버런/use-after-free/double free/누수와 ASan·Valgrind 사용 기준을 정리합니다.
// gcc -fsanitize=address -g -o prog prog.c
int *arr = malloc(5 * sizeof(int));
arr[5] = 99; // ASan이 바로 잡아내기 좋은 버그
free(arr);8 cards
C 문자열을 별도 타입이 아니라 `char` 배열과 널 종료 규약으로 읽는 법, 비교/복사/입력에서 가장 흔한 함정을 정리합니다.
char name[20] = "Mina";
size_t len = strlen(name);
int same = (strcmp(name, "Mina") == 0);구조체를 관련 데이터 묶음으로 쓰는 기준, `typedef`와 필드 접근, 값 복사와 얕은 복사의 차이, 큰 구조체를 포인터로 넘겨야 하는 이유를 정리합니다.
typedef struct {
char name[32];
int age;
double score;
} Student;구조체 포인터로 원본을 수정하는 흐름, `->`가 `(*ptr).field`의 축약이라는 점, 동적 할당 구조체에서 무엇을 조심해야 하는지 정리합니다.
typedef struct {
char name[20];
int score;
} Student;
Student s = {"Kim", 90};
Student *ptr = &s;
ptr->score += 5;`strncat`, `strstr`, `strchr`, `strtok`, `snprintf`, `strtol` 같은 문자열 함수들을 언제 써야 하는지와 각각의 위험 지점을 정리합니다.
char buf[64] = "Hello";
strncat(buf, ", World", sizeof(buf) - strlen(buf) - 1);
char *pos = strstr(buf, "World");
if (pos) printf("found at offset %td\n", pos - buf);`union`은 여러 멤버를 동시에 담는 구조가 아니라 같은 메모리를 여러 해석으로 공유하는 구조입니다. 태그 공용체와 메모리 공유 감각을 중심으로 정리합니다.
union Value {
int i;
float f;
};구조체 크기가 멤버 크기 합과 다를 수 있는 이유는 정렬과 패딩 때문입니다. 필드 순서, `offsetof`, 직렬화 위험을 중심으로 정리합니다.
struct Padded {
char a;
int b;
char c;
};`isdigit`, `isalpha`, `isspace`, `toupper`, `tolower`는 입력 검증과 문자 정규화에서 자주 쓰입니다. 특히 `unsigned char` 캐스트가 왜 필요한지 중심으로 정리합니다.
if (isdigit((unsigned char)c)) { }
if (isspace((unsigned char)c)) { }
ch = (char)tolower((unsigned char)ch);연결 리스트는 문법보다 포인터 연결, head 교체, 해제 책임이 핵심입니다. 삽입/삭제/순회/해제에서 어디가 자주 깨지는지 중심으로 정리합니다.
typedef struct Node {
int val;
struct Node *next;
} Node;6 cards
`printf`, `fprintf`, `fgets`, `getchar`를 언제 고르는지와 표준 스트림 세 가지의 역할을 빠르게 구분할 수 있게 정리합니다.
printf("count=%d\n", count);
fprintf(stderr, "error: %s\n", msg);파일 I/O는 `fopen`보다도 열기 실패, 읽기 결과 확인, 텍스트와 바이너리 구분, 닫기 책임이 핵심입니다. 자주 쓰는 흐름만 한 장으로 정리합니다.
FILE *fp = fopen("notes.txt", "r");
if (fp == NULL) {
perror("fopen");
return 1;
}
/* read or write */
fclose(fp);`scanf` 뒤에 문자 입력이 꼬이는 이유, 반환값 검사와 버퍼 비우기 패턴, `fgets + sscanf`가 더 나은 상황을 한 장에 모았습니다.
if (scanf("%d", &n) != 1) {
/* 실패 처리 */
}
scanf(" %c", &ch);`argc`와 `argv`는 어렵지 않지만, 숫자 변환과 옵션 파싱에서 금방 허술해집니다. `strtol`, 플래그 처리, usage 출력 기준을 중심으로 정리합니다.
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "usage: %s <input>\n", argv[0]);
return 1;
}
return 0;
}`assert`는 개발 중 전제 확인용이고, `errno`는 실패 원인을 꺼내는 도구입니다. 둘 다 오류 처리처럼 보이지만 쓰는 시점이 다르다는 점에 초점을 맞춥니다.
assert(ptr != NULL);
FILE *fp = fopen(path, "r");
if (fp == NULL) {
perror("fopen");
}현재 시각, 경과 시간, CPU 시간, 날짜 포맷을 각각 어떤 API로 보는지 나눠서 정리합니다. `time`, `difftime`, `clock`, `strftime`의 역할이 섞이지 않게 보는 카드입니다.
time_t now = time(NULL);
double elapsed = difftime(end, start);5 cards
전처리기는 C 문법을 이해하지 않고 텍스트를 바꿉니다. 그래서 `#include`, `#define`, 조건부 컴파일을 쓸 때도 타입 안정성보다 치환 규칙을 먼저 봐야 합니다.
#include <stdio.h>
#define MAX_BUFFER 256
#ifdef DEBUG
#define LOG(msg) fprintf(stderr, "[DBG] %s\n", (msg))
#else
#define LOG(msg) ((void)0)
#endif헤더는 인터페이스를 공유하는 곳이고, include guard는 같은 선언이 여러 번 들어오는 것을 막는 장치입니다. 무엇을 헤더에 두고 무엇을 `.c`에 둘지 빠르게 판단하게 정리합니다.
/* state.h */
#ifndef STATE_H
#define STATE_H
extern int g_count;
void reset_count(void);
#endifC 빌드는 컴파일과 링크가 분리됩니다. 그래서 선언과 정의가 다른 파일에 있어도 컴파일은 되지만, 링크에서 `undefined reference`나 `multiple definition`이 터질 수 있습니다.
cc -Wall -Wextra -c main.c
cc -Wall -Wextra -c math_utils.c
cc main.o math_utils.o -o app함수형 매크로가 위험한 이유는 결국 텍스트 치환과 중복 평가입니다. 어떤 경우에 매크로를 버리고 `static inline`으로 옮겨야 하는지 판단 기준을 정리합니다.
#define MAX(a, b) ((a) > (b) ? (a) : (b))
/* MAX(i++, j++) 는 위험 */`inline`은 성능 마법이 아니라 작은 함수 표현 방식입니다. 헤더에서 왜 `static inline`을 쓰는지, 매크로 대체로 언제 적절한지 중심으로 정리합니다.
static inline int max_int(int a, int b) {
return a > b ? a : b;
}