이번 chapter에서는 C언어의 메모리 구조에 대해 알아보고자 한다.
프로그램을 실행하면 해당 프로그램의 실행을 위한 메모리 공간이 운영체제에 의해서 미리 마련이 된다.
그리고 바로 이 메모리 공간 내에서 변수가 선언되고, 문자열이 선언되는 것이다.
프로그램 실행 시 운영체제에 의해서 마련되는 메모리의 구조는 다음과 같이 네 개의 영역으로 구분된다.
메모리 공간을 나눠놓은 이유는 유사한 성향의 데이터를 묶어서 저장하면 관리가 용이해지고 메모리의 접근속도가 향상되기 때문이다.
이어서 각 영역별 특성에 대해서 구체적으로 살펴보자.
코드 영역 (Code Area)
코드 영역은 이름 그대로 실행할 프로그램의 코드가 저장되는 메모리 공간이다.
따라서 CPU는 코드 영역에 저장된 명령문들을 하나씩 가져가서 실행한다.
데이터 영역 (Data Area)
데이터 영역에는 전역변수와 static으로 선언되는 static 변수가 할당된다.
즉, 이 영역에 할당되는 변수들은 프로그램의 시작과 동시에 메모리 공간에 할당되어 프로그램 종료 시까지 남아있게 된다는 특징이 있다.
스택 영역 (Stack Area)
스택 영역에는 지역변수와 매개변수가 할당된다.
이 영역에 할당되는 변수들은 선언된 함수를 빠져나가면 소멸된다는 특징이 있다.
힙 영역 (Heap Area)
데이터 영역에 할당되는 변수와 스택 영역에 할당되는 변수들은 생성과 소멸의 시점이 이미 결정되어 있다.
그러나 프로그램을 구현하다 보면, 이 두 영역의 변수들과는 다른 성격의 변수가 필요하기도 하다.
원하는 시점에 변수를 할당하고 또 소멸하도록 지원하는 변수들이 할당되는 영역이 힙 영역이다.
이 힙 영역을 대상으로 하는 변수의 할당과 소멸에 대해서는 잠시 후 배울 예정이다.
프로그램의 실행과정에서 보이는 메모리 공간의 변화를 통해서 각 영역별 특징에 대해 다시 한번 정리하겠다.
(코드 영역은 변수가 할당되는 영역이 아니기 때문에 생략한다.)
아래 그림에서는 왼편의 코드가 실행된 직후(main 함수가 호출되기 직전)의 상황을 보이고 있다.
실제로 main함수가 호출되기 이전에 데이터 영역이 먼저 초기화된다.
전역변수와 static 변수가 먼저 데이터 영역에 할당이 되고 나서 main 함수가 호출된다. (이 그림에서는 static 변수가 없다.)
이어서 main 함수가 호출되고 main 함수 내에 선언된 지역변수 num1이 스택에 할당된다.
다음으로 main 함수 내에서 fct 함수가 호출된다.
fct 함수의 매개변수가 스택에 할당되고 fct 함수의 지역변수도 그 뒤에 할당된다.
다음으로 fct 함수가 반환을 하면서 fct 함수호출 시 할당되었던 매개변수와 지역변수가 소멸된다.
위 그림은 fct 함수를 빠져 나온 이후에 main 함수 내에서 num1의 값이 증가한 상황까지의 결과를 보여주고 있다.
이어서 다시 fct 함수의 호출이 진행되고, 더불어 매개변수와 지역변수가 다시 스택에 할당된다.
마지막으로 fct 함수가 반환되고, main 함수의 return문이 실행되면서 프로그램이 종료된다.
프로그램이 종료되면 운영체제에 의해서 할당된 메모리 공간 전체를 반환하게 되는데 그때 전역변수가 소멸된다.
지금까지 살펴 본 내용을 기준으로 스택 영역의 특징을 하나 더 배워보자.
다음 순서로 함수가 호출되었다고 가정해보자.
main 함수의 호출 → fct1 함수의 호출 → fct2 함수의 호출
이는 fct1 함수 내에서 fct2 함수가 호출되었다는 뜻이다.
(fct1 함수 호출되고 반환된 이후 fct2 함수 호출이 이루어진게 아니다.)
이 경우 지역(매개)변수의 소멸순서는 다음과 같다.
fct2의 지역변수 소멸 → fct1의 지역변수 소멸 → main의 지역변수 소멸
먼저 호출된 함수의 스택공간일수록 늦게 해제된다는 것을 알 수 있다.
그래서 메모리 영역의 이름이 스택이다. (자료구조에서 배웠던 거!)
다음 예제는 프로그램 사용자로부터 입력 받은 문자열의 정보를 반환하는 함수가 정의되어 있다.
이 함수의 문제점이 뭔지 찾아보자.
#include <stdio.h>
char * ReadUserName(void)
{
char name[30];
printf("Enter your name: ");
gets(name);
return name;
}
int main()
{
char * name1;
char * name2;
name1 = ReadUserName();
printf("name1: %s \n", name1);
name2 = ReadUserName();
printf("name2: %s \n", name2);
return 0;
}
> 출력
gcc .\ReadStringFault1.c
.\ReadStringFault1.c: In function 'ReadUserName':
.\ReadStringFault1.c:8:12: warning: function returns address of local variable [-Wreturn-local-addr]
return name;
^~~~
위 예제에서 문제점은 함수 내에 지역적으로 선언된 배열(변수)의 주소 값을 반환하는데 있다.
함수 내에서 프로그램 사용자로부터 문자열을 입력 받아서 그 결과를 반환하고 싶어보이는데 그 문자열이 저장되어 있는 배열이 지역적으로 선언되었기 때문에 함수를 빠져나오면서 소멸된다.
다음 예제는 어떨까?
#include <stdio.h>
char name[30];
char * ReadUserName(void)
{
printf("Enter your name: ");
gets(name);
return name;
}
int main()
{
char * name1;
char * name2;
name1 = ReadUserName();
printf("name1: %s \n", name1);
name2 = ReadUserName();
printf("name2: %s \n", name2);
printf("name1: %s \n", name1);
printf("name2: %s \n", name2);
return 0;
}
> 출력
Enter your name: Yoon sung woo
name1: Yoon sung woo
Enter your name: Choi jun kyung
name2: Choi jun kyung
name1: Choi jun kyung
name2: Choi jun kyung
하나의 전역변수(전역으로 선언된 배열)을 이용하면, 이 전역변수를 덮어쓰게 되기 때문에, 함수 호출을 통해서 얻게 된 이름정보가 유지되지 않는다.
따라서, 지역변수도 전역변수도 답이 아니다...
그렇다면 어떠한 성격의 변수가 필요할까?
"함수가 매번 호출될 때마다 새롭게 할당되고 또 함수를 빠져나가도 유지가 되는 유형의 변수"
다시 말해서, 지역변수와 같이 함수가 호출될 때마다 매번 할당이 이뤄지지만, 할당이 되면 전역변수와 마찬가지로 함수를 빠져나가도 소멸되지 않는 성격의 변수가 필요하다.
생성과 소멸의 시기가 지역변수나 전역변수와 다른 유형의 변수는 malloc과 free라는 이름의 함수를 통해서 힙 영역에 할당하고 소멸할 수 있다.
malloc
함수를 이용해서 힙 영역 메모리 공간에 할당하고,
free
함수를 이용해서 힙 영역에 할당된 메모리 공간에서 해제할 수 있다.
힙 영역을 흔히 '프로그래머가 관리하는 메모리 공간'이라 한다.
그 이유는 malloc
함수 호출로 할당된 메모리 공간은 프로그래머가 직접 free
함수의 호출을 통해 해제하지 않으면 계속 남아있기 때문이다.
따라서 위 두 함수는 쌍을 이뤄 호출하게 되고 호출의 형태는 다음과 같다.
int main()
{
void * ptr1 = malloc(4); // 4바이트가 힙 영역에 할당
void * ptr2 = malloc(12); // 12바이트가 힙 영역에 할당
...
free(ptr1); // ptr1이 가리키는 4바이트 메모리 공간 해제
free(ptr2); // ptr2가 가리키는 12바이트 메모리 공간 해제
....
}
malloc
함수는 인자로 전달된 정수 값에 해당하는 바이트 크기의 메모리 공간을 힙 영역에 할당하고, 이 메모리 공간의 주소 값을 반환한다.
free
함수는 호출될 때마다 ptr1과 ptr2가 가리키는 메모리 공간이 소멸된다.
따라서 malloc
함수와 free
함수의 호출위치 및 시점에는 제한이 없다. 원하는 시점에 할당하고 원하는 시점에 소멸이 가능하다.
힙에 할당된 메모리 공간은 포인터를 이용해서 접근하는 방법 밖에 없다.
malloc
함수는 주소 값을 반환하는데 이때 반환형이 무엇일까?
malloc
함수의 반환형은 void형 포인터이다.
따라서 malloc
함수의 반환 값에 아무런 가공도 가하지 않으면 이를 이용해서는 할당된 메모리 공간에 접근이 불가능하다.
void * ptr = malloc(sizeof(int)); // int형 변수 크기의 메모리 공간 할당
*ptr = 20; // ptr이 void형 포인터이므로 컴파일 에러
그럼에도 불구하고 malloc
함수의 반환형이 void형 포인터인 이유가 무엇일까?
malloc
함수는 전달받은 데이터의 형이 정해지지 않았기 때문에 이 데이터가 어떤 자료형을 변할지 몰라서 void형 포인터로 반환하는 것이다.
void * ptr1 = malloc(sizeof(int));
void * ptr2 = malloc(sizeof(double));
void * ptr3 = malloc(sizeof(int)*7);
void * ptr4 = malloc(sizeof(double)*9);
이렇게 하면 malloc
함수에 충분한 자료형 정보를 제공한 것일까?
sizeof 연산과 곱셈연산 이후 정작 malloc 함수에게 전달되는 인자는 다음과 같다.
void * ptr1 = malloc(4);
void * ptr1 = malloc(8);
void * ptr1 = malloc(28);
void * ptr1 = malloc(72);
때문에 malloc
함수는 원하는 크기만큼 메모리 공간을 할당하고, 그 메모리의 주소값을 반환한다. 이 후 포인터 형의 변환을 통해 직접 결정하는 것이다.
따라서 다음과 같이 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
함수를 호출해야 한다.
이제 malloc
함수와 free
함수를 알게 되었으니 이 문제를 해결할 수 있다.
올바른 예제를 통해 알아보자.
#include <stdio.h>
#include <stdlib.h>
char * ReadUserName()
{
char * name = (char *)malloc(sizeof(char)*30);
printf("Enter your name: ");
gets(name);
return name;
}
int main()
{
char * name1;
char * name2;
name1 = ReadUserName();
printf("name1: %s \n", name1);
name2 = ReadUserName();
printf("name2: %s \n", name2);
printf("name1: %s \n", name1);
printf("name2: %s \n", name2);
free(name1);
free(name2);
return 0;
}
> 출력
Enter your name: Yoon Sung Woo
name1: Yoon Sung Woo
Enter your name: Hong Sook Jin
name2: Hong Sook Jin
name1: Yoon Sung Woo
name2: Hong Sook Jin
이렇든 malloc
함수와 free
함수를 이용하면 메모리 공간의 할당과 소멸의 시점을 프로그래머가 직접 결정할 수 있어 전역변수나 지역변수가 감당하지 못하는 일들을 감당할 수 있다.
힙 영역에 메모리 공간을 할당하는 함수로는 calloc
함수도 있다.
malloc
함수와 유일한 차이점은 메모리 공간의 할당을 위한 인자의 전달방식에 있다.
malloc
함수와 달리 calloc
함수는 두 개의 숫자를 인자로 받는다.
첫 번째 전달인자로는 할당할 블록의 갯수 정보가 전달되고, 두 번째 전달인자로는 블록 하나당 바이트 크기의 정보가 전달된다.
따라서 calloc
함수의 호출방식은 "(elt_size)크기의 블록을 (elt_count)갯수만큼 힙 영역에 할당해주세요"다.
또 다른 차이점은 malloc
함수는 할당된 메모리 공간을 별도의 값으로 초기화하지 않는다.
따라서 할당된 메모리 공간이 쓰레기 값으로 채워진다.
calloc
함수는 할당된 메모리 공간의 모든 비트를 0으로 초기화시킨다.
calloc
함수의 호출로 할당된 메모리 공간을 해제할 때에도 malloc
함수와 동일하게 free
함수를 사용하면 된다.
한번 할당된 메모리 공간은 그 크기를 확장할 수 없다.
이는 모든 영역의 메모리 공간에 해당되는 말이다.
하지만 그 영역이 힙이고 realloc 함수를 사용하면 가능하다.
이 함수의 첫 번째 전달인자는 확장하고자 하는 힙 메모리의 시작 주소 값을 전달한다.
두 번째 전달인자는 확장하고자 하는 메모리의 전체 크기를 전달한다.
→ "ptr이 가리키는 메모리의 크기를 size의 크기로 조절해줘(늘려줘)"
함수호출 성공 시에는 새로 할당된 메모리 주소값이 반환되고, 실패 시에는 NULL이 반환된다.
realloc
함수의 호출 형태는 다음과 같다.
int main(void)
{
int * arr = (int *)malloc(sizeof(int)*3); // 길이가 3인 int형 배열 할당
....
arr = (int *)realloc(arr, sizeof(int)*5); // 길이가 5인 int형 배열로 확장
}
위 코드의 실행결과는 반환 값을 기준으로 두 가지로 구분된다.
malloc
함수가 반환한 주소 값과 realloc
함수가 반환한 주소 값이 같은 경우malloc
함수가 반환한 주소 값과 realloc
함수가 반환한 주소 값이 같지 않은 경우<Review>
드디어 동적 할당까지 왔다!
malloc과 free, calloc, realloc 모두 다 재밌는 녀석들이다~!
사실 이 게시글을 올리고 있는 지금 딱 생일이 되었다,,, 헿
이번 생일은 성인된 이후에 처음으로 솔로로 보내는데... ㅋㅋ 감회가 새롭다 ㅎㅎㅎ
인턴십이 있으니깐~ 외롭진 않다!
계속해서 가보자구~~