처방: 할당한 쪽만 해제하고 검증 함수는 판단만, free 후 NULL 대입 (FREE 매크로는 do { } while (0)로 정의)
상세 보기
int verify_size(char *list, size_t size) { if (size < MIN_SIZE_ALLOWED) { free(list); // 여기서 해제하고 return -1; } return 0;}void process_list(size_t number) { char *list = (char *)malloc(number); ... if (verify_size(list, number) == -1) { free(list); // 여기서 또 해제 → double free return; } free(list);}
해제 책임자는 할당한 쪽 하나로 둔다. 검증 함수는 판단만 하고 해제하지 않는다.
권장: free 후 NULL을 대입하고, free 전에 NULL을 검사한다. free(NULL)은 안전하다.
#define FREE(ptr) do { if ((ptr) != NULL) { free(ptr); (ptr) = NULL; } } while (0)
같은 모듈·같은 추상화 수준에서 할당하고 해제한다 (교육자료 9-1). 다른 함수·모듈이 해제하면 “누가 언제 해제했는지”를 알 수 없어서 leak, double free, 해제 후 접근이 생긴다.
해제 직후 NULL을 대입하는 이유는 같은 포인터가 나중에 한 번 더 쓰일 수 있기 때문이다.
교육자료의 예: message_type & bitmask_1일 때 한 번, & bitmask_2일 때 또 free(message)를 하면 두 비트가 모두 켜진 메시지에서 double free가 난다.
반대로 지역(non-static) 포인터가 free 직후 곧바로 스코프를 벗어나면 더 쓸 수 없으니 NULL 대입은 생략해도 된다.
교육자료의 FREE는 if (...) { ... }로만 정의돼 있다. if (c) FREE(p); else ...처럼 쓰면 } 뒤의 ;가 if문을 끝내서 else가 짝을 잃고 컴파일 에러가 난다. 위 do { ... } while (0) 형태로 감싸는 이유다.
free 후 참조 — 9-08 (치명적)
요약
문제: for (p = head; p; p = p->next) free(p);가 해제된 노드의 next를 읽음 (운영 장애 사례)
원인: free(p) 뒤 p->next 접근. 테스트에서는 이전 값이 남아 통과하기 쉬움
처방: 해제 전에 next = p->next를 먼저 저장
상세 보기
for (p = head; p != NULL; p = p->next) { // free(p) 이후 p->next를 읽는다 free(p);}
struct node *next;for (p = head; p != NULL; p = next) { next = p->next; // 해제 전에 다음 노드를 저장 free(p);}
WARNING
교육자료(9-8)는 이 패턴이 실제 운영 장애로 이어진 사례를 함께 싣고 있다 (고객사·티켓 정보는 생략). 연결리스트를 해제하는 함수가 free(p) 뒤에 p->next를 읽어서 해제된 메모리를 읽은 채 동작하다가 프로세스 블록이 재기동됐다.
테스트에서는 해제된 메모리에 우연히 이전 값이 남아 있어서 통과하기 쉽다는 점이 이 결함의 위험이다.
해제 누락 — 9-05, 9-09
요약
문제: 에러·정상 경로에서 free 누락 (9-05, 9-09)
원인: fopen/fgets 실패 경로에서 text_buffer를 해제하지 않음
처방: 빠져나가는 모든 경로에서 해제 → 단일 출구 정리(§3)
상세 보기
malloc/calloc 후 정상 경로, 에러 경로 어디서든 빠져나가기 전에 free가 있어야 한다. 9-09는 fopen/fgets 실패 경로에서 text_buffer를 해제하지 않았다. 아래 goto 패턴으로 한 곳에 모으면 누락이 사라진다.
2. 할당 크기 관련 오류
타입을 잘못 계산 — 9-03, 9-11
요약
문제: malloc(sizeof(gadget)), malloc(sizeof(tmb))로 작게 할당해 힙 오버플로 (9-03, 9-11)
원인: sizeof(tmb)는 포인터 크기(8바이트)
처방: p = malloc(sizeof(*p)) 관용구 (이 저장소의 XNEW/XNEWN도 같은 방식)
결과: struct tm의 tm_year는 1900년 기준, tm_mon은 0~11
상세 보기
widget *p; // 20 + 4 + 8 바이트 구조체p = malloc(sizeof(gadget)); // 더 작은 구조체 크기로 할당 → 쓰는 순간 힙 오버플로struct tm *tmb = malloc(sizeof(tmb)); // sizeof(포인터) = 8바이트, struct tm은 훨씬 크다
p = malloc(sizeof(*p)); // 타입이 바뀌어도 따라가는 관용구tmb = malloc(sizeof(*tmb));
교육자료(9-3)는 sizeof(*p) 대신 할당 함수의 반환값을 대상 타입으로 바로 cast하는 방법을 권한다.
C에서 void *는 어떤 포인터로도 암시적 변환되므로 widget *p = malloc(sizeof(gadget))는 경고 없이 통과하지만, p = (widget *)malloc(sizeof(widget))처럼 타입을 쓰면 sizeof(...)와 대입 대상의 타입이 어긋날 때 컴파일러가 잡을 여지가 생긴다.
노드는 유연 배열 멤버로 가변 길이 데이터를 한 번의 malloc에 담고, 별도의 list 구조체가 head·tail·count를 관리하는 구조다.
typedef struct msg_node { struct msg_node *next; size_t len; // 데이터 길이 (널 문자 제외) unsigned char data[]; // 가변 길이, 항상 '\0'으로 끝남} msg_node_t;typedef struct msg_list { msg_node_t *head; msg_node_t *tail; int count;} msg_list_t;
insert — tail 덕분에 O(1)
요약
문제: 새 노드 할당 크기와 next 초기화 오류
원인: 유연 배열 노드 크기 계산이 틀리고 next가 쓰레기값
처방: sizeof(msg_node_t) + len + 1, next = NULL, list->tail로 O(1) 삽입
상세 보기
int insert(msg_list_t *list, const char *src, size_t len) { if (list == NULL || src == NULL) return -1; if (list->count >= MAX_SIZE) { printf("list full\n"); return -1; } msg_node_t *new_ptr = malloc(sizeof(msg_node_t) + len + 1); // 구조체 + 데이터 + 널 문자 if (new_ptr == NULL) return -1; new_ptr->next = NULL; new_ptr->len = len; memcpy(new_ptr->data, src, len); new_ptr->data[len] = '\0'; if (list->tail == NULL) list->head = new_ptr; // 빈 리스트 else list->tail->next = new_ptr; list->tail = new_ptr; list->count++; return 1;}
delete — head/tail까지 갱신
요약
문제: 첫·마지막 노드 삭제에서 head/tail 미갱신
원인: pre_node가 없는 경우와 target == tail 경우를 구분하지 않음
처방: pre_node = NULL로 시작해 경우를 나누고 tail도 갱신, index <= 0 || index > count 검사
상세 보기
int delete(msg_list_t *list, int index) { // index는 1부터 msg_node_t *pre_node = NULL, *target; if (list == NULL || list->head == NULL) return -1; if (index <= 0 || index > list->count) return -1; target = list->head; for (int i = 1; i < index; i++) { pre_node = target; target = target->next; } if (pre_node == NULL) list->head = target->next; // 첫 노드 삭제 else pre_node->next = target->next; if (target == list->tail) list->tail = pre_node; // 마지막 노드 삭제 free(target); list->count--; return 1;}
free_list
코드 보기
void free_list(msg_list_t *list) { msg_node_t *cur, *next; if (list == NULL) return; for (cur = list->head; cur != NULL; cur = next) { next = cur->next; // 해제 전에 저장 (9-08의 교훈) free(cur); } free(list);}