배열은 int arr[5]처럼 크기를 코드에 박아 썼지만, 사용자가 몇 명 들어올지는 실행 전에는 모릅니다. 실행 도중에 필요한 만큼 메모리를 빌려 오는 것이 malloc이고, 결과를 컴퓨터를 꺼도 남기려면 파일에 씁니다. 전처리기는 컴파일러가 문법을 보기 전에 소스를 미리 손보는 단계입니다. 셋 다 반환값 검사를 빼먹으면 조용히 무너지니, 예제로 따라가 봅니다.
22.1 실행 중에 크기를 정하는 malloc
malloc은 실행 도중에 원하는 만큼 메모리를 빌려 주는 함수로, stdlib.h를 포함해야 씁니다. 빌릴 바이트 수를 넘기면 그 메모리의 시작 주소를 돌려줍니다. 정수 다섯 개짜리 방이 필요하면 5에 sizeof(int)를 곱해 넘깁니다.
#include <stdio.h>
#include <stdlib.h> // malloc, free
int main(void) {
int n = 5;
int *arr = malloc(n * sizeof(int));
if (arr == NULL) return 1; // 실패 확인
for (int i = 0; i < n; i++) arr[i] = (i + 1) * 10;
for (int i = 0; i < n; i++) printf("%d ", arr[i]);
printf("\n");
free(arr); // 다 쓰면 반납
return 0;
}
실행하면 이렇게 나옵니다.
10 20 30 40 50
malloc이 돌려준 주소를 포인터 arr에 담으면, 그다음부터는 arr[0]처럼 평범한 배열과 똑같이 씁니다. 크기 n을 실행 중에 정한 변수로 넘길 수 있는 점만 다릅니다.
함정. malloc은 메모리가 부족하면 NULL을 돌려줍니다. 이 NULL을 arr[0]처럼 배열로 쓰면 널 포인터 역참조가 되어 프로그램이 죽습니다(정의되지 않은 동작). 그래서 malloc 바로 다음 줄에 if (arr == NULL) 검사를 습관처럼 넣습니다.
![[C 22] 동적 메모리와 파일, 전처리기](https://img.thenullpage.com/posts/6557/6557_2_c67b03.webp)
22.2 다 쓴 메모리는 free, 그 뒤엔 건드리지 않기
malloc으로 빌린 메모리는 프로그램이 알아서 반납하지 않습니다. free로 돌려주지 않으면 안 쓰는 메모리가 쌓이는 메모리 누수(memory leak)가 생깁니다. 앞 예제의 free(arr)가 그 반납입니다.
함정. free한 뒤의 포인터는 이미 넘어간 방을 가리키는 허상 포인터(dangling pointer)입니다. 이것으로 값을 읽거나 같은 포인터를 두 번 free하면 정의되지 않은 동작입니다. free 직후에 그 포인터를 NULL로 두면(free(arr); arr = NULL;), 실수로 다시 쓰거나 free해도 NULL 검사에 걸려 안전합니다.
![[C 22] 동적 메모리와 파일, 전처리기 (2)](https://img.thenullpage.com/posts/6557/6557_1_9d0798.webp)
22.3 파일에 저장하기, fopen과 fprintf
C에서 파일은 FILE 포인터로 다루고 fopen으로 엽니다. 두 번째 인자인 모드가 성격을 정하는데, "w"는 쓰기(기존 내용을 지움), "r"은 읽기, "a"는 이어 쓰기입니다.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("memo.txt", "w"); // 쓰기 모드
if (fp == NULL) return 1;
fprintf(fp, "안녕, 파일!\n");
fprintf(fp, "숫자: %d\n", 42);
fclose(fp); // 반드시 닫기
printf("저장 완료\n");
return 0;
}
화면에는 저장 완료 한 줄만 뜨고, 실제 내용은 memo.txt 파일에 들어갑니다.
// 화면
저장 완료
// memo.txt 파일 안
안녕, 파일!
숫자: 42
fprintf는 printf 앞에 파일 포인터 fp만 붙인 모습이라 서식 사용법이 똑같습니다. 파일을 다 쓰면 fclose로 닫아야 내용이 실제로 저장됩니다.
함정. 경로가 틀리거나 쓰기 권한이 없으면 fopen도 NULL을 돌려줍니다. 검사 없이 NULL에 fprintf를 넘기면 프로그램이 죽으니, malloc과 똑같이 fopen 다음 줄에서 NULL부터 확인합니다.
![[C 22] 동적 메모리와 파일, 전처리기 (3)](https://img.thenullpage.com/posts/6557/6557_3_264b11.webp)
22.4 파일을 한 줄씩 읽기, fgets
저장한 파일을 다시 읽을 때는 "r" 모드로 열고 fgets로 한 줄씩 가져옵니다. fgets는 버퍼, 버퍼 크기, 파일 포인터를 받아 한 줄을 읽고, 더 읽을 것이 없으면 NULL을 돌려줍니다. 크기를 함께 넘기니 버퍼보다 긴 줄이 와도 넘치지 않습니다.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("memo.txt", "r"); // 읽기 모드
if (fp == NULL) {
printf("파일 없음\n");
return 1;
}
char line[100];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%s", line);
}
fclose(fp);
return 0;
}
안녕, 파일!
숫자: 42
fgets가 NULL을 돌려줄 때가 파일 끝이라 반복이 자연스럽게 멈춥니다. 읽은 줄에 줄바꿈 문자가 그대로 들어 있어 %s만 써도 줄이 나뉘어 출력됩니다.
22.5 숫자를 통째로 저장하기, fwrite와 fread
fprintf가 숫자를 글자로 바꿔 저장한다면, 메모리에 있는 모습 그대로 저장할 때는 fwrite와 fread를 씁니다. 모드에 b를 붙여 "wb"(쓰기)와 "rb"(읽기)로 열고, 원소 크기와 개수를 넘깁니다.
#include <stdio.h>
int main(void) {
int data[3] = {10, 20, 30};
FILE *fp = fopen("nums.bin", "wb");
if (fp == NULL) return 1;
fwrite(data, sizeof(int), 3, fp); // 3개 통째로 저장
fclose(fp);
int back[3];
fp = fopen("nums.bin", "rb");
if (fp == NULL) return 1;
fread(back, sizeof(int), 3, fp); // 다시 읽어 옴
fclose(fp);
printf("%d %d %d\n", back[0], back[1], back[2]);
return 0;
}
10 20 30
주의. 윈도우의 텍스트 모드는 줄바꿈을 자동으로 바꿔 버리니, 바이너리 데이터에는 반드시 "wb"와 "rb"를 써야 바이트가 변하지 않습니다.
22.6 반복되는 값에 이름 붙이기, 매크로
여기부터는 전처리기입니다. 컴파일러가 문법을 보기 전에 소스를 텍스트로 손보는 단계로, #include로 헤더를 펼치고 #define으로 정해 둔 글자를 치환합니다. 매크로에는 값에 이름을 붙이는 객체형과, 괄호로 인자를 받는 함수형이 있습니다.
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main(void) {
printf("%d\n", SQUARE(5)); // 25
printf("%d\n", SQUARE(2 + 3)); // (2+3)*(2+3)=25
return 0;
}
25
25
SQUARE(5)는 컴파일 전에 ((5) * (5))로 글자 그대로 바뀝니다. 계산이 아니라 자리에 코드를 끼워 넣을 뿐입니다.
함정. 매크로가 글자 치환이라서, 괄호를 빼면 우선순위가 깨집니다. #define SQUARE(x) x * x로 쓰면 SQUARE(2 + 3)이 2 + 3 * 2 + 3, 즉 11로 펼쳐집니다. 인자 하나하나와 전체 식을 모두 괄호로 감싸야 합니다. 또 #define N 100 처럼 끝에 세미콜론을 붙이지 않습니다. 세미콜론까지 같이 치환되어 엉뚱한 문법 오류가 납니다.
22.7 필요할 때만 켜는 코드, 조건부 컴파일
#if와 #endif로 코드 일부를 조건에 따라 넣거나 뺍니다. 개발 중에만 디버그 메시지를 켜고 싶을 때 자주 씁니다.
#include <stdio.h>
#define DEBUG 1
int main(void) {
int result = 3 + 4;
#if DEBUG
printf("[디버그] 계산 시작\n");
#endif
printf("결과: %d\n", result);
return 0;
}
[디버그] 계산 시작
결과: 7
DEBUG를 0으로 바꾸면 #if와 #endif 사이의 줄이 컴파일에서 통째로 빠져 디버그 메시지가 사라집니다. 코드를 지웠다 살렸다 하지 않고 값 하나로 끄고 켜는 셈입니다.
동적 메모리와 파일은 규칙이 같습니다. 빌린 것은 반드시 돌려주고(malloc은 free, fopen은 fclose), 빌려 오는 함수의 반환값이 NULL인지 확인하는 것입니다. 이 두 줄이 조용한 오류를 대부분 막습니다. 전처리기는 문법을 모르는 치환기이니 괄호와 세미콜론만 조심하면 됩니다.
![[C 22] 동적 메모리와 파일, 전처리기 (4)](https://img.thenullpage.com/posts/6557/6557_4_333724.webp)