활동 목표: 동적 메모리 학습
활동 결과:
1. 메모리 구조
동적 메모리에 대해 알기 위해서는 메모리 구조에 대해 알아야 한다고 생각했다.
메모리는 그저 하나의 저장소가 아닌 다양한 메모리 공간을 제공해준다.
전체적인 구조는 4가지로 나뉜다.
1. 코드(text) 영역
2. 데이터(data) 영역
3. 스택(stack) 영역
4. 힙(heap) 영역
1.1. 코드(text) 영역
실행할 코드의 기계어 코드(컴파일된 프로그램 명령어), 제어문, 함수, 상수 등이 저장된다.
1.2. 데이터(data) 영역
전역 변수, 정적 변수 등이 저장된다.
아직 이 부분에 대해 제대로 배우진 않았는데 가볍게 설명하면 C언어의 지역 변수는 함수 호출이 끝날 때 메모리에서 사라지지만 전역 변수와 정적 변수는 프로그램이 시작될 때 생성되고 끝날 때까지 메모리에 남아있다.
이는 데이터 영역에 저장되어 있기 때문인 것이다.
1.3. 스택(stack) 영역
지역 변수, 매개 변수 등이 저장된다.
이름이 스택(stack)인 것에서 예상한것과 같이 push, pop 을 통해 데이터를 저장하고 내보낸다.
전역 변수, 정적 변수와 달리 함수가 종료되면 메모리에서 해제한다.
재귀를 돌리면서 볼 수 있었던 에러 메시지인 스택 오버플로우(Stack Overflow)도 이와 관련된 것인데
재귀를 통해 지역 변수, 매개 변수가 스택 영역에 할당되다가 스택 영역을 넘어가 버리게 되면 스택 오버플로우 에러가 뜨게 된다.
1.4. 힙(heap) 영역
힙 영역이 바로 이번에 배울 동적으로 할당되는 메모리가 저장된다.
동적으로 메모리를 할당하기 위해 malloc(), calloc(), realloc() 과 같은 함수를 사용하는데, 이 함수에 관해서는 아래에서 다시 언급하도록 하겠다.
힙 영역 또한 힙 오버플로우(Heap Overflow)가 나타날 수 있다.
1.5. 낮은 주소, 높은 주소
스택과 힙을 배우는 도중 언급되는 단어이다.
스택 영역은 메모리가 높은 주소에서 낮은 주소로 할당된다.
힙 영역은 메모리가 낮은 주소에서 높은 주소로 할당된다.
말 그대로 100에서 104로(낮은 주소에서 높은 주소로), 200에서 196으로(높은 주소에서 낮은 주소로) 할당된다.
왜 이런 방식을 사용할까?
스택 영역과 힙 영역은 Free Space를 공유하고 있다.
힙과 스택은 Free Space를 사용하며 각 영역의 크기를 늘린다.
이는 메모리의 효율성과 유동성을 위한 것인데, 힙과 스택을 반대 방향으로 늘려 Free Space를 최대한으로 하기 위함이다.
스택 오버플로우와 힙 오버플로우가 일어나는 이유도 이와 같은 구조 때문이다.
2. 정적 할당
앞서 설명한 개념을 이해했다면 동적 메모리가 어디에 저장되는지, Free Space에 대해 알았을 것이다.
그러면 동적 메모리를 왜 사용하는지에 대해 이해하기 위해 동적 메모리와 대비되는 정적 할당에 대해 알아보도록 하겠다.
2.1. 메모리 크기
컴파일 시기에 그 크기가 정해지는 정적 할당. 예를 들면 문자열, 배열은 그 길이를 미리 지정해 주어야 한다.
동적 메모리에 할당을 하게 된다면 컴파일 시기가 아닌 런타임 시기에 그 크기를 정할 수 있다.
또한 그 크기를 지정해 주더라도 그 크기보다 작은 크기를 사용한다면 메모리가 낭비될 것이다.
예를 들면 문자열 배열의 경우 문자열의 길이를 지정해 주어야 하는데 다른 요소는 더 작은 길이를 가질 수 있는 것이다.
#include <stdio.h>
int main(){
char strarr[4][11] = {
"apple",
"banana",
"greenapple", // greenapple의 길이는 11(문자열 null 종료 문자로 인한 길이 + 1 (\0))
"melon" // melon의 길이는 11보다 작음
};
printf("%s", strarr[2]);
return 0;
}
>> greenapple
따라서 메모리를 더욱 효율적, 유동적으로 관리하기 위해 동적 메모리 할당을 사용하는 것이다.
2.2. 정적 할당의 메모리 공간 위치
동적 메모리 할당에 들어가기 전에 오해할 만한 요소가 있기에 정리하려고 한다.
앞서 소개한 정적 메모리를 동적으로 할당하면 heap에 저장된다.
정적 할당은 데이터 영역이 아닌 주로 stack에 저장된다.
데이터 영역에 정적 변수와 정적 할당은 다른 개념이다.
3. 동적 메모리 할당
동적 메모리 할당은 앞서 언급한 바와 같이 런타임 시기에 heap에 할당된다.
3.1. malloc(), free()
malloc 함수를 통해 동적 메모리 할당을 진행할 수 있다.
인자로 할당할 메모리의 크기를 전달해 void 포인터(첫 번째 주소값)를 반환한다.
어? 동적 메모리로 배열을 만들면 배열의 크기를 유연하게 설정할 수 있다고 하지 않았나요?
C에서 배열의 크기는 변수로 둘 수 없다. 상수로 정해져 있다.
하지만 동적 메모리 할당을 할 경우 size 인자에 변수를 넣을 수 있다.
...이에 관련하여 오해의 여지가 있는데 C99표준에서는 arr[var]과 같이 배열의 크기에 변수를 사용할 수 있다.
C99표준에 가변 길이 배열(VLA) 이 도입되어 배열의 크기를 런타임에 결정되는 변수로 사용할 수 있게 되었다.
VLA을 사용하면 동적 할당이지만 stack에 저장되어 함수가 종료되면 메모리에서 해제된다.
현재 VSCode C++ 컴파일러를 사용하고 있기에 VLA이 가능하지만 이는 C++ 표준이 아니고 VLA를 지원하지 않는 컴파일러도 존재하기 때문에 지양하는 편이 좋을 것 같다.
#include <stdlib.h>
void* malloc(size_t size) // malloc 함수 원형
변수 n 을 받아 arr이라는 포인터에 동적 할당을 한 주소를 저장하여 int * n size를 가진 heap 영역을 가리키도록 하였다.
할당이 되었는지를 체크하며 free함수를 통해 해제하고 해제된 메모리 영역을 가리키는 댕글링 포인터(Dangling Pointer)문제를 방지하기 위해 포인터를 NULL로 설정하였다.
#include <stdio.h>
#include <stdlib.h>
int main(){
int n;
int *arr;
scanf("%d", &n);
arr = (int*)malloc(sizeof(int) * n);
if (arr == NULL){
printf("할당실패 ㅠ");
return 0;
}
for (int i = 0; i < n; i++){
arr[i] = i + 1;
printf("%d\n", arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
>> 5 // 5 입력
1
2
3
4
5
free는 malloc과 함께 다닌다. free를 통해 할당된 공간을 해제시켜야 메모리 누수를 막을 수 있다.
3.2. calloc
calloc은 malloc과 거의 유사하다. 차이점은 calloc은 할당한 메모리를 0으로 초기화시켜준다는 것이다. malloc으로 할당된 메모리에는 쓰레기 값(초기화되지 않은 변수에 존재하는 임의의 값)을 포함할 수 있기에 사용한다.
또한 malloc과 다르게 자료형의 개수와 자료형의 크기를 인자로 받는다.
#include <stdlib.h>
void* calloc(size_t number, size_t size)
3.3. realloc
realloc은 동적 할당받은 메모리의 크기를 변경할 때 사용한다.
#include <stdlib.h>
void* realloc(void* ptr, size_t new_size)
4. 포인터 배열과 동적 메모리 할당
이제 3회차 때 설명하기로 한 포인터 배열을 쓰는 이유에 대해 여기서 설명하려고 한다.
또한 위에서 설명한 길이 문제를 해결하는데 이는 포인터 배열을 이용하는 것이다.
지금까지 배운 것을 가지고 코드를 짜 보았다.
문자열 배열이기 때문에 문자열을 포인터로 가리키는 포인터 배열이 필요하다.
포인터 배열을 동적으로 할당하기 위해 포인터 배열을 가리키는 포인터(이중 포인터)로 작성했다.
각 문자열 요소의 길이를 문자열의 길이 + 1(\0) 로 동적으로 할당하였다.
strcpy는 할당된 메모리 공간에 문자열을 복사해 주는 함수이다.
a[10] = apple; a = banana; 에서 a는 a[0]의 주소를 가지고 있는 상수 포인터이므로 이 코드는 오류를 발생시킨다.
이를 위해 각 요소를 개별적으로 변경해야 하는데, strcpy는 내부적으로 a[0] = b; a[1] = a;......; a[5] = a를 한 번에 처리한다.
다중 포인터에서 동적으로 할당할 때 주의해야할 점이 있는데
안쪽부터 free를 해주어야 한다는 것이다.
바깥쪽부터 free를 하게되면 댕글링 포인터가 되어버려 안쪽을 해제할 수 없으므로 메모리 누수가 생긴다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(){
char **fruits;
int num = 4;
char names[4][20] = {
"apple",
"banana",
"greenapple",
"melon"
};
fruits = (char**)malloc(num * sizeof(char*));
for (int i = 0; i < num; i++){
fruits[i] = (char*)malloc(strlen(names[i]) + 1);
strcpy(fruits[i], names[i]);
printf("%s\n", fruits[i]);
};
for (int i = 0; i < num; i++){
free(fruits[i]);
fruits[i] = NULL;
}
free(fruits);
fruits = NULL;
return 0;
}
>> apple
banana
greenapple
melon
배우면 배울수록 포인터에 대한 개념이 정말 중요한 것 같다.
동적 메모리에 대해 배우면서 포인터에 대해 더욱 자세히 알아가고 있는 것 같다 :)
'모각코' 카테고리의 다른 글
[2025 하계 모각코] 6회차 활동 결과 - 운영체제와 운영체제의 구조 (1) | 2025.07.31 |
---|---|
[2025 하계 모각코] 5회차 결과 - 구조체 (3) | 2025.07.29 |
[2025 하계 모각코] 3회차 활동 결과 - C언어 포인터 (3) | 2025.07.22 |
[2025 하계 모각코] 2회차 활동 결과 - C언어 표준과 전처리기(선행처리기) (2) | 2025.07.15 |
[2025 하계 모각코] 1회차 활동 결과 - C 언어 개발환경 및 기본 문법 (1) | 2025.07.15 |