동적 할당의 처리 단계
C언어에서는 프로그래머가 직접 메모리 동적 할당받고 사용 후 반드시 해제를 해야 한다
1. 메모리 할당
힙 메모리 관리자(OS에서 제공)에게 원하는 메모리 바이트만큼 요청하고
힙 메모리 관리자는 해당 크기의 연속된 메모리의 시작 주소를 반환한
2. 메모리 사용
할당된 힙 메모리 시작 주소를 가지고 원하는 작업을 수행한다
3. 메모리 해제
힙 메모리 관리자에게 해당 메모리 주소를 돌려주면서 다 썼다고 알린다
힙 메모리 관리자는 해당 메모리를 점유되지 않은 메모리 상태로 바꾼다
메모리 주소를 돌려주지 않으면 해당 메모리를 계속 점유하고 있는 상태가 되며,
메모리 누수(Memory leak)가 발생한다
동적 메모리 관련 함수
할당: malloc() - 쓰레기값 / calloc() - 0으로 초기화
재할당: realloc() - 이미 할당된 메모리의 크기를 바꾸는 경우 사용
해제: free()
기타: memset() / memcpy() / memcmp() /...
malloc(), free() 예시
#include <stdio.h>
#include <stdlib.h>
enum { LENGTH = 10 };
int main(void)
{
size_t i;
int* nums = (int*)malloc(LENGTH * sizeof(int));
for (i = 0; i < LENGTH; ++i)
{
nums[i] = i * LENGTH;
}
for (i = 0; i < LENGTH; ++i)
{
printf("%d ", nums[i]);
}
free(nums);
nums = NULL;// 해제 후, NULL을 넣어줘서 오류 발생을 방지
return 0;
}
동적할당 주의점
1. 동적 할당받은 메모리 시작 주소를 연산에 사용한다면.
최초에 받아온 시작 주소를 잃어버릴 가능성이 높고
다른 메모리 주소를 free() 함수의 인자로 보낸다면, Undefined Behavior가 발생한다
따라서, 사본을 만들어서 포인터 연산에 사용해야 한다
2. 이미 해제된 동적 할당 메모리 시작주소를 또 해제하거나
이미 해제된 동적 할당 메모리 시작주소를 사용한다면,
모두 Undefined Behavior가 발생한다
따라서,
free()를 통해 메모리 해제 후, NULL 포인터를 대입하거나
동적할당 메모리를 사용하기 전에 NULL 포인터인지 먼저 검사 후 사용하는 것이 일반적인 코딩 컨벤션이다
free()의 진실
동적 할당을 한다면, 일반적으로 동적 할당 메모리 요청한 size보다 좀 더 큰 메모리를 할당한다
그리고 제일 앞부분에 size에 대한 정보를 적어둔다
따라서, free() 함수는 몇 바이트를 해제해야 하는지를 알고 있다
memset()
memset() 함수는 메모리 블록의 일부 또는 전체를 특정 값으로 설정한다
주로 배열이나 구조체와 같은 메모리 영역을 초기화하는 데 사용된다
// void* memset(void* ptr, int value, size_t count);
int arr[5];
memset(arr, 0, sizeof(arr)); // 배열을 0으로 초기화
동적 할당된 배열에서 memset()을 사용할 때 주의할 점으로는
매개변수 count는 char 형이 기본이기 때문에
count * sizeof(원하는 자료형)으로 해줘야 한다
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
size_t count = 5;
int *arr = (int *)malloc(count * sizeof(int));
if (arr == NULL)
{
fprintf(stderr, "메모리 할당 오류\n");
return 1;
}
// memset(arr, 0, sizeof(arr));// 이렇게 작성하면 안된다
memset(arr, 0, size * sizeof(int));
for (size_t i = 0; i < size; ++i)
{
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
return 0;
}
memcpy()
memcpy() 함수는 한 메모리 영역의 데이터를 다른 메모리 영역으로 복사한다
원본 데이터를 보존하면서 데이터를 복사할 때 사용된다
// void* memcpy(void* dest, const void* src, size_t num);
int src[5] = {1, 2, 3, 4, 5};
int dest[5];
memcpy(dest, src, sizeof(src)); // src 배열의 데이터를 dest 배열로 복사
memcmp()
memcmp() 함수는 두 메모리 영역을 비교하여 같은지 다른지를 판단한다
반환 값으로는
0: 두 메모리 영역이 같음
양수: 첫 번째 다른 바이트의 값이 두 번째 바이트의 값보다 큼
음수: 첫 번째 다른 바이트의 값이 두 번째 바이트의 값보다 작음
// int memcmp(const void* ptr1, const void* ptr2, size_t num);
char str1[] = "Hello";
char str2[] = "World";
int result = memcmp(str1, str2, 5); // 첫 5바이트를 비교
이러한 함수들은 메모리 조작 및 비교를 위해 유용하게 사용된다
다만, C++에서는 memset(), memcpy(), memcmp() 대신에 표준 라이브러리의 <algorithm> 헤더에 있는 함수들이 더 선호되고 있다 (std::fill, std::copy, std::equal 등)
calloc()
calloc()은 0으로 초기화를 하는 반면,
memset()을 쓰면 0 이외의 값으로도 초기화 가능하기 때문에
malloc() + memset() 함수의 조합으로 대체가 가능하다
// void* calloc(size_t num_elements, size_t element_size);
// calloc()
ap_nums1 = (int*)calloc(COUNT, sizeof(int));
// malloc() + memset()
ap_nums2 = (int*)malloc(COUNT * sizeof(int));
memset(ap_nums2, 0, COUNT * sizeof(int));
realloc()
void* realloc(void* ptr, size_t new_size)
이미 동적 할당받은 메모리 시작 주소 ptr을 new_size 바이트로 재할당 해주는 함수이다
새로운 크기가 허용하는 한 기존 데이터를 그대로 유지해 준다
realloc() 함수는 malloc() + memcpy() + free()로 대체가 된다
재할당은 결국 새로운 동적 메모리를 할당하고(malloc)
기존 동적 할당 메모리 내용 복사하고(memcpy)
기존 동적 할당 메모리 해제(free)와 같기 때문이다
realloc() 함수의 반환값에 중요한 문제점이 있다
성공 시 새롭게 동적할당된 메모리의 시작 주소가 반환되고 기존 동적할당된 메모리 시작주소는 해제된다
실패 시 NULL 포인터가 반환되고 기존 동적 할당된 메모리 시작주소는 해제되지 않는다
따라서, 실패 시에 아래와 같은 코드를 작성하면 메모리 누수가 날 수도 있다
int* ap_nums;
ap_nums = (int*)malloc(COUNT);
...
ap_nums = (int*)realloc(ap_nums, 2 * COUNT);
/*
만약, 동적 메모리 재할당에 실패한다면
ap_nums에 NULL 포인터가 대입되고
기존 동적 메모리의 시작 주소는 날라가게 된다
또한, 기존 동적 메모리는 해제되지도 않으면서 최악의 상황이 발생된다
*/
이처럼, realloc()을 사용할 때,
realloc() 함수는 실패 시 NULL을 반환할 수 있으므로 반환값을 체크하는 것이 좋다
또한, 새로운 크기의 메모리 블록이 새로 할당되지 않는다면,
realloc()은 원래의 메모리 블록을 그대로 반환하므로 반환값을 적절히 처리해야 한다
아래처럼 말이다
#include <stdio.h>
#include <stdlib.h>
int main() {
int* arr = (int*)malloc(5 * sizeof(int)); // int형 원소 5개를 갖는 배열을 할당
if (arr == NULL) {
printf("메모리 할당 오류\n");
return 1;
}
// 할당된 배열 초기화
for (int i = 0; i < 5; ++i) {
arr[i] = i + 1;
}
// 배열의 현재 내용 출력
printf("현재 배열의 내용: ");
for (int i = 0; i < 5; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
// 크기를 10으로 변경
int* new_arr = (int*)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
printf("메모리 재할당 오류\n");
// 기존 메모리 블록은 그대로 유지되므로 이전의 데이터를 사용할 수 있음
free(arr); // 할당한 메모리를 해제
return 1;
}
// 추가된 부분 초기화
for (int i = 5; i < 10; ++i) {
new_arr[i] = i + 1;
}
// 새로운 배열의 내용 출력
printf("새로운 배열의 내용: ");
for (int i = 0; i < 10; ++i) {
printf("%d ", new_arr[i]);
}
printf("\n");
// 메모리 해제
// realloc()이 새로운 메모리를 반환하면서 기존 메모리를 자동으로 해제하지만
// 안전을 위해 수동으로도 해제
free(new_arr);
return 0;
}
'C > [코드조선] C 핵심' 카테고리의 다른 글
[C] 구조체 복사 (0) | 2024.02.12 |
---|---|
[C] typedef (0) | 2024.02.12 |
[C] 메모리 구조 (0) | 2024.02.11 |
[C] 문자열 정렬 (0) | 2024.02.10 |
[C] strrev() (0) | 2024.02.10 |