C언어, 메모리 할당

Moon·2022년 10월 24일
1

해당 게시글은 동영상 강좌 - 동영상 강좌 - 'Do it! C언어 입문' - 16장 메모리 할당 (1/2)
를 학습하며 정리한 글입니다.

게시글에 작성된 그림은 모두 업로드된 유튜브 영상에서 캡처해온 것입니다.


프로그래밍에서 메모리 할당은 수행속도를 고려하여 데이터 처리를 효율적으로 하기 위한 중요한 파트다. C언어 포인터보다 어려울 수 있으나 효과적으로 설계하려면 필수.

프로그램 : 프로그래머가 만든 프로그램 실행 파일
프로세스 : CPU가 실행 파일에 있는 명령들을 실행할 수 있도록 OS가 실행 파일의 명령들을 읽어서 메모리에 재구성한 것. 실행중인 프로그램이라고 함. 세그먼트(여러 정보나 데이터를 기억하는 메모리 공간)의 집합으로 구성됨.

세그먼트는 다음 그림과 같이 구성됨(코드 세그먼트, 데이터 세그먼트, 스택 세그먼트)

메모리 할당은 두 가지(정적, 동적 메모리할당)로 구분된다.
1. 정적 메모리 할당

  • 컴파일러가 소스 코드를 기계어로 번역하는 시점에 변수에 크기에 맞게 메모리를 할당
  • 컴파일 하는 시점에 결정되기 때문에 메모리 변경을 하려면 다시 번역해야 함
  • 프로그램이 실행될 때 메모리의 '위치'가 결정됨.

변수가 메모리에서 유지되는 시간

  • 전역 변수 : 프로그램이 시작해서 종료될 때 까지.(실행 중 전역 변수 추가, 삭제 불가)
    -> 전역 변수는 프로세스의 데이터 세그먼트에 저장
  • 지역 변수 : 함수 호출 시 할당되었다가 함수 종료 시 사라짐.
    -> 지역 변수는 프로세스의 스택 세그먼트에 저장

정적으로 할당된 메모리를 관리하는 방법

  • 지역 변수를 사용하려면 그 변수의 현재 주소를 알아야한다.
  • 지역 변수의 현재 주소를 기억하려면, 지역 변수의 개수만큼 메모리(포인터)가 더 필요하다.
  • 같은 함수에 선언한 지역 변수들을 하나의 메모리 그룹으로 관리 가능(수많은 포인터가 만들어지는 거 방지)

지역변수를 시작 위치 포인터(START)와 끝 위치 포인터(END)를 사용하여 관리할 수 있다. 만약, 이렇게 하지 않으면 a,b,c,d 각각에 대한 포인터를 전부 관리해야한다!

그렇다면, start + @ 값만 하던데 왜 end가 필요할까요? C언어는 함수의 집합체 이기 때문에, end는 특정 변수를 가리키기 위한 게 아니라 다음에 할당된 함수를 위해 설정된 값. end가 있음으로 다음 함수가 시작될 값을 설정해주기 때문에, 다른 필요없는 포인터를 더 만들 필요가 없어짐.


스택(Stack)

  • 스택은 자료 구조의 한 종류
  • 두 개의 포인터로 많은 양의 데이터를 효과적으로 관리하는 이론(앞에서 end)
  • 베이스 포인터를 기준으로 데이터를 추가할 때마다 순서대로 데이터 쌓아올림
  • 스택포인터로 새로운 데이터가 추가될 위치를 가리킴
  • 스택은 스택 세그먼트에 관리 됨

컴파일러가 지역 변수를 저장할 메모리 공간을 확보하는 방법

  • C언어 변수를 위한 메모리 공간 확보를 위해 어셈블리어에 push를 수행
  • 4바이트 크기 메모리 공간이 스택에 추가
  • 필요하지 않다면 pop을 해서 삭제
  • 컴퓨터는 이런 push, pop을 일일히 하지는 않고 sub, add 명령어를 통해 메모리 할당

컴파일러가 스택에 할당된 지역변수를 사용하는 원리

  • C언어 컴파일러는 지역 변수가 선언된 순서대로 메모리 할당
  • 최근에 저장된 스택 포인터(SP) 바로 아래의 데이터부터 차례대로 꺼냄
  • 이 방식은 이론적인 거고 실제로 이렇게 하나하나 빼지는 않음

베이스 포인터(BP)를 사용하여 스택에 할당된 지역 변수 사용한다.

  • 스택 메모리로 간접 주소 지정 방식 개념을 이용하여 값을 읽거나 저장함
  • 그러나 이런 스택 메모리 방식도 수많은 함수가 호출될 때마다 BP, SP가 늘어난다는 단점이 있다.
    -> 이 단점을 커버하는 것이 스택 프레임!인데 일단은 아래 그림을 먼저 분석해보자.
  1. main함수가 test함수를 호출할 때 main 함수 IP를 저장(push IP)해두는데, 그 이유는 main()에서 Test()함수 호출이 끝나면 다시 main()으로 돌아올 때 IP를 타고 다시 돌아오는 것이다.
  2. IP가 저장된 위에 push BP를 통해 main함수의 BP를 그대로 가져와 저장한 다음, SP의 주소값을 대입한다. SP주소 값을 4만큼 감소한 후, TEST함수를 실행한다.

스택 프레임이란?

  • 스택 프레임 : 함수를 호출 할 때 일어나는 변화
  • 컴파일러가 C언어 코드를 기계어로 번역하는 시점에 결정됨

정적 메모리 할당의 한계

  • 프로세스 안에서 지역 변수가 저장되는 기본 스택 메모리 크기는 1메가바이트
  • 단일 변수의 크기가 1MByte를 넘거나 함수 내 모든 변수 크기를 합산하여 1MByte를 넘으면 프로그램 실행 시 오류 메시지 출력
char data[1024*1024]; // 단일 변수가 1MByte를 넘어 오류발생

동적 메모리 할당이란?

  • 스택 세그먼트에 저장
  • 원하는 시점에 원하는 크기만큼 힙(Heap)에 메모리 할당 가능
  • 사용이 끝나면 언제든 메모리 해제 가능
  • Giga Byte까지 할당 가능(컴퓨터 성능이 좋아짐에 따라 점점 많이 사용)
  1. malloc 함수
  • C 표준 함수인 malloc를 사용해 메모리 할당 가능
  • void * 형식으로 주소를 반환
  • 할당된 메모리 주소를 받는 시점에 미리 형 변환을 하여 사용
short *p = (short *)malloc(100);
int *p = (int *)malloc(100);

  1. free함수
  • 힙에 할당된 메모리는 프로그램이 끝날 때까지 자동 해제되지 않음
  • free함수를 이용하여 명시적으로 메모리 해제
  • malloc.h파일을 포함해야 함

*malloc 함수를 사용할 때 주의할 점!
1. free를 사용하지 않으면, 메모리 할당을 끝내지 않는다. 함수가 종료되더라도, free를 사용하지 않았다면 메모리 할당은 끝없이 일어난다
-> malloc과 free는 세트로 만들고 코딩을 시작해야한다.
-> 버그 중에 제일 잡기 힘든 버그..

  1. 할당되지 않은 메모리를 해제하면 실행 시 오류가 발생
    -> 포인터 선언, free를 사용해놓고 메모리 할당을 하지 않음

  2. 동적 할당된 주소를 중복 해제하면 실행 시 오류가 발생

동적 메모리 할당의 장단점

  1. 장점
  • 스택에 비해 큰 크기의 메모리 할당 가능
  • 메모리를 할당(malloc)하고 해제(free)하는 시점 지정 가능
  • 할당되는 메모리 크기를 프로그램 실행 중에 변경 가능
  1. 단점
  • 코드가 복잡해짐
  • 작은 메모리 사용할 때 비효율적(포인터 변수를 사용해야 하기 때문에 4바이트가 쓰임)
  • 힙을 많이 사용하면, 속도가 비교적 떨어짐.

배열과 비슷한 형식으로 동적 메모리를 사용할 수 있다.
-> 예)12바이트로 할당된 포인터를 3개의 4바이트(int)로 나눠서 사용하겠다.
-> int *p를 int data[3];으로 사용할 수 있다.

배열의 단점1 : 기본적으로 스택을 쓰기 때문에 1MByte밖에 못 쓴다.
배열의 단점2 : int data_size = 3; int data[data_size]; 는 오류가 발생. 배열의 요소 개수는 상수로만 명시되기 때문

동적 메모리를 할당하는 또 다른 방법

int *p = (int *)malloc(12); // 라고도 쓸 수 있지만
int *p = (int *)malloc(sizeof(int) * 3); //으로 쓰는 게 더 좋다!

short *p = (short *)malloc(sizeof(short)*6); // 다른 프로그래머들과 협업할 때도 좋다

정적 메모리 할당을 사용하여 숫자를 입력 받아 합산하기
-> 단점 : 코드는 간단할지 몰라도, 메모리가 낭비되거나 수정하려면 다시 컴파일 해야함.

#include <stdio.h>
#define MAX_COUNT 5
void main(){
    int num[MAX_COUNT], count = 0, sum = 0, i;
    while(count < MAX_COUNT){
        printf("숫자 입력 : ");
        scanf("%d", &num[count]);
        if(num[count] == 9999) break;
        count++;
    }
    for (i=0; i < count; i++){
        if(i > 0) printf(" + ");
        printf("%d", num[i]);
        sum = sum + num[i];
    }
    printf(" = %d\n", sum);
}

malloc 함수를 사용하면, 변수로 메모리 동적 할당 가능

int data_size = 12;
int *p = (int *)malloc(data_size); //데이터 개수(MAX_COUNT)를 따로 지정할 필요가 없음
#include <stdio.h>
#include <malloc.h>
void main(){
    int *p_num_list, count = 0, sum = 0, limit = 0,i;
    printf("사용할 최대 개수 입력 : ");
    scanf("%d", &limit);
    p_num_list = (int *)malloc(sizeof(int) * limit);
    while(count < limit){
        printf("숫자 입력 : ");
        scanf("%d", p_num_list + count);
        if(p_num_list[count] == 9999) break;
        count++;
    }
    for (i=0; i < count; i++){
        if(i > 0) printf(" + ");
        printf("%d", *(p_num_list + i));
        sum = sum + *(p_num_list + i);
    }
    printf(" = %d\n", sum);
    free(p_num_list);
}
profile
안녕하세요. Moon입니다!

0개의 댓글