C - 메모리

업데이트:

메모리의 종류

아래 영역들은 물리적으로 같은 메모리

  • 스택 섹션
  • 힙 섹션
  • 데이터 섹션, 코드 섹션
    • 특정 코드 및 데이터용으로 고정

힙 메모리가 범용적인 기본 형태

스택은 특별한 용도를 가진 메모리

  • 프로그램의 쓰레드마다 특별한 용도에 사용하라고 별도로 떼어준 것

CPU에도 저장 공간이 있음

  • 레지스터 : CPU에서만 사용하는 고속 저장 공간

스택 메모리

함수 호출 시 스택에 어떻게 들어가는지는 함수 호출 규약에 따라 달라짐 calling convention

https://getchan.github.io/cs/c_stack_memory/

레지스터

메모리는 아니다. CPU 전용 휘발성 데이터 저장 공간.

메모리에 읽고 쓰는게 상대적으로 느려서 레지스터를 사용한다

왜 메모리는 느릴까?

  • Bus를 타야 하기에 느림
  • 대부분 메모리는 DRAM임
    • 가격이 쌈. 기록된 내용을 유지하기 위해 주기적으로 정보를 다시 써야 함. 따라서 시간 소모 큼
  • SRAM은 빠르지만 비싸다

CPU 속도 개선을 위해 적은 용량의 SRAM을 CPU 안에 넣어버림. 그것이 레지스터.

명령 실행 패턴

  1. 변수의 값을 메모리 어딘가에 저장
  2. 그 변수의 값을 읽어와 레지스터에 저장
  3. 레지스터에 저장된 값을 가지고 계산한 뒤, 계산 결과도 레지스터에 저장
  4. 그 결과를 다시 메모리에 저장
  5. 다시 그 결과를 사용해서 계산하면 레지스터에 또 복사

CPU연산은 대부분 레지스터를 거친다.

register 키워드

레지스터 사용을 ‘요청’하는 예 - 컴파일러에서 사용 안 할 수도. 대부분 사용 안 함..

int num;
register size_t i;
num = 0;
for (i = 0; i < 1000; ++i) {
  num += i;
}

레지스터 변수의 제약

  1. 변수의 주소를 구할 수 없음

    register int num = 10;
    int* p;
    p = &num; // 컴파일 오류
    
  2. 레지스터 배열을 포인터로 사용 불가

    register int nums[10];
    int* p;
    p = &nums; // 컴파일 오류
    p = &nums[0]; // 컴파일 오류
    
  3. 블록 범위에서만 사용 가능 전역 변수에는 사용할 수 없음

    register int g_num; // 컴파일 에러
    int main(void){
      return 0;
    }
    

요즘엔 보통 컴파일러가 배포 모드에서 알아서 최적화함. 더 이상 프로그래머가 수동으로 사용하지 않는 키워드

힙 메모리

스택 메모리의 단점

  1. 수명
    • 지역 변수의 수명은 함수가 끝날 때까지
    • 전역 변수나 static 변수는 프로그램 실행 내내
    • 중간 정도의 수명을 가질 수는 없나?
  2. 크기
    • 스택 메모리의 크기는 컴파일 시에 결정하므로 크게 못 잡음
    • 엄청 큰 데이터를 처리해야 할 경우 스택 메모리에 못 넣음

힙 메모리

  • 범용적 메모리
  • 스택처럼 특정 용도로 할당한 것이 아님
  • CPU가 자동적으로 메모리 관리를 안 해줌
  • 프로그래머가 원할 때 원하는 만큼 메모리를 할당받아 원할 때 반납(해제)할 수 있음

장점

  • 용량 제한이 없음 : 컴퓨터에 남아있는 메모리만큼 사용 가능
  • 프로그래머가 데이터의 수명을 직접 제어

단점

  • 할당받은 메모리를 직접 해제 안 하면 누구도 그 메모리를 쓸 수 없음
  • 빌려간 쪽에서 메모리 주소 잃어버리면 메모리 누수 발생
  • 스택에 비해 할당 / 해제 속도가 느림
    • 스택은 오프셋 개념 vs 힙은 사용/비사용 중인 메모리 관리 개념
    • 메모리 공간에 구멍이 생겨 효율적인 메모리 관리가 어려움

정적 메모리 vs 동적 메모리

  • 스택 메모리는 정적 메모리
    • 이미 공간이 따로 할당되어 있음
    • 할당/해제가 자동으로 관리되도록 컴파일됨
    • 오프셋 개념으로 정확히 몇 바이트씩 사용해랴 하는지 컴파일시 결정
  • 힙 메모리는 동적 메모리
    • 런타임에 크기와 할당/해제 시기가 결정됨

동적 메모리

메모리 할당

  • 힙 관리자에게 메모리를 x바이트만큼 달라고 요청
    • 연속되는 x만큼의 메모리를 찾아서 반환
  • 시작 주소값을 반환하므로 반환 데이터형은 포인터

메모리 사용

  • 할당받은 메모리를 원하는대로 사용

메모리 반납 / 해제

  • 힙 관리자에게 반납 요청
    • 관리자는 메모리 주소를 아무도 사용하지 않는 상태로 바꿈

메모리 할당 함수

void* malloc(size_t size);
  • memory allocation
  • size 바이트 만큼의 메모리를 반환해줌
  • void*형으로 반환
  • 반환된 메모리에 있는 값은 쓰레기 값
    • 초기화 안 해줌
  • 메모리가 더 이상 없다거나 해서 실패하면 NULL 반환

메모리 해제 함수

메모리 누수를 방지하기 위해 malloc() 코드를 작성하자마자 free()코드도 추가하자

void free(void* ptr);
  • 할당받은 메모리를 해제하는 함수
  • 메모리 할당 함수들을 통해서 얻은 메모리만 해제 가능
  • 그 외의 주소를 매개변수로 전달할 경우 결과가 결과가 정의되지 않음

동적 메모리 할당 시 문제

  • 메모리 할당 함수가 반환한 주소가 저장된 변수를 그대로 포인터 연산에 사용하면 메모리 해제 시 문제 발생할 수 있음
  • 할당 받은 포인터로 연산 금지
    • 포인터 연산이 필요하다면 새로운 포인터 변수에 주소값 할당하여 사용하자

해제한 메모리를 또 해제해도 문제

  • 해제한 메모리를 또 해제하려고 할 때도 결과가 정의되지 않음. 크래시 날 수도.
  • 해제 후 널 포인터를 대입하자.

해제한 메모리 사용해도 문제

  • 포인터 연산이 필요해서 할당한 새로운 포인터 변수를 메모리 해제 후 사용하면, 결과 정의되지 않음
  • memory stomp
  • 해제 후 널 포인터를 대입하자.

malloc()free()는 한 몸

memset()

void* memset(void* dest, int ch, size_t count);
  • char로 초기화(1바이트씩) 됨
  • 다음의 경우 결과가 정의되지 않음
    • dest가 NULL pointer인 경우(널 포인터 역참조)
    • countdest영역을 넘어설 경우(소유하지 않은 메모리에 쓰기)

realloc()

void* realloc(void* ptr, size_t new_size);
  • 이미 존재하는 메모리(ptr)의 크기를 new_size 바이트로 변경
  • 새로운 크기가 허용하는 한 기존 데이터를 그대로 유지
  • 반환값은 아래 경우에 따라 달라짐

크기가 커져야 하는 경우

  1. 갖고 있는 메모리 뒤에 충분한 공간이 없으면 새로운 메모리를 할당한 뒤, 기존 내용을 복사하고 새 주소 반환
  2. 갖고 있는 메모리 뒤에 공간이 있으면 기존 주소를 반환. 그리고 추가된 공간을 쓸 수 있게 됨 (보장하지는 않음)

크기가 작아질 때도

  1. 기존 주소가 반환되거나
  2. 새롭게 메모리 할당 후 새 주소 반환해줄 수도 있다.

메모리 할당 실패 시, NULL을 반환하지만 기존 메모리는 해제되지 않음

  • 실패 시 메모리 누수 발생 가능

memcpy()

void* memcpy(void* dest, const void* src, size_t count);
  • src의데이터를 count바이트만큼 dest에 복사
  • 다음의 경우 결과가 정의되지 않음
    • dest 영역 뒤에 데이터를 복사할 경우 (소유하지 않은 메모리에 쓰기)
    • srcdest가 널 포인터일 경우 (널 포인터 역참조)

memcmp()

int memcmp(const void* lhs, const void* rhs, size_t count);
  • count바이트만큼의 메모리를 비교하는 함수
  • strcmp() 와 유사
    • 널 문자 만나도 계속 진행
  • 결과가 정의되지 않는 경우
    • lhs, rhs 를 넘는 비교
    • 널 포인터 역참조

구조체 두 개를 비교할 떄도 유용하다.

주소를 가진 구조체면 값이 같아도 주소가 달라 다르다고 판단함

베스트 프랙티스 : 정적 vs 동적 메모리

  • 정적 메모리를 우선적으로 사용할 것
  • 안 될 때 (스택 메모리 제한 등) 에만 동적 메모리 사용하자.
  • 동적 메모리는 어렵다. 메모리 누수 발생 가능

동적 메모리 소유권 문제

소유주 : 메모리를 반드시 책임지고 해제해야 하는 주체. 메모리를 생성한 함수

소유주가 아닐 때는 빌려 사용할 뿐 해제하면 안 됨!

동적 메모리 할당 후 해제 전 함수를 탈출할 경우 : goto문 사용

동적으로 할당 후 반환을 피할 수 없다면? : 코딩 표준으로 함수에 메모리 할당을 표기하자.

베스트 프랙티스

  1. malloc() 작성한 뒤에 곧바로 free()추가하자

  2. 동적 할당을 한 메모리 주소를 저정하는 포인터 변수와 포인터 연산에 사용하는 포인터 변수를 분리해 사용하자

    원래 포인터 변수를 사용할 경우, 주소를 잃어버려 해제를 못 할 수 있음

    void* pa_nums;
    int* p;
    pa_nums = malloc(LENGTH * sizeof(int));
    p = pa_nums;
    // pa_nums는 이제부터 사용 금지, p로만 작업
       
    free(pa_nums);
    pa_nums = NULL;
    
  3. 메모리 해제 후, 널 포인터 대입하자

  4. 정적 메모리를 우선으로 사용하고 어쩔 수 없을 때만 동적 메모리를 사용하자

  5. 동적 메모리 할당을 할 경우, 변수와 함수 이름에 사실을 표기하자. 메모리 할당한 함수에서 해제하는 것이 가장 바람직하다.

출처

POCU 아카데미 ‘C 언매니지드 프로그래밍’

태그: ,

카테고리:

업데이트:

댓글남기기