레지스터, 스택 & 힙, 동적 메모리. 다중 포인터
메모리의 종류
- 프로그램에서 주로 사용하는 부품은
- CPU
- 메모리
- 메모리는 또 다시 나뉘는데...
- 스택 메모리
- 힙 메모리
- 기본은 힙 메모리
- 힙 메모리가 범용적인 기본 형태
- 스택은 특별한 용도를 가진 메모리
- 프로그램마다 특별한 용도에 사용하라고 별도로 떼어놔 준 것이 스택 메모리
- 엄밀히 말하면 그 프로그램의 thread마다
- CPU 안에도 저장공간이 있음
- 레지스터 : CPU에서만 사용할 수 있는 고속 저장 공간
- 엄밀한 의미의 메모리는 아님
레지스터
메모리를 읽고 쓰는 게 느린 이유
- CPU가 메모리에 접근할 때마다 버스를 타야 함
- 대부분 컴퓨터에 장착하는 메모리는 DRAM임
- DRAM은 가격이 저렴한 대신, 한 가지 큰 단점이 있음
- 기록된 내용을 유지하기 위해서 주기적으로 정보를 다시 써야함
- 다시 쓰는 동안 또 시간을 소모
- 이러한 단점이 없는 메모리가 있긴 함
- SRAM
- 비용이 DRAM에 비해 매우 비쌈
- DRAM은 가격이 저렴한 대신, 한 가지 큰 단점이 있음
-
그래서 나온 방법이 SRAM을 CPU와 메모리 사이에 두는 것
-
CPU랑 가까이 두고 싶어서 아예 CPU 안에 넣어버림
-
그게 바로 레지스터
-
레지스터는 CPU가 사용하는 저장 공간 중에 가장 빠른 저장공간
-
CPU가 연산을 할 때 보통 레지스터에 저장되어 있는 데이터를 사용
-
그 연산 결과도 레지스터에 다시 저장한느 게 보통
-
다시 강조한느데, 레지스터는 흔히 말하는 메모리가 아님
register 키워드
register <자료형> <변수명>;
- 저장 유형 지정자 (storage-class specifier)
- 가능하다면 해당 변수를 레지스터에 저장할 것을 요청
- 실제로 레지스터를 사용할지 말지는 컴파일러가 결정
- 레지스터는 메모리가 아님!
- 따라서, 레지스터 변수들은 몇 가지 제약을 받음
제약1 : 변수의 주소를 구할 수 없음
제약2 : 레지스터 배열을 포인터로 사용 불가
제약3 : 블록 볌위에서만 사용 가능
-
전역 변수에는 사용할 수 없음
-
이제는 보통 컴파일러가 release모드에서 알아서 최적화
-
더 이상 프로그래머가 수동으로 사용하지 않는 키워드
힙 메모리
스택 메모리의 단점1 - 수명
- 함수가 반환하면 그 안에 있던 데이터가 다 날아감
- 데이터를 오래 보존하려면 전역 변수, 또는 static 키워드를 사용해야 했음
- 그 중간 어딘가를 원할 수도?
스택 메모리의 단점2 - 크기
- 특정 용도에 쓰라고 별도로 떼어 놓은 메모리
- 그 크기는 컴파일 시에 결정하므로 너무 크게 못 잡음
- 그래서 엄청 큰 데이터를 처리해야 할 경우 스택 메모리에 못 넣음
힙 메모리
- 컴퓨터에 존재하는 범용적 메모리
- 스택 메모리처럼 특정 용도로 떼어 놓은 게 아님
- 스택과 달리 컴파일러 및 CPU가 자동적으로 메모리 관리를 안 해줌
- 따라서 프로그래머가 원하는 때 원하는 만큼 메모리를 할당 받아와 사용하고 원할 때 반납(해제)할 수 있음
힙 메모리의 장점
- 용량 제한이 없음
- 프로그래머가 데이터의 수명을 직접 제어
힙 메모리의 단점1
- 빌려온 메모리를 직접 해제 안 하면 누구도 그 메모리를 쓸 수 없음
- 그 메모리는 계속 '누군가'에게 빌려준 상태
- 만약, 빌려간 쪽에서 그 메모리 주소를 잃어버리면 메모리 누수 발생
힙 메모리의 단점2
- 스택에 비해 할당/해제 속도가 느림
- 스택은 오프셋 개념 vs 힙은 사용/비사용 중인 메모리 관리 개념
- 메모리 공간에 구멍이 생겨 효율적으로 메모리 관리가 어렵기도 함
정적 메모리 vs 동적 메모리
- 스택 메모리는 정적 메모리
- 이미 공간이 따로 잡혀 있음
- 할당/해제가 자동으로 관리되게 코드가 컴팡리 됨
- 오프셋 개념으로 정확히 몇 바이트씩 사용해야 하는지 컴파일시 결정
- 힙 메모리는 동적 메모리
- 실행 중에 크기와 할당/해제 시기가 결정됨
동적 메모리
- 프로그램이 동적 메모리를 가져다 사용할 때는 총 세가지 단계를 거침
- 메모리 할당
- 메모리 사용
- 메모리 해제
메모리 할당 및 해제 함수
<stdlib.h>
동적 메모리에만 사용 가능
- 할당
- malloc()
- calloc()
- 해제
- free()
- 재할당
- realloc()
기타 메모리 관련 함수
<string.h>
동적 메모리 및 정적 메모리에 사용 가능
- memset()
- memcpy()
- memcmp()
malloc()
void* malloc(size_t size);
- 메모리 할당의 약자
- size 바이트 만큼의 메모리를 반환해줌
- 범용적이고 프로그래머가 알아서 사용할 거니까 void*
- 반환된 메모리에 들어있는 값은 쓰레기 값
- 메모리가 더 이상 없다거나 해서 실패하면 NULL 반환
free()
- malloc() 코드를 작성하면 곧바로 free() 코드도 추가하는 습관을 들이는 게 좋음
#include <stdlib.h>
#define LENGTH (10)
size_t i;
int* nums = malloc(LENGTH * siszeof(int));
for(i = 0; i<LENGTH; ++i){
nums[i] = i * LENGTH;
}
for(i = 0; i<LENGTH; ++i){
printf("%d ", nums[i]);
}
free(nums);
여러 줄의 입력을 받아 출력하기 예
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define NUM_LINES (5)
#define LINE_LENGTH (2048)
char* lines[NUM_LINES];
char line[LINE_LENGTH];
size_t i;
size_t j;
for(i=0; i<NUM_LINES; ++i){
if(fgets(line, LINE_LENGTH, stdin)){
clearerr(stdin);
break;
}
lines[i] = malloc(strlen(line) + 1);
if(liens[i] == NULL){
fpringf(stderr, "%s\n", "out of mem");
break;
}
strcpy(lines[i], line);
}
for(j = 0; j< i; ++j){
printf("%s", lines[j]);
}
for(j = 0; j< i; ++j){
free(lines[i]);
}
- 한계 : 줄 수가 NUM_LINES로 고정
학생 정보 입력 받기 예
#define INCREMENT (2)
typedef struct{
char firstname[NAME_LEN];
char lastname[NAME_LEN];
unsigned int id;
float gpa;
} student_t;
student_t read_student()
{
student_t s;
// 생략
return s;
}
while(TRUE){
student = read_student();
if(current_index == max_students){
max_students += INCREMENT;
tmp = malloc(max_students * sizeof(student_t));
memcpy(tmp, students, current_index * sizeof(student_t));
free(students);
students = tmp;
}
students[current_index++] = student;
}
free(students);
students = NULL;
파일에서 학생 정보 입력 받기 예
FILE* file;
size_t num_students;
size_t num_read;
student_t* students;
file = fopen("test.dat", "rb");
num_read = fread(&num_students, sizeof(size_t), 1, file); // 학생 수 읽어옴(파일 형식이 그렇게 되어 있음)
students = malloc(num_students * sizeof(student_t));
num_read = fread(students, sizeof(student_t), num_students, file);
fclose(file);
free(students);
제대로 된 free() 설명
void free(void* ptr);
- 할당 받은 메모리를 해제하는 함수
- 즉, 메모리 할당 함수들을 통해서 얻은 메모리만 해제 가능
- 그 외의 주소를 매개변수로 전달할 경우 결과가 정의되지 않음
동적 메모리 할당 시 문제
할당 받아 온 주소를 그대로 연산에 사용하면
- 메모리 할당 함수가 반환한 주소가 저장된 변수를 그대로 포인터 연산에 사용하면 메모리 해제할 때 문제가 발생할 수도 있음
코딩 표준 : 할당 받은 포인터로 연산 금지
- 메모리 할당 함수에서 받아온 포인터와 포인터 연산에 사용하는 포인터를 분리하자
void* nums;
int* p;
size_t i;
nums = malloc(LENGTH * sizeof(int));
p = nums;
for(i = 0; i < LENGTH; i++){
*p++ = 10 * (i + 1);
}
free(nums);
해제한 메모리를 또 해제해도 문제
- 잘못하면 크래시가 날 수도...
해제한 메모리르 사용해도.. 문제
코딩 표준 : 해제 후 널 포인터를 대입
- free()한 뒤에 변수에 NULL을 대입해서 초기화
- 안 그러면 해제된 놈인지 나중에 모르니
- 널 포인터를 free()의 매개변수로 전달해도 안전
void* nums;
int* p
size_t i;
nums = malloc(LENGTH * sizeof(int));
p = nums;
// 코드 생략
free(nums);
nums = NULL;
- malloc()한 뒤 free() 까먹으면 메모리 누수
- malloc()으로 받아온 주소를 지역 변수에서 저장해놨는데 해제 안하고 함수에서 나가면 주소가 사라져서 지울 방법이 아예 없어짐
free()는 몇 바이트를 해제할지 어떻게 알지? - calloc(), memset(), realloc()
-
구현마다 다르지만 보통 malloc(32)하면 그것보다 조금 큰 메모리를 할당한 뒤, 제일 앞부분에 어떤 데이터들을 채워 놓음
-
그리고 그만큼 오프셋을 더한 값을 주소로 돌려줌
-
나중에 그 주소 해제를 요청하면 free()가 다시 오프셋만큼 빼서 그 앞 주소를 본 뒤, 실제 몇 바이트가 할당됐었는지 확인 후 해제
calloc() void* calloc(size_t num, size_t size);
- 메모리를 할당할 때 자료형의 크기(size)와 수(num)를 따로 지정
- 모든 바이트를 0으로 초기화 해 줌
- 잘 안 씀
calloc() = malloc() + memset()
보통 calloc() 대신 malloc()와 memset()을 조합해서 씀
- memset()을 쓰면 0 이외의 값으로도 초기화 가능
void* nums;
nums = calloc(LENGTH, sizeof(int));
free(nums);
nums = NULL;
void* nums;
nums = malloc(LENGTH * sizeof(int));
memset(nums, 0, LENGTH * sizeof(int));
free(nums);
nums = NULL;
memset()
void* memset(void* dest, int ch, size_t count);
- <string.h>에 있음
- char로 초기화(1바이트씩) 됨
- 그 외의 자료형으로 초기화하려면 직접 for문을 서야 함
- 다음과 같은 경우 결과가 정의되지 않음
- count가 dest의 영역을 넘어설 경우
- dest가 널 포인터일 경우
char로만 초기화해주는 memset()
void* nums;
nums = malloc(LENGTH * sizeof(int));
memset(NUMS, 1000, LENGTH * sizeof(int));
free(nums);
nums = NULL;
int로 초기화하고 싶다면?
void* nums;
int* p;
size_t i;
nums = malloc(LENGTH * sizeof(int));
p = nums;
for(i = 0; i < LENGTH; i++){
*p++ = 1000;
}
free(nums);
nums = NULL;
realloc()
void* realloc(void* ptr, size_t new_size);
- 이미 존재하는 메모리(ptr)의 크기를 new_size 바이트로 변경
- 새로운 크기가 허용하는 한 기존 데이터를 그대로 유지
- 아까 봤던 학생 정보 입력 받기 예에서 동적 배열 크기를 늘리던 코드를 이 함수로 대체 가능
크기가 커져야할 때, 두가지 경우가 있음
- 지금 갖고 잇는 메모리 뒤에 충분한 공간이 없으면 새로운 메모리를 할당한 뒤, 기존 내용을 복사하고 새 주소 반환
- 지금 갖고 잇는 메모리 뒤에 공간이 충분하다면 그냥 기존 주소를 반환(보장은 x). 그리고 추가된 공간을 쓸 수 있게 됨
str = malloc(LENGTH * sizeof(char)); // length : 4 str = realloc(str, 2 * LENGTH * sizeof(char));
realloc()의 메모리 누수 문제, memcpy()
void* realloc(void* ptr, size_t new_size);
- 반환값
- 성공 시, 새롭게 할당된 메모리의 시작 주소를 반환하며 기존 메모리는 해제됨
- 실패 시, NULL을 반환하지만 기존 메모리는 해제되지 않음
- 실패 시 메모리 누수가 발생할 수 있음!!!
메모리 누수가 나는 경우
void* nums;
nums = malloc(SIZE);
nums = realloc(nums, 2 * SIZE); // null 반환
- 원래 nums에 저장되어 있던 주소가 사라짐
- NULL이 반환되었다는 이야기는 재할당에 실패했다는 의미
- 따라서 기존 메모리는 해제되지 않았음
- 그러나 그 주소를 잃어버려서 해제할 수 없다. 메모리 누수 발생!
올바른 재할당 방법
void* nums;
void* tmp;
nums = malloc(SIZE);
tmp = realloc(nums, 2 * SIZE);
if(tmp != NULL){
nums = tmp;
}
realloc() = malloc() + memcpy() + free()
void* nums;
void* tmp;
nums = malloc(LENGTH);
tmp = realloc(nums, 2 * LENGTH);
if(tmp != NULL){
nums = tmp;
}
free(nums);
void* nums;
void* tmp;
nums = malloc(LENGTH);
tmp = malloc(2 * LENGTH);
if(tmp != NULL){
memcpy(tmp, nums, LENGTH);
free(nums);
nums = tmp;
}
free(nums);
memcpy()
void* memcpy(void* dest, const void* src, size_t count);
- <string.h>에 있음
- src의 데이터를 count 바이트 만큼 dest에 복사
- 다음과 같은 경우 결과가 정의되지 않음
- dest의 영역 뒤에 데이터를 복사할 경우
- src나 dest가 널 포인터일 경우
메모리 누수 안 나게 코드를 작성할 것!
- realloc()을 쓸 때는 정말 조심해야 함
- 그래서 차라리 malloc() + memcpy() + free()로 좀 더 명싲거으로 드러나게 코딩하는 게 나을지도
- 그냥 신경 안쓰고 realloc()을 쓰는 경우도 많음
- 메모리 시작 주소가 변하지 않는 경우 데이터 복사를 하지 않아 성능상 이득
- malloc()에서 실패하는 일이 없다고 가정하고 코딩을 하는 경우가 많은 이유도 마찬가지
realloc()의 특수한 경우
nums = realloc(NULL, LENGTH);
- 새로운 메모리 할당
- malloc(LENGTH)와 동일
memcmp(), 정적 vs 동적 메모리
여러 줄 입력 받아 출력하기
#define LINE_LENGTH (2048)
#define INCREMENT (2)
char** lines;
char line[LINE_LENGTH];
size_t max_lines;
size_t num_lines;
size_t i;
char** tmp;
max_lines = 0;
num_lines = 0;
lines = NULL;
while(1){
if(fgets(line, LINE_LENGTH, stdin) == NULL){
clearerr(stdin);
break;
}
if(num_lines == max_lines){
tmp = realloc(lines, (max_lines + INCREMENT) * sizeof(char*));
if(tmp == NULL){
fprintf(stderr, "%s\n", "out of memory");
break;
}
lines = tmp;
max_lines += INCREMENT;
}
lines[num_lines] = malloc(strlen(line) + 1);
if(lines[num_lines] == NULL){
fprintf(stderr, "%s\n", "out of memory");
break;
}
strcpy(lines[num_lines++], lines);
}
for(i = 0; i < num_lines; ++i){
printf("[%d] %s", i, lines[i]);
}
for(i = 0; i < num_lines; ++i){
free(lines[i]);
}
free(lines);
return 0;
memcmp()
int memcpy(const void* lhs, const void* rhs, size_t count);
- 첫 count 바이트 만큼의 메모리를 비교하는 함수
- strcmp()와 매우 비슷
- 단, 널 문자를 만나도 계속 진행
- 다음의 경우 결과가 정의되지 않음
- lhs과 rhs의 크기를 넘어서서 비교할 경우
- lhs이나 rhs이 널 포인터일 경우
구조체 두 개를 비교할 때도 유용
typedef struct{
char firstname[64];
char lastname[64];
unsigned int id;
} student_t;
// 생략
int result;
result = memcmp(&s1, &s2, sizeof(student_t));
단, 구조체가 포인터 변수를 가질 경우에는...
주소를 가진 구조체면 값이 같아도 주소가 다를 수 있음
typedef struct{
char* firstname;
char* lastname;
} name_t;
// 동적 메모리 할당을 이용하여 이름을 복사
int result;
result = memcmp(&s1, &s2, sizeof(name_t));
동적 메모리 할당을 이용한 깊은 복사
typedef struct{
char* firstname;
char* lastname;
} name_dynamic_t;
name_dynamic_t s1; // {"Pope", "Kim"}
name_dynamic_t s2;
size_t size;
size = strlen(s1.firstname) + 1;
s2.firstname = malloc(size);
// s2.firstname 널 포인터 체크 코드 생략
memcpy(s2.firstname, s1.firstname, size);
size = strlen(s1.lastname) + 1;
s2.lastname = malloc(size);
// s2.lastname 널 포인터 체크 코드 생략
memcpy(s2.lastname, s1.lastname, size);
// free() 호출 생략
구조체 멤버 변수 - 배열 vs 포인터
고정된 길이인 배열
typedef struct{
char firstname[NAME_LEN];
char lastname[NAME_LEN];
} name_fixed_t;
- 그대로 대입 가능
- 파일에 곧바로 저장 가능
- memcpy()를 곧바로 사용 가능
- 낭비하는 용량이 있음
- 메모리 할당/해제 속도 빠름
동적 메모리를 사용하는 포인터
typedef struct{
char* firstname;
char* lastname;
} name_dynamic_t;
- 그대로 대입 불가
- 이 경우는 얕은 복사가 되어버림
- 파일에 곧바로 저장 불가능
- memcpy()를 곧바로 사용 불가능
- 낭비하는 용량이 없음
- 메모리 할당/해제 속도 느림
베스트 프랙티스 : 정적 vs 동적 메모리
- 정적 메모리를 우선적으로 사용할 것
- 안 될 때만 동적 메모리
동적 메모리의 소유권 문제
동적으로 할당한 메모리의 큰 문제
- 동적으로 할당한 메모리의 소유주는 누구인가?
- 바로, 메모리를 생성한 함수
- 소유주란? 그 메모리를 반드시 책임지고 해제해야 하는 주체
- 소유주가 아닐 때는 그냥 빌려 사용할 뿐 해제하면 안 됨!
호출자가 이 함수를 호출하는 순간 내부에서 새 메모리가 할당해서 반환한다는 사실을 어떻게 알지?
const char* combine_string(const char* a, const char* b)
{
void* str;
char* p;
// a와 b의 길이 및 두 길이를 더한 값을 보관한 변수 생략
str = malloc(size);
// a와 b를 str에 복사하는 코드 생략
return str;
}
//...
result = combine_string("Hello", "World");
C++에서는 RAII로 해결
- 자원 획득은 초기화(RAII, Resource Acquisition Is Initialization)
- 여기서 자원은 메모리
- C++은 개체지향을 지원하는 언매니지드 언어
- 한 개체가 생성될 때 필요한 메모리를 할당(생성자)
- 그 개체의 수명이 다할 때 그 메모리를 해제(소멸자)
- 즉, 개체의 수명이라는 범위에 메모리의 수명을 종속시킴
- 이 원칙을 잘 따르면 실수할 여지가 적음
하지만 C는 개체가 없다
- C에서 RAII를 할 수 있는 부붑ㄴ
- 한 함수 안에서 malloc(), free()를 다 호출할 수 있는 경우
하지만 중간에 함수를 탈출할 경우?
- 코드 중간에 return하면 말짱 도루묵
- goto 문을 사용
void do_someting() { void* nums = NULL; nums = malloc(10 * sizeof(int)); // 코드 생략 if(조건){ goto free_and_exit; } // 코드 생략 free_and_exit: free(nums); }
그럼 원래의 문제는 어떻게 해결할까?
- 이런 함수가 최대한 없게 한다
- combine_string() 예의 경우, 함수 안에서 할당하는 대신 함수 밖에서 할당 후 매개변수로 전달
size_t calculated_combined_length(const char* a, const char* b); void combine_string (const char* a, const char* b, char* out_str);
void combine_string(const char* a, const char* b, char* out_str)
{
// out_str에 a와 b를 복사
}
const char* str_combined;
size_t size;
size = calculate_combined_length(str1, str2)
str_combined = malloc(size + 1);
combined_string(str1, str2, str_combined);
free(str_combined);
## 동적으로 할당 후 반환을 피할 수 없다면?
- 딱히 모두가 동의하는 표준을 없음
- 어떤 함수가 메모리 할당을 한다면 함수에 그 사실을 표기
- 동적 메모리를 할당하는 변수라면 변수명에 표기하는 법도 있음
## 코딩 표준 : 동적 메모리 할당을 하는 함수명
```c
const char* combine_string_malloc(const char* str1, const char* str2)
{
void* pa_str;
char* p;
pa_str = malloc(strlen(str1) + strlen(str2) + 1);
p = pa_str;
// 문자열 합치는 코드 생략
return pa_str;
}
베스트 프랙티스 정리
- malloc() 작성한 뒤에 곧바로 free()도 추가하자
- 동적 할당을 한 메모리 주소를 저장하는 포인터 변수와 포인터 연산에 사용하는 포인터 변수를 분리해 사용하자
- 메모리 해제 후, 널 포인터를 대입하자
- 정적 메모리를 우선적으로 사용하고 어쩔 수 없을 때만 동적 메모리를 사용
- 동적 메모리 할당을 할 경우, 변수와 함수 이름에 그 사실을 알리자
다중 포인터
이중 포인터
포인터 변수의 주소를 저장한느 변수를 이중 포인터라고 함
int num = 10;
int* p = #
int** pp = &p;
이중 포인터는 왜 쓰지?
- 2차원 배열이 사실 이중 포인터와 비슷
메인 함수의 매개변수에도 있었다!
- 메인 함수의 매개변수 argv도 엄밀히 말하면 이중 포인터
int main(int argc, char* argv[]); int main(int argc, char** argv);
'프로그래머 > C, C++' 카테고리의 다른 글
[면접 대비] C를 사용한 해시 맵 구현 (0) | 2020.11.29 |
---|---|
[면접 대비] C를 사용한 linked list 구현 (0) | 2020.11.29 |
[포프 tv 복습] C 자료구조 기초 (0) | 2020.11.29 |
[포프 tv 복습] 가변 인자 함수, 올바른 오류 처리 방법 (0) | 2020.11.26 |
[포프 tv 복습] 구조체, 공용체, 함수 포인터 (0) | 2020.11.25 |