본문 바로가기
휴지통/C 프로그래밍

메모리 관리와 메모리의 동적할당

by 신재권 2021. 1. 27.

1. C언어의 메모리 구조

프로그램을 실행하면 해당 프로그램의 실행을 위한 메모리 공간이 운영체제에 의해서 미리 마련이 된다. 그리고 바로 이 메모리 공간 내에서 변수가 선언되고, 문자열이 선언된다는 것이다.

-메모리 구성

프로그램 실행시 운영체제에 의해서 메모리 구조는 다음과 같이 네개의 영역으로 구분이 된다.

메모리 공간을 나눠놓은 이유는 커다란 서랍자으이 수납공간이 나뉘어 있는 이유와 유사하다. 메모리 공간을 나눠서 유사한 성향의 데이터를 묶어서 저장하면, 관리가 용이해지고 메모리의 접근 속도가 향상된다.

-메모리 영역별로 저장되는 데이터 유형

*코드 영역 (Code Area)

코드 영역은 이름 그대로 실행할 프로그램의 코드가 저장되는 메모리 공간이다. 따라서 CPU는 코드 영역에 저장된 명령문들을 하나씩 가져가서 실행을 한다.

*데이터영역(Data Area)

데이터 영역에는 전역 변수와 static 으로 선언되는 static변수가 할당된다. 즉 , 이 영역에 할당되는 변수들은 프로그램의 시작과 동시에 메모리 공간에 할당되어 프로그램 종료 시까지 남아있게 된다는 특징이 있다.

*스택영역(Stack Area)

스택영역에는 지역변수와 매개변수가 할당된다. 이렇듯 이 영역에 할당되는 변수들은 선언된 함수를 빠져나가면 소멸된다는 특징이 있다.

*힙 영역(Heap Area)

데이터 영역에 할당되는 변수와 스택 영역에 할당되는 변수들은 생성과 소멸의 시점이 이미 결정되어 있다. 그러나 프로그램을 구현하다 보면, 이 두영역의 변수들과는 다른 성격의 변수가 필요하기도 하다. 그래서 C언어에서는 프로그래머가 원하는 시점에 변수를 할당하고 또 소멸하도록 지원을 하는데, 바로 이러한 유형의 변수들이 할당되는 힙 영역이다. 이 힙영역을 대 상으로 하는 변수의 할당과 소멸에 대해서는 곧 설명을 하겠다.

-프로그램의 실행에 따른 메모리의 상태 변화

그럼 프로그램의 실행과정에서 보이는 메모리 공간의 변화를 통해서 각 영역별 특징에 대해 다시한번 정리한다. 단 코드 영역은 변수가 할당되는 영역이 아니니 생략을 하고 설명을 진행한다. 다음 그림에서는 왼편의 코드가 실행된 직후 (main 함수가 호출되기 직전)의 상황을 보인다.

int sum=25;

//int main(void){
//int num1=10;
//fct(num1);
//num1++;
//fct(num1);
//return 0;
//}

//void fct(int n)
//{
//int num2 =12;
//}

전역변수가 할당되었으므로 데이터 영역에 num= 25가 존재한다

지금까지는 '프로그램의 시작은 main함수의 호출에서부터이다'라고 이야기 해왔지만, 실제로는 main함수가 호출되기 이전에 데이터 영역이 먼저 초기화 된다. 위 그림에서 보이듯이 전역변수가, 그리고 그림에는 없지만 static변수가 먼저 데이터 영역에 할당이 되고 나서 main함수가 호출된다. 이어서 main함수가 호출되고 main함수내에 선언된 지역 변수 num1이 스택에 할당된다.

//int sum=25;

//int main(void){
//int num1=10;
//fct(num1);
//num1++;
fct(num1);
//return 0;
//}

//void fct(int n)
//{
int num2 =12;
//}

스택영역에 num1=10 -> n= 10 -> num2=12 가 순서대로 할당이 되었다.

다음으로 fct함수가 반환을 하면서 fct함수 호출시 할당되었던 매개변수와 지역변수가 소멸되면서 다음의 형태가 된다.

//int sum=25;

//int main(void){
//int num1=10;
//fct(num1);
num1++;
//fct(num1);
//return 0;
//}

//void fct(int n)
//{
//int num2 =12;
//}

num2= 12 가 소멸되고 다음 n=10이 소멸되고 num1++연산자로 num1=11로 스택영역에 남아있게된다.

이어서 다시 fct함수의 호출이 진행되고, 더불어 매개변수와 지역변수가 다시 스택에 할당되어 다음의 구조가 된다.

//int sum=25;

//int main(void){
//int num1=10;
//fct(num1);
//num1++;
fct(num1);
//return 0;
//}

//void fct(int n)
//{
int num2 =12;
//}

스택영역에는 num1=11 -> n=11 -> num2=12 가 차례로 할당이 되었다.

마지막으로 fct함수가 반환되고 , main함수의 return 문이 실행되면서 프로그램이 종료된다. 그리고 프로그램이 종료되면, 운영체제에 의해서 할당된 메모리 공간 전체를 반환하게 되는데, 바로 그때가 전역변수가 소멸되는 시점이다.

모든 영역이 소멸된다.

지금까지 살펴 본 내용을 기준으로 스택 영역의 특징을 하나 더 찾아보겠다. 다음의 순서로 함수가 호출 되었다고 가정해보자

main함수의 호출 ->fct1 함수의 호출 ->fct2의 함수의 호출

이는 fct1 함수가 반환된 이후에 fct2 함수가 호출되었다는 뜻이 아니고, fct1 함수 내에서 fct2함수가 호출되었다는 뜻이다. 그리고 이러한 경우 지역(매개)변수의 소멸순서는 다음과 같다.

fct2의 지역변수 소멸 -> fct1의 지역변수 소멸 -> main의 지역변수 소멸

이렇듯 먼저 호출된 함수의 스택공간일수록 늦게 해제된다는 것을 알 수 있다. 그래서 메모리 영역의 이름이 스택이다. 스택(Stack)은 '쌓아 올려진 더미'를 뜻한다. 쟁반 위에 접시를 쌓아 올리고 나서, 위에서 부터 접시를 하나씩 내려 놓는다고 생각해보자. 가장 아래에 위치한 접시가 마지막에 내려질 것이다. 그리고 이는 지금까지 설명한 스택 영역과 일치하는 특성이다.

이로써 메모리 구조에 대한 전반적인 이야기가 끝이 났다. 남은 것은 힙영역이 필요한 이유와 힙 영역을 활용하는 이유이다.

2. 메모리의 동적 할당

언뜻 생각하면 전역변수와 지역변수만 있으면 충분하다는 생각이 든다. 하지만 프로그램을 구현하다 보면 이 둘이 아닌 다른 유형의 변수를 필요로 하게된다.

-전역변수와 지역변수로 해결이 되지 않는 상황

다음예제는 프로그래 사용자로부터 입력 받은 문자열의 정보를 반환하는 함수가 정의되어 있다. 우선 이 함수의 문제점이 뭘까?

#include <stdio.h>

char * ReadUserName(void){
char name[30];
printf("Whawt's your name? ");
gets(name);
return name;
}

int main(void){
char *name1;
char *name2;
name1=ReadUserName();
printf("name 1: %s \n", name1);
name2=ReadUserName();
printf("name2 : %s \n", name2);
return 0;
}

위 예제의 문제점은 무엇인가? 그것은 함수 내에 지역적으로 선언된 배열(변수)의 주소 값을 반환하는데 있다. 함수 내에서 프로그램 사용자로부터 문자열을 입력 받아서 그 결과를 반호나하는 것은 좋다. 문제는 그 문자열이 저장되어 있는 배열이 지역적으로 전달되었기 때문에 함수를 빠져나오면서 소멸되는데 문제가 있다. 실제로 실행을 해보면 정상적인 못한 결과로 보인다.

이 문제를 지역변수로 해결할라해도 지역변수에는 한 종류의 데이터만 담을 수 있으므로 결국 name2를 입력하면서 원래 있던 데이터가 덮여진다.

이로인해 함수 호출을 통해서 얻게된 이름정보가 유지되지 않는다. 즉 프로그램 사용자에게서 이름 정보를 입력 받아서 이를 반환하는 함수를 정의하기에는 지역변수도 전역변수도 답이 될수 없다. 그렇다면 어떠한 성격의 변수가 필요한가?

"함수가 매번 호출될때마다 새롭게 할당되고 또 함수를 빠져나가도 유지가 되는 유형의 변수"

다시 말해서, 지역 변수와 같이 함수가 호출될 때마다, 매번 할당이 이뤄지지만, 할당이 되면 전역변수와 마찬가지로 함수를 빠져나가도 소멸되지 않는 성격의 변수가 필요하다. 그런데 다행스럽게도 이렇듯 생성과 소멸의 시기를 지역변수나 전역변수와 다른 유형의 변수는 malloc과 free라는 이름의 함수를 통해 힙 영역에 할당하고 소멸할 수 있다. 그럼 위 예제에서 보인 문제점의 해결은 잠시 뒤로하고 먼저 malloc와 free함수에 대해서 살펴본다.

-힙 영역의 메모리 공간 할당과 해제 : malloc 과 free함수

아래에서 보이는 malloc함수를 이용해서 메모리 공간을 할당하고, 또 할당된 메모리 공간은 free함수의 호출을 통해서 해제한다.

void *malloc(size_t size); //힙 영역으로의 메모리 공간 할당

void free(void *ptr); //힙 영역에 할당된 메모리 공간 해제

-> malloc함수는 성공 시 할당된 메모리의 주소 값, 실패시 NULL반환

힙 영역을 흔히 '프로그래머가 관리하는 메모리 공간'이라 한다. 이유는 malloc함수호출로 할당된 메모리 공간은 프로그래머가 직접 free함수의 호출을 통해서 해제하지 않으면 계속 남아 있기 때문이다. 즉 위 두함수는 다음과 같이 쌍을 이루어 호출하게 된다.

int main(){
void *ptr1 = malloc(4);  //4바이트가 힙 영역에 할당
void *ptr2 = malloc(12); //12바이트가 힙 영역에 할당
....
free(ptr1);   //ptr1이 가리키는 4바이트 메모리 공간 해제
free(ptr2);   //ptr2가 가리키는 12바이트 메모리 공간 해제
....
}

이렇듯 malloc함수는 인자로 전달된 정수 값에 해당하는 바이트 크기의 메모리 공간을 힙 영역에 할당하고, 이 메모리 공간의 주소 값을 반환한다. 따라서 위의 코드를 실행하게 되면 ptr1은 첫번째 malloc함수 호출을 통해서 할당된 메모리 공간의 첫 번째 바이트를 가리키게 되고, ptr2는 두 번째 malloc함수호출을 통해서 할당된 메모리 공간의 첫 번째 바이트를 가리키게 된다.

이어서 ptr1을 인자로 free함수를 호출하는 시점에 ptr1이 가리키는 메모리 공간이 소멸되고, ptr2을 인자로 free함수를 호출하는 시점에 ptr2가 가리키는 메모리 공간이 소멸된다. 무엇보다도 malloc함수와 free함수를 호출위치 및 시점에는 제한이 없다. 따라서 원하는 시점에 할당하고 원하는 시점에 소멸이 가능하다.

malloc 함수는 주소값을 반환한다. 그리고 그 주소값을 이용해서 힙에 접근을 해야한다. 따라서 포인터를 이용해서 메모리 공간에 접근하는 수 밖에 없다. 그러니 이제 malloc함수의 반환형에 관심을 두자.

-malloc 함수의 반환형이 void형 포인터인 이유와 힙 영역으로의 접근

malloc 함수의 반환형은 void형 포인터이다. 따라서 malloc함수의 반환 값에 아무런 가공도 가하지 않으면 이를 이용해서는 할당된 메모리에 접근이 불가하다.

void * ptr = malloc(sizeof(int) ; // int형 변수 크기의 메모리 공간 할당

*ptr = 20; //ptr이 void형 포인터 이므로 컴파일 에러

void형으로 반환되는 주소 값을 적절히 형 변환 해서 할당된 메모리 공간에 접근해야 한다.

int *ptr1 = (int *)malloc(sizeof(int));

double *ptr2(double *)malloc(sizeof(double);

int *ptr3 = (int *) malloc(sizeof(int)*7);

double *ptr4= (double *)malloc (sizeof(double)*9);

이러한 내용으로 힙영역에 int형 변수와 int형 배열을 각각 하나 씩 선언해서 접근하고 해제하겠다.

#include <stdio.h>
#include <stdlib.h>

int main(){
int *ptr1 = (int *)malloc(sizeof(int));
int *ptr2 = (int *)malloc(sizeof(int)*7);
int i;

*ptr1= 20;
for(i=0; i<7; i++)
ptr2[i] = i+1;

printf("%d \n" , *ptr1);
for(i=0; i<7; i++)
printf("%d ", ptr2[i]);

free(ptr1);
free(ptr2);
return 0;
}

==== 실행 결과 =====
20
1 2 3 4 5 6 7

참고로 malloc함수는 메모리 공간에 실패할 경우 NULL을 반환한다. 따라서 메모리 할당의 성공여부를 확인하고자 한다면 다음과 같이 코드를 작성해야 한다.

int *ptr = (int *)malloc(sizeof(int));

if(ptr ==NULL){

//메모리 할당 실패에 따른 오류의 처리

}

그리고 malloc 함수의 호출을 통한 메모리 공간의 할당을 가리켜 '동적 할당(dynamic allocation)'이라 한다. 이유는 할당되는 메모리의 크기를 컴파일러가 결정하지 않고, 프포그램의 실행 중간에 호출되는 malloc함수가 결정하기 때문이다.

-free함수를 호출하지 않으면 프로그램 종료 후에도 메모리가 남을까?

프로그램 실행 시 할당된 메모리 공간은 프로그램이 종료되면 운영체제에 의해서 전부 해제가 된다.

앞서 보인 예제에서의 상황만 놓고 보면 free함수의 호출이 불필요하다 생각할 수 있지만 후에 구현할 프로그램들은 간단히 실행되고 종료되는 프로그램이 아니다. 따라서 앞서 보인 예제와 같은 상황은 말 그대로 예제에서나 볼 수 있는 상황일 뿐, 실제로 프로그램 만큼 free 함수를 호출하는 것이 좋다. 프로그램을 종료하기 직전에 free함수를 호출해서 메모리 공간을 정리한다고 해서 손해볼 건 없다.

-문자열을 반환하는 함수를 정의하는 문제의 해결

맨처음의 예제를 malloc와 free함수로 해결할 수 있다.

문제를 해결하기 위해서 필요한 것은 다음과 같다

"함수가 호출될 때마다 문자열 저장을 위한 메모리 공간의 할당이 가능해야 하고, 이 메모리 공간은 함수를 빠져나가도 소멸되지 않고 존재해야 한다."

#include <stdio.h>
#include <stdlib.h>

char * ReadUserName(void){
char *name = (char *)malloc(sizeof(char)* 30);
printf("What's your name? : ");
fgets(name, 30, stdin);
return name;
}

int main(){
char *name1;
char *name2;
name1 = ReadUserName();
printf("name 1 : %s \n" ,name1);
name2 = ReadUserName();
printf("name 2 : %s \n", name2);

printf("again name1 : %s \n", name1);
printf("again name2 : %s \n", name2);
free(name1);
free(name2);
return 0;
}
===========실행 결과 ===============
What's your name ?  xxx
name1 : xxx
What's your name ?  yyy
name2 : yyy
again name1: xxx
again name2: yyy

이렇듯 malloc함수와 free함수를 이용하면 메모리 공간의 할당과 소멸의 시점을 프로그래머가 직접 결정할 수 있다. 때문에 전역변수나 지역변수가 감당하지 못하는 일들을 감당할 수 있다.

-malloc 함수의 사촌 뻘 되는 calloc함수

힙 영역에 메모리 공간을 할당하는 함수로 calloc이라는 함수가 다음과 같이 추가로 정의되어 있다. 아래의 함수와 malloc함수의 유일한 차이점은 메모리 공간의 할당을 위한 인자의 전달방식에 있다.

#include <stdlib.h>

void * calloc(size _t elt_countm size_t elt_size);

->성공시 할당된 메모리의 주소 값, 실패 시 NULL반환

위의 함수 원형에서 보여주듯이 malloc함수와 달리 calloc함수는 두 개의 숫자를 인자로 전달받는다. 반면 앞서 보인 malloc함수의 전달인자는 하나였다. 즉 malloc함수의 호출 방식은 다음과 같았다.

"총 120바이트를 힙 영역에 할당해주세요 "

반면 calloc함수의 첫번째 인자로는 할당할 블록의 갯수 정보가 전달되고, 두 번째 전달인자로는 블록 하나당 바이트의 크기의 정보가 전달된다. 즉 calloc 함수의 호출 방식은 다음과 같다.

"4바이트의 크기의 블록(elt_size) 30개(elt_count)힙 영역에 할당해주세요."

120바이트를 할당해 달라는 것과, 4바이트 크기의 블록 30개를 할당해 달라는 것은 결과적으로 완전히 동일하다. 다시 말해서 calloc 함수는 malloc함수와 인자를 전달하는 방식에서 차이를 보인다.

그런데 이것 말고도 한가지 차이점이 더 있다. malloc함수는 할당된 메모리 공간을 별도의 값으로 초기화하지 않는다. 따라서 할당된 메모리 공간이 쓰레기 값으로 채워지지만 calloc함수는 할당된 메모리공간의 모든 비트를 0으로 초기화시킨다. 바로 이러한 특성 떄문에 calloc 함수가 대신 사용되는 경우도 많다. 그리고 calloc 함수의 호출로 할당된 메모리 공간을 해제할 때에도 malloc 함수와 마찬가지로 free함수를 사용하면 된다.

-힙에 할당된 메모리 공간 확장시 호출하는 realloc 함수

한번 할당된 메모리 공간은 그 크기를 확장할 수 없다. 이는 모든 영역의 메모리 공간에 해당하는 말이다. 이미 할당되어버린 배열의 길이를 늘릴 수 있는가? 어느 영역에 선언을 하건 간에 이런 이러한 일은 불가능 하다. 하지만 그 영역이 힙이라면, 그리고 realloc함수를 사용한다면 이러한 일이 가능해진다.

#include <stdlib.h>

void *realloc(void*ptr, size_t size);

->성공시 새로 할당된 메모리의 주소 값, 실패시 NULL반환

이 함수의 첫번째 전달 인자로, 확장하고자 하는 힙 메모리의 시작 주소 값을 전달한다. 그리고 두번째 전달 인자로는 확장하고자 하는 메모리의 전체 크기를 전달한다. 즉 매개변수 ptr과 size를 이용해서 다음과 같은 요구를 할 때 호출하는 것이 realloc함수이다.

"ptr이 가리키는 메모리의 크기를 size크기로 조절해줘(늘려줘)"

그리고 함수 호출의 성공시에는 새로 할당된 메모리의 주소 값이 반환되고, 실패 시에는 NULL이 반환된다. 즉 위의 함수는 다음의 형태로 호출이 된다.

int main(){

int *arr = (int *)malloc(sizeof(int)*3); //길이가 3인 배열 할당

arr = (int *)realloc(arr, sizeof(int)*5); //길이가 5인 int형 배열로 확장

}

위 코드의 실행결과는 반환 값 을 기준으로 다음과 같이 두 가지로 구분이 된다.

"malloc함수가 반환하는 주소 값과 realloc함수가 반환한 주소 값이 같은경우"

"malloc함수가 반환한 주소 값과 realloc함수가 반환한 주소 값이 같지 않은 경우"

전자는 기존에 할당된 메모리 공간의 뒤를 이어서, 확장할 영역이 넉넉한 경우에 발생한다. 하지만 넉넉하지 않은 경우에는 힙의 다른 위치에 , 새로이 요구하는 크리그이 메모리 공간을 별도로 할당해서 이전 배열에 저장된 값을 복사하기도 한다. 그리고 이러한 경우에는 후자의 경우와 같이 malloc함수와 realloc함수의 반환 값이 같지 않다.

 

'휴지통 > C 프로그래밍' 카테고리의 다른 글

도전 프로그래밍 4-1, 4-2  (0) 2021.01.29
매크로와 선행처리기(Preprocessor)  (0) 2021.01.28
문자와 문자열 관련 함수  (0) 2021.01.26
파일 입출력  (0) 2021.01.23
구조체와 사용자 정의 자료형2  (0) 2021.01.22