본문 바로가기

BackEnd/C

메모리 공간의 동적 할당

반응형

1. 메모리 구조 복습- https://cg-developer.tistory.com/162


가상 메모리는 다음 크게 4가지 영역으로 나뉜다.


- 코드영역: 실행할 프로그램의 바이너리 코드를 저장할 공간


- 데이터 영역: 프로그램이 종료될때까지 유지해야 할 데이터를 저장할 공간


- 스택 영역: 아주 잠깐 사용하고 삭제할 데이터의 저장 공간 


- 힙 영역: 프로그래머가 원하는 방식으로 쓸 수 있는 공간, 일반적이지 않은 특성의 변수 선언이 목적이라면 힙영역 활용


2. 힙 영역의 필요성


전역변수는 데이터 영역에 할당이 되어 프로그램이 종료될 때가지 남아있는 변수이고,


지역변수는 스택에 할당이 되었다가 해당 변수를 선언한 함수가 종료되면 소멸이 되는 변수이다.


따라서 이 두 가지 특성의 변수로는 충족되지 않는 부분이 있는데, 이 부분은 힙 영역을 통해서 해


결해야 한다


3. 지역변수와 전역변수가 갖는 한계


일단 int형 1차원 배열을 생성하는 함수를 정의해 보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
 
int * MakeIntArray(int len, int init)
{
    int arr[len];
 
    int i;
    for(i=0; i<len; i++)
        arr[i]=init;
 
    return arr;
}
 
int main(void)
{
    int * arr1=MakeIntArray(50);
    int * arr2=MakeIntArray(72);
 
    int i;
    for(i=0; i<5; i++)
        printf("%d ", arr1[i]);
 
    printf("\n");
 
    for(i=0; i<7; i++)
        printf("%d ", arr2[i]);
 
    return 0;
}

cs


3행의 MakelntArray라는 함수는 두 개의 인자를 전달 받는다. 하나는 배열의 길이이고,다른 하나


는 배열을 초기화 할 값이다. 즉 이 함수는 첫 번째 전달인자에 해당하는 길이의 int 형 배열을 만들


어서(선언해서), 두 번째 전달인자로 배열 전체 요소를 초기화하여 함수를 호출한 대상에게 배열


의 주소 값을 반환해 준다. 하지만 이 함수는 문제가 있다.


5행에서 선언된 배열이 MakelntArray 함수를 빠져나오면 소멸된다는 것이다. 


즉 main 함수에서 반환된 주소 값을 가지고 배열에 접근하는 것이 잘못되었다. 


main함수에서 이 주소 값을 가지고 배열에 접근할 시점이면 이미 소멸된 상태이기 때문이다.


즉 여기서 요구하는 함수의 특성(함수가 호출될때마다 함수내에서 배열을 선언해 메모리공간에 


할당이 되고, 함수를 빠져나가도 메모리공간이 유지가 되어 배열을 반환 할 수 있어야 하는것)을


만족 시키기에는 지역변수 형태의 배열도, 전역변수 형태의 배열도 답이 될 수 없다.


지역변수의 특성은 함수내에서 메모리공간에 할당은 되지만 함수밖으로 빠져나가면 그대로 유지

가 되지 않는다. 


전역변수의 특성은 프로그램 시작과 동시에 메모리공간에 할당이 되어 함수가 호출될때마다 반복해서 할당할 수 있는 메모리 특성이 아니다.


4. 배열을 힙영역에 할당


이 한계를 해결할려면 힙 영역에다가 할당해야 한다. 


힙영역은 프로그래머가 메모리공간의 할당시기와 소멸시기를 결정할 수 있기 때문이다.


단, 힙에 한번 할당이 된 메모리는 결코 자동으로 소멸 되지 않는다. 


따라서 힙 영역에 메모리를 할당만 하고 해제를 시켜주지 않는 실수를 범하면 안 된다.


그리고 이렇게 힙 영역에 메모리를 할당하는것을 가리켜 ‘동적 할당(dynamic allocation)’이라 한다. 


5. 힙영역의 메모리 할당



malloc 함수는 반환형 이 ‘void형 포인터(void *)’이다. 


이는 참조하는 대상의 자료형 정보는 없고 주소값 정보만 있는 포인터이다. 


이것은 말이 포인터이지 그냥 주소 값 정보 만을 담을 수 있는 변수에 지나지 않는다. 


즉 malloc 함수는 반환되는 주소 값의 포인터 형을 결정하지 못한다.


그리고 매개변수 형인 size_t이다. size_t는 typedef에 의해 만들어진 자료형이다. 


일반적으로 unsigned int 또는 unsigned long으로 선언되어 있어서, 말 그대로 사이즈(크기)에 


대한 정보를 담기 좋도록 선언되어 있다. 이 함수는 호출되는 과정에서 0 보다 큰 하나의 숫자


를 입력 받는다. 그러면 malloc 함수는 숫자의 크기만큼 바이트 단위로 힙 영역에 메모리 공간


을 할당한다. 그리고 할당된 메모리 공간의 주소 값을 반환한다.



위 그림에서 보여주듯이 힙에 메모리 공간을 할당하고 얻는 것은 할당된 메모리의 주소 값이 


부이다. 때문에 할당된 힙 영역에 접근하기 위해서는 어쩔 수 없이 포인터 연산을 해야만 


한다. 그리고 이 malloc 함수는 메모리 할당에 실패할 경우 NULL 포인터를 반 환한다. 


따라서 malloc 함수를 호출한 후에는 가급적 NULL 포인터 의 반환여부를 확인하는 것이 코드


의 안전성을 위해서 좋다


6. 할당된 메모리 공간 활용


malloc함수에서 반환된 void형 포인터형을 사용해보자.


다음 예제는 malloc 함수의 호출 형식을 보여준다.


참고로 이 예제에서는 4바이트 int형 변수를 힙에 할당하고자 하는 의도가 담겨있다.


1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
void * ptr=malloc(sizeof(int));
*ptr=10// 문제 발생
....
}
cs


힙 영역에 int형 변수 크기의 메모리 공간이 할당되었다. 


할당된 메모리 공간의 주소가 반환되어 ptr에 저장되었다.


그런데 이렇게 반환된 주소 값을 저장하고 있는 포인터 ptr을 이용해서 다음과 같은 연산을 하


면 컴파일 오류가 발생한다.


*ptr=10; 


앞서 void형 포인터는 포인터가 가리키는 대상에 대한 정보 없이 그냥 주소 값만 저장하고 있


는 변수라고 하였다. 따라서 이렇게 10을 저장하는 순간에,해당 메모리 공간에 4바이트 정수


의 형태로 10을 저장 해야 할지, 8바이트 정수의 형태로 10을 저장해야 할지 판단이 서지 않는


다. 때문에 void형 포인터는 메모리 참조를 위한 * 연산이 불가능하다.


따라서 힙에 할당된 메모리 공간은 형 변환을 통해서 직접 포인터의 형을 결정해야 한다.


1
2
3
4
5
6
7
8
#include <stdio.h>
int main(void)
{
void * vPtr=malloc(sizeof(int));
int * iPtr=(int *)vPtr;
*iPtr=10// 정상 동작
....
}
cs

위 코드에서 보면 void형 포인터를 int형 포인터로 변환하고 있다. 그리고 이 int형 포인터를 이용해


서 메모리 공간에 10을 저장한다. 따라서 할당된 메모리 공간에는 숫자 10이 4바이트 정수의 형태


로 저장이 된다. 이처럼 malloc으로 할당된 메모리 공간은 포인터 형을 결정하는 방식으로 사용의 


용도를 정하면 된다. 참고로 다음과 같이 한 줄에 표현하는 것이 가능하다.


1
int * iPtr= (int *)malloc(sizeof(int))
cs


7. 힙영역의 메모리 소멸


힙 영역은 메모리의 할당도, 그리고 할당된 메모리 공간의 해제도 프로그래머가 담당하는 영역이다.


malloc 함수의 호출을 통해서 할당한 메모리 공간을 해제해주지 않으면, 그만큼 메모리 공간의 낭


비가 발생하게 된다. 따라서 malloc 함수의 호출을 통해 할당된 메모리 공간은 직접 해제해야 하


며, 이 때 사용하는 함수가 바로 free 함수이다. 



malloc 함수를 호출할 때 반환된 값을 인자로 전달하면, malloc 함수 호출 시 할당되었던 메모리 공


간 전부가 해제된다. 그리고 인자로 NULL 포인터가 전달되면 아무런 일도 발생하지 않는다



8.  malloc 함수와 free 함수의 사용 예


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <stdio.h>
#include <stdlib.h>
 
char * ReadString(void);
int * ReadInteger(void);

int main(void)
{
    char * strPtr;
    int * numPtr;
 
    strPtr=ReadString();
    numPtr=ReadInteger();
 
    if(strPtr!=NULL)
        printf("읽어 들인 문자열: %s \n", strPtr);
 
    if(numPtr!=NULL)
        printf("읽어 들인 정수: %d \n"*numPtr);
 
    free(strPtr);
    strPtr=NULL;
 
    free(numPtr);
    numPtr=NULL;
 
    return 0;
}
char * ReadString(void)
{
    char * sPtr=(char*)malloc(sizeof(char)*20);
    if(sPtr==NULL)
        return NULL;
 
    printf("문자열 입력: ");
    scanf("%s", sPtr);
    return sPtr;
}
int * ReadInteger(void)
{
    int * iPtr=(int*)malloc(sizeof(int));
    if(iPtr==NULL)
        return NULL;
 
    printf("정수 입력: ");
    scanf("%d", iPtr);
    return iPtr;
}



cs

31행 : 우선 ReadString 함수부터 보도록 하자. 이 함수는 메모리 공간을 할당해서 문자열을 읽어


이는 일까지 담당하는 함수이다. 31행에서는 널 문자를 포함하여 최대 길이가 20인 문자열을 읽


어 들이기 위해서 malloc 함수의 호출을 통해 메모리 공간을 할당하고 있다.


32, 33행 : 메모리 공간 할당에 실패하지 않았는지를 확인하고 있다. 그리고 메모리 공간 할당에 실 


패를 한다면 그냥 NULL을 반환하면서 함수를 빠져나간다.


21~25행 : malloc 함수 호출을 통해서 할당 받은 메모리 공간을 free 함수 호출을 통해서 반환하고 


있다. 그리고 반환된 메모리를 가리키는 포인터 변수를 NULL로 초기화하고 있다.


그런데 NULL로 초기화를 하지않으면 문제가 발생할 수 있다.


free 함수의 호출을 통해 주소값에 저장된 데이터는 삭제가됬는데, 주소값 자체는 strPtr에 저장되어


있다. 그런데 혹여라도 실수로 다른 값을 *strPtr=20; 이렇게 저장한다면 문제가 생길수 있다. 왜냐


하면 그 주소값에 해당하는 메모리 공간이 나중에 다른용도로 할당되었을때 그 메모리공간을 침범


할 여지가 있는것이다.


9. calloc 함수


이 함수도 malloc과 제공하는 기능은 동일하다. 즉 힙 영역에 메모리 공간을 할당하는 기능을 제공한다.



그런데 차이점이 있다.


첫 번째 전달인자는 할당할 블록의 개수를 의미하고,두 번째 전달인자는 블록 하나당 바이트 크


기를 의미한다. 예를들면 총 30개를(elt count) 4바이트씩(elt size) 힙 영역에 할당하는 것이다.


그리고 malloc 함수는 할당된 메모리 공간을 별도의 값으로 초기화하지 않는다. 


따라서 할당된 메모리 공간이 쓰레기 값으로 채워지지만 calloc 함수는 할당된 메모리 공간의 모든 


비트를 0으로 초기화시킨다. 바로 이러한 특성 때문에 calloc 함수가 자주 사용되기도 한다.


그리고 calloc 함수의 호출로 할당된 메모리 공간을 해제할 때에는 malloc 함수와 마찬가지로 free 


함수를 사용하면 된다.


반응형

'BackEnd > C' 카테고리의 다른 글

volatile 과 restict  (0) 2019.03.15
포인터의 const 선언  (0) 2019.03.14
자료형에 이름을 부여하는 typedef 키워드  (0) 2019.03.14
Call-By-Value vs Call-By-Reference  (0) 2019.03.14
다차원 배열 이름의 포인터형  (0) 2019.03.14