17. Advanced Uses of Pointers(1)
이전 chapter들에서, 우리는 2개의 중요한 포인터의 사용을 보았다. Chapter 11에서는 포인터가 변수를 가리키는 포인터를 어떻게 함수의 argument로써 사용하는지에 대해 알아보았고, 이를 통해 함수가 변수를 수정하도록 하였다. Chapter 12에서는 배열 요소에 대한 포인터에 arithmetic을 수행하는 것으로 어떻게 배열을 처리하는지에 대해 알아보았다. 이번 chapter에서는 두 가지 추가적인 적용인 dynamic storage allocation과 함수를 가리키는 포인터를 알아보는 것으로 포인터에 대한 사용법을 완성할 것이다.
동적 공간 할당(dynamic storage allocation)을 사용하는 것으로, 프로그램은 실행 도중에 필요한 메모리의 블록을 얻을 수 있다.
동적으로 할당된 구조체(dynamically allocated structures)는 C 프로그래밍에서 큰 역할을 하는데, 이것들은 서로 링크되어 lists, trees 등등 아주 유연한 데이터 구조를 만든다.
1. Dynamic Storage Allocation
C의 데이터 구조는 일반적으로 크기가 고정되어 있다. 예를 들어, 배열 내부의 요소 갯수는 프로그램이 일단 컴파일되고 나면 고정된다.(C99에서는 가변길이 배열의 길이는 런타임에 결정되지만, 배열의 남은 lifetime동안에는 고정된다.) 고정된 크기의 데이터 구조는 문제가될 수 있는데, 왜냐하면 프로그램을 작성할 때 구조의 크기를 강제로 결정해야 하기 때문이다. 우리는 프로그램을 수정하고 다시 컴파일하지 않으면 이 크기를 고칠 수 없다.
Section 16.3의 inventory
프로그램을 생각해보자. 이 프로그램은 유저가 part를 데이터베이스에 추가할 수 있도록 해준다. 데이터베이스는 길이 100의 배열 안에 저장된다. 이 데이터베이스의 크기를 넓히기 위해서는, 배열의 크기를 증가시키고 다시 프로그램을 컴파일해야할 것이다. 그러나 배열을 얼마나 크게 만들든 간에 그 배열이 꽉 채워질 가능성은 항상 존재한다. C언어는 프로그램 실행 도중에 공간을 할당할 수 있는 능력인 동적 공간 할당(dynamic storage allocation)을 지원한다. dynamic storage allocation을 사용하는 것으로, 필요한 만큼 데이터 구조를 키우거나 줄일 수 있도록 설계할 수 있다.
dynamic storage allocation이 데이터의 모든 자료형에 이용가능하지만, 대부분 문자열, 배열, 구조체에 사용된다. 동적으로 할당된 구조체는 특별한 관심이 필요한데, 왜냐하면 list, tree 등의 다른 데이터 구조에 연관시킬 것이기 때문이다.
Memory Allocation Functions
동적으로 공간을 할당하기 위해서는, 우리는 <stdlib.h>
헤더에 선언된 3개의 메모리 할당 함수 중 하나를 호출할 필요가 있다.
malloc
: 메모리의 블록을 할당하지만 초기화하지는 않는다.calloc
: 메모리의 블록을 할당하고 비운다.realloc
: 이전에 할당된 메모리의 블록의 크기를 바꾼다.
이 중 하나로 malloc
이 가장 자주 사용된다. malloc
은 calloc
보다 조금 더 효율적인데, 왜냐하면 할당한 메모리 블록을 비우지 않기 때문이다.
메모리의 블록을 요청하기 위해 메모리 할당 함수를 호출할 때, 함수는 우리가 블록에 어떤 자료형의 데이터를 저장할지에 대한 것을 알 수가 없다. 그래서 int
나 char
를 가리키는 포인터를 반환하지 못한다. 대신에, 함수는 void *
자료형의 값을 반환한다. void *
값은 "generic" 포인터인데, 이는 본질적으로는 메모리 주소이다.
Null Pointers
메모리 할당 함수가 호출되었을 때, 우리의 요청을 충분히 만족시킬만큼 큰 메모리의 블록을 할당하지 못할 수도 있다. 만약 이러한 일이 일어난다면, 함수는 null pointer
를 반환할 것이다. null pointer는 "아무것도 가리키지 않는 포인터"인데, 이는 다른 모든 유효한 포인터들과 구별되는 특별한 값이다. 포인터 변수에 함수의 반환값을 저장하고 난 이후에, 우리는 이것이 null pointer인지 확인하기 위해 반드시 검사해야 한다.
어떠한 메모리 할당 함수의 반환 값을 검사하고, 만약 그것이 null pointer라면 적절한 행동을 취하는 것은 프로그래머의 책임이다. null pointer를 통해 메로리에 접근하려는 시도는 undefined이다. 프로그램은 충돌하거나 예상할 수없이 행동할 것이다.
null pointer는 NULL
이라는 이름의 매크로에 의해 표현된다. 그래서 우리는 malloc
의 반환 값을 아래와 같은 방법으로 검사할 수 있다.
p = malloc(10000);
if (p == NULL)
{
/* allocation failed; take appropriate action */
}
어떤 프로그래머는 malloc
의 호출과 함께 NULL
검사를 결합한다.
if ((p = malloc(10000)) == NULL)
{
/* allocation failed; take appropriate action */
}
NULL
매크로는 6개의 헤더파일 내부에 정의되어 있다. <locale.h>
, <stddef.h>
, <stdio.h>
, <stdlib.h>
, <string.h>
, <time.h>
이다. (C99헤더인 <wchar.h
> 또한 NULL
을 정의한다) 이 헤더중 하나가 포함되어 있는 한, 컴파일러는 NULL
을 인식할 것이다. 당연하게도 NULL
을 이용가능하게 만들기 위해, 어떠한 메모리 할당 함수를 사용하는 프로그램은 <stdlib.h>
를 포함하고 있을 것이다.
C언어에서, 포인터는 숫자들과 같은 방식으로 true나 false를 검사할 수 있다. null pointer가 아닌 모든 포인터는 true로 검사되고, 오직 null pointer만 false로 검사된다.
if (p == NULL) ...
if (p != NULL) ...
if (!p) ...
if (p) ...
그래서 우리는 전자의 구문으로 쓰는 대신, 후자의 구문을 쓸 수 있다.
어떠한 스타일을 사용할지는 선호에 따라 결정하면 되지만, King은 NULL
과의 비교를 주로 사용한다.
2. Dynamically Allocated Strings
동적 공간 할당은 문자열과 함께 사용될 때 유용하다. 문자열은 문자 배열 내부에 저장되는데, 이 배열이 얼마나 길어야 하는지 예상하는 것이 어렵다. 그렇지만 문자열을 동적으로 할당한다면, 프로그램이 실행될 때까지 여기에 대한 결정을 미룰 수 있다.
Using malloc
to Allocate Memory for a String
malloc
함수는 아래와 같은 prototype을 가진다.
void *malloc(size_t size);
malloc
은 size
byte만큼의 블록을 할당하고, 그것을 가리키는 포인터를 반환한다. size
가 C라이브러리 내부에서 unsigned integer 자료형으로 정의된 size_t
자료형을 가진다는 것을 주목하자. 우리가 아주 큰 메모리 블록을 할당하는 것이 아니라면, size
는 일반적인 정수라고 생각할 수 있다.
문자열에 메모리를 할당하기 위해 malloc
을 사용하는 것은 간단한데, 왜냐하면 C언어는 char
값이 실제로 1byte의 공간만 필요로 한다는 것을 보장하기 때문이다(sizeof(char)
은 1이다.). n
개의 문자의 문자열에 대한 공간을 할당하기 위해서는 아래와 같이 작성한다.
p = malloc(n + 1);
p
는 char *
변수이다. (argument가 n
이 아닌 n + 1
인 것은 null 문자에 대한 공간을 마련하기 위해서이다) malloc
이 반환하는 generic 포인터는 대입이 수행되었을 때 char *
자료형으로 변환된다. cast가 필요하지 않다. (일반적으로, void *
값을 다른 어떤 포인터 자료형의 변수에 대입하는 것 또한 이와 동일하다) 그래도, 몇몇의 프로그래머들은 malloc
의 반환값을 cast하는 것을 선호한다.
p = (char *) malloc(n + 1);
문자열에 대한 공간을 할당하기 위해 malloc
을 사용할 때, null 문자가 들어갈 공간을 포함하는 것을 잊어버리면 안된다.
malloc
을 사용하여 할당된 메모리는 비워지거나 초기화되지 않는데, 그래서 p
는 초기화되지 않은 n + 1
개의 문자의 배열을 가리킨다.
strcpy
를 호출하는 것은 이 배열을 초기화하는 하나의 방법이다.
strcpy(p, "abc");
배열 내부의 첫번째 4개의 문자는 a
, b
, c
, \0
이다.
Using Dynamic Storage Allocation in String Functions
동적 공간 할당은 함수가 호출되기 전에는 존재하지 않았던 "새로운" 문자열을 가리키는 포인터를 반환하는 함수를 작성할 수 있도록 한다. 두 개의 문자열 중 어떠한 문자열도 변화시키지 않고 두 개의 문자열을 잇는 함수를 작성하는 문제를 생각해보자. C의 표준 라이브러리는 이러한 함수를 포함하고 있지 않다.(strcat
는 우리가 원하는 함수가 아닌데, 왜냐하면 전달된 문자열 중 하나를 수정하기 때문이다.) 하지만 우리는 쉽게 이러한 함수를 작성할 수 있다.
이 함수는 이어질 두 개의 문자열의 길이를 측정하고, 그 후 malloc
을 호출하는 것으로 결과값에 대한 올바른 공간을 할당할 것이다. 그리고 첫번째 문자열을 새로운 공간에 복사하고 두번째 문자열을 잇기 위해 strcat
를 호출할 것이다.
char *concat(const char *s1, const char *s2)
{
char *result;
result = malloc(strlen(s1) + strlen(s2) + 1);
if (result == NULL)
{
printf("Error: malloc failed in concat\n");
exit(EXIT_FAILURE);
}
strcpy(result, s1);
strcat(result, s2);
return result;
}
malloc
이 null pinter를 반환한다면, concat
은 에러메시지를 출력하고 프로그램을 종료한다. 이것이 언재나 적절한 동작은 아닌데, 어떠한 프로그램은 메모리 할당이 실패한 이후 원래대로 다시 돌아온다음(recover) 프로그램 실행(running)을 이어갈 것이다.
아래의 concat
함수가 어떻게 호출되는지 보자.
p = concat("abc", "def");
호출 이후, p
는 동적으로 할당된 배열 내부에 저장된 문자열 "abcdef"
를 가리킬 것이다. 배열은 null 문자를 포함하여 7문자 만큼의 길이를 가진다.
공간을 동적으로 할당하는 concat
과 같은 함수는 사용할 때 반드시 주의를 기울여야 한다. concat
이 반환하는 문자열이 더이상 필요가 없을 경우, 우리는 free
함수를 사용하여 문자열이 차지하는 공간을 해제(release)해야 할 것이다. 만약 그렇지 않는다면 프로그램은 결국에 메모리가 바닥날 수도 있다.
Arrays of Dynamically Allocated Strings
Section 13.7에서, 우리는 배열 내부에 문자열을 저장하는 문제를 보았었다. 문자의 two-dimensional array 내부의 row에 문자열을 저장했었는데, 이는 공간을 낭비할 수 있어서 문자열 리터럴을 가리키는 포인터의 배열로 설정하는 시도를 했었다. Section 13.7의 이 기법은 동적으로 할당한 문자열을 가리키는 포인터의 배열로 해도 동일하게 작동한다.
3. Dynamically Allocated Arrays
동적으로 할당된 배열은 동적으로 할당된 문자열과 동일한 이점을 가진다(이는 전혀 놀랍지 않은데, 왜냐하면 문자열이 배열이기 때문이다). 우리가 프로그램을 작성할 때, 배열에 대한 적절한 크기를 추정하는 것은 종종 어렵다. 배열이 얼마나 커야하는지 결정하기 위해 프로그램이 실행될 때까지 기다리는 것은 편리하다. C언어는 프로그램 실행 도중에 배열에 대한 공간을 할당하고, 그 후 그 배열의 첫번째 요소를 가리키는 포인터를 통해 배열에 접근하는 것으로 이 문제를 해결했다. 배열과 포인터 사이에는 Chapter 12에서 본것처럼 밀접한 관련이 있는데, 이는 동적으로 할당된 배열이 더 쉽게 일반적인 배열처럼 사용되도록 만들어준다.
malloc
이 배열에 대한 공간을 할당할 수 있더라도, calloc
함수가 대신에 사용될 때가 있는데 왜냐하면 calloc
은 할당한 메모리를 초기화하기 때문이다. realloc
함수는 배열을 "키우거나" "축소"시킬 수 있도록 한다.
Using malloc
to Allocate Storage for an Array
배열에 대한 공간을 할당하기 위해 malloc
을 사용하는 것은 우리가 문자열에 대한 공간을 할당한것과 같은 방식이다. 주된 차이는 임의(arbitrary) 배열의 요소가 문자열처럼 반드시 1byte의 길이를 가질 필요가 없다는 점이다. 결과적으로, 우리는 각각의 요소에 대해 필요한 공간의 총량을 계산하기 위해 sizeof
연산자를 사용할 필요가 있을 것이다.
프로그램 실행 도중에 계산되는 n
개의 정수의 배열을 필요로 하는 프로그램을 작성한다고 가정해보자. 일단 포인터 변수를 선언해야한다.
int *a;
n
의 값이 알려지고 나면, malloc
을 호출하여 배열에 대한 공간을 호출해야 한다.
a = malloc(n * sizeof(int));
배열에 필요한 공간이 얼마나 되는지 계산할 때 언제나 sizeof
를 사용하는 것이 좋다. 충분한 메모리를 할당하는 것에 실패하는 것은 끔찍한 결과를 초래할 수 있다. n
개의 정수 배열에 대한 공간을 할당하려는 아래의 시도를 보자.
a = malloc(n * 2);
만약 int
값이 2 byte보다 크다면(대부분의 컴퓨터에서 그렇다), malloc
은 충분하게 큰 메모리의 블록을 할당하지 못할 것이다. 나중에 이 배열의 요소에 접근하려는 시도를 한다면, 프로그램은 충돌(crash)하거나 일반적으로 작동하지 않을 것이다.
동적으로 할당된 메모리의 블록을 가리키고 나면, C언어의 배열과 포인터의 밀접한 관련성 덕분에 a
가 포인터라는 사실을 무시하고, a
를 배열의 이름처럼 사용할 수 있다. 예를 들어 a
가 가리키는 배열을 초기화하기 위해 아래의 루프를 사용할 수 있다.
for (i = 0; i < n; i++)
a[i] = 0;
또한 배열의 요소에 접근하기 위해 subscripting대신에 포인터 연산을 사용하는 선택지도 있다.
The calloc
Function
malloc
함수는 배열에 대한 메모리를 할당하는 것에 사용되기는 하지만, C언어는 여기에 대안 대안인 calloc
함수또한 제공한다. 이 calloc
함수는 종종 malloc
보다 더 괜찮을 때가 있다. calloc
은 <stdlib.h>
내부에서 아래와 같은 prototype을 가진다.
void *calloc(size_t nmemb, size_t size);
calloc
은 nmemb
개의 요소의 배열에 대한 공간을 할당하고, 요소 각각의 크기는 size
byte만큼의 길이이다. calloc
은 요청한 공간이 사용가능하지 않을 때 null pointer를 반환한다. 메모리를 할당한 이후, calloc
은 모든 비트를 0으로 설정하는 것으로 초기화한다. 예를 들어 n
개의 정수 배열에 대한 공간을 할당하는 아래의 calloc
의 호출은 모든 요소의 값이 처음에 0임을 보장한다.
a = calloc(n, sizeof(int));
calloc
이 할당한 메모리를 비우지만 malloc
은 그렇게 하지 않기 때문에, 배열 이외의 object에 대한 공간을 할당하기 위해 calloc
을 호출하고 싶을 수도 있다. calloc
의 첫번째 argument를 1
로 하는 것으로 우리는 단일 데이터 항목에 대한 공간을 할당할 수 있다.
struct point { int x, y; } *p;
p = calloc(1, sizeof(struct point));
이 구문이 실행되고 난 이후에, p
는 x
와 y
멤버가 0으로 설정된 구조체를 가리키게 된다.
The realloc
Function
일단 배열에 대한 메모리를 할당하고 나면, 우리는 나중에 이것이 너무 크거나 너무 작다고 생각할 수도 있다. realloc
함수는 우리의 필요에 맞추어 배열의 크기를 다시 조정하도록 해준다. <stdlib.h>
내부에서 realloc
의 prototype은 아래와 같다.
void *realloc(void *ptr, size_t size);
realloc
이 호출되었을 때, ptr
은 반드시 이전에 malloc
, calloc
, realloc
으로 호출되어 얻어진 메모리 블록을 가리켜야한다. size
parameter는 블록의 새로운 크기를 나타되는 메모리를 가리키도록 하는 것을 필요로 하지는 않지만, 실제로는 이러한 방식으로 사용된다.
이전에 malloc
, calloc
, realloc
의 호출로부터 온 포인터가 realloc
에 전달되도록 해야한다. 그렇지 않다면, realloc
을 호출하는 것은 undefined behavior를 야기한다.
C표준은 rellloc
의 행동에 관련된 여러 개의 규칙을 제시한다.
- 메모리 블록이 확장될 때,
realloc
은 추가된 블록의 byte는 초기화하지 않는다. realloc
이 요청된 메모리 블록만큼 확장시키지 못했다면, null pointer를 반환한다. 원래 메모리 블록에 있던 데이터는 변화하지 않을 것이다.realloc
이 첫번째 argument로 null pointer를 전달받은 채 호출되었다면,malloc
처럼 행동할 것이다.realloc
이 두번째 argument로0
을 전달받은 채 호출되었다면, 메모리 블록을 해제(free)한다.
C표준에서는 realloc
이 실제로 어떻게 작동하는지 명시하는 것에 그치지 않는다. 메모리 블록의 사이즈를 줄이도록 요청했을 때, realloc
은 블록에 저장된 데이터를 옮기지 않고, "그 자리에서" 블록을 축소시켜야할 것이다. 같은 이유로, realloc
은 항상 메모리 블록을 움직이지 않고 확장시켜야할 것이다. 만약 블록을 확장시킬 수 없다면(블록 이후의 byte들이 이미 다른 목적으로 사용되고 있다면), realloc
은 새로운 블록을 다른곳에 할당할 것이고, 그 후 원래 블록에 있던 내용물을 복사하여 새로운 블록에 저장할 것이다.
realloc
이 반환되고 나면, 메모리 블록을 가리키는 모든 포인터를 최신화해야 하는데, 왜냐하면 realloc
이 블록을 다른 곳에 옮겼을 수도 있기 때문이다.
4. Deallocating Storage
malloc
과 다른 메모리 할당 함수는 heap
이라고 알려진 storage pool로부터 메모리 블록을 얻는다. 이 함수를 너무 자주 호출하거나 메모리에 대해 큰 블록을 요청하는 것은 heap을 힘들게(exhaust) 할 수 있고, 함수가 null pointer를 반환할 수도 있다.
또, 메모리의 블록을 할당하고 이를 추적하지 않는다면 공간을 낭비할 수 있다. 아래의 예시를 보자.
p = malloc(...);
q = malloc(...);
p = q;
첫번째와 두번째 구문이 실행되고 난 후, p
가 하나의 메모리 블록을 가리키고, q
는 또다른 메모리 블록을 가리킨다.
q
가 p
에 대입되고 난 후, 두 변수는 이제 두번째 메모리 블록을 가리킨다.
첫번째 블록을 가리키는 포인터는 없다. 그래서 우리는 다시는 저 블록을 사용할 수 없게 된다.
프로그램이 더이상 접근할 수 없는 메모리의 블록은 garbage
라고 부른다. garbage를 남기는 프로그램은 메모리 누수(memory leak)가 생긴다. 어떠한 언어들은 자동적으로 garbage를 옮기고 재활용하는 garbage collector를 제공하지만 C언어에는 존재하지 않는다. 대신에, 각각의 C 프로그램은 free
함수를 호출하는 것으로 필요없는 메모리를 해제하여 garbage를 재활용할 책임이 있다.
The free
Function
free
함수는 <stdlib.h>
에서 아래와 같은 prototype을 가진다.
void free(void *ptr);
free
를 사용하는 것은 간단하다. 우리는 간단하게 더이상 필요하지 않은 메모리 블록을 가리키는 포인터를 전달하면 된다.
p = malloc(...);
q = malloc(...);
free(p);
p = q;
free
를 호출하는 것은 p
가 가리키는 메모리의 블록을 해제한다. 이 블록은 이제 이후에 malloc
이나 다른 메모리 할당 함수의 호출에서 다시 사용할 수 있다.
free
에 대한 argument는 반드시 이전에 메모리 할당 함수에 의해 반환된 포인터여야한다. (argument는 null pointer일 수도 있는데, 이러한 경우에 free
의 호출은 아무 효과도 가지지 않는다.) free
에 다른 object(변수나 배열 요소같은)를 가리키는 포인터를 전달하는 것은 undefined behavior를 발생시킨다.
The "Dangling Pointer" Problem
free
함수가 더이상 필요없는 메모리를 해제하고 다시 사용할 수 있도록 하더라도, 새로운 문제를 발생시킨다. 바로 dangling pointers
이다. free(p)
의 호출은 p
가 가리키는 메모리 블록을 해제하지만, p
자체를 바꾸지는 않는다. 만약 p
가 더이상 유효한 메모리 블록을 가리키고 있지 않다는 것을 잊어버린다면, 혼돈이 올 수도 있다(chaos may ensue).
char *p = malloc(4);
...
free(p);
...
strcpy(p, "abc"); /*** WRONG ***/
p
가 가리키는 메모리를 수정하는 것은 아주 중대한 에러인데, 왜냐하면 우리의 프로그램은 p
가 가리키는 메모리에 대한 제어를 가지고 있지 않기 때문이다.
할당 해제된 메모리 블록을 수정하거나 접근하려는 시도는 undefined behavior를 야기한다. 할당 해제된 메모리 블록을 수정하려고 시도하는 것은 프로그램 충돌(crash)을 포함하여 아주 재앙적인 결과를 가진다.
dangling pointer는 찾기 힘든데, 왜냐하면 많은 포인터가 같은 블록 메모리를 가리키기 때문이다. 그 블록이 free되었을 때, 남은 모든 포인터는 dangling된 채로 남을 것이다.
Author And Source
이 문제에 관하여(17. Advanced Uses of Pointers(1)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@hamoci/17.-Advanced-Uses-of-Pointers저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)