A strength of C is its unification of arrays and pointers. Pointers can be conveniently used to access arrays and to simulate dynamically allocated arrays. The so-called equivalence between arrays and pointers is so close, however, that programmers sometimes lose sight of the remaining essential differences between the two, imagining either that they are identical or that various nonsensical similarities of identities can be assumed between them.
The cornerstone of the “equivalence” of arrays and pointers in C is the fact that most array references decay into pointers to the array's first element, as described in question 6.3. Therefore, arrays are “second-class citizens” in C: You can never manipulate an array in its entirely (i.e., to copy it or pass it to a function), because whenever you mention its name, you're left with a pointer rather than the entire array. Because arrays deacy to pointers, the array subscripting operator [] always find itself, deep down, operating on a pointer. In fact, the subscripting expression a[i] is defined in terms of the equivalent pointer expression *((a) + (i)).
Parts of this chapter (especially the “retrospective” questions in 6.8- 6.10) may seem redundant -- people have many confusing ways of thinking about arrays and pointers, and this chapter tries to clear them up as best it can. If you find yourself bored by the repetition, stop reading and move on. But if you're confused or if things don't make sense, keep reading until they fall into place.
char a[6]
이라고 정의하고
extern char *a
라고
선언해 두었는데 왜 동작하지 않을까요?
extern char a[]
을 사용하기 바랍니다.
char a[]
는 char *a
와 같지 않나요?
char a[6]
과 같은 선언은 문자
여섯 개를 저장할 수 있는 공간을 요청하고, 그 결과 “a”라는
이름이 이 공간을 대표하게 됩니다.
반면 포인터 선언 char *p
는 포인터를 저장할 수 있는 공간을
요구하고 이 위치는 “p”라고 이름지어집니다.
이 포인터는 이제 어떤 곳도 가리킬 수 있습니다: 정확히 말해 어떤
문자나, 문자로 이루어진 배열의 한 요소를 가리킬 수도 있으며,
아무것도 가리키지 않을 수도6.1
있습니다 (질문
5.1과
1.30을 참고).
그림으로 보는 것이 훨씬 더 나을 것입니다. 다음 두 선언은:
char a[] = "hello"; char *p = "world";다음과 같이 데이터를 초기화합니다:
x[3]
과 같은 reference가 x가 배열이냐
포인터이냐에
따라 서로 다른 코드를 생성한다는 것을 알아야 합니다.
위에 선언한 코드를 예로 들면, 컴파일러는 a[3]
을 봤을 때,
주소 `a'에서 세 개만큼 지나서 그 위치의 문자를 가져옵니다.
그리고 컴파일러가 p[3]
과 같은 코드를 봤을 때, 주소 `p'에
셋을 더한 다음, 그 위치의 문자를 가져옵니다.
다시 말하면, a[3]
은 `a'라고 이름붙은(named) 곳에서 3만큼
지난 곳을 의미하며, p[3]
은 `p'가 가리키는(pointed) 곳에서
3만큼 지난 곳을 의미합니다.
위의 예에서는 a[3]
이나 p[3]
이
모두 문자 `l'을 가지고 있지만, 컴파일러는 이 곳을 찾기 위해서
다른 코드를 사용합니다. (가장 큰 차이는 `a'와 같은 배열과
`p'와 같은 포인터의 값을 계산할 때 컴파일러가 서로 다른 방법을
써서 계산한다는 것입니다. 이 계산은 subscript 연산자 []
를
쓰는지의 여부와 관계없습니다. 다음 질문을 참고하기 바랍니다.)
T 타입의 배열의 `lvalue'가 수식에서 나타날 때에는 배열의 첫번째
요소를 가리키는 포인터로 변경(decay)됩니다. 여기에는 세 가지의
예외가 있습니다. 그리고 변경된 포인터의 타입은 T 타입을 가리키는
포인터입니다.
(이 때 예외 사항은 다음과 같습니다:
배열이 sizeof의 피연산자로 쓰일때, 배열이 &
연산자의
피연산자로 쓰일때, 문자 배열에서, 초기값인 문자열로 쓰일
때6.2)
이 정의에 따라서, 내부적으로 배열이 포인터와는 매우 다르지만, 배열이냐 포인터냐에 상관없이 [] 연산자를 쓸 수 있습니다.6.3 배열 a와 포인터 p가 있다고 가정하고, a[i]는 위 정의에 따라서, 배열이 포인터로 퇴화하게(decay) 되므로, 포인터로 변경되고, 이는 포인터 변수를 쓴 p[i]와 의미가 같아집니다. (물론 질문 6.2에서 설명한 것처럼, 실제 메모리 접근은 다릅니다.) 배열의 주소를 다음과 같이 포인터에 대입하면:
p = a;p[3]은 a[3]과 같은 곳을 가리키게 됩니다.
이렇기 때문에, 포인터를 써서 배열에 접근할 수 있는 것이고, 함수 parameter로 쓰일 수 (질문 6.4 참고) 있으며, dynamic array를 흉내낼 수 (질문 6.14 참고) 있습니다.
An lvalue of type array-of-T which appears in an
expression decays (with three exceptions) into a
pointer to its first element; the type of the
resultant pointer is pointer-to-T. (The exceptions are when the array
is the operand of a sizeof or an &
operator or is a string
literal initializer for a character array.)
배열 이름은 즉시 포인터로 바뀌기 때문에6.4, 배열은 함수로 전달되지 않습니다. 포인터 파라메터를 선언할 때 배열처럼 쓸 수 있는 것은, 파라메터가 그 함수 내부에서 배열처럼 쓰일 수 있기 때문입니다. 특히, 다음과 같이 배열처럼 선언된 파라메터는:
void f(char a[]) { ... }컴파일러에 의해 포인터로 인식됩니다. 즉, 다음과 같습니다:
void f(char *a) { ... }그래서, 함수가 배열에 대한 어떤 작업을 하거나, 파라메터가 함수 내부에서 배열처럼 취급될 때, 함수가 배열을 받는다고 말하는 것은 나쁘지 않습니다.
이런 사항들은 함수의 formal parameter 선언에만 적용되며, 다른 곳에서는 적용될 수 없습니다. 만약 배열을 받는 것처럼 선언한 함수가 신경쓰인다면, 안 쓰면 됩니다; 비록 몇몇은 함수의 선언이나, 함수 내부에서 배열을 받아 쓰는 것처럼 보이게 하는 것이 이득이라고 하지만, 많은 부분에서 혼동을 가져오는 것은 사실입니다. (이러한 변환은 오직 한 번만 일어납니다; char a2[][]와 같은 표현은 쓸 수 없습니다. 질문 6.18, 6.19를 보기 바랍니다.)
extern char *getpass(); char str[10]; str = getpass("Enter password: ");
strcpy(str, getpass("Enter password: "));복사하는 대신, 단순히 전해주고 싶다면, 포인터와 대입 연산을 쓸 수 있습니다. 덧붙여 질문 4.1, 8.2도 참고하시기 바랍니다.
int f(char str[]) { if (str[0] == '\0') str = "none"; ... }
[]
를 쓰지 않은
배열 이름 자체를 의미합니다.
배열과 포인터가 같다는 (so-called equivalence) 말 때문에
(질문
6.3 참고), 배열과 포인터는 자주 섞여 쓰입니다 (interchangeable).
특히 malloc으로 할당된 메모리에 접근할
때 쓰는 것은 포인터이지만, 배열처럼([]
를 써서) 쓰는 경우가
많습니다. 질문
6.14와
6.16을 참고하기 바랍니다. (그러나 sizeof
연산자를 쓸 때에는 배열과 포인터가 서로 다르니 주의하기 바랍니다.
질문
7.28 참고)
But to reiterate, here are two ways not to think about it:
5["abcdef"]
와 같은 이상한 표현을 봤습니다.
이것이 C 언어에서 쓸 수 있는 표현인가요?
[]
에는 교환 법칙
(commutative law)이 성립합니다.6.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을 참고하기 바랍니다).
C 언어에서 문자열은 char 타입 배열이기 때문에, "abcdef"[5]란 표현은 틀린 표현이 아니며, 'f'로 평가됩니다. 이 것을 다음 코드의 줄인 표현으로 생각할 수 있습니다:
char *tmpptr = "abcdef"; ... tmpptr[5] ...
arr
과 &arr
의 차이는 무엇인가요?
&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”입니다.
배열 자체를 가리키는 포인터 (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 arrays6.8) 대상으로 할 때에만 의미가 있습니다. (질문 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
#include <stdlib.h> int *dynarray; dynarray = malloc(10 * sizeof(int));(malloc이 성공했다는 가정 아래에서)
dynarray[i]
처럼 (i는 0에서 9까지) 쓸 (reference) 수
있습니다.
즉 dynarray를 int a[10]
과 같이 선언된 것처럼 쓸 수
있습니다. 정적(static)으로 선언된 배열과 동적으로 할당한 메모리를
가리키는 포인터의 차이는 sizeof 연산자를 쓸 때입니다.
덧붙여 질문
1.31b,
6.16,
7.28,
7.7,
7.29도 참고하시기 바랍니다.
[C9X]에서는 크기를 변경할 수 있는 `variable-length array(VLA)'을 소개하고 있으며, 이 VLA을 써서 이 문제를 해결할 수 있습니다; 지역 배열은 변수나 다른 수식(대개 함수 파라메터로 전달된 값)을 써서 크기를 지정할 수 있습니다. (GCC는 파라메터화된(parameterized) 배열이라는 확장 기능을 제공합니다.) [C9X]나 GCC를 쓸 수 없다면 malloc()을 쓰는 수 밖에 없습니다. 단, 배열을 다 사용했다면 반드시 free()를 써서 썼던 메모리를 돌려 주어야 합니다. 질문 6.14, 6.16, 6.19, 7.22, 또 7.32를 참고하기 바랍니다.
#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) 메모리에 접근하는 것이 어떤 이유로 인하여 불가능하다면6.10, 다음과 같이 메모리를 한 번만 할당할 수도 있습니다. 즉 일차원 배열을 다차원 배열로 흉내내는 것입니다:
int *array3 = malloc(nrows * ncolumns * sizeof(int));
그러나, 위와 같은 식으로 만들었다면,
각각의 element에 접근하기 위해 조금 더 계산이 필요합니다. 즉,
(i, j) 번째 element에 접근하기 위해,
array3[i * ncolumns + j]
라고6.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도 참고하시기 바랍니다.
int realarray[10]; int *array = &realarray[-1];
배열의 첫 요소가 1에서부터 시작하는 것처럼 흉내낼 수 있습니다. 이것이 안전할까요?
함수에 이차원 배열을 전달한다면:
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) 때문입니다.
[0][0]
요소의 주소를 전달하고,
배열의 subscript를 흉내내는 것입니다:
void f2(int *aryp, int nrows, int ncolumns) { ... array[i][j]에 접근하기 위해 aryp[i * ncolumns + j]를 씀 ... }수동으로 접근하기 위해, 열의 갯수가 (the number of rows) 아닌, 각 열의 폭을 (the “width” of each row) 뜻하는 ncolunms을 쓰는 것에 주의하기 바랍니다; 바꿔 쓰는 실수가 흔합니다.
이 함수는 질문 6.18에 나온 배열을 다음과 같이 전달받을 수 있습니다:
f2(&array[0][0], NROWS, NCOLUMNS);
예전에도 말했지만, 이런 식으로 다차원 배열의 subscript를 직접 하는 것은
ANSI C 표준에 정확히 부합하지 않습니다; 공식 설명에 따르면
(&array[0][0])[x]
와 같은 표현은
x >= NCOLUMNS
일 경우,
“undefined behavior”에 속합니다.
[C9X]는 가변 크기 배열을 제공하며, [C9X] 확장 기능을 제공하는 컴파일러가 널리 퍼지게 되면, 이 방법이 가장 바람직한 방법이 될 수 있을 것입니다. (GCC는 이미 가변 크기 배열을 제공합니다.)
함수가 다양한 크기를 가지는 다차원 배열을 전달받을 수 있게 하는 한 방법으로, 질문 6.16에 나오는, 배열을 동적으로 시뮬레이션하는 방법이 있습니다.
int array[NROWS][NCOLUMNS]; int **array1; /* ragged */ int **array2; /* contiguous */ int *array3; /* "flattened" */ int (*array4)[NCOLUMNS]; int (*array5)[NROWS][NCOLUMNS]포인터들은 질문 6.16에 나온 것처럼 초기화되어 있다고 가정하고 함수 선언은 다음과 같다고 가정합니다:
void f1a(int a[][NCOLUMNS], int nrows, int ncolumns); void f1b(int (*a)[NCOLUMNS], int nrows, int ncolumns); void f2(int *aryp, int nrows, int ncolumns); void f3(int **pp, int nrows, int ncolumns);이때, f1a()와 f1b()는 일반적인 이차원 배열을 인자로 받으며, f2()는 펼쳐진(flattened) 이차원 배열을 인자로 받으며, f3()은 `포인터를 가리키는 포인터'를 인자로 받습니다 (질문 6.18과 6.19를 참고). 따라서 다음과 같이 호출할 수 있습니다:
f1a(array, NROWS, NCOLUMNS); f1b(array, NROWS, NCOLUMNS); f1a(array4, nrows, NCOLUMNS); f1b(array4, nrows, NCOLUMNS); f1a(*array5, NROWS, NCOLUMNS); f1b(*array5, NROWS, NCOLUMNS); f2(&array[0][0], NROWS, NCOLUMNS); f2(*array, NROWS, NCOLUMNS); f2(*array2, nrows, ncolumns); f2(array3, nrows, ncolumns); f2(*array4, nrows, NCOLUMNS); f2(**array5, NROWS, NCOLUMNS); f3(array1, nrows, ncolumns); f3(array2, nrows, ncolumns);대부분의 컴퓨터에서 다음의 호출들도 동작할 것이나, 복잡한 캐스트가 필요하고, 동적인 ncolumns가 (대개 변수) 정적인 NCOLUMNS와 (대개 매크로 상수) 일치할 경우에만 쓸 수 있습니다:
f1a((int (*)[NCOLUMNS])(*array2), nrows, ncolumns); f1a((int (*)[NCOLUMNS])(*array2), nrows, ncolumns); f1b((int (*)[NCOLUMNS])array3, nrows, ncolumns); f1b((int (*)[NCOLUMNS])array3, nrows, ncolumns);
동적 또는 정적으로 할당한 배열 모두에 편하게 쓸 수 있는 함수는
f2입니다. 그러나 “ragged” 형태로 만든 배열인 array1에는
동작하지 않을 수도 있습니다. 그리고 &array[0][0]
을
(또는 *array
) f2()에 전달하는 것은 표준에
정확히 맞지 않습니다; 질문
6.19를 참고하기 바랍니다.
만약 위에 나열한 호출이 어떻게 동작하고 어떻게 작성되는지 이해한다면, 그리고 왜 다른 조합이 동작하지 않는지 않다면, 여러분은 C 언어에서 배열과 포인터에 대해 매우 잘 이해하고 있다고 말할 수 있습니다.
위에서 어떤 것을 쓸 것인지 결정하는데 고민이 된다면, 질문
6.16에
나온 것처럼, 다양한 크기를 가지는 다차원 배열을 모두 동적으로 만드는
것이 한 방법이 될 수 있습니다 -- 만약 모든 배열이 질문
6.16의
array1
, array2
처럼 만들어 진다면 -- 모든 함수는
f3()처럼 만들어야 합니다.
f(char a[10]) { int i = sizeof(a); printf("%d\n", i); }
int array[] = { 1, 2, 3 }; int narray = sizeof(array) / sizeof(array[0]);
Seong-Kook Shin