C프로그래밍 14강 - 메모리 동적 할당
프로그램이 사용하는 코드·데이터·스택·힙 영역의 특징을 살펴보고, 정적 할당과 동적 할당의 차이를 정리한다. 이어서 malloc(), calloc(), realloc(), free()를 이용한 힙 메모리 관리와 memcmp(), memcpy(), memset()의 활용법을 학습한다.
1. 프로그램의 메모리 영역
프로그램이 실행되면 운영체제는 실행 코드와 자료를 저장할 기억공간을 마련한다. 이 공간은 역할과 수명에 따라 코드 영역, 데이터 영역, 스택, 힙으로 구분할 수 있다.
| 영역 | 저장 내용 | 특징 |
|---|---|---|
| 코드 영역 | 실행할 기계어 명령 | 일반적으로 읽기 전용으로 보호된다. |
| 데이터 영역 | 전역변수, 정적변수, 문자열 상수 | 프로그램 시작 시 생성되어 종료할 때 소멸한다. |
| 스택 | 지역 자동변수, 함수 매개변수 | 함수 호출 시 생성되고 반환 시 자동으로 소멸한다. |
| 힙 | 실행 중 동적으로 요청한 자료 | 할당과 해제를 프로그래머가 명시적으로 관리한다. |
자료를 저장하는 메모리는 보통 읽고 쓸 수 있지만 문자열 리터럴이나 const로 선언한 객체처럼 변경이 제한되는 자료도 있다. 스택은 자동 관리되어 편리하지만 용량이 제한적이며, 힙은 실행 중 필요한 크기를 정할 수 있는 대신 올바른 해제가 필요하다.
데이터 영역은 프로그램 전체 수명, 스택 객체는 함수의 실행 수명, 힙 객체는 할당 명령부터 해제 명령까지의 수명을 갖는다.
2. 정적 할당과 동적 할당의 비교
정적 할당은 프로그램을 작성하거나 컴파일할 때 기억공간의 크기가 결정되는 방식이고, 동적 할당은 프로그램 실행 중 입력값이나 처리 상황에 따라 필요한 크기를 결정하는 방식이다.
| 구분 | 정적 할당 | 동적 할당 |
|---|---|---|
| 크기 결정 시점 | 코딩·컴파일 시점 | 프로그램 실행 시점 |
| 주요 영역 | 데이터 영역, 스택 | 힙 |
| 대표 대상 | 일반 변수, 배열, 구조체 | 할당 함수가 확보한 이름 없는 공간 |
| 접근 방법 | 변수 이름 | 반환된 주소를 저장한 포인터 |
| 해제 방법 | 시스템이 수명에 따라 자동 처리 | free()로 직접 해제 |
| 장점 | 단순하고 안정적이다. | 필요한 만큼 유연하게 사용할 수 있다. |
| 주의점 | 크기를 미리 알아야 하고 낭비가 생길 수 있다. | 누수, 이중 해제, 잘못된 포인터 사용을 방지해야 한다. |
예를 들어 최대 인원을 1,000명으로 예상해 배열을 선언하면 실제 인원이 10명이어도 전체 공간을 확보한다. 반면 실행 중 인원수를 입력받아 그 수만큼만 힙에 할당하면 메모리를 효율적으로 사용할 수 있다.
3. 메모리 정적 할당
정적 할당에서는 선언문에 의해 공간의 크기와 자료형이 정해진다. 전역변수와 static 변수는 데이터 영역에 놓여 프로그램이 끝날 때까지 유지되고, 함수 안의 일반 지역변수와 매개변수는 스택에 놓여 함수 호출이 끝나면 사라진다.
int a = 100; /* 데이터 영역 */
void test1(void) {
int c, d; /* test1 호출 동안 스택에 존재 */
}
int main(void) {
int b; /* main 실행 동안 스택에 존재 */
test1();
return 0;
}
함수를 호출할 때마다 그 함수의 매개변수와 지역변수를 위한 스택 프레임이 만들어지고 함수가 반환되면 해당 프레임이 제거된다. 이 자동 수명 관리는 간단하고 안정적이지만, 실행 전에 크기를 알 수 없는 자료에는 적합하지 않다.
지역 배열을 지나치게 크게 선언하면 제한된 스택 공간을 많이 차지한다. 크기가 크거나 실행 중 크기가 정해지는 자료는 힙 사용을 검토한다.
4. 메모리 동적 할당의 절차
동적 할당 함수가 확보한 힙 공간에는 변수 이름이 없다. 함수가 돌려주는 시작 주소를 포인터에 저장하고, 그 포인터를 통해 공간을 사용한다.
- 포인터 선언: 저장할 자료형과 맞는 포인터를 준비한다.
- 공간 할당:
malloc()또는calloc()으로 필요한 바이트를 요청한다. - 성공 여부 확인: 반환값이
NULL인지 검사한다. - 자료 처리: 배열 첨자나 간접참조 연산자로 공간을 사용한다.
- 크기 변경: 필요하면
realloc()을 사용한다. - 공간 해제: 사용이 끝나면
free()를 호출하고 필요하면 포인터를NULL로 설정한다.
int *p = malloc(sizeof(int) * count);
if (p == NULL) {
/* 할당 실패 처리 */
}
/* p[0]부터 p[count - 1]까지 사용 */
free(p);
p = NULL;
할당한 공간을 더 이상 가리키는 포인터가 없는데 해제하지 않았다면 그 공간을 다시 사용할 수 없는 메모리 누수가 발생한다. 반대로 해제한 주소를 계속 사용하면 댕글링 포인터로 인한 예측 불가능한 오류가 생긴다.
5. malloc과 free
malloc()은 지정한 바이트 수만큼 연속된 힙 공간을 확보하고 그 시작 주소를 void *형으로 반환한다. 어떤 자료형의 포인터로도 받을 수 있지만, 공간의 실제 크기는 프로그래머가 정확하게 계산해야 한다.
void *malloc(size_t size);
void free(void *p);
| 함수 | 동작 | 주의 사항 |
|---|---|---|
malloc(size) | size바이트를 할당하고 시작 주소 반환 | 초기화되지 않으며 실패하면 NULL을 반환한다. |
free(p) | p가 가리키는 동적 공간 해제 | 이미 해제한 공간을 다시 해제해서는 안 된다. |
int *p = malloc(sizeof(int) * 100);
if (p != NULL) {
for (int i = 0; i < 100; i++)
p[i] = i;
free(p);
p = NULL;
}
sizeof(int) * 100처럼 자료형의 크기와 원소 수를 곱하면 시스템마다 달라질 수 있는 자료형의 크기를 안전하게 반영할 수 있다. free(NULL)은 아무 동작도 하지 않으므로 안전하다.
malloc()으로 받은 공간의 값은 정해져 있지 않다. 값을 읽기 전에 반드시 필요한 초기화를 수행한다.
6. calloc과 realloc
calloc()은 같은 크기의 원소 여러 개를 위한 공간을 할당하고 모든 바이트를 0으로 초기화한다. realloc()은 이미 할당된 공간의 크기를 변경한다.
void *calloc(size_t n, size_t size);
void *realloc(void *p, size_t new_size);
| 함수 | 사용 예 | 결과 |
|---|---|---|
calloc() | calloc(100, sizeof(int)) | 정수 100개 분량을 확보하고 바이트를 0으로 초기화 |
realloc() | realloc(p, sizeof(int) * 100) | p의 공간을 정수 100개 크기로 조정 |
realloc()으로 공간을 줄이면 남는 뒷부분이 해제된다. 공간을 늘릴 때 뒤쪽에 여유가 있으면 같은 위치에서 확장하고, 그렇지 않으면 다른 위치에 새 공간을 확보하여 기존 내용을 복사할 수 있다. 따라서 성공 후 주소가 달라질 수 있다.
int *tmp = realloc(p, sizeof(int) * 100);
if (tmp != NULL) {
p = tmp;
} else {
/* 기존 p는 여전히 유효하므로 필요한 실패 처리를 수행 */
}
반환값을 곧바로 p에 대입하면 실패 시 NULL이 저장되어 기존 공간의 주소를 잃을 수 있다. 임시 포인터로 결과를 받은 뒤 성공했을 때만 원래 포인터를 갱신하는 것이 안전하다.
7. 기억공간 관리 함수
메모리에 저장된 바이트들을 비교·복사·설정할 때는 <string.h>에 선언된 기억공간 관리 함수를 사용할 수 있다. 이 함수들은 자료형 자체보다 시작 주소와 처리할 바이트 수를 기준으로 동작한다.
| 함수 | 원형 | 기능 |
|---|---|---|
memcmp() | int memcmp(const void *s1, const void *s2, size_t n) | 두 공간의 앞에서부터 n바이트 비교 |
memcpy() | void *memcpy(void *dest, const void *src, size_t n) | src의 n바이트를 dest로 복사 |
memset() | void *memset(void *dest, int c, size_t n) | dest의 n바이트를 값 c로 채움 |
이 함수들은 처리 범위를 스스로 판단하지 않는다. 대상 공간의 실제 크기를 넘는 바이트 수를 전달하면 다른 메모리를 침범할 수 있으므로 경계를 정확히 계산해야 한다.
memcpy()의 원본과 목적지 영역이 겹치면 결과가 보장되지 않는다. 겹칠 가능성이 있을 때는 겹침을 고려해 복사하는 memmove()를 사용한다.
8. memcmp·memcpy·memset 활용
memcmp()는 처음 다른 바이트를 기준으로 두 영역의 대소를 판정한다. 같으면 0, 첫 번째 영역이 작으면 음수, 크면 양수를 반환한다.
char a[] = "aaa";
char b[] = "bbb";
int result = memcmp(a, b, 3); /* 음수 */
memcpy()는 지정한 바이트를 그대로 복사하고 목적지 주소를 반환한다. 다음 예에서는 목적지 문자열의 앞 10바이트가 숫자로 바뀐다.
char src[] = "1234567890";
char dest[] = "abcdefghijklmnopqrstuvwxyz";
memcpy(dest, src, 10);
printf("%s\n", dest); /* 1234567890klmnopqrstuvwxyz */
memset()은 문자 개수가 아니라 바이트 수를 기준으로 같은 값을 채운다. 배열 전체를 0으로 만들거나 문자 버퍼의 일부를 특정 문자로 채울 때 유용하다.
char data[] = "1234567890";
memset(data, '*', 10);
printf("%s\n", data); /* ********** */
메모리 함수의 세 번째 인수는 원소 수가 아니라 바이트 수이다. 배열 전체를 처리할 때는 sizeof 배열명을 활용할 수 있다.
핵심 개념 정리
- 코드 영역에는 명령, 데이터 영역에는 전역·정적 자료, 스택에는 지역 자동변수와 매개변수, 힙에는 동적 자료가 저장된다.
- 정적 할당의 크기는 코딩·컴파일 시점에 정해지며 시스템이 수명에 따라 자동 관리한다.
- 동적 할당은 실행 중 크기를 정해 힙에 공간을 마련하고 포인터로 접근한다.
malloc()은 초기화되지 않은 공간을,calloc()은 0으로 초기화된 공간을 할당한다.- 할당 함수가
NULL을 반환하는지 확인하고 사용이 끝난 공간은free()로 해제한다. realloc()의 실패에 대비해 반환값은 임시 포인터로 받는 것이 안전하다.memcmp()는 비교,memcpy()는 복사,memset()은 바이트 단위 채우기에 사용한다.- 복사할 메모리 영역이 겹치면
memcpy()대신memmove()를 사용한다.
최종 정리: 동적 메모리의 핵심은 필요한 크기를 정확히 계산하고, 할당 성공을 확인하며, 유효한 범위 안에서 사용한 뒤 반드시 해제하는 것이다. 포인터의 소유와 수명을 분명히 관리하면 메모리 누수와 댕글링 포인터를 예방할 수 있다.
예상문제 20선
1. 전역변수와 정적변수가 주로 저장되는 영역은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
전역변수와 정적변수는 데이터 영역에 놓이며 프로그램 전체 실행 기간 동안 유지된다.
2. 함수의 지역 자동변수와 매개변수가 주로 저장되는 영역은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
지역 자동변수와 매개변수는 함수 호출 때 스택에 생성되고 함수 반환 때 사라진다.
3. 실행 중 동적으로 할당한 기억공간이 놓이는 영역은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
동적 할당 함수가 확보하는 기억공간은 힙에 마련된다.
4. 동적 할당 공간에 접근하는 일반적인 방법은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
힙의 할당 공간에는 변수 이름이 없으므로 그 시작 주소를 가진 포인터로 접근한다.
5. 정적 할당의 특징으로 옳은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
정적 할당은 선언을 바탕으로 실행 전에 공간의 크기가 정해진다.
6. malloc(sizeof(int) * 100)의 의미는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
정수 한 개의 크기와 원소 수 100을 곱한 만큼 힙 공간을 요청한다.
7. malloc()이 확보한 공간의 초기 상태는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
malloc은 공간만 확보하고 내용을 초기화하지 않으므로 읽기 전에 값을 저장해야 한다.
8. 동적 할당 함수가 공간 확보에 실패했을 때 반환하는 값은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
할당 함수의 반환값이 NULL인지 확인한 뒤에 공간을 사용해야 한다.
9. free(p)의 기능은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
free는 malloc 계열 함수로 확보한 힙 공간을 반환한다.
10. 해제한 공간의 주소를 계속 가리키는 포인터는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
이미 유효하지 않은 공간을 가리키는 댕글링 포인터를 역참조하면 정의되지 않은 동작이 발생할 수 있다.
11. 할당한 공간을 해제하지 않고 주소마저 잃은 상황은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
더 이상 접근할 수 없는 힙 공간이 해제되지 않은 채 남으면 메모리 누수가 된다.
12. calloc()의 특징으로 옳은 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
calloc은 지정한 개수와 크기만큼 연속 공간을 마련하고 모든 바이트를 0으로 만든다.
13. 이미 할당된 공간의 크기를 바꾸는 함수는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
realloc은 기존 동적 할당 공간의 크기를 새 크기로 조정한다.
14. realloc()의 결과를 임시 포인터에 받는 주된 이유는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
실패 시 반환되는 NULL을 원래 포인터에 바로 대입하면 기존 공간에 접근할 주소를 잃을 수 있다.
15. 두 기억공간의 앞에서부터 지정한 바이트를 비교하는 함수는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
memcmp는 두 영역의 n바이트를 비교해 같으면 0, 대소에 따라 음수나 양수를 반환한다.
16. memcmp(a, b, n)이 0을 반환하는 경우는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
비교 대상의 지정된 n바이트가 모두 같으면 memcmp는 0을 반환한다.
17. memcpy(dest, src, n)의 기능은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ①
memcpy는 원본 영역의 지정된 바이트를 목적지 영역으로 복사한다.
18. 원본과 목적지 메모리 영역이 겹칠 수 있을 때 적합한 함수는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ②
memcpy는 겹치는 영역의 복사를 보장하지 않으므로 이런 경우에는 memmove를 사용한다.
19. 지정한 기억공간의 바이트들을 같은 값으로 채우는 함수는?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ③
memset은 목적지의 n바이트를 지정한 바이트 값으로 채운다.
20. memcpy()와 memset()의 세 번째 인수가 나타내는 것은?
정답입니다.
오답입니다. 답안을 다시 선택해 보세요.
정답 및 해설 보기
정답: ④
메모리 처리 함수는 원소 개수가 아니라 지정한 바이트 수를 기준으로 동작한다.
댓글
댓글 쓰기