chapter 11에서는 pointer에 대한 기본적인 내용과 함수로 넘겨줘서 원래 값을 수정하는 방법에 대해 배웠다.
chapter 12에서는 pointer와 array의 관계에 대해 알아봤다.
이번 chapter에서는 dynamic storage allocation과 pointers to functions에 대해 알아보자.
모든 type에 적용 가능하지만, 주로 string, array, structure에 쓰인다. 특히 structure를 이용하면 list, tree 같은 다양한 형태의 data structure를 구현할 수 있다.
<stdlib.h>
헤더의 memory allocation fuctions
1. malloc
: 메모리 할당, 초기화 X
2. calloc
: 메모리 할당, 초기화 O
3. realloc
: 이전에 할당된 메모리 resize
이 함수들을 호출하면 return 값으로 void *
이 온다. (어떤 type이 필요한지 이 함수들은 알 수 없으므로)
`malloc`은 초기화 할 필요가 없어서 `calloc`에 비해 효율적이라 더 자주 쓰인다.
위 함수들을 호출했을 때, 요청사항을 수행할 수 없다면
null pointer
를 반환한다.
이는 아무것도 가리키지 않는 특별한 포인터로, 다른 valid pointer와 구분된 값을 가진다.
null pointer
는 macro NULL
로 정의된다.
(<locale.h>
, <stddef.h>
, <stdio.h>
, <stdlib.h>
, <string.h>
, <time.h>
이렇게 6개 헤더에 NULL pointer macro가 정의돼있다.)
그래서 위 함수들 사용하고 return 값이 null pointer
인지 아닌지 확인해야 한다.
확인하기위해 NULL
macro와 비교해도 되지만, C에선 pointer도 숫자처럼 true, false를 test할 수 있다. null pointer이면 false를 반환한다.
Q&A에 NULL macro 에 대한 설명이 더 있다.
malloc prototype
void *malloc(size_t size);
string에서의 사용법)
char *p;
p = malloc(n+1);
//malloc의 반환값인 void*은 assign할 때 자동변환되므로 굳이 casting할 필요 없다.
//적어두고 싶으면 적어도 됨. (char *) malloc(n+1);
strcpy(p, "abc");
string은 그 크기를 처음부터 알기 어려우므로 이 방법을 쓰면 좋다.
함수 내에서 malloc을 사용해도 그 메모리는 계속 유지가 된다.(변수들은 static으로 선언되지 않는 한, 함수 끝나면 같이 없어짐)
그래서 함수 호출 전에 원래 없던 object를 함수 안에서 만들어서 반환해줄 수도 있다.(함수 끝나도 deallocate 안되니까)
다 사용한 메모리는 free
함수를 통해 제거해야한다.
(메모리 관리하기 힘들어질 수 있음)(말했듯이 다른 변수들처럼 자동으로 안없어짐)
Section 13.7에서 이차원 배열에 string들을 저장하면 공간이 낭비되기 때문에 포인터의 배열에 저장을 했다. 근데 이렇게 하는건 초기화할때나 가능한거고, 실행 중간에는 그런 식으로 처리할 수 없다.
하지만 배열의 elements를 dynamically allocated string의 pointer로 하면 Section 13.7에서 처럼 공간을 절약할 수 있다.(p. 419)
string이 결국 array이므로, 17.2에서 말한 장점과 같은 장점을 가진다.
array에서의 사용법)
int *a;
a = malloc(n * sizeof(int));
이렇게 한번 해놓고 나면 array와 pointer의 관계 덕분에 그냥 array이름처럼 subscripting도 할 수 있고, pointer arithmetic도 당연히 사용할 수 있다.
a[0] = 1;
//or
*(a + 0) = 1;
때로는 calloc
을 통해 초기화한 배열을 만들기도 하고, realloc
을 통해 배열 크기를 줄이거나 늘리기도 한다.
calloc prototype
void *calloc(size_t nmemb, size_t size);
nmemb * size 만큼의 byte를 할당한다.
모든 bits를 0으로 초기화 한다.
int *a = calloc(n, sizeof(int));
다른 object를 할당할때도 사용된다.
struct point { int x, y; } *p;
p = calloc(1, sizeof(struct point));
0으로 초기화된 structure를 할당받음.
calloc 첫번째 arugment가 1로 하면 어떤 type도 할당 받을 수 있다.
realloc prototype
void *realloc(void *ptr, size_t size);
여기서 ptr은 malloc
or calloc
or realloc
에서 할당된 memory block을 가리키고 있어야 한다.(아니면 UB임)
size는 새로 할당될 크기를 적어주면 된다.
주로 배열에서 많이 활용된다.
realloc의 작동원리가 C standard에 정확히 기술된건 아니지만, 어느정도 짐작할 수 있다.
우선 resize를 해도 최대한 메모리 이동은 하지 않을 것이다. 하지만 크기를 키웠을 때, 뒷부분을 다른 놈이 쓰고 있어서 이동하지않고는 크기를 키울 수 없다면 그 자체를 이동해서 size를 키울 것이다.
그래서 realloc의 반환값을 받고 나면, 그 memory block을 가리켰던 pointer들을 모두 update해야한다.(이동 됐을 수 있으므로..)
memory allocation function은 heap이라고 하는 저장공간에서 메모리를 할당받는다.
그래서 이런 함수를 너무 자주 호출하거나 너무 큰 메모리 용량을 요청하면 heap이 고갈될 수 있다.(null pointer만 계속 반환되겠지)
메모리를 할당받아놓고, 그걸 가리키는 포인터를 잃어버려도 마찬가지다. 이런 메모리를 'garbage'라고 한다. C는 따로 garbage collector같은게 없으므로, free
function을 이용해서 알아서 필요없는거 없애줘야된다.
<stdlib.h>
에 있다.
free prototype
void free(void *ptr);
p가 가리키는 메모리 블럭을 release한다.
p는 memory allocation function으로부터 할당받은 메모리를 가리키는 포인터여야 한다. 다른 object(ex. 변수, array element 등)라면 UB.
deallocated된 메모리는 건들지 말자. 건들면 UB이다.
근데 보통 한 메모리를 여러 포인터가 가리키고 있을 수 있어서 이걸 찾기가 쉽지는 않다. 주의하자.
array에 비해 편리하지만, random access 능력을 잃는다.(array에선 어떤 element든 같은 시간을 써서 접근할 수 있었음)
struct node { //tag가 아니고선 next member를 선언할 길이 없음
int value;
struct node *next;
};
struct node *first = NULL;
첫번째 노드를 가리킬 포인터 변수가 필요하다. NULL 로 선언한 것은 list가 비어있다는 뜻이다.
메모리를 할당받아서 node에 넣기 전까지 임시로 가리킬 포인터가 필요하다.
struct node *new_node;
//sizeof 안에 되도록 type이 오게 하자.
//여기서 실수로 sizeof(new_node)라고 해버리면 (struct node *) 을 넣는거나 마찬가지다.
new_node = malloc(sizeof(struct node));
//precedence 때문에 괄호는 필수다.
(*new_node).value = 10;
->
: right arrow selection (minus sign + '>')
indirectio operator(*
)과 selection operator(.
)의 조합이다.
new_node->value = 10;
== (*new_node).value = 10;
연산 결과는 lvalue 이다.
linked list의 장점은, node를 어디에나 넣을 수 있다는 것이다.
첫번째에 넣는 함수)
struct node *add_to_list(struct node *list, int n)
{
struct node *new_node;
new_node = malloc (sizeof(struct node));
if(new_node == NULL) {
printf("Error: malloc failed in add_to_list\n");
EXIT(EXIT_FAILURE);
}
new_node->value = n;
new_node->next = list;
return new_node;
}
first = add_to_list(first, 10);
for (p = first; p != NULL; p = p->next) { ... }
struct node *search_list(struct node *list, int n)
{
for (; list != NULL && list->value != n; list = list->next)
;
return list;
}
inserting도 쉽지만, deleting도 쉽다.
step 2. 는 trailing pointer를 만듦으로써 해결한다.
아래는 step 1 + step 2 이다.
for (cur = list, prev = NULL;
cur != NULL && cur->value != n;
prev = cur, cur = cur->next)
;
그 다음 step 3에서 삭제할 이전 노드의 링크를 삭제할 다음 노드로 걸어주면 되지만,
(1) 삭제할 node가 발견되지 않은 경우
(2) 삭제할 node가 처음 node인 경우
를 고려해서 처리해야 한다.
위에 for문을 실행했다고 치고,
if (cur == NULL) //삭제할 node 발견되지 않음
return list;
if (prev == NULL) //삭제할 node가 처음 node
list = list->next; //처음을 삭제했으니, 다음 node를 시작점으로 해서 반환해야됨
else
prev->next = cur->next;
free(cur);
return list;
순서대로 들어간 list는 inserting이 어렵지만, searching은 빠르다.
ordered list를 활용한 프로그램 예시는 p.433~p.438 참고
linked list에서도 pointers to pointers 라는 개념이 좀 나온다.
위에 나온 add_to_list
함수를 보면 알겠지만, 첫번째 argument는 그냥 pointer여서 그 pointer의 값을 수정할 수는 없음. 즉, 그 pointer가 가리키는 곳을 다른 곳으로 바꿀 순 없음.
pointer를 수정하고 싶다면 pointer to pointer를 사용해야한다. 왜냐하면 함수는 passed by value이니까.
함수가 넘겨받은건 그저 주소값일 뿐이다.
우리가 수정하고싶은게 그 주소값이라면 pointer to pointer를 넘겨받는게 당연하다.(그냥 포인터로 수정하면 원래 값에는 변화가 없다. 그냥 값을 copy받아온걸 수정하는 것 뿐이니..)
void add_to_list(struct node **list, int n)
{
struct node *new_node;
new_node = malloc (sizeof(struct node));
if(new_node == NULL) {
printf("Error: malloc failed in add_to_list\n");
EXIT(EXIT_FAILURE);
}
new_node->value = n;
new_node->next = *list;
*list = new_node;
}
마지막줄을 위처럼 쓰고 싶으면 이중포인터를 인자로 넘겨받아야된다.
add_to_list 원래 버전(첫번째 argument가 그냥 포인터인 버전)에서 list가 가리키는 걸 수정하려고 해봤자, 그 함수안에선 list가 가리키는게 수정이 되겠지만 함수 밖으로 나왔을때 list로 넘겨준 포인터는 수정이 안돼있다.
copy해온 값을 수정해봤자, 밖에는 전혀 영향을 못미친다.
pointer는 다양한 data를 가리킨다.
function 또한 memory에 공간을 차지하므로, pointer가 기리킬 수 있다.
double integrate(double (*f)(double), double a, double b);
함수 포인터를 argument로 사용하려면 위와 같이 하면 된다.(return type과 parameter를 명시)
double integrate(double f(double), double a, double b)
이렇게 그냥 함수처럼 작성해도 OK (위 prototype과 동일)
parameter에 적힌 함수와 실제 넘겨주는 함수는 return type과 parameter type이 동일해야한다.
result = integrate(sin, 0.0, PI / 2);
넘겨줄때는 이렇게 함수 이름만 띡 넘겨준다. 뒤에 괄호붙이지 않는다.(괄호 붙이면 함수 호출임)
integrate 함수 안에서 sin을 호출하고 싶다면,
y = (*f)(x);
아니면
y = f(x);
(그냥 일반 함수호출하듯이 해도 OK)
하지만 저자는 전자의 방법으로 기술해서 f가 함수 포인터임을 확실히 한다고 함.
function pointer가 생각보다 많이 쓰이는데 그 중 하나가
<stdlib.h>
의 qsort
함수이다. (이름은 qsort인데 꼭 quicksort algorithm을 쓰라고 standard에 명시돼있진 않다. 물론 많은 version의 qsort가 quicksort 쓰긴 함.)
qsort 함수는 array를 정렬하는 함수로, 각각을 어떻게 비교할지를 알려주는 comparison function
을 제공해야 한다.
(array의 elemetn가 어떤 type일지 모르니까... structure일 수도 있고, union일 수도 있음)
comparison 함수는 입력받은 두 argument인 pointer p, q에 대해, *p가 *q보다 크다면 음수를, 같다면 0을, 반대라면 양수를 반환해야한다. (이러면 오름차순으로 정렬이 됨)
qsort prototype
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
base는 array의 첫번째 element를 가리켜야 한다.
nmemb는 정렬될 element의 개수다.
size는 각 element의 크기다.(byte 단위)
compar은 comparison function의 포인터이다.
compar 함수의 예시)
int compare_parts(const void *p, const void *q)
{
return ((struct part *) p) -> number -
((struct part *) q) -> number;
}
//이 방법은 overflow 발생 할 수도 있으니 그냥 if문 써서 return 하는게 안전하다
void *
로 입력받았기 때문에 사용하려면 (1)해당 type으로 다시 casting하거나 (2)해당 type의 변수에 assign해야한다.
2번째 방법에서 주의할 점은, parameter에서 const로 정의하고 있으므로 입력받을 변수도 const로 정의돼야 한다는 것이다.
argument로 쓰는 방법 외에도,
함수를 가리키는 포인터 변수를 선언하거나
array의 element로 만들거나
structure나 union의 member로 만들 수 있다.
void (*pf)(int);
얘(pf)는 이제 return type이 void이고 parameter이 int인 함수를 가리킬 수 있는 포인터이다.
pf = f;
void (*file_cmd[])(void)
= {new_cmd, open_cmd, ...};
위의 file_cmd는 return type과 parameter type이 모두 void인 함수 포인터를 elements로 가지는 배열이다.
사용하려면 (*file_cmd[n])();
or file_cmd[n]();
int * restrict p;
이를 restricted pointer라고 한다.
restricted pointer로 선언됐으면, 해당 object는 무조건 그 restricted pointer로만 접근할 수 있다.
object에 접근하는 방법이 2가지 이상일 때 이를 aliasing이라고 부른다.
restricted pointer인데 alias를 쓰면 UB이다.
extern storage class가 아니라면 restricted pointer는 해당 scope안에서만 적용된다. file scope라면 프로그램 실행 내내 적용된다.(자세한 rule은 C99 standard 참고)
사용되는 곳을 보며 얘기해보면,
void *memcpy(void * restrict s1, void * restrict s2, size_t n;
memcpy 함수 parameter에서 사용된다.
s1과 s2가 저렇게 선언됐다는건 둘의 저장공간이 overlap되지 않는다는거다.(그걸 보장해주진 않는다. overlap되면 UB지 뭐)
이걸 사용하는 이유는 최적화이다. (18.2 register도 최적화때문에 쓰임)
compiler에게 두 포인터는 다른 곳을 가리키고 있다고 알려줌으로써 compiler가 더 효과적인 code를 만들 수 있다.
하지만 모든 컴파일러가 최적화를 수행하는 것은 아니다.
그렇기 때문에 해당 keyword를 제거해도 똑같이 작동하도록 C99에서 정의하고 있다.
최적화 관련 자세한 설명은 아래 링크 참고..
https://dojang.io/mod/page/view.php?id=760
string을 저장할때 마지막에 null character('\0'
)가 오도록 저장한다.
하지만 마지막에 null character를 저장하지 않고 그냥 길이 변수와 함께 structure에 저장하는 방법도 있다.
struct vstring {
int len;
char chars[N];
};
하지만 이렇게 저장하면 공간이 낭비되므로 예전부터 C programmer들이 사용하던 방법이 있다.
struct vstring {
int len;
char chars[1];
};
struct vstring *str = malloc(sizeof(struct vstring) + n - 1);
str->len = n;
이렇게 chars의 길이를 1로 해두고, 해당 구조체 포인터 변수를 생성해서 malloc을 이용해 원하는 string 길이만큼 공간을 할당해준다.
이걸 struct hack이라고 부른다.
몇몇 compiler에선 chars 배열의 길이를 0으로 적어도 되게 했다.(struct hack인지 분명히 보이게) 하지만 C89에선 struct hack이나 배열 길이 0이도록 가능하게 보장하진 않았다.
이게 가능한 이유는, structure는 member가 선언된 순서대로 memory에 저장되기 때문이다.(endian과는 상관없다. endian은 1byte 이상인 "한" 데이터가 어떻게 저장되는지고, structure의 member는 순차적으로 저장된다.)
따라서 뒷부분 빈공간은 전부 chars 변수가 사용할 수 있게 되는 것이다.
C99에선 flexible array member를 도입했다.
structure의 마지막 member가 array라면 그것의 length는 생략될 수 있다.
struct vstring {
int len;
char chars[]; //flexible array member
};
struct vstring *str = malloc(sizeof(struct vstring) + n);
// +n 만 한 이유는 flexible array member는 structure에서 아무 공간도 차지하지 않기 때문이다.
str->len = n;
FAM rule
1. FAM은 무조건 마지막에 와야한다.
2. 다른 멤버가 최소한 하나 더 있어야 한다.
3. FAM을 포함하는 structure를 copy하면 FAM을 제외하고 copy된다.
얼마의 memory가 필요할지에 대한 정보가 부족할 때 이를 incomplete type이라고 부른다.(Section 19.3에 또 나옴) 그래서 위에서 말한 FAM이 있는 structure도 incomplete type이다.
incomplete type은 다른 structure의 member나 array의 element가 될 수 없다. 하지만 FAM을 포함하는 structure의 pointer는 array가 포함 할 수 있다.
NULL macro는 무엇을 나타내나?
0
을 나타낸다. pointer가 필요한 곳에 0이 쓰이면 compiler가 null pointer로 알아서 해석한다.
그저 혼란을 막기 위해 NULL
macro를 사용하는 것 뿐이다.
그럼 null pointer는 그냥 주소가 모두 0인 곳을 나타내는 건가?
그건 아니다. compiler마다 null pointer를 다르게 표현하는데, 0으로 표현할 수도 있고, 아니면 그냥 존재하지 않는 메모리 주소를 사용해서 null pointer를 나타낼 수도 있다.
그렇다고 우리가 걱정할 필요는 없다. 우리가 pointer context에서 0
을 사용하면 compiler에 의해 자동으로 해당 compiler에 알맞은 internal form으로 변환된다.
header file에 #define NULL (void *) 0
으로 정의돼있던데 void *
로 casting하는 이유가? 그냥 0이라고 해도 되지 않나?
첫번째로 compiler가 null pointer의 잘못된 사용을 찾게 해준다.
예를들어 int
에 NULL
을 assign하려고 하면 (void *) casting 덕분에 pointer를 integer에 assign한다고 warning이 뜬다.
두번째로 variable-length argument list에 넣을때 NULL
의 type이 뭔지 알 수 있다. 다른 함수와 달리 variable-length argument list를 가진 함수는 parameter에 대한 정보가 부족해 NULL
이 그냥 0
으로 정의돼있으면 integer 0
를 넘겨버린다.
NULL
이 0L
로 정의된 경우도 있는데 이건 예에에전에 pointer와 integer가 compatible일 때 쓰이던 거다.
대부분 경우에선 NULL
이 어떻게 정의돼있나는 중요하지 않다. 그냥 null pointer의 이름이라는 것만 잘 기억해라.
NULL
을 null character에 써도 되나?
안된다. null pointer와 null character 둘 다 0
을 나타내니까 뭐 몇몇 compiler에선 작동이 할 수도 있긴하겠다. 근데 null pointer가 0으로 저장되는게 보장되지도 않고(pointer 문맥에서 0으로 나타낼 뿐), 심지어 NULL
도 (void *) 0
로 정의돼 있으면 골치아프다. 그리고 그렇게 섞어서 쓰면 혼란스러울 수 있다. null character를 macro로 사용하고 싶으면 다음과 같이 정의해서 쓰자.
#define NUL '\0'
"Null pointer assignment" message
bad pointer를 사용했을 때 뜨는 메세지이다. 꼭 null pointer를 사용하지 않아도 뜬다. scanf에 &를 빼먹거나(물론 빼도 될때는 빼도 됨), 초기화되지 않은 포인터를 사용하거나 하면 발생한다.
프로그램이 종료될때 뜨는데, 어떻게 detect하냐면..
data들은 single segment에 저장되는데 각각 앞에 작은 hole이 있다. hole은 모두 0으로 초기화돼있다. 만약 이 hole이 0이 아니라면 bad pointer에 의해 수정된 것이므로 이를 detect할 수 있는 것이다.
malloc 같은 함수 return 값 casting하는게 장점이 있나?
어차피 자동변환 되니까 딱히 없다. 예에에전 버전에서 memory allocation function이 char *
을 return할 때 쓰던 습관이다. C89에선 오히려 casting안하면 장점이 있었는데 C99에선 이마저 사라졌다.
C++ code로 compile되는 곳에선 이득이 될 수도 있다.
calloc 함수가 할당된 memory의 모든 bits를 0
으로 만든다고 했는데 이게 모든 data item이 0이 된다는 뜻인가?
대부분 그렇지만 항상 그렇진 않다. integer라면 0이 된다. 하지만 floating point number는 보통 0이 되긴 하지만 얘가 저장되는 방식에 따라 다르게 해석된다. pointer도 마찬가지인데, (위에서 말했듯이) 모든 bits가 0이어도 null pointer라는 건 아니다.
structure tag로 그 안에서 포인터 선언하는건 좋았다. 근데 두개의 structure가 서로 다른 structure를 member로 가지려면 어떻게 해야되나?
incomplete type을 이용하면 된다.
struct s1; //incomplete declaration of s1
struct s2 {
struct s1 *p;
};
struct s1 {
struct s2 *q;
};
아래 s1을 다시 declare하며 complete한다.
incomplete declaration of structure type은 C가 허용한다.
그 사용은 제한적인데, 해당 incomplete type의 pointer를 생성하는 것이 그 사용법 중 하나이다.(위에서 p 선언한 것 처럼 할 수 있음)
malloc에 argument 잘 못 넣어서 공간을 너무 적거나 너무 크게 할당 받는걸 방지할 수 있나?
p = malloc(sizeof(*p));
몇몇 programmer는 위 방식을 사용해서 딱 필요한 만큼만 할당받는다. p가 null pointer거나 초기화 안돼있어도 size만 측정하는 것이기 때문에 이렇게 사용해도 전혀 문제없다.
qsort 첫번째 인자가 (void *)
이던데 argument 넘겨줄때 (void *)
로 casting 해야되나?
No. 어떤 type의 pointer도 void *
로 자동으로 convert됨.
즉, 둘은 서로서로 필요할 때 자동으로 바뀜. pointer of any type
<-> void *
qsort에 comparison function 만드는데 integer처리가 잘 안됨
int compare_ints(const void *p, const void *q)
{
return *(int *)p - *(int *)q;
}
이렇게 int *
로 먼저 casting해준 후 indirection operator를 적용시켜야된다. 그냥 *
써버리면 얘 type이 void *
이니까 어떤 type인지 모른다.
(물론 위 함수도 빼기를 사용해서 overflow 발생할 수 있음. if문으로 대체해주는게 안전)
array of string을 정렬하기 위해 strcmp를 이용해서 comparison function을 작성하는데 문제가 있다.
int compare_strings(const void *p, const void *q)
{
return strcmp(p, q);
}
어디에 문제가 있나?
일단 strcmp를 바로 qsort의 comparison function으로 넘겨주는건 불가능하다. 왜냐하면 애초에 둘의 parameter type이 맞지 않기 때문이다.
다음 위 함수의 문제점을 보자면, p와 q가 무엇인지 제대로 파악해야한다. p와 q는 그냥 string이 아니라, char *
이 원소인 배열의 원소를 하나하나 가리키는 포인터이다.
그렇기 때문에 p와 q의 원래 type인 char **
로 casting 해주고, 원소 하나씩 넘겨줘야 되니까 다시 *
operator를 사용해준다.
int compare_strings(const void *p, const void *q)
{
return strcmp(*(char **)p, *(char **)q);
}
바로 위 Q&A에
p, q를 그냥 바로 넘겨주면 안되나.. 라고 생각할 수도 있는데
p랑 q는 int 배열에 있어서 int *
같은 존재임.
array of string이란게, 배열의 각 원소에 각 string의 시작점 주소가 저장돼있는 것(즉 각 원소가 그저 char *
type인 것이다. int 배열의 원소가 그저 int
type인 것처럼)
그 시작점 주소의 type이 char *
이고,
그 시작점 주소인 char *
을 strcmp를 포함한 string function에서 요구한다.
근데 p랑 q는 그 시작점 주소인 char *
을 한번 더 가리키는 char **
type이다. 즉 p랑 q에는 string의 시작점 주소의 주소가 저장돼있는거다. 그러므로 한번 더 *
를 적용하는 것이 옳다.