Q 6.1
소스 파일에 char a[6]이라고 정의하고 extern char *a라고 선언해 두
었는데 왜 동작하지 않을까요?
Answer 소스 파일에 정의한 것은 문자(char)로 이루어진 배열입니다. 그리고 선언
한 것은 문자를 가리키는 포인터입니다. 따라서 선언과 정의가 일치하지 않
는 경우입니다. 일반적으로, T 타입을 가리키는 포인터(pointer to type T)
의 타입은 T 타입의 배열(array of type T)과 다릅니다. 대신 extern char a[]을
사용하기 바랍니다.
Q 6.4
그럼 왜 함수의 formal parameter로 배열과 포인터 선언을 마음대로 바꿔
쓸 수 있다는 것일까요?
Answer 편의상 그런 것입니다.
배열 이름은 즉시 포인터로 바뀌기 때문에4, 배열은 함수로 전달되지 않습
니다. 포인터 파라메터를 선언할 때 배열처럼 쓸 수 있는 것은, 파라메터가
그 함수 내부에서 배열처럼 쓰일 수 있기 때문입니다. 특히, 다음과 같이
배열처럼 선언된 파라메터는:
void f(char a[])
{ ... }
4Since arrays decay immediately into pointers,
CHAPTER 6. ARRAYS AND POINTERS 103
컴파일러에 의해 포인터로 인식됩니다. 즉, 다음과 같습니다:
void f(char *a)
{ ... }
그래서, 함수가 배열에 대한 어떤 작업을 하거나, 파라메터가 함수 내부에서
배열처럼 취급될 때, 함수가 배열을 받는다고 말하는 것은 나쁘지 않습니다.
이런 사항들은 함수의 formal parameter 선언에만 적용되며, 다른 곳에서는
적용될 수 없습니다. 만약 배열을 받는 것처럼 선언한 함수가 신경쓰인다
면, 안 쓰면 됩니다; 비록 몇몇은 함수의 선언이나, 함수 내부에서 배열을
받아 쓰는 것처럼 보이게 하는 것이 이득이라고 하지만, 많은 부분에서 혼
동을 가져오는 것은 사실입니다. (이러한 변환은 오직 한 번만 일어납니다;
char a2[][]와 같은 표현은 쓸 수 없습니다. 질문 6.18, 6.19를 보기 바랍
니다.)
Q 6.5
왜 다음과 같이 할 수 없을까요?
extern char *getpass();
char str[10];
str = getpass("Enter password: ");
Answer C 언어에서 배열은 “second-class citizens”입니다; 그 결과 배열에 대입 연
산을 쓸 수 없습니다 (질문 6.7도 참고하기 바랍니다). 한 배열의 내용을
다른 곳으로 복사하려면, 직접 해야 합니다. char 타입의 배열인 경우에는,
대개 strcpy를 써서 복사할 수 있습니다:
strcpy(str, getpass("Enter password: "));
복사하는 대신, 단순히 전해주고 싶다면, 포인터와 대입 연산을 쓸 수 있습
니다. 덧붙여 질문 4.1, 8.2도 참고하시기 바랍니다.
Q 6.6
배열에 대입 연산을 쓸 수 없다면, 다음 코드는 왜 동작할까요?
int f(char str[])
{
if (str[0] == '\0')
str = "none";
...
}
Answer 위 코드에서 str은 함수 파라메터입니다. 따라서 컴파일러는 위 코드를 질
문 6.4에서 다룬 것처럼 바꿉니다. 다시 말하는데, 이 때 str은 (타입이
char *인) 포인터입니다. 그리고 포인터에는 대입 연산을 쓸 수 있습니다.
Q 6.11
5["abcdef"]와 같은 이상한 표현을 봤습니다. 이것이 C 언어에서 쓸 수
있는 표현인가요?
Answer 쓸 수 있습니다. Subscript 연산자인 []에는 교환 법칙 (commutative law)
이 성립합니다.6 a[e]는 어떤 expression a와 e에 대해, 하나가 포인터
expression이고, 하나가 정수 수식이란 전제 아래에서, *((a) + (e))와 완
전히 같습니다 (identical). 이 증명은 다음과 같습니다:
a[e]
*((a) + (e)) (by definition)
*((e) + (a)) (by commutativity of addition)
e[a] (by definition)
어떤 C 책에서는 이런 것을 자랑삼아 보여주기는 하지만, ‘혼동스러운 C 컨
테스트 (Obfuscated C Contest)’에 쓰이지 않는한, 따로 특별히 쓸모 있는
표현이 아닙니다 (질문 20.36을 참고하기 바랍니다).
Q 6.12
배열 참조가 포인터로 변환된다면7, arr이 배열일때, arr과 &arr의 차이
는 무엇인가요?
Answer 타입이 서로 다릅니다. 표준 C 언어에서, &arr은 포인터를 만들어 내며, 이
포인터의 타입은 배열 T 전체를 가리키는 포인터(pointer to array of T)
입니다 (ANSI C 이전의 오래된 C 언어에서는 &arr에 쓰인 &에서 경고를
발생시키며, 무시됩니다.). 모든 C 언어 컴파일러에서 (&를 사용하지 않는)
간단한 배열 reference는 포인터를 만들어내며, 이 타입은 T를 가리키는 포
인터(pointer to T)이며, 배열의 첫 요소를 가리킵니다.
다음과 같은 간단한 배열에서:
int a[10];
a에 대한 reference는 “pointer to int”란 타입을 가지며, &a에 대한 reference는
“pointer to array of 10 ints”란 타입을 가집니다. 다음과 같은
이차원 배열에서는:
int array[NROWS][NCOLUMNS];
array 타입은 “pointer to array of NCOLUMNS ints”이며, &array 타입은
“pointer to array of NROWS array of NCOLUMNS ints”입니다.
(질문 6.3, 6.13, 6.18을 참고하기 바랍니다.)
Q 6.13
배열을 가리키는 포인터(pointer to an array)는 어떻게 선언하죠?
Answer 보통 이런 작업은 필요하지 않습니다. 사람들이 대개 배열을 가리키는 포인
터라고 말하는 것은 배열의 첫번째 요소를 가리키는 포인터를 말하는 경우
가 많습니다.
배열 자체를 가리키는 포인터 (a pointer to an array) 대신, 배열의 요소를
가리키는 포인터를 (a pointer to one of the array’s elements)생각해보시기
바랍니다. 타입 T의 배열은 (편리하게도) 타입 T의 포인터로 변환됩니다
(decay) (질문 6.3을 참고). 이 포인터에 [] (subscript) 연산자를 쓸 수 있
고, 또는 증가/감소시켜서 배열의 요소에 접근할 수 있습니다. 정말로 배
열 자체를 가리키는 포인터에 (pointers to arrays), subscript 연산자를 쓰
거나, 증가시키면, 배열 전체를 건너뛰게 되므로, 배열을 요소로 가진 배열
을 (arrays of arrays8) 대상으로 할 때에만 의미가 있습니다. (질문 6.18을
참고하기 바랍니다.)
그래도 배열 전체에 대한 포인터가 필요하다면, int (*ap)[N];과 같이 선
언할 수 있으며, 이 때 N은 배열의 크기입니다. (질문 1.21을 참고.) 만약
배열의 크기를 모른다면, N은 생략될 수 있습니다. 그러나 이 경우 “크기를
모르는 배열에 대한 포인터”가 되기 때문에 전혀 쓸모가 없습니다.
아래는 간단한 포인터와, 배열에 대한 포인터의 차이에 관한 예입니다. 다
음과 같은 선언이 있을 때:
int a1[3] = { 0, 1, 2 };
int a2[2][3] = { { 3, 4, 5 }, { 6, 7, 8 } };
int *ip; /* pointer to int */
int (*ap)[3]; /* pointer to array [3] of int */
일차원 배열 a1의 요소를 다루기 위해서, int에 대한 포인터인, ip를 다
음과 같이 쓸 수 있습니다:
ip = a1;
printf("%d ", *ip);
ip++;
printf("%d\n", *ip);
그 결과, 다음과 같이 출력됩니다:
0 1
만약 a1에 ap를 쓰려 하면:
ap = &a1;
printf("%d\n", **ap);
ap++; /* WRONG */
printf("%d\n", **ap); /* undefined */
처음 printf에서는 0을 출력하고, 그 다음에서는 어떻게 동작할 지 알 수
없습니다 (프로그램이 깨질 수도 있습니다). 배열 자체에 대한 포인터는 a2
와 같은, 배열에 대한 배열에서만 의미가 있습니다:
ap = a2;
printf("%d %d\n", (*ap)[0], (*ap)[1]);
ap++; /* steps over entire (sub)array */
printf("%d %d\n", (*ap)[0], (*ap)[1]);
그 결과 다음과 같은 출력을 얻을 수 있습니다:
3 4
6 7
덧붙여 질문 6.12도 참고하시기 바랍니다.
Q 6.16
다차원(multidimensional) 배열을 동적으로 할당할 수 있나요?
CHAPTER 6. ARRAYS AND POINTERS 111
Answer 가장 널리 쓰이는 방법은 포인터의 배열9을 할당하고, 각 포인터가 동적으
로 할당한 “열(row)”을 가리키게 하는 것입니다. 다음 코드는 2 차원 배
열을 동적으로 할당한 것입니다:
#include <stdlib.h>
int **array1 = malloc(nrows * sizeof(int *));
for (i = 0; i < nrows; i++)
array1[i] = malloc(ncolumns * sizeof(int));
실제 코드를 쓸 때에는 malloc의 리턴 값을 검사해 주어야 합니다.
배열의 내용을 연속적으로 만들려면, 다음과 같이 약간의 포인터 계산을 해
야 합니다 (이 경우, 나중에 각 열을 재배치(reallocation)하기 매우 힘듭
니다.)
int **array2 = malloc(nrows * sizeof(int *));
array2[0] = malloc(nrows * ncolumns * sizeof(int));
for (i = 1; i < nrows; i++)
array2[i] = array2[0] + i * ncolumns;
둘 (array1 또는 array2) 중 어떤 것이라도, 동적으로 할당한 배열의 각
요소는 일반적인 배열 subscript 연산자인 []를 써서 다룰 수 있습니다:
arrayx[i][j] (이 때 0 <= i < nrows와 0 <= j < ncolumns를 만족해
야 함).
위와 같이 두번 간접적으로 (double indirection) 메모리에 접근하는 것이
어떤 이유로 인하여 불가능하다면10, 다음과 같이 메모리를 한 번만 할당할
수도 있습니다. 즉 일차원 배열을 다차원 배열로 흉내내는 것입니다:
int *array3 = malloc(nrows * ncolumns * sizeof(int));
그러나, 위와 같은 식으로 만들었다면, 각각의 element에 접근하기 위해 조
금 더 계산이 필요합니다. 즉, (i, j) 번째 element에 접근하기 위해, array3[i * ncolumns + j]
라고11 해야 합니다. 그리고 이 배열은 다차원 배열을 받는 함수에 전달할
수 없습니다. 덧붙여 질문 6.19도 참고하시기 바랍니다.
대신, 배열에 대한 포인터를 (pointers to arrays) 쓸 수 있습니다:
int (*array4)[NCOLUMNS] =
(int (*)[NCOLUMNS])malloc(nrows * sizeof(*array4));
또는,
int (*array5)[NROWS][NCOLUMNS] =
(int (*)[NROWS][NCOLUMNS])malloc(sizeof(*array5));
도 가능합니다.
그러나 이런 방식은 매우 복잡한 문법을 써야 합니다. (배열 array5에 접
근하기 위해서는 (*array5)[i][j]와 같이 씀) 그리고 최대, 한 차원은 실
행 시간에 정해져야 합니다.
이 모든 테크닉과 함께, 이렇게 할당한 배열이 더 이상 필요없을 때에는,
free를 써서 돌려 주어야 합니다; array1과 array2의 경우 여러 단계를
거쳐야 합니다 (질문 7.23 참고):
for (i = 0; i < nrows; i++)
free((void *)array1[i]);
free((void *)array1);
free((void *)array2[0]);
free((void *)array2);
그리고, 이런 배열과 원래 C 언어에서 제공하던, 정적으로 할당된 배열과
섞어 쓸 수 없습니다 (질문 6.20, 6.18 참고).
또, 여기에 소개했던 기술들은 삼차원 또는 그 이상의 고차원 배열에서도
쓸 수 있습니다. 첫번째 기술을 써서 삼차원 배열을 다룬 예입니다:
int ***a3d = (int ***)malloc(xdim * sizeof(int **));
for (i = 0; i < xdim; i++) {
a3d[i] = (int **)malloc(ydim * sizeof(int *));
for (j = 0; j < ydim; j++)
a3d[i][j] = (int *)malloc(zdim * sizeof(int));
}
덧붙여 질문 20.2도 참고하시기 바랍니다.
Q 6.18
이차원 배열을 ‘포인터를 가리키는 포인터12’를 인자로 받는 함수에 전달하
면, 제 컴파일러는 경고를 발생합니다.
Answer 배열이 포인터로 변경된다는 규칙(질문 6.3)은 재귀적으로(recursively) 적
용되는 규칙이 아닙니다. 배열의 배열(arrays of arrays, 즉 이차원 배열)
은 배열을 가리키는 포인터로 (a pointer to an array) 변경되지 (decay),
포인터를 가리키는 포인터로 (a pointer to a pointer) 변경되지 않습니다.
배열을 가리키는 포인터는 어렵고, 또 매우 조심스럽게 다루어야 합니다;
질문 6.13 참고. (더욱 혼란스러운 것은, 잘못된 컴파일러들이 존재한다는
것입니다. 오래된 버전의 pcc나 이 컴파일러를 기초로 한 어떤 lint들은
multilevel pointer에 다차원 배열을 대입하는 것을 허락해버립니다)
함수에 이차원 배열을 전달한다면:
int array[NROWS][NCOLUMNS];
f(array);
함수의 선언은 다음과 같아야 합니다:
void f(int a[][NCOLUMNS])
{ ... }
또는 다음과 같아야 합니다.:
void f(int (*ap)[NCOLUMNS])
/* ap is a pointer to an array */
{ ... }
첫번째 선언에서 컴파일러는 “배열에 대한 배열”을 “배열을 가리키는 포인
터”로 바꾸어 줍니다 (질문 6.3과 6.4를 참고); 두번째 선언은 좀 더 정확
하게 선언한 것입니다. 이 함수가 배열을 위해 메모리를 추가적으로 할당
하지 않기 때문에, 전체 배열의 크기나, 배열의 행(row, 여기에서는 NROWS)
의 갯수를 생략할 수 있습니다. 그러나 이 배열의 형태(shape)는 그래도
중요합니다. 따라서 배열의 폭(column)은 꼭 적어 주어야 합니다 (삼차원
이상의 배열에서는 첫번째 열의 수를 제외하고는 다 유지되어야 함).
함수가 이미 ‘포인터를 가리키는 포인터’를 받도록 선언되어 있다면 여기에
이차원 배열을 직접 전달하는 것은 무의미합니다. 가끔 중간에 임시 포인터
변수를 두어 이차원 배열을 전달하려 하는 코드를 볼 수 있지만:
extern g(int **ipp);
int *ip = &array[0][0];
g(&ip); /* PROBABLY WRONG */
이렇게 쓰는 것은 틀렸습니다. 왜냐하면 배열의 형태가 망가졌기(‘flatten’,
its shape has been lost) 때문입니다.
덧붙여 질문 6.12, 6.15도 참고하시기 바랍니다.