동적 메모리 할당 문서의 구현 코드다.
/* 동적 메모리 할당: calloc, malloc, realloc, free와 누수.
*
* 문서: 06.동적 메모리 할당 (예시, 네 함수, 누수, 카드)
* 실행: cc -std=c11 -Wall -Wextra 06_dynamic-memory-allocation_impl.c -o dyn_mem && ./dyn_mem
*
* 받은 덩어리와 돌려준 덩어리를 세는 감싸개(counted_...)를 쓴다.
* 프로그램이 끝날 때 "받은 횟수 == 돌려준 횟수"이고 살아 있는 덩어리가 0개인지 확인한다.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int fails = 0;
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
printf("[FAIL] %s (%d번째 줄)\n", (msg), __LINE__); \
fails++; \
} \
} while (0)
/* ---------- 세는 감싸개 ---------- */
#define MAX_LIVE 64
static void *live[MAX_LIVE]; /* 아직 돌려주지 않은 덩어리의 시작 주소 */
static int live_count = 0;
static int total_allocs = 0; /* 새 덩어리를 받은 횟수 */
static int total_frees = 0; /* 덩어리를 돌려준 횟수 */
static void remember(void *p)
{
if (live_count >= MAX_LIVE) {
fprintf(stderr, "장부가 가득 찼다\n");
exit(EXIT_FAILURE);
}
live[live_count++] = p;
total_allocs++;
}
static void forget(void *p)
{
int i;
for (i = 0; i < live_count; i++) {
if (live[i] == p) {
live[i] = live[--live_count];
total_frees++;
return;
}
}
fprintf(stderr, "받은 적 없는 주소를 돌려주려 했다\n");
exit(EXIT_FAILURE);
}
static void *counted_malloc(size_t size)
{
void *p = malloc(size);
if (p == NULL) {
fprintf(stderr, "메모리가 모자란다\n");
exit(EXIT_FAILURE);
}
remember(p);
return p;
}
static void *counted_calloc(size_t num, size_t esize)
{
void *p = calloc(num, esize);
if (p == NULL) {
fprintf(stderr, "메모리가 모자란다\n");
exit(EXIT_FAILURE);
}
remember(p);
return p;
}
/* realloc은 옛 덩어리를 돌려주고 새 덩어리를 받는 것으로 센다 */
static void *counted_realloc(void *old, size_t size)
{
void *p;
if (old != NULL)
forget(old); /* 장부에서 먼저 지운다. 이 뒤로 old의 주소 값은 쓰지 않는다 */
p = realloc(old, size);
if (p == NULL) {
fprintf(stderr, "메모리가 모자란다\n");
exit(EXIT_FAILURE);
}
remember(p);
return p;
}
static void counted_free(void *p)
{
if (p == NULL)
return;
forget(p);
free(p);
}
/* ---------- 1. 예시: 개수를 실행 중에 알게 되는 수들의 평균과 분산 ---------- */
static void mean_and_variance(const double *input, int n, double *mean, double *variance)
{
double *nums = counted_calloc((size_t)n, sizeof *nums); /* n은 실행 중에 정해진다 */
double sum = 0.0, sq = 0.0;
int i;
for (i = 0; i < n; i++)
nums[i] = input[i]; /* 수를 배열에 읽어 넣는다 */
for (i = 0; i < n; i++)
sum += nums[i];
*mean = sum / n; /* 평균: 모든 수를 한 번 본다 */
for (i = 0; i < n; i++)
sq += (nums[i] - *mean) * (nums[i] - *mean);
*variance = sq / n; /* 분산: 평균을 알아야 구한다 */
counted_free(nums); /* 다 썼으면 돌려준다 */
}
/* ---------- 2. 누수: 문서의 함수 problem ---------- */
static void problem(int n)
{
float *nums = counted_calloc((size_t)n, sizeof *nums);
nums[0] = 1.0f;
/* free(nums)가 없다. 함수가 끝나면 변수 nums는 사라지고 덩어리만 남는다 */
}
static void no_problem(int n)
{
float *nums = counted_calloc((size_t)n, sizeof *nums);
nums[0] = 1.0f;
counted_free(nums);
}
static int all_zero(const void *p, size_t bytes)
{
const unsigned char *b = p;
size_t i;
for (i = 0; i < bytes; i++)
if (b[i] != 0)
return 0;
return 1;
}
int main(void)
{
/* 1. 평균과 분산 */
{
const double data[8] = {2, 4, 4, 4, 5, 5, 7, 9};
double m = 0, v = 0;
int before = live_count;
mean_and_variance(data, 8, &m, &v);
CHECK(m == 5.0 && v == 4.0, "2 4 4 4 5 5 7 9의 평균 5, 분산 4");
mean_and_variance(data, 3, &m, &v);
CHECK(m == 10.0 / 3.0, "앞의 세 개만 읽으면 평균 10/3");
CHECK(live_count == before, "함수가 받은 덩어리를 모두 돌려줬다");
printf("[OK] 예시: 개수 n을 실행 중에 받아 n칸만 잡는다 (평균 5, 분산 4)\n");
}
/* 2. calloc은 0으로 채운다 */
{
size_t n;
int ok = 1;
for (n = 1; n <= 200; n++) {
float *c = counted_calloc(n, sizeof *c);
size_t i;
if (!all_zero(c, n * sizeof *c))
ok = 0;
for (i = 0; i < n; i++)
if (c[i] != 0.0f)
ok = 0;
memset(c, 0xAB, n * sizeof *c); /* 더럽혀 놓고 돌려준다. 다음 calloc이 이 자리를 다시 받아도 0이어야 한다 */
counted_free(c);
}
CHECK(ok, "calloc이 준 자리는 n = 1..200에서 모든 바이트가 0");
printf("[OK] calloc(n, sizeof(float)): n = 1..200에서 모든 바이트가 0\n");
}
/* 3. malloc(N * sizeof(float))과 calloc(N, sizeof(float)): 크기는 같고 처음 내용이 다르다 */
{
size_t N = 64, i, nonzero = 0;
float *m = counted_malloc(N * sizeof(float));
float *c = counted_calloc(N, sizeof(float));
const unsigned char *raw = (const unsigned char *)m;
for (i = 0; i < N * sizeof(float); i++)
if (raw[i] != 0)
nonzero++; /* malloc이 준 자리의 처음 내용은 정해져 있지 않다. 세기만 하고 믿지 않는다 */
CHECK(all_zero(c, N * sizeof(float)), "calloc 쪽은 0");
for (i = 0; i < N; i++)
m[i] = (float)i; /* malloc 쪽은 쓰기 전에 직접 채운다 */
CHECK(m[63] == 63.0f && c[63] == 0.0f, "둘 다 N칸을 쓸 수 있다");
printf("[OK] malloc과 calloc: 둘 다 %zu바이트. calloc은 모두 0, malloc은 이번 실행에서 0이 아닌 바이트 %zu개\n",
N * sizeof(float), nonzero);
counted_free(m);
counted_free(c);
}
/* 4. realloc: 앞부분은 그대로 옮겨진다. 돌려받은 주소를 써야 한다 */
{
int *p = counted_malloc(4 * sizeof *p);
int i, kept = 1;
for (i = 0; i < 4; i++)
p[i] = 10 * (i + 1); /* 10 20 30 40 */
p = counted_realloc(p, 8 * sizeof *p); /* 4칸 -> 8칸 */
for (i = 0; i < 4; i++)
if (p[i] != 10 * (i + 1))
kept = 0;
CHECK(kept, "늘린 뒤에도 앞의 4칸은 10 20 30 40");
for (i = 4; i < 8; i++)
p[i] = 10 * (i + 1); /* 새로 생긴 칸은 직접 채운다 */
CHECK(p[7] == 80, "새 칸을 쓸 수 있다");
p = counted_realloc(p, 2 * sizeof *p); /* 8칸 -> 2칸 */
CHECK(p[0] == 10 && p[1] == 20, "줄인 뒤에는 앞의 2칸만 남는다");
counted_free(p);
printf("[OK] realloc: 4칸 -> 8칸 -> 2칸, 남는 범위의 내용은 그대로\n");
}
/* 5. realloc(NULL, size)는 malloc(size)처럼 일한다 */
{
int *p = counted_realloc(NULL, 3 * sizeof *p);
p[0] = 1; p[1] = 2; p[2] = 3;
CHECK(p[0] + p[1] + p[2] == 6, "realloc(NULL, ...)으로 받은 자리");
counted_free(p);
counted_free(NULL); /* free(NULL)은 아무 일도 하지 않는다 */
}
/* 6. 누수 */
{
int live_before = live_count, allocs_before = total_allocs, frees_before = total_frees;
no_problem(100);
CHECK(live_count == live_before, "free가 있는 함수: 남은 덩어리 없음");
problem(100);
problem(100);
problem(100);
CHECK(live_count == live_before + 3, "free가 없는 함수를 세 번 부르면 덩어리 3개가 남는다");
CHECK(total_allocs - allocs_before == 4 && total_frees - frees_before == 1, "받기 4번, 돌려주기 1번");
printf("[OK] 누수: problem(100)을 세 번 부른 뒤 받기 %d번, 돌려주기 %d번, 남은 덩어리 %d개(%zu바이트)\n",
total_allocs - allocs_before, total_frees - frees_before, live_count - live_before,
3 * 100 * sizeof(float));
/* 실제 프로그램에서는 이 주소를 아는 변수가 없어 돌려줄 방법이 없다.
여기서는 감싸개가 장부에 적어 둔 주소로 찾아서 돌려준다. */
while (live_count > live_before)
counted_free(live[live_count - 1]);
}
/* 끝: 받은 것을 모두 돌려줬는가 */
CHECK(live_count == 0 && total_allocs == total_frees, "받은 횟수와 돌려준 횟수가 같다");
printf("[OK] 전체: 받기 %d번, 돌려주기 %d번, 남은 덩어리 %d개\n", total_allocs, total_frees, live_count);
if (fails) {
printf("%d개 확인 실패\n", fails);
return 1;
}
printf("ALL CHECKS PASSED\n");
return 0;
}