C: 관용적이고 효율적인 레퍼런스
목차
1. 메모리 관리 {#memory}
// ❌ 반환값 검사 없이 malloc 사용
char *buf = malloc(size);
strcpy(buf, src);
// ✅
char *buf = malloc(size);
if (!buf) return -ENOMEM;
memcpy(buf, src, size);
// ❌ malloc 결과를 캐스팅 (C에서는 불필요하며, #include 누락을 숨김)
int *p = (int *)malloc(n * sizeof(int));
// ✅
int *p = malloc(n * sizeof *p);
// ❌ free 후 널로 설정하지 않음 (오래 지속되는 범위에서 댕글링 포인터 위험)
free(ptr);
// ... 이후 코드에서 ptr을 사용할 수 있음
// ✅
free(ptr);
ptr = NULL;
// ❌ 초기 반환 경로에서 free를 잊음
char *a = malloc(100);
char *b = malloc(200);
if (!b) return -1; // a 누수
// ✅ — 단일 정리 레이블 사용
char *a = NULL, *b = NULL;
a = malloc(100);
if (!a) goto cleanup;
b = malloc(200);
if (!b) goto cleanup;
// ... a, b 사용 ...
cleanup:
free(b);
free(a);
sizeof(Type) 대신 sizeof *ptr을 사용하세요 — 타입이 변경되어도 정확합니다.
2. 포인터와 배열 {#pointers}
// ❌ 수동 배열 크기 추적
void process(int *arr, int len) { ... }
process(data, 10);
// ✅ — 포인터와 함께 크기를 전달하거나 구조체 사용
typedef struct { int *data; size_t len; } IntSlice;
// ❌ 배열 인덱싱이 더 명확한데 포인터 연산 사용
*(arr + i) = value;
// ✅
arr[i] = value;
// ❌ 프로덕션 코드에서 VLA 사용 (스택 오버플로 위험, C11+에서 선택적)
int arr[n];
// ✅
int *arr = malloc(n * sizeof *arr);
if (!arr) return -ENOMEM;
// ... arr 사용 ...
free(arr);
3. 에러 처리 {#errors}
// ❌ 에러 반환에 매직 넘버 사용
if (do_thing() == -1) { ... }
// ✅ — 명명된 에러 코드 정의 또는 사용
#include <errno.h>
if (do_thing() < 0) {
perror("do_thing");
return errno;
}
// ❌ 깊게 중첩된 에러 검사
int r1 = step1();
if (r1 == 0) {
int r2 = step2();
if (r2 == 0) {
int r3 = step3();
// ...
}
}
// ✅ — 조기 반환 / goto cleanup 사용
if (step1() < 0) goto fail;
if (step2() < 0) goto fail;
if (step3() < 0) goto fail;
return 0;
fail:
cleanup();
return -1;
goto cleanup은 C에서 리소스 정리를 위한 관용적 패턴입니다 — 원칙적으로 피하지 마세요.
4. 문자열 {#strings}
// ❌ 범위 검사 없이 strcpy 사용
strcpy(dest, src);
// ✅
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';
// 또는 더 나은 방법: snprintf(dest, sizeof(dest), "%s", src);
// ❌ strcmp 잘못 사용
if (str == "hello") { ... } // 포인터를 비교, 내용 비교 아님
// ✅
if (strcmp(str, "hello") == 0) { ... }
// ❌ 반복된 strcat으로 문자열 생성 (O(n²))
char result[1024] = "";
for (int i = 0; i < n; i++) {
strcat(result, items[i]);
}
// ✅ — 쓰기 위치 추적
char result[1024];
int pos = 0;
for (int i = 0; i < n && pos < (int)sizeof(result); i++) {
pos += snprintf(result + pos, sizeof(result) - pos, "%s", items[i]);
}
sprintf보다 snprintf를 선호하세요 — 항상.
5. 구조체와 열거형 {#structs}
// ❌ 모든 곳에서 `struct` 키워드가 필요한 베어 구조체
struct point { int x, y; };
struct point p = {1, 2};
// ✅
typedef struct { int x, y; } Point;
Point p = {1, 2};
// ❌ 초기화되지 않은 구조체
Point p;
use(p.x); // 정의되지 않은 동작
// ✅
Point p = {0};
// ❌ 매직 정수 상수
if (state == 3) { ... }
// ✅
typedef enum { STATE_IDLE, STATE_RUNNING, STATE_DONE } State;
if (state == STATE_DONE) { ... }
6. 전처리기와 헤더 {#preprocessor}
// ❌ 인라인 함수로 가능한 매크로 (타입 안전성 없음, 이중 평가)
#define MAX(a, b) ((a) > (b) ? (a) : (b))
MAX(x++, y) // x > y이면 x가 두 번 증가
// ✅
static inline int max_int(int a, int b) { return a > b ? a : b; }
// ❌ include 가드 없음
// my_header.h
struct Foo { int x; };
// ✅
#ifndef MY_HEADER_H
#define MY_HEADER_H
struct Foo { int x; };
#endif
// 또는: #pragma once (널리 지원되지만 표준은 아님)
매크로는 조건부 컴파일과 상수에 사용하세요. 로직에는 static inline을 사용하세요.
7. C 언어 특성 안티패턴 {#antipatterns}
| 안티패턴 | 권장 방식 |
|---|---|
sprintf | snprintf와 버퍼 크기 |
gets | fgets (gets는 C11에서 제거됨) |
malloc 결과 캐스팅 | 암시적 void* 변환 사용 |
malloc에서 sizeof(Type) | sizeof *ptr |
| 대형/런타임 배열에 VLA | 힙 할당 |
컨텍스트 매개변수 없는 void* 콜백 | 함수 포인터와 함께 void *ctx 전달 |
| 전역 가변 상태 | 구조체 포인터를 통해 상태 전달 |
런타임 에러 처리에 assert 사용 | 적절한 에러 반환 코드 |
읽기 전용 포인터 매개변수에 const 누락 | const char *str |
| 비교에서 signed/unsigned 혼합 | 일관된 타입 사용, 명시적 캐스팅 |
제한사항
- 이는 언어별 가이드라인이며 전체 아키텍처 결정을 다루지 않습니다.
- 과도한 압축은 가독성을 떨어뜨릴 수 있으니 판단하여 적용하십시오.