Programming Language/C

열혈 C - Chapter 25. 메모리 관리와 메모리의 동적 할당

hustle_D 2024. 11. 15. 18:11
반응형

25-1 C언어의 메모리 구조

메모리의 구성

메모리의 구성은 코드영역/데이터 영역/힙 영역/스택 영역으로 나뉜다.

이는 운영체제에 의해 할당된 메모리 공간이다.

 

1. 코드(Code)영역
코드 영역은 실행할 프로그램의 코드가 저장되는 영역이다.
CPU가 여기에 저장된 명령어를 하나씩 가져다 처리한다.

2. 데이터(Data)영역
프로그램의 전역 변수와 정적(static)변수가 저장된다.
프로그램 시작 시 할당되고 종료 시 소멸된다.

3. 스택(stack)영역
함수 호출과 관련된 지역 변수와 매개변수가 저장된다.
함수 호출 시 할당되고 종료시 소멸된다.
후입선출(LIFO) 방식으로 동작한다.
메모리의 높은 주소에서 낮은 주소의 방향으로 할당된다.


4. 힙(Heap)영역
사용자가 직접 관리하는 동적 메모리의 할당 영역으로 메모리의 낮은 주소에서 높은 주소의 방향으로 할당된다.

사용자가 직접 관리하기에 원하는 시점에 메모리 공간에 할당 소멸을 하기 위한 영역이다.

 

이렇게 메모리의 공간을 나눠놓은 이유는 서랍장에 수납공간을 분리해놓은 이유와 같다.

공간을 나눠서 유사한 성향의 데이터를 묶어 저장하면 관리가 쉽고 접근 속도가 향상되기 때문이다.

 

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

 

이런 코드를 빌드해서 빌드 파일을 실행시키면 기존에는 메인함수를 실행시킨다라고 알고 있었지만, 이 메인함수를 호출하기 이전에 먼저 전역변수들을 데이터 영역에 할당한다.

 

그래서 데이터 영역에 가장 먼저 sum = 25라는 값을 할당시킨다

물론 이 때 스태틱 변수도 전역 변수처럼 데이터 영역에 할당된다.

 

이제 모든 전역변수와 스태틱 변수를 할당했다면 main 함수를 호출한다.

그리고 그 안에 있는 내부에 있는 지역변수를 스택 영역에 할당한다.

이 다음에 함수를 만나면

 

함수 내부에 있는 매개변수와 지역변수를 모두 스택영역에 할당한다.

그리고 나서 fct가 반환 이 되면 매개변수 n과 num2가 스택 영역에서 소멸되고 다음 코드를 실행한다.

다음 코드인 num1이 ++ 되고 이 값이 스택영역의 num1을 변경시키고 

다시 fct함수로 들어가면서 매개변수와 지역변수가 스택영역에 할당된다.

그리고 main함수를 return하면서 데이터 영역, 스택 영영에 할당되었던 값들이 모두 소멸된다.

25-2 메모리의 동적 할당

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

 

이 코드를 보면 ReadUserName의 경우는 return 값으로 name을 던지고 이건 char * 형태로 해당 값이 함수가 종료되어도 존재해야함을 알 수 있다.

그런데 전역변수 혹은 지역변수의 경우는 함수가 종료되면서 소멸되게 되어 있기에 이 코드는 현재 상태로 사용할 수 가 없다.

 

전역 변수로 선언하게 되면 문제가 해결 될까?

그러면 name1과 name2가 바라보는게 결국 동일한 값이 되게 될것이다.

그러면 name1에 사용하는 어떤 코드들이 name2에 영향을 끼치게 되기 때문에 이것도 문제가 있는 사용방법이다.

 

이럴 때 사용할 수 있는 방법은 

힙 영역의 메모리 공간 할당과 해제

힙 영역에 할당하는 방법이 해결책인 이유는 힙 영역의 경우는 함수의 생명주기에 영향을 받지 않기 때문이고 사용자가 직접 메모리 공간에 할당하고 해제할 수 있기 때문이다.

 

이 힙 메모리에 공간을 할당하는 함수로는 malloc 함수가 있고 해제하는 함수로는 free함수가 있다.

#include <stdlib.h>

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

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

 

malloc 함수와 free함수를 사용하는 방법은 

void * ptr1 = malloc(4);    // 4바이트가 힙 영역에 할당
void * ptr2 = malloc(12);   // 12바이트가 힙 영역에 할당

......

free(ptr1);    //ptr1이 가리키는 4바이트 메모리 공간 해제
free(ptr2);    //ptr2가 가리키는 12바이트 메모리 공간 해제

와 같다.

 

여기서 주의 해야 할 점은 malloc함수는 반환값을 void로 반환한다는 것이고, malloc함수 자체로 사용하는 데이터의 크기를 모르기 때문에 이를 형변환해서 사용해야 안전하게 사용할수 있다는 점이다.

void * ptr1 = malloc(sizeof(int));
void * ptr2 = malloc(sizeof(double));
void * ptr3 = malloc(sizeof(int)*7);
void * ptr4 = malloc(sizeof(double)*9);

↓
sizeof 연산 이후 실질적인 malloc 의 호출
↓

void * ptr1 = malloc(4);
void * ptr2 = malloc(8);
void * ptr3 = malloc(28);
void * ptr4 = malloc(72);

 

보시다 싶이 ptr1부터 ptr4까지 malloc 함수가 힙 메모리에 공간을 할당하고 포인터를 반환하는데 이 값의 형태가 어떤 데이터를 표현하는 것인지 확인할 수 없어 메모리에는 포인터 형을 결정하지 못한다.(왜냐면 malloc함수는 결국 그냥 얼만큼 할당할것인지에 대한 크기만 제공되기 때문...)

그렇기에 형변환을 해주면서 메모리의 주소 값을 저장해야한다.

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);

이게 malloc함수의 가장 모범적인 형태의 호출방법이다.

 

힙 영역으로의 접근

이렇게 힙 영역으로의 접근은 포인터로만 가능하게 된다.

 

* 동적할당이라고 한 이유는 컴파일시 할당에 필요한 메모리 공간에 계산되지 않고 실행할때 할당에 필요한 메모리 공간이 계산 되기 때문이다.

 

free 함수를 호출하지 않으면...

할당된 메모리 공간은 소멸되지 않고 계속 자리를 차지하게 된다.

물론 프로그램이 종료되면 모든 자원이 반환되지만 fopen을 하면 fclose를 하듯이 비슷한 이유 때문에 프로그램의 종료 이전에 사용할때마다 해제를 해주는 것이 좋다.

그렇기에 malloc 또한 free랑 쌍을 이뤄서 항상 사용하도록 하자.

 

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

이제 그럼 이전에 

이 코드를 malloc 함수를 사용해서 해결 해보도록 하자.

 

malloc함수가 사용될 곳은 name부분이다.

길이가 30인 배열이기에 char형 30개의 데이터 공간을 할당하면 되기에 

char * name에 malloc함수를 사용해서 char형 30개의 데이터 공간을 힙 공간에 동적으로 할당하도록 만들어주자.

이러고 실행시켜보면

실행이 잘 되는 것을 확인할 수 있고 해당 name이 함수를 벗어나도 소멸되지 않고 유지되는 것을 볼 수 있다.

 

물론 메인 함수 내부에서는 해당 값을 사용한 후에 소멸시켜줘야만 한다.

calloc & realloc

calloc

특정 크기의 메모리를 연속적으로 할당하고 초기화하는데 사용한다.

malloc과의 차이는 메모리 할당을 위한 전달인자의 전달 방식에 있다.

#include <stdlib.h>

void* calloc(size_t num_elements, size_t element_size);
=>> 성공 시 할당된 메모리의 주소 값, 실패 시 NULL 반환

num_elements x element_size 크기의 바이트를 동적 할당한다.

그리고 malloc과 또 다른점은 모든 비트를 0으로 초기화 하면서 할당한다는 점이다.

추가적으로 다른점은 연속적으로 메모리를 할당한다는 점이다.

 

realloc

이미 동적으로 할당된 메모리 블록의 크기를 조정하는 데 사용한다.

#include <stdlib.h>

void* realloc(void* ptr, size_t size);
=>> 성공 시 새로 할당된 메모리의 주소 값, 실패 시 NULL 반환

 

  • ptr : 재할당할 메모리 블록을 가리키는 포인터로 이전에 malloc, calloc 다른 realloc을 통해서 할당된 메모리를 가리키고 있어야 한다.
  • size :  새로 할당할 메모리 블록의 크기(바이트 단위)

 

ptr이 가리키는 힙 메모리 공간을 size의 크기로 늘리거나 줄인다.

 

realloc의 경우는 동작에 대해서 조금 알아야할 부분이 있는데 

  • 메모리 확장 : 요청한 새로운 크기가 기존 크기보다 클 경우, 기존 블록을 확장하기 위한 시도를 한다. 이때 인접한 공간이 확장하기에 공간이 충분하다면 기존 블록을 그대로 두고 공간을 확장한다.
  • 메모리 축소: 요청한 새로운 크기가 기존보다 작은 경우, 기존 블록의 크기를 줄이는데, 이 경우는 데이터는 유지되며 남은 공간은 해제된다.
  • 새로운 블록 할당 : 만약 요청한 새로운 크기로 확장할때 기존 블록의 인접 공간이 확장하기에는 부족할때 아예 새로운 블록을 할당하고 데이터를 복사한 후 기존 블록을 해제한다.
  • NULL처리 : ptr이 만약 동적공간을 가리키지 않고 그냥 NULL일 경우엔 새로운 메모리를 할당하고 그 주소를 반환한다.

이 부분을 확인할 수 있는 방법은 기존의 malloc 함수와 realloc 함수가 반환한 주소값을 좀 눈여겨 볼 필요가 있다.

 

malloc 함수가 가리키던 주소와 realloc함수가 반환한 주소가 같다면 기존에 할당된 메모리 공간을 이어서 확장할만큼 인접한 공간에 여유가 있는 경우이고, malloc 함수가 가리키던 주소와 realloc 함수가 반환한 주소가 다르다면 기존에 할당된 메모리 공간의 인접 공간이 확장할 만큼 여유가 있지 않은 경우이다.

 

새로 공간을 마련하는 경우에는 메모리의 복사 과정이 추가된다는 점을 인지하고 있자.

반응형