C - 메모리
업데이트:
메모리의 종류
아래 영역들은 물리적으로 같은 메모리
- 스택 섹션
- 힙 섹션
- 데이터 섹션, 코드 섹션
- 특정 코드 및 데이터용으로 고정
힙 메모리가 범용적인 기본 형태
스택은 특별한 용도를 가진 메모리
- 프로그램의 쓰레드마다 특별한 용도에 사용하라고 별도로 떼어준 것
CPU에도 저장 공간이 있음
- 레지스터 : CPU에서만 사용하는 고속 저장 공간
스택 메모리
함수 호출 시 스택에 어떻게 들어가는지는 함수 호출 규약에 따라 달라짐 calling convention
https://getchan.github.io/cs/c_stack_memory/
레지스터
메모리는 아니다. CPU 전용 휘발성 데이터 저장 공간.
메모리에 읽고 쓰는게 상대적으로 느려서 레지스터를 사용한다
왜 메모리는 느릴까?
- Bus를 타야 하기에 느림
- 대부분 메모리는 DRAM임
- 가격이 쌈. 기록된 내용을 유지하기 위해 주기적으로 정보를 다시 써야 함. 따라서 시간 소모 큼
- SRAM은 빠르지만 비싸다
CPU 속도 개선을 위해 적은 용량의 SRAM을 CPU 안에 넣어버림. 그것이 레지스터.
명령 실행 패턴
- 변수의 값을 메모리 어딘가에 저장
- 그 변수의 값을 읽어와 레지스터에 저장
- 레지스터에 저장된 값을 가지고 계산한 뒤, 계산 결과도 레지스터에 저장
- 그 결과를 다시 메모리에 저장
- 다시 그 결과를 사용해서 계산하면 레지스터에 또 복사
CPU연산은 대부분 레지스터를 거친다.
register
키워드
레지스터 사용을 ‘요청’하는 예 - 컴파일러에서 사용 안 할 수도. 대부분 사용 안 함..
int num;
register size_t i;
num = 0;
for (i = 0; i < 1000; ++i) {
num += i;
}
레지스터 변수의 제약
-
변수의 주소를 구할 수 없음
register int num = 10; int* p; p = # // 컴파일 오류
-
레지스터 배열을 포인터로 사용 불가
register int nums[10]; int* p; p = &nums; // 컴파일 오류 p = &nums[0]; // 컴파일 오류
-
블록 범위에서만 사용 가능 전역 변수에는 사용할 수 없음
register int g_num; // 컴파일 에러 int main(void){ return 0; }
요즘엔 보통 컴파일러가 배포 모드에서 알아서 최적화함. 더 이상 프로그래머가 수동으로 사용하지 않는 키워드
힙 메모리
스택 메모리의 단점
- 수명
- 지역 변수의 수명은 함수가 끝날 때까지
- 전역 변수나
static
변수는 프로그램 실행 내내 - 중간 정도의 수명을 가질 수는 없나?
- 크기
- 스택 메모리의 크기는 컴파일 시에 결정하므로 크게 못 잡음
- 엄청 큰 데이터를 처리해야 할 경우 스택 메모리에 못 넣음
힙 메모리
- 범용적 메모리
- 스택처럼 특정 용도로 할당한 것이 아님
- 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인 경우(널 포인터 역참조)
count
가dest
영역을 넘어설 경우(소유하지 않은 메모리에 쓰기)
realloc()
void* realloc(void* ptr, size_t new_size);
- 이미 존재하는 메모리(
ptr
)의 크기를new_size
바이트로 변경 - 새로운 크기가 허용하는 한 기존 데이터를 그대로 유지
- 반환값은 아래 경우에 따라 달라짐
크기가 커져야 하는 경우
- 갖고 있는 메모리 뒤에 충분한 공간이 없으면 새로운 메모리를 할당한 뒤, 기존 내용을 복사하고 새 주소 반환
- 갖고 있는 메모리 뒤에 공간이 있으면 기존 주소를 반환. 그리고 추가된 공간을 쓸 수 있게 됨 (보장하지는 않음)
크기가 작아질 때도
- 기존 주소가 반환되거나
- 새롭게 메모리 할당 후 새 주소 반환해줄 수도 있다.
메모리 할당 실패 시, NULL
을 반환하지만 기존 메모리는 해제되지 않음
- 실패 시 메모리 누수 발생 가능
memcpy()
void* memcpy(void* dest, const void* src, size_t count);
src
의데이터를count
바이트만큼dest
에 복사- 다음의 경우 결과가 정의되지 않음
dest
영역 뒤에 데이터를 복사할 경우 (소유하지 않은 메모리에 쓰기)src
나dest
가 널 포인터일 경우 (널 포인터 역참조)
memcmp()
int memcmp(const void* lhs, const void* rhs, size_t count);
- 첫
count
바이트만큼의 메모리를 비교하는 함수 strcmp()
와 유사- 널 문자 만나도 계속 진행
- 결과가 정의되지 않는 경우
lhs
,rhs
를 넘는 비교- 널 포인터 역참조
구조체 두 개를 비교할 떄도 유용하다.
주소를 가진 구조체면 값이 같아도 주소가 달라 다르다고 판단함
베스트 프랙티스 : 정적 vs 동적 메모리
- 정적 메모리를 우선적으로 사용할 것
- 안 될 때 (스택 메모리 제한 등) 에만 동적 메모리 사용하자.
- 동적 메모리는 어렵다. 메모리 누수 발생 가능
동적 메모리 소유권 문제
소유주 : 메모리를 반드시 책임지고 해제해야 하는 주체. 메모리를 생성한 함수
소유주가 아닐 때는 빌려 사용할 뿐 해제하면 안 됨!
동적 메모리 할당 후 해제 전 함수를 탈출할 경우 : goto
문 사용
동적으로 할당 후 반환을 피할 수 없다면? : 코딩 표준으로 함수에 메모리 할당을 표기하자.
베스트 프랙티스
-
malloc()
작성한 뒤에 곧바로free()
추가하자 -
동적 할당을 한 메모리 주소를 저정하는 포인터 변수와 포인터 연산에 사용하는 포인터 변수를 분리해 사용하자
원래 포인터 변수를 사용할 경우, 주소를 잃어버려 해제를 못 할 수 있음
void* pa_nums; int* p; pa_nums = malloc(LENGTH * sizeof(int)); p = pa_nums; // pa_nums는 이제부터 사용 금지, p로만 작업 free(pa_nums); pa_nums = NULL;
-
메모리 해제 후, 널 포인터 대입하자
-
정적 메모리를 우선으로 사용하고 어쩔 수 없을 때만 동적 메모리를 사용하자
-
동적 메모리 할당을 할 경우, 변수와 함수 이름에 사실을 표기하자. 메모리 할당한 함수에서 해제하는 것이 가장 바람직하다.
출처
POCU 아카데미 ‘C 언매니지드 프로그래밍’
댓글남기기