C 치트시트 — 빠른 레퍼런스
C(C11/C17) 문법, 포인터, 메모리, 문자열, 구조체, 파일 I/O를 다루는 자체 포함형 간결 레퍼런스. 추가로 스레드, 소켓, 정규식, 시간, 프로세스, 빌드/디버그 요점도 포함. 일상적인 필요의 약 80%를 충족합니다.
C C17 (ISO/IEC 9899:2018)
ISO C · 절차적, 수동 메모리 관리 · 정적 · 약한 타입(캐스트 있음)
추천 학습 경로
C 초보자이신가요? 다음 순서로 읽어 보세요. 1. Hello World — 컴파일, 실행, 종료 코드 2. 변수와 상수 — 타입, 크기, const 3. 포인터와 배열 — &, *, ->, 포인터 연산 4. 제어 흐름 — if / switch / 루프 5. 함수와 가변 인자 — 값 전달 vs 포인터 전달 6. 문자열과 변환 — printf 포맷 표 필요할 때 파일 I/O와 오류 처리를 참고하세요. 구조체, 객체 스타일 패턴, 고급 섹션은 익숙해진 후 보세요. 특정 작업이 있나요? 위 검색 상자를 사용하세요 — 「malloc」「socket」「pthread」「qsort」「regex」를 시도해 보세요.
1.Hello World 및 빌드 환경
C 프로그램 컴파일·실행·구성: 도구 체인, IDE 선택, 여러 파일, 명령줄 인자, 종료 코드.
최소 프로그램
모든 C 프로그램은 main()에서 실행을 시작하고 운영체제에 int 상태 값을 반환합니다. 0은 성공을, 그 외의 값은 오류를 의미합니다. 맨 위의 #include가 선언을 가져와야 컴파일러가 printf가 무엇인지 알 수 있습니다.
컴파일과 실행 (gcc / clang)
C는 인터프리터 언어가 아니라 컴파일 언어입니다. 컴파일러가 소스 코드를 실행 파일로 번역한 뒤 그것을 실행합니다. 항상 경고를 켜고(-Wall -Wextra) 언어 표준을 고정해서(-std=c17) 이식성 문제가 컴파일 시점에 드러나도록 하세요.
툴체인과 IDE 목록
컴파일러와 편집기가 필요합니다. 흔한 조합은 Linux에서는 gcc, macOS에서는 clang, Windows에서는 MinGW-w64 또는 MSVC이며 모두 VS Code나 CLion에서 호출할 수 있습니다. 다른 문제를 파헤치기 전에 먼저 버전 명령을 한 번 실행해 툴체인이 동작하는지 확인하세요.
명령줄 인자
main은 사용자가 명령줄에 입력한 내용을 받을 수 있습니다. argc는 인자의 개수이고 argv[]는 인자 자체이며, argv[0]은 언제나 프로그램 이름입니다. 인자는 문자열로 파싱되므로 수치가 필요하면 strtol/strtod로 변환하세요.
다중 파일
큰 프로그램은 여러 개의 .c 파일로 나누고 선언을 담은 공용 .h 헤더 파일을 함께 둡니다. 각 .c 파일은 개별로 컴파일된 뒤 링크됩니다. 헤더 파일은 선언과 정의를 일치시켜 주므로 시그니처를 하나 바꾸면 그것을 사용하는 모든 곳이 동시에 드러납니다.
종료 코드
main이 반환한 값은 셸로 전달됩니다. 0(EXIT_SUCCESS)은 성공을, 그 외의 값은 실패를 뜻합니다. 스크립트와 CI가 모두 이 관례에 의존하므로 종료 코드는 의미를 가져야 합니다. Linux/macOS에서는 $?로, Windows에서는 %ERRORLEVEL%로 확인합니다.
환경 변수
getenv()는 프로세스 환경의 변수를 읽어 그 값을 반환하며, 설정되어 있지 않으면 NULL을 반환합니다. 재컴파일 없이 설정을 주입하는 표준적인 방법이지만, 읽어 온 값은 신뢰할 수 없는 입력으로 다뤄야 합니다.
전처리기 기초
전처리기는 컴파일 전에 실행됩니다. #define은 매크로를 정의하고, #와 ##는 토큰을 연결하며, #ifdef/#if는 코드를 조건부로 컴파일합니다. 순수한 텍스트 기반의 타입 없는 계층이므로 대부분의 경우에는 진짜 함수와 const를 우선하세요.
2.변수와 상수
선언, 타입 크기, 고정 너비 정수, const 위치, 저장 클래스, typedef, 열거형.
기본 선언
변수를 선언할 때는 타입을 먼저 쓰고 이름을 쓴 뒤 선언 위치에서 초기화합니다(C99+). 선언과 동시에 초기화하면 나중에 초기화되지 않은 쓰레기 값을 읽는 일을 피할 수 있습니다.
타입 크기 (64비트 일반값)
C는 최소 크기만 보장하므로 int는 플랫폼에 따라 16비트일 수도 32비트일 수도 있습니다. sizeof로 대상 플랫폼의 실제 크기를 출력해 정직하게 확인하고, 바이트 수를 절대 하드코딩하지 마세요.
고정 폭 타입 (C99+, <stdint.h>)
메모리 배치가 정확해야 할 때(파일 형식, 네트워크 프로토콜, 해시) <stdint.h>의 int32_t/uint64_t 등을 사용하세요. 이들은 플랫폼이 실제로 제공할 때만 존재하며, 바로 그 점이 정확한 폭을 나타내는 이름이 선호되는 이유입니다.
부호 없는 / 부호 있는
부호 없는 타입은 결코 음수가 되지 않고 2^N으로 모듈러 연산되어 순환합니다. 부호 있는 타입은 음수가 될 수 있습니다. 둘이 한 표현식에 섞이면 부호 있는 피연산자가 부호 없는 타입으로 승격되는데, 이는 대표적인 함정입니다(faq 절 참고).
const — 읽기 전용 (컴파일 시점 검사)
const는 변수를 읽기 전용으로 만들어 컴파일러가 빌드 시점에 의도치 않은 쓰기를 거부할 수 있게 합니다. 바뀌어서는 안 되는 것에는 모두 const를 붙이세요. 의도를 드러낼 뿐 아니라 컴파일러 최적화에도 도움이 됩니다.
const 포인터 위치 (대표적 혼동 지점)
포인터 선언에서 const의 위치가 무엇을 보호할지 결정합니다. * 앞에 있으면 가리키는 값을, * 뒤에 있으면 포인터 자체를 보호합니다. 타입을 오른쪽에서 왼쪽으로 읽으면 이 두 줄의 차이를 이해할 수 있습니다.
저장 클래스
저장 클래스는 변수가 어디에 저장되고 얼마나 오래 사는지를 결정합니다. auto는 지역 변수의 기본값이고, 파일 스코프에 붙은 static은 이름이 파일 밖에서 보이지 않게 하며, 지역 변수에 붙은 static은 호출 사이에 값을 유지시키고, extern은 다른 곳에 정의된 이름을 참조합니다.
typedef — 타입 별칭
typedef는 타입에 새 이름을 붙입니다. 새로운 타입을 만드는 것이 아니라 별칭일 뿐이지만, 별칭이 있으면 더 짧고 일관된 타입을 쓸 수 있어 struct와 함수 포인터의 잡음을 읽기 쉬운 한 단어 속에 감출 수 있습니다.
enum — 이름 있는 상수
enum은 서로 관련된 정수 상수 묶음에 이름을 붙입니다. 값은 0부터 시작해 1씩 증가하지만 명시적으로 값을 주거나 범위를 지정할 수도 있습니다. 열거형은 그 자체로 문서 역할을 하며 switch에도 쓸 수 있습니다.
지정 초기화 (C99+)
C99는 멤버를 이름으로 초기화하거나(.port = 8080) 배열 요소를 첨자로 초기화하는 것을([3] = 9) 순서에 상관없이 허용합니다. 언급하지 않은 멤버는 자동으로 0이 되므로, 길게 늘어선 위치 기반 초기화보다 훨씬 명확합니다.
복합 리터럴 (C99+)
복합 리터럴은 캐스트와 비슷한 문법으로 임시 struct/배열 값을 인라인으로 만듭니다. 함수에 일회성 값을 넘기고 싶을 때 이름 있는 변수를 먼저 선언할 필요가 없어 편리합니다.
volatile (메모리 맵 I/O / 시그널)
volatile은 어떤 변수가 프로그램의 정상적인 흐름 밖에서 바뀔 수 있음을 컴파일러에 알립니다. 하드웨어 레지스터, 시그널 핸들러 또는 다른 스레드가 그렇습니다. 이는 컴파일러가 접근을 캐시하거나 재배열하지 못하게 막습니다.
3.구조체 · 공용체 · 집계 타입
구조체 레이아웃, 공용체, 비트 필드, sizeof, _Static_assert, 정렬 — 실제 C 데이터의 구성 요소.
struct — 이종 레코드
struct는 타입은 다르지만 서로 관련된 값들을 하나의 레코드로 묶습니다. 멤버는 선언 순서대로 저장되고 사이에 정렬용 패딩이 삽입됩니다. 그래서 sizeof(struct)는 멤버 크기의 합보다 큰 경우가 많습니다.
struct 포인터 — -> 로 멤버 접근
화살표 p->member는 (*p).member의 축약형으로, 구조체 포인터를 역참조해 멤버에 접근합니다. 구조체를 포인터로 넘기면 비용이 매우 적고(워드 하나) 함수가 호출자의 레코드를 수정할 수도 있습니다.
익명 struct + typedef
태그가 없는 struct에 typedef를 붙이면 선언 한 줄로 깔끔한 타입 이름을 얻을 수 있습니다. 자주 쓰는 레코드 타입에 이름을 정의하는 관용적인 방법으로, 곳곳에 struct를 쓰지 않아도 됩니다.
union — 겹치는 저장 공간 (크기 = 최대 멤버)
union은 여러 멤버를 같은 메모리 블록에 두며 크기는 가장 큰 멤버를 따릅니다. 마지막에 쓴 멤버만 유효하고 다른 멤버를 읽는 것은 미정의 동작이므로 태그 필드를 함께 두어야 합니다.
비트 필드 — 플래그 비트 패킹
비트 필드는 여러 개의 작은 플래그를 정수의 비트에 몰아넣어 불리언 값이 많은 레코드의 메모리를 절약합니다. 배치는 컴파일러가 결정하므로 비트 필드는 하나의 번역 단위 안에만 두고 ABI 경계를 넘기지 마세요.
sizeof — 객체가 차지하는 바이트 수
sizeof는 타입이나 객체가 차지하는 바이트 수를 알려 줍니다. 여전히 스코프 안에 있는 배열이라면 sizeof a / sizeof a[0]으로 요소 개수를 얻을 수 있지만, 배열이 포인터로 감쇠하는 순간 이 방법은 소리 없이 무너집니다(faq 참고).
_Static_assert (C11+) 컴파일 시점 검사
_Static_assert(cond, msg)는 컴파일 시점에 단언하며 cond가 거짓이면 빌드가 실패합니다. 크기와 ABI 가정이 깨지는 바로 그 순간에 검증하기에 좋으며, 런타임에야 발견하는 일을 막아 줍니다.
정렬 — & _Alignof (C11+)
정렬은 객체가 메모리의 어느 주소에 놓일 수 있는지를 결정하며, 컴파일러는 각 멤버가 자연스럽게 정렬되도록 struct에 빈 공간을 채웁니다. _Alignof는 객체의 정렬 값을 알려 주고, _Alignas는 필요할 때 그 값을 높입니다(SIMD, 캐시 라인 공유).
가변 배열 멤버 (C99+) — struct 끝의 배열
struct는 길이가 정해지지 않은 배열(data[])로 끝날 수 있으며, 길이는 할당 시점에 정해집니다. malloc 한 번으로 struct와 추가 바이트를 함께 할당합니다. 버퍼 크기를 런타임에야 알 수 있을 때 쓰는 C의 관용구입니다.
독립 타입으로서의 열거형 (C23)
C23은 열거형의 기반 타입을 고정할 수 있게 해(enum Status : unsigned char) 크기를 더 이상 컴파일러가 결정하지 않도록 합니다. 그렇지 않으면 열거형은 int와 호환되며, 대부분의 코드에는 이것으로 충분합니다.
4.포인터와 배열
주소 가져오기, 역참조, 포인터 연산, 배열 붕괴, 포인터 vs 배열, const 정확성.
주소 연산자 (&)와 역참조 (*)
&는 객체의 주소를 취해 포인터를 얻고, *는 그 반대로 포인터를 통해 읽고 씁니다. 이 둘을 함께 쓰면 값에 간접적으로 접근하고 다른 스코프에서 그 값을 수정할 수 있습니다.
NULL — 널 포인터
NULL은 "아무것도 가리키지 않는" 포인터로, 어떤 유효한 주소와도 다릅니다. 이를 역참조하는 것은 미정의 동작이며 보통 바로 크래시가 나므로, 사용하기 전에 반드시 NULL인지 확인하세요.
포인터 산술 (sizeof(*p) 단위로 스케일)
포인터에 정수를 더하면 바이트가 아니라 요소 단위로 전진합니다. p + 2는 int 두 개를 건너뜁니다. 첨자 p[i]는 사실 *(p + i)입니다. 배열의 끝을 넘어서는 것은 미정의 동작입니다.
배열 감쇠 (표현식에서 포인터로)
대부분의 표현식에서 배열 이름은 첫 요소를 가리키는 포인터가 됩니다. 그 결과 함수 내부에서는 매개변수로부터 배열 길이를 복원할 수 없으므로, 길이를 배열과 함께 넘겨야 합니다.
포인터 vs 배열
char 배열은 바이트를 스스로 소유하므로 수정할 수 있지만, 문자열 리터럴을 가리키는 char*는 보통 읽기 전용 메모리를 가리킵니다. 리터럴 포인터를 통해 쓰는 것은 미정의 동작이며 자주 크래시로 이어집니다.
다중 포인터
포인터를 가리키는 포인터(int**)는 간접 참조 단계가 하나 더 있습니다. 문자열 배열, 2차원 구조, 그리고 "함수가 메모리를 할당하고 호출자가 해제를 책임지는" 출력 매개변수에 쓰입니다.
함수 포인터
함수 포인터는 함수의 주소를 저장하며 간접적으로 호출할 수 있습니다. 콜백과 디스패치 테이블, 그리고 oop 절에 나오는 객체 지향 패턴들의 토대가 됩니다.
포인터 배열 (예: argv)
char* 배열은 문자열 목록을 담는 관용적인 컨테이너이며 argv가 바로 그것입니다. 각 요소는 각자 NUL로 끝나는 문자열을 가리키고, 배열 자체는 포인터의 나열일 뿐입니다.
const 정확성
포인터 매개변수를 const로 선언하면(예: const char* s) 함수가 가리키는 내용을 바꾸지 않겠다고 약속하는 것입니다. 그래야 호출자가 문자열 리터럴과 배열을 안심하고 넘길 수 있으며, 컴파일러가 그 약속을 강제합니다.
배열 포인터 vs 포인터 배열
int *a[5]는 포인터 5개로 이루어진 배열이고, int (*b)[5]는 int 5개짜리 배열을 가리키는 포인터입니다. 괄호가 모든 것을 바꿉니다. 복잡한 선언은 오른쪽에서 왼쪽으로 읽거나 cdecl로 해독하세요.
5.제어 흐름
if/else, 삼항 연산자, switch, for, while, do-while, break/continue, goto, setjmp/longjmp.
if / else if / else
조건에 따라 분기합니다. 조건이 참인 첫 번째 분기가 실행되고, 그렇지 않으면 else로 갑니다. else-if 체인은 여러 후보를 순서대로 검사하며 첫 번째로 맞는 것에서 멈춥니다.
삼항 연산자 (표현식)
cond ? a : b는 문장이 아니라 표현식으로, cond에 따라 a 또는 b를 취합니다. 간결한 값 선택에 적합하며, 한 줄에 담기지 않거나 읽기 어렵다면 if/else로 바꾸세요.
switch — break가 없으면 폴스루
switch는 정수 값으로 분기합니다. break가 없으면 실행이 다음 case로 흘러 들어가므로, 폴스루를 의도적으로 활용하거나(빈 case 쌓기) 각 case마다 break를 두어야 합니다. default는 일치하지 않는 값을 처리합니다.
for — init에서 카운터 선언 (C99+)
for(init; cond; step)은 표준적인 카운팅 루프입니다. init 부분에서 카운터를 선언하면(C99+) 그 범위가 루프 안으로 한정되어 바깥의 동명 변수와 충돌하는 것을 피할 수 있습니다.
while / do-while
while은 매 반복 전에 조건을 검사하므로 한 번도 실행되지 않을 수 있습니다. do-while은 항상 먼저 한 번 실행한 뒤 검사하므로 메뉴나 입력 검증 프롬프트처럼 "최소 한 번은 물어야 하는" 상황에 적합합니다.
break / continue
break는 가장 안쪽 루프나 switch를 즉시 빠져나가고, continue는 다음 반복으로 건너뜁니다. 잘 쓰면 중첩 로직을 평탄하게 만들고 플래그 변수를 없앨 수 있지만, 잘못 쓰면 제어 흐름을 감추므로 중첩을 최대한 줄이세요.
goto — 깊은 중첩 탈출 (드물게, 신중히)
goto는 특정 줄의 레이블로 점프합니다. 정당한 용도는 깊은 중첩에서 빠져나와 오류 정리를 한곳에 모으는 것, 즉 goto-cleanup 관용구입니다. 그 외의 경우에는 return이나 플래그 변수가 거의 항상 더 명확합니다.
setjmp / longjmp 비지역 점프
setjmp는 실행 컨텍스트를 저장하고 longjmp는 임의의 위치에서 그것을 복원해 저장 지점까지 되감습니다. 그 과정의 모든 정리 작업은 건너뛰어지며 실행되는 소멸자도 없으므로, goto나 명시적인 return에 비해 대개 이득이 없습니다.
루프 관용구
알아둘 만한 반복 형태: 카운트 범위에 대한 인덱스 반복, 배열에 대한 포인터 순회, 그리고 break가 있는 무한 반복. 의도를 가장 직접적으로 표현하는 형태를 선택하세요.
단락 평가 (Short-circuit)
&&와 ||는 왼쪽에서 오른쪽으로 평가되며, 결과가 확정되면 즉시 멈춥니다. 이것이 ptr && ptr->x가 안전한 이유입니다: 두 번째 피연산자는 첫 번째가 참일 때만 실행되므로, 인라인에서 역참조를 보호할 수 있습니다.
6.함수와 가변 인자
값 전달 vs 포인터 전달, 함수 포인터와 typedef, 가변 인자, 재귀, _Generic.
값으로 전달 — 호출자의 변수는 변하지 않음
C의 모든 인자는 값으로 전달됩니다 — 호출된 함수는 복사본을 받고, 이를 수정해도 호출자에게 영향을 주지 않습니다. 간단한 스칼라와 작은 구조체는 이렇게 전달합니다; 수정이 필요하거나 큰 객체가 걱정될 때는 포인터 형식을 사용하세요.
포인터로 전달 (호출자 수정)
호출자의 변수를 수정하려면 그 주소를 전달하세요. 호출된 함수가 포인터를 역참조하여 쓰면, 변경 사항이 호출자에게 반영됩니다 — scanf 같은 함수가 바로 이런 방식으로 출력을 생성합니다.
const 인자 — 읽기 전용 계약
const 인자(예: const char* s)는 읽기 전용 계약입니다: 함수가 그 내용을 수정하지 않겠다고 약속합니다. 이를 통해 호출자는 안전하게 문자열 리터럴을 전달할 수 있으며, API의 의도가 명확해집니다.
배열 인자의 decay (길이 전달 필요)
배열 인자는 포인터로 decay되며, 길이는 함께 전달되지 않습니다. 반드시 명시적으로 길이를 전달하세요 — 그렇지 않으면 함수는 데이터의 끝을 알 수 없습니다.
함수 포인터 + typedef
함수 포인터 시그니처에 한 번 typedef를 적용한 다음, 별칭을 사용하여 변수와 인자를 선언하세요. 이렇게 하면 디스패치 테이블과 콜백이 괄호와 별표의 벽처럼 보이지 않습니다.
가변 인자 — printf 스타일 함수
시그니처에 ...가 있는 함수는 printf처럼 가변 개수의 인자를 받습니다. va_start/va_arg/va_end로 읽습니다; 고정 인자는 그 뒤에 몇 개가 더 있는지 반드시 명시해야, 그렇지 않으면 호출을 안전하게 디코딩할 수 없습니다.
가변 인자 포워딩 — v* 계열 함수
가변 인자를 다른 가변 인자 함수(예: 로그 래퍼가 vfprintf로 포워딩)로 전달하려면, v* 계열을 사용하고 va_list 자체를 전달하세요. va_arg로 다시 읽으면 목록이 깨집니다.
재귀
재귀 함수는 더 작은 같은 종류의 문제에서 자기 자신을 호출하며, 기저 조건에 도달할 때까지 이어집니다. 각 호출은 스택 공간을 차지하므로, 깊이가 깊거나 제어할 수 없는 경우에는 반복을 우선하세요.
_Generic (C11+) — 컴파일 타임 타입 디스패치
_Generic(C11)은 제어 표현식의 타입에 따라 컴파일 타임에 결과 표현식을 선택합니다 — 함수 오버로딩을 가볍게 모방하고 타입 안전한 매크로를 작성하는 방법입니다.
noreturn (C11+)과 inline
noreturn은 절대 반환하지 않는 함수(예: 치명적 오류 루틴)를 표시하여, 컴파일러가 도달할 수 없는 코드를 추론할 수 있게 합니다. inline은 단지 요청일 뿐 보장은 아닙니다 — 컴파일러는 여전히 실제 호출을 생성할 수 있습니다.
7.문자열과 숫자 변환
NUL 종료 문자열, 안전한 쓰기, strtol/strtok/sscanf 변환, printf 포맷 표.
기초 — NUL로 끝나는 문자 배열
C 문자열은 NUL 바이트('\0')로 끝나는 문자 배열입니다. strlen은 종결 문자에 도달할 때까지 셉니다; 문자열을 담는 버퍼는 반드시 그 공간을 남겨둬야 합니다, 그렇지 않으면 쓰기가 오버플로됩니다.
복사 / 비교 / 연결 (안전하지 않은 것 vs 안전한 것)
strcpy/strcat은 경계를 확인하지 않으므로, 소스 문자열이 너무 크면 조용히 오버플로됩니다. 경계가 있고 항상 NUL로 끝나는 쓰기를 하려면 snprintf를 우선 사용하세요; strcmp/strncmp는 비교; strncat은 최대 n개 문자를 복사한 후 종결 문자를 추가합니다.
검색
strchr은 문자의 첫 등장을 찾고, strrchr은 마지막 등장을 찾으며, strstr은 부분 문자열의 첫 등장을 찾습니다. 모두 문자열 내부를 가리키는 포인터를 반환하며, 찾지 못하면 NULL을 반환합니다.
mem* — 원시 바이트 (NUL을 포함한 임의의 메모리 처리 가능)
mem* 함수(memcpy, memmove, memset, memcmp)는 원시 바이트를 처리하여 내장된 NUL도 처리할 수 있습니다. memcpy와 memmove의 차이는 겹침 허용 여부입니다 — 겹칠 가능성이 있으면 memmove를 사용하세요.
숫자 변환: strtol/strtod
strtol/strtod로 숫자를 파싱하세요: 이들은 오류를 보고하고 파싱이 멈춘 위치를 알려줍니다. atoi는 잘못된 입력에 대해 조용히 0을 반환하여 불법 입력을 가립니다 — 정확성을 추구하는 코드에서는 사용하지 마세요.
견고한 파싱 — 전체 문자열 검증
한 번의 완전한 파싱은 세 가지를 검증해야 합니다: strtol 후 errno가 설정되지 않음, 최소한 한 자리 숫자가 소비됨(end != s), 전체 문자열이 읽혀짐(*end == '\0'). 그 중 하나라도 건너뛰면 꼬리 쪽의 쓰레기 데이터가 들어옵니다.
토큰화: 재진입 가능한 strtok_r
strtok은 문자열을 제자리에서 수정하여 각 구분 문자에 '\0'을 삽입합니다; _r 접미사가 있는 버전은 재진입 가능(스레드 안전)합니다. 원래 문자열이 여전히 필요하면 먼저 복사하세요.
sscanf — 포맷 문자열로 파싱 (매칭 수 반환)
sscanf는 포맷 문자열로 문자열을 파싱하여 성공적으로 매칭된 필드 수를 반환합니다. 이 반환 값을 확인하세요: 예상보다 낮다면 입력은 당신이 생각한 모양이 아닙니다.
printf 포맷 표
printf 계열은 동일한 포맷 언어를 공유합니다: %d int, %ld long, %zu size_t, %f double, %s 문자열, %p 포인터, 그리고 폭과 정밀도 지정자. 포맷 지정자는 인자 타입과 정확히 일치해야 합니다 — 불일치는 정의되지 않은 동작입니다.
안전하게 문자열 연결하기
snprintf는 최대 n 바이트를 쓰고 항상 NUL로 끝나므로, 안전한 문자열 연결 방법입니다. 계속 늘어나는 텍스트의 경우, 길이를 기억하고 버퍼를 realloc하거나 작은 동적 버퍼 헬퍼 함수를 사용하세요.
8.배열 · 정렬 · 검색
배열 반복, 비교 함수를 사용한 qsort/bsearch, 2차원 배열, 구조체 배열.
고정 길이 배열 — 길이를 사용한 반복
C 배열은 고정 크기의 연속된 요소 블록입니다. 길이로 반복하세요: 진짜 배열이 여전히 스코프에 있을 때 sizeof a / sizeof *a가 유효하지만, 일단 함수에 전달되면 크기는 사라집니다.
초기화 패턴
배열은 위치별, 지정된 인덱스([2] = 9), 또는 전체를 0으로 ({0}) 초기화할 수 있습니다. 2차원 배열은 배열의 배열이며, 행 우선 순서로 저장됩니다: m[i][j]는 각 행 내에서 연속됩니다.
qsort — 비교 함수로 정렬
qsort는 비교 함수를 사용하여 제자리에서 정렬하며, 비교 함수는 <0, 0, >0을 반환합니다. 비교 함수는 요소를 가리키는 포인터를 받으므로, 안전하게 비교하기 위해 먼저 void* 인자를 요소 타입으로 변환하세요(오버플로 방지).
bsearch — 정렬된 배열에서 이진 검색
bsearch는 이진 검색을 수행합니다 — 하지만 이미 정렬된 배열에만 사용 가능합니다. 일치하는 요소를 가리키는 포인터를 반환하며, 찾지 못하면 NULL을 반환합니다. 큰 배열에서는 정렬 후 검색이 선형 스캔보다 낫습니다.
문자열 비교 함수 (char* 배열)
char* 배열을 정렬할 때는 포인터 값이 아닌 문자열을 비교해야 합니다. 비교 함수는 char* const*로 캐스팅하고, 역참조하여 각 문자열 포인터를 얻은 다음 strcmp를 호출합니다.
구조체 배열 + 필드별 정렬
특정 필드로 구조체 배열을 정렬하려면, 두 개의 구조체 포인터를 받아 해당 필드를 비교하는 비교 함수(예: a->key와 b->key의 3방향 비교)를 작성하세요. qsort가 전체 레코드를 재배열합니다.
배열 포인터 인자 (2차원 형상 유지)
2차원 배열을 함수에 전달할 때, 인자 타입에 열 수를 포함해야 합니다 — int m[][COLS] — 그래야 컴파일러가 각 행의 시작점을 계산할 수 있습니다. 첫 번째 차원(행 수)은 자유롭게 변할 수 있습니다.
동적 배열 패턴 (mem 섹션도 참조)
성장 가능한 컬렉션은 malloc된 블록이며, 가득 차면 realloc합니다: 용량과 길이를 별도로 기록하고, 증가 시 두 배로 늘리며, 사용 후 free합니다. 이것은 고수준 언어의 vector/list에 해당하는 C의 구현입니다.
가변 길이 배열 (C99 옵션)
가변 길이 배열은 런타임 값에 따라 스택에 할당됩니다(int vla[n]). C11에서는 선택 사항이며, n이 너무 크면 스택이 폭발할 수 있습니다 — 크거나 이식성이 필요한 것은 malloc을 우선 사용하세요.
2차원 배열 — 행 우선 순서
int m[ROWS][COLS]는 연속된 행 우선 블록입니다; m[i][j]는 *(*(m + i) + j)와 같습니다. 인덱싱은 행의 시작점을 계산하기 위해 행의 길이를 알아야 하므로, 형상은 컴파일 타임에 결정되어야 합니다.
9.동적 메모리와 소유권
malloc/calloc/realloc/free, 스택 vs 힙, 정렬, 소유권 규칙, goto cleanup 패턴.
할당과 해제
malloc은 초기화되지 않은 힙 메모리를 반환하며, 각 할당은 결국 해제되어야 합니다. 사용 전에 반드시 NULL인지 확인하세요; 해제 후 포인터를 NULL로 설정하면, 이후의 중복 해제는 안전한 no-op이 됩니다.
calloc: count×size 0으로 초기화
calloc(count, size)는 메모리를 0으로 초기화하고, 두 인자를 곱하며, 오버플로도 감지해줍니다. 초기화된 저장 공간이 필요하고 요소 개수가 알려진 경우 우선 사용하세요.
realloc — 크기 조정; 메모리 블록 이동 가능
realloc은 할당 크기를 조정하며, 블록을 새 주소로 이동할 수 있습니다. 먼저 임시 변수로 반환 값을 받아 확인하세요: 실패 시 기존 블록은 여전히 유효하지만, 포인터를 먼저 덮어쓰면 누수됩니다.
스택 vs 힙
스택 메모리는 자동으로 관리되고 빠르며, 반환 시 회수되지만 작고 수명이 짧습니다. 힙 메모리는 수동으로 관리되며, 크고 함수를 넘어 생존할 수 있습니다. 크고 수명이 긴 버퍼는 힙에 두어야 합니다.
소유권 — 간단한 규칙 모음
최소한의 소유권 규율 집합이 메모리 문제를 통제 가능하게 만듭니다: 할당한 자가 해제한다; 각 포인터는 한 번만 해제; 해제 후 NULL로 설정; realloc 실패 시 이전 포인터 유지. 규약이 C의 유일한 자원 관리자입니다.
힙의 문자열
힙의 문자열은 strlen(s) + 1 바이트가 필요합니다 — 추가된 1바이트는 NUL 종결 문자입니다. malloc(strlen(s) + 1)로 할당 후 복사하세요; +1을 잊으면 폭발을 기다리는 1바이트 오버플로가 됩니다.
내부 포인터를 포함하는 struct
힙 데이터를 소유하는 struct는 해제할 두 개의 할당이 있습니다: 먼저 내부 버퍼를, 그 다음 struct 자체를. 내부 포인터의 소유자를 결정하고 문서화한 다음, 할당의 역순으로 해제하세요.
goto cleanup (단일 종료 지점)
C에는 소멸자가 없으므로, goto-cleanup은 C의 RAII입니다: 각 함수당 하나의 종료 지점, 하나의 정리 블록, 모든 경로가 그곳에 도달. 각 자원은 각 경로에서 정확히 한 번 해제됩니다.
정렬 / 과잉 정렬 (C11)
aligned_alloc은 지정된 정렬로 메모리를 할당할 수 있으며(SIMD, 캐시 라인 공유), 크기는 정렬 값의 정수배여야 합니다. 일반 malloc은 플랫폼의 기본 정렬만 보장합니다.
메모리 누수 / 오버플로 감지 도구 (build 섹션 참조)
누수, 오버플로, 해제 후 사용은 모두 프로세스가 죽을 때까지 숨겨져 있습니다 — valgrind 또는 AddressSanitizer로 기계적으로 찾아내세요. 둘 다 build 섹션에서 다루며, 추가할 수 있는 가장 저렴한 안전망입니다.
10.객체 스타일 패턴
C는 절차적이지만, 구조체와 함수 포인터로 클래스, 상속, 다형성을 흉내 낼 수 있습니다.
"클래스" = struct + vtable
C에서 클래스를 모방: struct로 데이터를 담고, 함수 포인터 테이블(vtable)로 동작을 담습니다. 메서드는 명시적인 self/this 포인터 인자를 가진 일반 함수입니다.
내장을 통한 상속 구현
기본 클래스의 struct를 첫 번째 멤버로 내장하면 상속이 됩니다: 파생 struct의 주소는 기본 클래스의 주소와 같으므로, 파생 값을 기본 클래스가 예상되는 모든 곳에서 사용할 수 있으며, 기본 클래스 메서드는 그대로 작동합니다.
생성자 / 소멸자 쌍
할당 및 초기화를 담당하는 init 함수와 해제 및 정리를 담당하는 free 함수를 짝지으세요 — C 버전의 생성자/소멸자. 생성된 각 객체는 정확히 한 번 소멸되어야 합니다.
불투명 포인터 — 구현 숨기기 (PIMPL)
헤더 파일에서는 struct를 전방 선언만 하고, 정의는 .c 파일에만 둡니다. 호출자는 포인터를 보유할 수 있지만 내부를 볼 수 없습니다 — 이것이 PIMPL이며, 구현을 숨겨 내부 변경이 호출자를 깨지 않게 합니다.
인터페이스 = 함수 포인터로 구성된 struct
함수 포인터로 가득 찬 struct가 바로 인터페이스입니다: 그것을 채우는 모든 구현이 연결될 수 있으며, 각 함수는 인스턴스 컨텍스트로 void* self를 추가로 받습니다. 이것이 C 형태의 다형성입니다.
타입 소거 데이터 (void *)
void*는 타입을 지워서, 하나의 컨테이너가 이종 값을 담을 수 있게 합니다. 타입을 안전하게 복원하려면, 일반적으로 포인터가 실제로 무엇을 가리키는지 기록하는 태그 필드를 추가해야 합니다.
*this를 반환하여 메서드 체이닝 구현
각 setter는 객체 자신(*this)을 반환하여 호출을 연결할 수 있게 합니다: builder->setA(x)->setB(y). 이는 빌더 스타일 구성을 간결하게 만들지만, 반환 값을 무시할 수 있다는 대가가 있습니다.
지연 초기화 싱글톤 (스레드 비안전)
지연 초기화 싱글톤은 첫 사용 시 인스턴스를 static 포인터에 할당합니다. 이 단순한 버전은 스레드 안전하지 않습니다 — 두 스레드가 최초 할당을 두고 경쟁할 수 있으므로 — 일단 공유되려면 락을 추가해야 합니다.
참조 카운팅 (간단 버전)
retain/release는 객체에 참조 카운트를 부여하고, 0이 되면 해제합니다. Objective-C의 기반이며, C 객체에 있어서는 가비지 컬렉션보다 가벼운 소유권 모델입니다.
완전한 OOP도 가능 (glib)
C에도 완전한 OOP 프레임워크가 존재합니다 — glib과 GObject가 그 증거입니다 — 하지만 실제 복잡도를 수반합니다. 가장 단순한 vtable과 불투명 포인터면 대부분의 요구를 충족합니다; 무거운 도구는 정말 필요한 프로젝트에만 남겨두세요.
11.오류 처리
예외 없음: errno, out 매개변수를 사용한 반환 코드, goto cleanup, 오류 열거형, 어서션.
호출 실패 후의 errno
라이브러리 함수와 시스템 함수는 실패 시 전역 errno를 설정하므로, 호출 직후 곧바로 읽거나 복사해야 합니다 — 이후 어떤 호출이든 이를 덮어쓸 수 있습니다. strerror(errno)는 오류 코드를 사람이 읽을 수 있는 메시지로 변환합니다.
반환 코드 + 출력 매개변수 (일반적인 C 관용구)
C의 주류 오류 관용구는 다음과 같습니다. 성공 시 0을 반환하고, 실패 시 음의 오류 코드를 반환하며, 결과는 출력 매개변수 포인터로 내보냅니다. 실패 시 결과 포인터는 NULL로 유지되거나 변경되지 않으므로, 호출자가 이를 기준으로 판단할 수 있습니다.
오류 열거형 — 명시적 오류 분류
함수가 발생시킬 수 있는 오류의 종류 — OK, INVALID_INPUT, NOMEM, IO — 를 열거하고 그것들을 반환하세요. 명명된 오류 코드는 그 자체로 문서가 되며, switch에 적합하고, 각 실패 모드를 곰곰이 생각하도록 강제합니다.
assert — 디버그용 (NDEBUG에서 제거됨)
assert(cond)는 cond가 거짓일 때 프로그램을 중단시켜 개발 단계에서 버그를 잡습니다. -DNDEBUG에서는 컴파일되지 않으므로, 사용자가 의존하는 동작을 절대 보호해서는 안 됩니다 — 참이라고 가정하는 불변식만 검증하세요.
perror — errno 설명을 출력
perror는 사용자의 메시지와 현재 errno의 설명을 stderr 한 줄에 함께 출력합니다. 시스템 호출 실패 원인을 보고하는 가장 빠른 방법입니다 — errno 부분을 위해 직접 형식 문자열을 쓸 필요가 없습니다.
_Static_assert — 컴파일 시점
_Static_assert는 런타임이 아닌 컴파일 시점에 가정을 검증합니다. 크기, 오프셋, ABI 제약을 검증하는 데 사용하세요. 프로덕션 바이너리에서 조용히 실패하는 일을 막아줍니다.
스레드 안전한 strerror_r (POSIX)
strerror는 스레드 안전하지 않으며 자신의 내부 버퍼를 덮어씁니다. strerror_r (POSIX)은 호출자가 제공한 버퍼에 쓰기 때문에, 동시 실행 스레드 각자가 각자의 메시지를 얻습니다.
시스템 호출 반환 값 검사
시스템 호출은 실패할 수 있습니다 — 파일, 소켓, 메모리 모두. 반환 값을 무시하면 사소한 한 번의 실패가 나중의 충돌이나 데이터 손상으로 변합니다; fread/fwrite는 성공하더라도 짧은 카운트를 반환할 수 있으므로 반드시 확인하세요.
ferror/feof로 오류와 EOF 구분
읽기 루프가 끝날 때 ferror()와 feof()가 그 이유를 알려줍니다. 진짜 I/O 오류인지, 파일 끝에 도달한 것인지. EOF는 오류가 아닌 정상 상태입니다 — ferror로 둘을 구분하세요.
12.파일 I/O
stdio 스트림, 줄·전체 파일 읽기, 바이너리 I/O, POSIX 디스크립터, mmap, 디렉터리 반복.
열기 / 닫기
fopen은 FILE* 스트림을 반환하며 실패 시 NULL을 반환합니다; fclose는 버퍼를 플러시하고 해제합니다. 모드는 r/w/a에 +는 읽기/쓰기를, b는 바이너리를 뜻합니다. 열기 실패 후 그 포인터를 계속 사용하면 안 됩니다.
파일 전체 읽기 (소형 파일)
소형 파일의 방법: 끝으로 seek, ftell로 크기 얻기, rewind, malloc size+1, fread로 전체 읽기. 읽은 바이트 수가 요청한 것과 일치하는지 확인하세요 — 호출 사이에 파일이 변했을 수 있습니다.
한 줄씩 읽기 (관용구)
fgets는 고정 버퍼로 한 줄을 안전하게 읽어, 개행 문자나 버퍼 한계에서 멈춥니다. strcspn으로 끝의 개행을 제거하세요 — 개행은 버퍼 안에 그대로 남기 때문입니다. 텍스트를 줄 단위로 처리하는 관용적인 방법입니다.
쓰기
fprintf/fputs/fputc는 각각 스트림에 형식화된 출력, 문자열, 문자를 씁니다. 오류는 쓰기 시점과 fclose 시점 모두에서 발생할 수 있으므로, fclose 결과를 확인해 실패한 플러시를 놓치지 않도록 하세요.
먼저 문자열로 포맷 (안전)
먼저 snprintf로 전체 출력을 버퍼로 조합한 다음 한 번에 쓰세요. 이렇게 하면 동시 출처의 인터리빙된 쓰기를 피하고, 쓰기 전에 검증할 수 있으며, 실제로 쓰기 원자성도 유지됩니다.
바이너리 I/O — 원시 바이트 읽기/쓰기
fread/fwrite는 원시 바이트를 옮기며, 고정 길이 레코드에 가장 적합합니다. sizeof(record)만큼 정확히 읽고 반환 값을 처리하세요. 짧은 읽기는 오류 또는 EOF입니다 — ferror/feof로 구분하세요.
POSIX 파일 디스크립터
stdio 아래에서, Linux/macOS는 int 파일 디스크립터용 open/read/write/close를 제공합니다. 제어가 더 세밀하고, 버퍼링이 없으며, 함정도 더 많습니다. 경계를 넘으려면 fdopen (fd를 FILE*로) 또는 fileno (FILE*를 fd로)를 쓰세요.
mmap — 파일을 메모리에 매핑
mmap은 파일을 주소 공간에 직접 매핑해, 읽기와 쓰기가 일반 메모리 접근이 되도록 합니다 — 대형 파일에 매우 빠릅니다. munmap으로 해제하고; 파일이 비어서는 안 되며, 매핑 크기는 페이지 단위로 정렬됩니다.
디렉터리 순회
opendir/readdir/closedir가 디렉터리 항목을 하나씩 나열합니다. 묵시적인 '.' 및 '..' 항목은 건너뛰고, stat으로 각 이름의 파일 유형과 크기를 확인하세요.
stat — 파일 메타데이터
stat/lstat/fstat은 파일의 메타데이터를 반환합니다 — 크기, 수정 시간, 권한, 유형. tmpfile()은 익명 임시 파일을 제공하며, 닫을 때 자동으로 삭제되어 임시 데이터를 저장하기에 적합합니다.
13.일반적인 함정
전형적인 C 함정 10가지 — 올바른 관용구에는 GOOD, 잘못된 것에는 BAD가 표시됩니다.
버퍼 오버플로
고정 버퍼의 끝 너머로 쓰면 인접 메모리를 손상시키며, 이는 최상급 보안 취약점입니다. 모든 쓰기에 한계를 두세요. strncpy는 직접 NUL을 채우고, snprintf 또는 길이를 받는 fgets를 사용하세요. Sanitizer는 사람의 눈이 놓치는 문제를 잡아냅니다.
해제 후 사용 / 이중 해제
해제 후 사용은 정의되지 않은 동작이며 종종 악용될 수 있습니다; 같은 메모리를 두 번 해제하면 할당기를 손상시킵니다. free 직후 곧바로 포인터를 NULL로 두세요 — free(NULL)은 안전한 빈 연산입니다.
메모리 누수
모든 할당은 모든 경로에서 해제되어야 합니다; 누수는 조용히 부풀어 오르다가 프로세스가 죽을 때까지 이어집니다. AddressSanitizer와 valgrind는 기본적으로 이를 보고합니다 — 충돌 이후가 아니라 개발 단계에 실행하세요.
= 와 ==
단일 =는 대입이고; ==는 비교입니다. if (x = 0)은 0을 대입하고 항상 거짓이 됩니다 — 조용히 잠입하는 발견하기 어려운 버그입니다. -Wall로 컴파일하고, 조건 안의 대입에는 괄호를 추가하세요.
부호 있는 오버플로는 UB
부호 있는 정수의 오버플로는 정의되지 않은 동작입니다. 컴파일러는 절대 발생하지 않는다고 가정하고 그에 따라 최적화할 수 있습니다. 큰 부호 있는 값을 더하기 전에 long long으로 확장하거나, 의도대로 부호 없는 wrap-around를 사용하세요.
초기화되지 않은 변수
초기화되지 않은 지역 변수를 읽는 것은 정의되지 않은 동작이며, 보통 쓰레기 값을 얻습니다. 선언 시점에 모든 변수를 초기화하세요 — 그래야 컴파일러의 -Wuninitialized에 진짜 검사할 게 생깁니다.
sizeof에서 배열 decay
함수 안의 배열 매개변수는 포인터이며, sizeof는 배열 크기가 아니라 8 (포인터 크기)을 얻습니다. sizeof a / sizeof a[0]은 실제 배열이 여전히 범위 안에 있을 때만 작동합니다 — 길이를 명시적으로 전달하세요.
Off-by-one 오류
루프 경계는 고전적인 off-by-one 오류입니다. n개 요소를 i < n (인덱스 0..n-1)으로 반복해야지, i <= n처럼 하나를 더 건드려 범위를 벗어나게 해서는 안 됩니다. 크기 n의 배열에서 마지막으로 유효한 인덱스는 n-1입니다.
printf 형식 불일치
형식 지정자와 인자 유형의 불일치는 정의되지 않은 동작입니다 — %s에 int를 쓰면 곧장 충돌할 수 있습니다. size_t에는 %zu를, %d/%ld/%lld는 유형에 맞추고, -Wformat를 켜서 불일치를 잡으세요.
부호 있음 / 부호 없음 비교
부호 있는 값과 부호 없는 값을 비교할 때, 부호 있는 피연산자가 부호 없음으로 승격되어 음수가 거대한 양수가 됩니다. 양쪽을 명시적으로 같은 부호 있는 유형으로 변환하거나, 아예 섞어 쓰지 마세요.
14.스레드와 동시성
pthread create/join, 뮤텍스, 조건 변수, 원자 연산, C11 스레드 — 빌드와 링크에 -pthread 사용.
컴파일과 헤더
스레드 프로그램은 -pthread로 컴파일하며, 이는 pthread 라이브러리를 링크합니다. <pthread.h>를 포함하고, 각 pthread_*의 반환 값을 검사해야 할 오류로 다루세요 — 이 API들은 errno를 설정하지 않습니다.
생성과 대기
pthread_create는 특정 함수를 실행할 스레드를 시작하며, 그 함수는 void* 인자를 받습니다; pthread_join은 끝날 때까지 블록하여 기다립니다. 전달된 인자와 반환된 void*가 스레드 간 데이터를 전달하는 통로입니다.
void*를 통한 데이터 반환
스레드는 결과를 void*로 반환합니다 — 보통 malloc으로 할당된 값을 가리키는 포인터입니다. 호출자는 join한 뒤 그 반환 포인터를 다시 캐스팅해 값을 읽고, 해제합니다.
뮤텍스 — 공유 상태 보호
뮤텍스는 공유 상태로의 접근을 직렬화합니다. 접근 전에 락을 걸고, 이후에 락을 풉니다. 임계 영역은 짧게 유지하세요 — 락은 병목이며, 오래 보유하면 데드락을 불러옵니다.
조건 변수 — 대기 / 통지
조건 변수는 다른 스레드가 어떤 술어의 변경을 통지할 때까지 스레드를 재웁니다. 항상 while 루프에서 대기하세요 (거짓 활성화가 존재합니다), 그리고 뮤텍스를 보유한 채, 깬 뒤 술어를 다시 확인하세요.
분리 스레드 — join 불필요
분리 스레드는 종료 시 스스로 정리되므로 join이 필요 없습니다. 절대 기다리지 않을 것이 확실할 때만 분리하세요 — 또한 호출자의 스택을 가리키는 포인터를 분리 스레드에 절대 전달하지 마세요. 그 시점에 스택은 이미 사라졌을 수 있습니다.
C11 스레드 (이식 가능)
C11의 <threads.h> (thrd_t)는 pthreads보다 얇고 이식 가능한 대안입니다 — 기능이 약하며, 종종 OS API의 얇은 래퍼에 불과합니다. POSIX에서 제대로 하려면 pthreads가 여전히 표준 선택입니다.
원자 연산 (C11) — <stdatomic.h>
atomic_int와 atomic_fetch_add는 정의된 메모리 순서를 가진 락-프리 로드, 스토어, 증분을 제공합니다 — 뮤텍스보다 빠른 단순 공유 카운터이며, 평범한 int보다 정확합니다 (경합 없음).
스레드 로컬 저장소 (C11)
_Thread_local (C11, GCC에서는 __thread)는 각 스레드가 자신의 변수 사본을 갖게 합니다. 스레드별 캐시와 서로 덮어쓰면 안 되는 오류 슬롯에 적합합니다.
스레드의 오류 처리
pthread 함수는 errno를 설정하는 대신 errno 스타일의 오류 코드를 반환합니다. 반환 값을 받아 strerror(rc)로 진단을 형식화하세요 — 검사하지 않은 실패는 보통 미스터리한 행이나 충돌로 나타납니다.
데드락 회피
데드락은 "필요한 것을 쥐고 다른 곳에서 필요한 것을 기다리는" 데서 옵니다. 고정된 전역 순서로 락을 걸고, 임계 영역을 가능한 한 작게 유지하며, 자원 하나에 락 하나를 우선시하고, 제한된 대기가 필요할 때는 타임아웃이 있는 락을 쓰세요.
15.네트워킹(소켓)
getaddrinfo, TCP 클라이언트와 서버, 최소 HTTP 요청 — Linux/macOS에서 추가 라이브러리 없이 링크.
컴파일과 헤더
POSIX 소켓은 <sys/socket.h>, <netinet/in.h>, <arpa/inet.h>가 필요합니다; Linux/macOS에서는 추가 링크 라이브러리가 필요 없습니다. getaddrinfo로 주소를 해석하고, sockaddr 구조체를 손으로 채우지 마세요.
TCP 클라이언트 — 해결, 연결, 송수신
TCP 클라이언트는 getaddrinfo로 서버를 해석하고, 소켓을 만든 다음 connect합니다. 주소 목록이 매우 길 수 있습니다 — 하나씩 시도해보고, 첫 번째 성공을 사용하며, 실패한 것은 그 자리에서 닫으세요.
송신 / 수신
send/recv는 연결된 소켓 위에서 바이트를 옮기지만, 한 번에 다 옮긴다고 보장하지 않습니다. 전체 메시지를 다 받거나 EOF를 만날 때까지 반복하고, 남은 바이트 수를 끝까지 추적하세요.
TCP 서버 — 소켓 절차
서버는 소켓을 만들고, SO_REUSEADDR을 설정하고, 포트에 bind하고, listen한 다음, accept 호출을 루프로 돕니다. 각 accept는 해당 클라이언트 전용 새 소켓을 돌려줍니다 — 리스닝 소켓은 읽지 않습니다.
호스트명 → IP 해석
getaddrinfo는 호스트명과 포트를 후보 주소의 목록으로 바꾸고, IPv4/IPv6과 서비스명도 함께 처리합니다. 하나씩 성공할 때까지 시도한 다음, freeaddrinfo로 해제하세요.
블로킹 vs 논블로킹
소켓은 기본적으로 블로킹입니다. recv는 데이터가 도착할 때까지 계속 기다립니다. O_NONBLOCK를 쓰면 준비가 안 되었을 때 호출이 EAGAIN을 돌려주며, 단일 스레드에서 poll/epoll/select로 많은 소켓을 감시할 수 있습니다.
UDP — sendto/recvfrom (비연결)
UDP는 비연결 방식입니다. sendto/recvfrom은 매 호출마다 목적지 주소를 지정해야 하며, 핸드셰이크도 없고 순서 보장도 전달 보장도 없습니다. 지연에 민감하고 손실을 허용하는 트래픽에 적합합니다.
바이트 순서 — htons/htonl과 네트워크 바이트 순서
네트워크 바이트 순서는 빅엔디안이고, 호스트는 리틀엔디안일 수 있습니다. sockaddr 필드를 채우거나 회선에서 숫자 프로토콜 필드를 읽을 때는 htons/htonl/ntohs/ntohl로 변환하세요.
Windows(Winsock)의 차이점
Windows는 winsock2.h를 사용하며, 소켓 사용 전에 먼저 WSAStartup을 한 번 호출해야 하고, close() 대신 closesocket()으로 소켓을 닫습니다. 나머지 API는 POSIX 소켓과 동일하므로 코드 자체는 대체로 이식 가능합니다.
오류 코드
소켓 호출이 실패하면 -1을 반환하고 errno를 설정합니다 (ECONNREFUSED, ETIMEDOUT, ECONNRESET). getaddrinfo는 자체 오류 코드를 반환하며 gai_strerror로 포맷할 수 있습니다. 모든 단계를 확인하세요. 여기서 조용히 실패하면 나중에 행이 걸리는 원인이 됩니다.
HTTPS는 TLS 라이브러리가 필요함
베어 소켓은 TCP만 제공합니다. 암호화는 TLS 라이브러리가 필요합니다. OpenSSL의 경우 먼저 SSL_CTX를 만들고 SSL_new/SSL_connect로 소켓을 감싼 뒤 SSL_read/SSL_write를 사용하거나, libcurl로 고수준 HTTP+TLS API를 사용하세요.
16.시간과 날짜
벽시계 시간, UTC/로컬 변환, strftime 포맷팅, 고해상도 모노토닉 타이밍.
Unix 에포크 초
time(NULL)은 1970-01-01 00:00 UTC 이후 경과된 정수 초(time_t)를 반환합니다. 타임스탬프 저장에 사용하는 무시역, 보편적인 순간입니다.
UTC / 로컬 struct tm으로 변환
gmtime과 localtime은 time_t를 struct tm(년, 월, 일, 시 등)으로 분해하며, 각각 UTC와 로컬 시간대를 나타냅니다. 오프셋에 주의하세요. tm_year는 1900부터의 연수이고 tm_mon은 0-11입니다.
strftime으로 포맷하기
strftime은 %Y %m %d %H:%M:%S 등을 사용해 struct tm을 가독성 있는 텍스트로 포맷합니다. 날짜 분야의 printf라고 할 수 있습니다. 로그 줄과 사람이 읽을 수 있는 타임스탬프를 만드는 표준적인 방법입니다.
날짜를 다시 time_t로 파싱하기
strptime(POSIX)은 텍스트를 struct tm으로 파싱하고, mktime은 이를 다시 time_t로 변환합니다. tm_isdst를 -1로 설정해 mktime이 직접 일광절약시간을 처리하도록 하세요. 추측하다 틀리는 일을 막아줍니다.
경과 월시 시간
difftime(b, a)는 두 time_t 사이의 월시 초(second)를 반환합니다. 단순하고 대략적인 시간 측정에는 충분하지만, 월시 시계는 점프할 수 있으므로 측정을 수행할 때는 단조 시계를 사용하세요.
고정밀 단조 시계
clock_gettime(CLOCK_MONOTONIC)은 NTP와 일광절약시간 보정 영향을 받지 않는 경과 시간을 측정합니다. 벤치마크, 타임아웃, 점프가 허용되지 않는 모든 종류의 측정에 사용하세요.
나노초 단위 슬립 (C11 <time.h>)
nanosleep은 timespec 정밀도(나노초 단위)로 스레드를 일시 중단합니다. 시그널에 의해 중단되면 남은 시간을 보고하므로 호출이 남은 시간 동안 계속 잠들 수 있도록 합니다.
본 프로세스가 소비한 CPU 시간
clock()은 본 프로세스가 소비한 CPU 시간을 반환하며, 월시 시간이 아닙니다. CPU 시간과 월시 시간을 비교하면 작업이 계산 중이 아니라 I/O 대기 중임을 알 수 있습니다.
시간대 오프셋 얻기
tm_gmtoff(GNU/glibc)는 로컬 시간의 UTC 기준 초 단위 오프셋(동쪽이 양수)을 저장해 추측 없이 RFC 822 스타일의 +0800 오프셋을 렌더링할 수 있게 합니다.
strftime 없이 ISO 8601 출력하기
snprintf를 사용해 UTC struct tm의 필드로부터 ISO 8601 타임스탬프를 조립합니다. 출력은 로케일 독립적이고 모호함이 없어 인터페이스와 로그에 이상적입니다.
17.프로세스와 시그널
fork, exec, wait, 프로세스 간 파이프, 시그널 처리, 최소 데몬.
플랫폼과 헤더 파일
fork/exec/wait와 시그널은 POSIX 표준(Linux/macOS)입니다. Windows는 CreateProcess를 사용하며 모델이 다릅니다. 본 절의 호출에는 <unistd.h>, <sys/wait.h>, <signal.h>를 포함해야 합니다.
fork — 현재 프로세스 복제
fork는 현재 프로세스를 복제합니다. 두 사본이 같은 반환 지점에서 계속 실행됩니다. 자식은 pid 0을, 부모는 자식의 pid를 봅니다. 자식은 _exit()을 사용해야 합니다. main에서 돌아가면 부모의 정리자가 실행되며 버퍼가 다시 플러시됩니다.
exec — 프로세스 이미지 교체
exec는 다른 프로그램으로 현재 프로세스 이미지를 교체합니다. pid는 유지되고 코드는 완전히 새것입니다. 실패할 때만 반환되며(그 후 _exit(127)을 호출하는 것이 셸 규칙입니다) 환경 변수, 파일 디스크립터, 작업 디렉터리는 보존됩니다.
fork/exec/wait로 명령 실행하기
외부 명령을 실행하는 것은 바로 셸이 사용하는 그 과정입니다. fork(복제), execvp(이미지 교체), waitpid(회수). WEXITSTATUS로 자식의 종료 코드를 추출하고, WIFSIGNALED로 시그널에 의해 종료되었는지를 알 수 있습니다.
파이프 — 자식 쓰기, 부모 읽기
pipe(fd)는 연결된 디스크립터 한 쌍을 만듭니다. 한쪽은 쓰기, 다른 쪽은 읽기입니다. fork 후 각 프로세스는 사용하지 않는 쪽을 닫고, 쓰기 쪽이 닫히면 읽기 쪽에서 EOF가 표시됩니다.
시그널 핸들러
signal()은 비동기 시그널에 대한 핸들러를 등록합니다. 핸들러 내부에서는 async-signal-safe 함수(write, _exit, signal, kill)만 호출할 수 있습니다. printf나 malloc은 재진입 가능하지 않으므로 절대 사용하면 안 됩니다.
sigaction — 견고한 API
sigaction은 signal()의 견고한 대안입니다. 핸들러 실행 중 다른 시그널을 차단하고, SA_RESTART로 중단된 시스템 호출을 재개하며, 핸들러에 어떤 시그널이 발생했는지 알릴 수 있습니다. 새 코드에는 이쪽을 우선 사용하세요.
시그널 임시 차단
sigprocmask는 크리티컬 섹션 주변에서 시그널을 일시 연기한 뒤 해제하여, 공유 상태가 절반만 변경된 시점에 핸들러가 개입하지 못하게 합니다. "저장된 이벤트"를 처리하는 핸들러와 함께 사용해야 시그널이 안전해집니다.
프로세스 죽이기 / 존재 여부 확인
kill(pid, sig)는 프로세스에 시그널을 보내고, kill(pid, 0)은 프로세스가 존재하며 시그널을 보낼 권한이 있는지만 확인합니다 (실제 전달은 하지 않음). 음수 pid는 전체 프로세스 그룹을 대상으로 합니다.
좀비 프로세스 — 자식 회수
부모가 wait하기 전에 종료된 자식은 좀비가 되어 wait/waitpid로 회수될 때까지 pid를 점유합니다. SIGCHLD를 무시하면 자식이 자동으로 회수되며, 아니면 핸들러에서 명시적으로 회수할 수 있습니다.
데몬 골격 (이중 fork)
데몬은 제어 터미널에서 분리됩니다. 먼저 fork하여 자식이 프로세스 그룹 리더가 되지 않게 한 다음, setsid로 새 세션을 열고, 한 번 더 fork한 뒤 chdir('/')을 수행하고 stdio를 /dev/null이나 로그로 리디렉션합니다.
18.POSIX 정규식
regcomp/regexec/regerror: 패턴 컴파일, 매칭 및 캡처 추출, 모든 매치 반복.
설정과 헤더 파일
POSIX 정규식(regcomp/regexec)은 Linux/macOS의 libc에 내장되어 있어 추가 라이브러리가 필요 없습니다. REG_EXTENDED를 전달하면 ERE 문법을, 기본값인 BRE는 더 오래된 별도의 방언을 사용합니다.
컴파일과 매칭
먼저 regcomp로 패턴을 한 번 컴파일하고(반환값을 꼭 확인하세요!), regexec로 문자열을 매칭합니다. regfree로 컴파일된 패턴을 해제합니다. regcomp마다 regfree가 하나씩 따라야 합니다.
캡처 그룹
괄호 그룹은 regmatch_t 항목에 들어갑니다. m[1]은 첫 번째 그룹의 매칭 텍스트 내 시작/끝 오프셋을 저장합니다. %.*s 정밀도 트릭과 오프셋 쌍을 사용해 캡처 조각을 출력하세요.
모든 매칭 순회 (REG_NOTBOL 루프 사용)
모든 매칭을 찾으려면 문자열 끝까지 regexec를 루프 돌리고, 매번 매칭을 하나씩 건너뜁니다. 첫 반복 이후에는 REG_NOTBOL을 전달해 ^가 문자열 중간에서 다시 앵커되지 않게 하세요.
자주 쓰는 플래그
REG_EXTENDED는 ERE 문법을 활성화합니다(사용 권장). REG_ICASE는 대소문자를 무시하고, REG_NOSUB는 캡처 추적을 생략해 속도를 높이며, REG_NEWLINE은 ^/$가 전체 문자열이 아닌 줄의 시작/끝에 매칭되게 합니다.
ERE 문법 치트 시트
ERE 치트 시트: . 임의 문자, ^/$ 앵커, [] 문자 클래스, * + ? 반복, {m,n} 경계 반복, (a|b) 선택, 역참조, 그리고 괄호 안의 POSIX 클래스([[:alpha:]] 등).
치환 — 내장 기능 없음; 수동으로 출력 조립
POSIX 정규식에는 내장 치환 기능이 없습니다. 출력 조립: 매칭을 루프 돌리며 매칭 사이의 텍스트를 복사하고, 각 매칭 위치에 치환 문자열을 삽입합니다. 모든 매칭 순회 루프와 같은 패턴입니다.
사용자 입력 이스케이프
사용자 입력을 패턴에 포함하려면 먼저 각 메타문자([\\^$.|?*+()[])에 백슬래시를 붙여 이스케이프하세요. 그렇지 않으면 잘 설계된 입력이 패턴의 의미를 바꾸는데, 이는 단순한 로직 문제가 아니라 인젝션 취약점입니다.
regcomp 확인 및 regfree
regcomp는 잘못된 패턴을 만나면 실패합니다. 반환값을 꼭 확인하고 regerror로 포맷하세요. 또한 regcomp마다 regfree를 짝지어야 합니다. 그러지 않으면 루프에서 반복 매칭 시 컴파일된 패턴이 누수됩니다.
한계와 성능
컴파일된 regex_t를 재사용하세요. 매번 매칭마다 컴파일하는 것은 낭비입니다. POSIX 정규식은 백트래킹하며, 병적인 패턴은 신뢰할 수 없는 입력에서 매우 느릴 수 있습니다. 무겁거나 적대적인 사용에는 PCRE2 또는 RE2를 우선 사용하세요.
컴파일 시 매칭으로 switch식 디스패치 구현
시작 시 여러 패턴을 미리 컴파일해 순서대로 regexec를 체이닝 호출하세요. 입력이 여러 형태에 매칭될 수 있을 때, 이 방식은 명확한 라우팅 테이블처럼 읽혀 문자열 비교의 벽보다 낫습니다.
19.빌드와 디버그
컴파일러 플래그, 최소 Makefile, gdb, 새니타이저, valgrind — FAQ 섹션이 경고하는 버그를 찾는 도구.
자주 쓰는 gcc/clang 플래그
-std=c17은 언어 표준을 고정하고, -Wall -Wextra는 유용한 경고를 활성화하며(항상 켜두세요), -g는 디버그 정보를 추가하고, -O0/-O2로 최적화 단계를 고르며, -fsanitize=address,undefined는 런타임 검사를 추가합니다. 비용은 없으면서도 실제 버그를 잡습니다.
Makefile — 미니멀 빌드 드라이버
미니멀 Makefile은 컴파일러, 플래그, 타깃 이름, 객체 목록을 변수로 모아두고, %.o:%.c의 암묵적 규칙에 의존한 뒤 clean 타깃을 제공합니다. make는 변경된 부분만 재빌드합니다. 이것이 make의 존재 이유입니다.
디버그 빌드 + 릴리스 빌드
디버그 빌드는 -g -O0으로 전체 심볼을 보존하고 최적화하지 않습니다. 릴리스 빌드는 -O2 -DNDEBUG로 assert도 함께 컴파일에서 제외합니다. 사용자가 실행한 정확한 설정에서 충돌을 재현할 수 있도록 두 빌드를 모두 유지하세요.
gdb 기초
gdb ./prog로 시작합니다. break main, run, next/step(한 줄씩), print x(값 보기), backtrace(콜스택), continue, quit. -batch -ex 옵션을 조합하면 비인터랙티브로 CI용 크래시 덤프를 생성할 수 있습니다.
AddressSanitizer (ASan)
컴파일 시 -fsanitize=address,undefined를 추가하면 프로그램이 런타임에 스택 역추적과 함께 오버플로, use-after-free, 누수를 정확히 보고합니다. valgrind보다 빠르고 명확하므로 첫 번째로 도입할 도구입니다.
valgrind 메모리 검사
valgrind --leak-check=full ./prog는 재컴파일 없이 잘못된 읽기/쓰기, use-after-free, 확실한 누수를 보고하지만, 약 20~50배 느립니다. 재컴파일이 불가능하거나 더 자세한 정보가 필요할 때 사용하세요.
UBSan 플래그
-fsanitize=undefined는 런타임에 부호 있는 오버플로, 비정렬 접근 등 미정의 동작을 정확한 보고서와 함께 잡아냅니다. -fsanitize=address와 결합하면 한 번의 빌드로 메모리와 UB를 동시에 다룰 수 있습니다.
어셈블리 / 전처리 출력 컴파일
gcc -S는 어셈블리를, gcc -E는 전처리된 소스를, gcc -c는 링크되지 않은 오브젝트 파일을 출력합니다. 각 옵션은 컴파일러와 전처리기가 실제로 무엇을 하는지 들여다볼 수 있는 작은 창입니다.
정적 분석 (선택)
gcc -fanalyzer, clang --analyze, cppcheck는 프로그램을 실행하지 않고도 null 역참조, 누수, 미초기화 사용 같은 버그를 찾습니다. CI에 추가하는 비용은 아주 낮으면서 테스트가 놓치는 오류 범주도 잡아냅니다.
빌드 시스템
Make는 중소규모 프로젝트에 적합하고, CMake는 Make나 Ninja 파일을 생성해 크로스 플랫폼 이식이 가능합니다. Meson은 더 빠른 최신 도구입니다. 단일 파일이면 Makefile 하나나 셸 별칭이면 충분합니다.
공식 링크
공식 문서와 리소스에 대한 직접 링크.
이 치트시트에 대해
이 페이지는 ISO C(C11/C17)를 위한 자체 포함형 빠른 레퍼런스입니다. 시스템 프로그래밍 작업의 약 80%를 처리하는 언어와 표준 라이브러리, 그리고 실제 프로젝트에서 쓰는 동시성, 소켓, 정규식, 빌드 도구까지 다룹니다. 정확한 참조는 C11 표준(ISO/IEC 9899:2011)과 cppreference의 C 섹션을 참고하세요. 19개 섹션은 각각 하나의 주제에 집중합니다 — 첫 프로그램, 포인터부터 문자열, 메모리 소유권, 일반적인 함정까지. 각 섹션은 6~10개의 하위 주제로 나뉘며 5~15줄의 간결한 예제가 있습니다. 코드 블록은 의도적으로 짧고 자명하며, 모든 블록에 복사 버튼이 있어 컴파일러에 바로 붙여넣을 수 있습니다. 모든 것이 브라우저 안에서 실행됩니다 — 업로드도 추적도 없습니다. 이 페이지는 GuruToolkit의 무료 개발자 도구 모음의 일부입니다. 여기 스니펫은 자유롭게 사용할 수 있으며 보증은 없습니다.
버전 2.3.0