Subsections


6. Arrays and Pointers

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.

6.1 Basic Relationship of Arrays and Pointers



Q 6.1
소스 파일에 char a[6]이라고 정의하고 extern char *a라고 선언해 두었는데 왜 동작하지 않을까요?

Answer
소스 파일에 정의한 것은 문자(char)로 이루어진 배열입니다. 그리고 선언한 것은 문자를 가리키는 포인터입니다. 따라서 선언과 정의가 일치하지 않는 경우입니다. 일반적으로, T 타입을 가리키는 포인터(pointer to type T)의 타입은 T 타입의 배열(array of type T)과 다릅니다. 대신 extern char a[]을 사용하기 바랍니다.
References
[ANSI] § 3.5.4.2
[C89] § 6.5.4.2
[CT&P] § 3.3 pp. 33-4, § 4.5 pp. 64-5



Q 6.2
그러나 char a[]char *a와 같지 않나요?
Answer
전혀 다릅니다. (여러분이 생각한 것은 아마도 함수의 formal parameter에 관한 것일 겁니다. 질문 [*]6.4를 참고하기 바랍니다.) 비록 서로 연관되어 있기는 하지만 (질문 [*]6.3 참고), 또 비슷하게 쓰이기도 하지만 (질문 [*]4.1, [*]6.8, [*]6.10, [*]6.14 참고), 배열은 포인터가 아닙니다.

char a[6]과 같은 선언은 문자 여섯 개를 저장할 수 있는 공간을 요청하고, 그 결과 “a”라는 이름이 이 공간을 대표하게 됩니다. 반면 포인터 선언 char *p는 포인터를 저장할 수 있는 공간을 요구하고 이 위치는 “p”라고 이름지어집니다. 이 포인터는 이제 어떤 곳도 가리킬 수 있습니다: 정확히 말해 어떤 문자나, 문자로 이루어진 배열의 한 요소를 가리킬 수도 있으며, 아무것도 가리키지 않을 수도6.1 있습니다 (질문 [*]5.1과 [*]1.30을 참고).

그림으로 보는 것이 훨씬 더 나을 것입니다. 다음 두 선언은:

  char a[] = "hello";
  char *p = "world";
다음과 같이 데이터를 초기화합니다:

\begin{center}\vbox{\input{cfaqs-ko-1.epic}
}\end{center}

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 연산자 []를 쓰는지의 여부와 관계없습니다. 다음 질문을 참고하기 바랍니다.)

References
[K&R2] § 5.5 p. 104
[CT&P] § 4.5 pp. 64-5



Q 6.3
그럼 “포인터와 배열은 같다(equivalent)”라는 말은 어디에서 나온것일까요?
Answer
C 언어에서 가장 혼동을 가져오는 부분은 바로 이 문장을 잘못 이해한 것에서 비롯됩니다. 배열과 포인터가 같다는 (equivalent) 의미는 서로 같다는(identical) 말도 아니고, 서로 마음대로 바꿔쓸 수 있다는 (interchangeable) 뜻도 아닙니다. 이 말이 의미하는 바는 다음과 같은 정의를 의미합니다:
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 참고) 있습니다.

덧붙여 질문 [*]6.8, [*]6.10도 참고하시기 바랍니다.

Note
위의 정의의 원문은 다음과 같습니다:
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.)

References
[K&R1] § 5.3 pp. 93-6
[K&R2] § 5.3 p. 99
[C89] § 6.2.2.1, § 6.3.2.1, § 6.3.6
[H&S] § 5.4.1 p. 124



Q 6.4
그럼 왜 함수의 formal parameter로 배열과 포인터 선언을 마음대로 바꿔 쓸 수 있다는 것일까요?

Answer
편의상 그런 것입니다.

배열 이름은 즉시 포인터로 바뀌기 때문에6.4, 배열은 함수로 전달되지 않습니다. 포인터 파라메터를 선언할 때 배열처럼 쓸 수 있는 것은, 파라메터가 그 함수 내부에서 배열처럼 쓰일 수 있기 때문입니다. 특히, 다음과 같이 배열처럼 선언된 파라메터는:

  void f(char a[])
  { ...  }
컴파일러에 의해 포인터로 인식됩니다. 즉, 다음과 같습니다:
  void f(char *a)
  { ...  }
그래서, 함수가 배열에 대한 어떤 작업을 하거나, 파라메터가 함수 내부에서 배열처럼 취급될 때, 함수가 배열을 받는다고 말하는 것은 나쁘지 않습니다.

이런 사항들은 함수의 formal parameter 선언에만 적용되며, 다른 곳에서는 적용될 수 없습니다. 만약 배열을 받는 것처럼 선언한 함수가 신경쓰인다면, 안 쓰면 됩니다; 비록 몇몇은 함수의 선언이나, 함수 내부에서 배열을 받아 쓰는 것처럼 보이게 하는 것이 이득이라고 하지만, 많은 부분에서 혼동을 가져오는 것은 사실입니다. (이러한 변환은 오직 한 번만 일어납니다; char a2[][]와 같은 표현은 쓸 수 없습니다. 질문 [*]6.18, [*]6.19를 보기 바랍니다.)

덧붙여 질문 [*]6.21도 참고하시기 바랍니다.

References
[K&R1] § 5.3 p. 95, § A10.1 p. 205
[K&R2] § 5.3 p. 100, § A8.6.3 p. 218, § A10.1 p. 226
[ANSI] § 3.5.4.3, § 3.7.1, § 3.9.6
[C89] § 6.5.4.3, § 6.7.1, § 6.9.6
[H&S] § 9.3 p. 271
[CT&P] § 3.3 pp. 33-4

6.2 Arrays Can't Be Assigned

If an array appears on the right-hand side of an assignment, only the pointer it decays to is copied, not the entire array. Furthermore, an array may not appear on the left-hand side of an assignment (in part because, by the previous sentence, there would never be an entire array for it to receive).



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도 참고하시기 바랍니다.
References
[ANSI] § 3.2.2.1
[C89] § 6.2.2.1
[H&S] § 7.9.1 pp. 221-2



Q 6.6
배열에 대입 연산을 쓸 수 없다면, 다음 코드는 왜 동작할까요?
  int f(char str[])
  {
    if (str[0] == '\0')
      str = "none";
    ...
  }

Answer
위 코드에서 str은 함수 파라메터입니다. 따라서 컴파일러는 위 코드를 질문 [*]6.4에서 다룬 것처럼 바꿉니다. 다시 말하는데, 이 때 str은 (타입이 char *인) 포인터입니다. 그리고 포인터에는 대입 연산을 쓸 수 있습니다.



Q 6.7
배열 자체에 값을 대입할 수 없다면 어떻게 배열이 `lvalue'가 될 수 있나요?
Answer
lvalue라는 단어가 “대입할 수 있는 것”을 뜻하지는 않습니다; “(메모리에) 어떤 위치를 가지고 있는 것”이 더 나은 표현입니다.6.5 ANSI C 표준은 “modifiable lvalue”을 정의했고, 배열은 여기에 포함되지 않습니다. 덧붙여 질문 [*]6.5도 참고하시기 바랍니다.
References
[C89] § 6.2.2.1
[ANSI Rationale] § 3.2.2.1
[H&S] § 7.1 p. 179.
Note
여기에서 배열이라 함은 subscript 연산자인 []를 쓰지 않은 배열 이름 자체를 의미합니다.

6.3 Retrospective

Because the basic relationship between arrays and pointers occasionally generates so much confusion, here are a few questions about that confusion.



Q 6.8
실제로 배열과 포인터의 차이는 무엇인가요?
Answer
배열은 같은 타입의 연속적인 요소들이 미리 할당된 공간이며, 크기와 위치가 고정되어 있습니다. (An array is a single, preallocated chunk of contiguous elements (all of the same type), fixed in size and location) 포인터는 다른 방법으로 할당된 메모리 공간을 가리켜야 하지만, 다른 곳을 가리키도록 그 값을 바꿀 수 있습니다. (만약 가리키는 공간이 malloc을 써서 얻은 것이라면, 크기도 바꿀 수 있습니다.) 포인터는 배열을 가리킬 수 있으며, (malloc과 함께 써서) 동적으로 할당한 배열처럼 (흉내내어) 쓸 수 있습니다. 그리고 포인터가 배열보다 더 일반화된 자료 구조입니다 (much more general data structure) (질문 [*]4.1 참고).

배열과 포인터가 같다는 (so-called equivalence) 말 때문에 (질문 [*]6.3 참고), 배열과 포인터는 자주 섞여 쓰입니다 (interchangeable). 특히 malloc으로 할당된 메모리에 접근할 때 쓰는 것은 포인터이지만, 배열처럼([]를 써서) 쓰는 경우가 많습니다. 질문 [*]6.14와 [*]6.16을 참고하기 바랍니다. (그러나 sizeof 연산자를 쓸 때에는 배열과 포인터가 서로 다르니 주의하기 바랍니다. 질문 [*]7.28 참고)

덧붙여 질문 [*]1.32, [*]6.10, [*]20.14도 참고하시기 바랍니다.



Q 6.9
누군가 제게 배열은 단지 포인터 상수라고 (constant pointer) 말한 것이 기억나는 군요. 맞는 말인가요?
Answer
지나치게 간소화한 말입니다 (oversimplification). 배열의 이름은 어떤 값을 대입시킬 수 없는 상수(constant)이지만, 질문 [*]6.2에서 설명하고 보여준 것처럼 배열 자체가 포인터는 아닙니다. 덧붙여 질문 [*]6.3, [*]6.8, [*]6.10도 참고하시기 바랍니다.



Q 6.10
아직도 잘 모르겠습니다. 포인터가 배열의 일종인가요, 아니면 배열이 어떤 포인터의 한 종류인가요?
Answer
배열은 포인터가 아니며, 포인터도 배열이 아닙니다. 배열 reference(즉, 값이 쓰이는 곳에 배열을 쓰는 것)는 포인터로 변경됩니다 (질문 [*]6.2, [*]6.3 참고).
  1. Pointers can simulate arrays (though that's not all; see question [*]4.1)
  2. There's hardly such a thing as an array (it is, after all, a “second-class citizen”); the subscripting operator [] is in fact a pointer operator.
  3. At a higher level of abstraction, a pointer to a block of memory is effectively the same as an array (although this says nothing about other uses of pointers).

    But to reiterate, here are two ways not to think about it:

  4. “They're completely the same.” (False; see question [*]6.2.)
  5. “Arrays are constant pointers.” (False; see question [*]6.9.)

덧붙여 질문 [*]6.8도 참고하시기 바랍니다.



Q 6.11
5["abcdef"]와 같은 이상한 표현을 봤습니다. 이것이 C 언어에서 쓸 수 있는 표현인가요?
Answer
쓸 수 있습니다. Subscript 연산자인 []에는 교환 법칙 (commutative law)이 성립합니다.6.6 a[e]는 어떤 expression ae에 대해, 하나가 포인터 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] ...

실제 이 코드가 쓰이는 예는 질문 [*]20.10에서 볼 수 있습니다.

References
[ANSI Rationale] § 3.3.2.1
[H&S] § 5.4.1 p. 124, § 7.4.1 pp. 186-7

6.4 Pointers to Arrays

Since arrays usually decay into pointers, it's particularly easy to get confused when dealing with the occasional pointer to an entire array (as opposed to a pointer to its first element).



Q 6.12
배열 참조가 포인터로 변환된다면6.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을 참고하기 바랍니다.)

References
[ANSI] § 3.2.2.1, § 3.3.3.2
[C89] § 6.2.2.1, § 6.3.3.2
[ANSI Rationale] § 3.3.3.2
[H&S] § 7.5.6 p. 198



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 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
만약 a1ap를 쓰려 하면:
  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도 참고하시기 바랍니다.

References
[ANSI] § 3.2.2.1
[C89] § 6.2.2.1

6.5 Dynamic Array Allocation

The close relationship between arrays and pointers makes it easy to use a pointer to dynamically allocated memory to simulate an array, of size determined at run time.



Q 6.14
배열의 크기를 실행 시간에 변경할 수 있나요? 고정 크기의 배열 말고 다른 배열이 있나요?

Answer
배열과 포인터의 같은 점 (equivalence) (질문 [*]6.3 참고) 때문에 malloc으로 할당한 메모리를 포인터가 가리키게 한 다음, 배열처럼 쓸 수 있습니다. 다음 코드를 실행한 다음:
  #include <stdlib.h>
  int *dynarray;
  dynarray = malloc(10 * sizeof(int));
(malloc이 성공했다는 가정 아래에서) dynarray[i]처럼 (i는 0에서 9까지) 쓸 (reference) 수 있습니다. 즉 dynarrayint a[10]과 같이 선언된 것처럼 쓸 수 있습니다. 정적(static)으로 선언된 배열과 동적으로 할당한 메모리를 가리키는 포인터의 차이는 sizeof 연산자를 쓸 때입니다. 덧붙여 질문 [*]1.31b, [*]6.16, [*]7.28, [*]7.7, [*]7.29도 참고하시기 바랍니다.



Q 6.15
(함수 인자로) 전달된 크기를 가지는 배열을 선언할 수 있나요?

Answer
현재는 그렇게 할 수 없습니다. C 언어에서 배열의 크기는 컴파일 시간에 정해지는 상수입니다. 일반적으로 이러한 문제는 mallocfree를 써서 해결합니다.

[C9X]에서는 크기를 변경할 수 있는 `variable-length array(VLA)'을 소개하고 있으며, 이 VLA을 써서 이 문제를 해결할 수 있습니다; 지역 배열은 변수나 다른 수식(대개 함수 파라메터로 전달된 값)을 써서 크기를 지정할 수 있습니다. (GCC는 파라메터화된(parameterized) 배열이라는 확장 기능을 제공합니다.) [C9X]나 GCC를 쓸 수 없다면 malloc()을 쓰는 수 밖에 없습니다. 단, 배열을 다 사용했다면 반드시 free()를 써서 썼던 메모리를 돌려 주어야 합니다. 질문 [*]6.14, [*]6.16, [*]6.19, [*]7.22, 또 [*]7.32를 참고하기 바랍니다.

References
[ANSI] § 3.4, § 3.5.4.2
[C89] § 6.4, § 6.5.4.2
[C9X] § 6.5.5.2



Q 6.16
다차원(multidimensional) 배열을 동적으로 할당할 수 있나요?
Answer
가장 널리 쓰이는 방법은 포인터의 배열6.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 < nrows0 <= 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를 써서 돌려 주어야 합니다; array1array2의 경우 여러 단계를 거쳐야 합니다 (질문 [*]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도 참고하시기 바랍니다.
References
[C9X] § 6.5.5.2



Q 6.17
만약 다음과 같이 코드를 작성하면:
  int realarray[10];
  int *array = &realarray[-1];

배열의 첫 요소가 1에서부터 시작하는 것처럼 흉내낼 수 있습니다. 이것이 안전할까요?

Answer
이 테크닉이 매우 매력적으로 보이긴 하지만 (이는 “Numerical Recipes in C”라는 책의 예전판에서 쓴 방식입니다.) 엄밀히 말해 C 표준을 만족하는 방법이 아닙니다. 포인터 연산은 포인터가 할당된 메모리 범위 안을 가리킬 때에나, 배열의 마지막 요소 바로 다음 가상의(imanary) 요소를 가리킬 때에만 정의될 수 있습니다 (Pointer arithmetic is defined only as long as the pointer points within the same allocated block of memory or to the imaginary “terminating” element one past it); 다른 경우에는 포인터 연산을 쓰면 dereference하지 않는다 하더라도, “undefined behavior”를 낳습니다. 따라서 위의 코드에서 뺄셈을 하는 곳에서 잘못된 주소값이 만들어질 수 있습니다 (아마도 주소가 어떤 메모리 세그먼트의 시작점에서 “wrap around”하기 때문일 수도 있습니다).

References
[K&R2] § 5.3 p. 100, § 5.4 pp. 102-3, § A7.7 pp. 205-6
[ANSI] § 3.3.6
[C89] § 6.3.6
[ANSI Rationale] § 3.2.2.3

6.6 Functions and Multidimensional Arrays

It's difficult to pass multidimensional arrays to functions with full generality. The rewriting of array parameters as pointers (as discussed in question [*]6.4) means that functions that accept simple arrays seem to accept arrays of arbitrary length, which is convenient. However, the parameter rewriting occurs only on the “outermost” array, so the “width” and higher dimensions of multidimensional arrays cannot be similarly variable. This problem arises in past because in standard C, array dimensions must always be compile-time constants; they cannot, for instance, be specified by other parameters of a function.



Q 6.18
이차원 배열을 `포인터를 가리키는 포인터6.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도 참고하시기 바랍니다.

References
[K&R1] § 5.10 p. 110
[K&R2] § 5.9 p. 113
[H&S] § 5.4.3 p. 126



Q 6.19
폭(width)을 컴파일 시간에 알 수 없는, 이차원 배열을 인자로 받는 함수를 만들 수는 없을까요?
Answer
쉬운 일이 아닙니다. 한가지 방법은 [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에 나오는, 배열을 동적으로 시뮬레이션하는 방법이 있습니다.

질문 [*]6.18, [*]6.20, [*]6.15를 참고하기 바랍니다.

References
[ANSI] § 3.3.6
[C89] § 6.3.6
[C9X] § 6.5.5.2



Q 6.20
정적 또는 동적으로 할당된 다차원 배열을 함수에 전달할때, 서로 구별하지 않고 쓸 수 있는 방법이 있을까요?
Answer
완벽한 방법은 없습니다. 다음 선언이 있다고 할 때:
  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()처럼 만들어야 합니다.

6.7 Sizes of Arrays

The sizeof operator will tell you the size of an array if it can, but it's not able to if the size is not known or if the array has already decayed to a pointer.



Q 6.21
배열이 함수의 파라메터로 전달될 경우 왜 sizeof 연산자는 제대로 동작하지 않을까요? 아래와 같이 테스트 함수를 만들었더니, 10이 아닌 4를 출력합니다.
  f(char a[10])
  {
    int i = sizeof(a);
    printf("%d\n", i);
  }

Answer
컴파일러는 배열 파라메터를 포인터로 선언된 것으로 간주합니다 (즉, 위 함수에서는 파라메터를 char *a로 여깁니다. 질문 [*]6.4 참고). 그래서 sizeof 연산자가 배열의 크기가 아닌, 포인터의 크기를 리턴합니다.

덧붙여 질문 [*]1.24, [*]7.28도 참고하시기 바랍니다.

References
[H&S] § 7.5.2 p. 195



Q 6.22
한 파일에 배열이 extern으로 선언되어 있을 때 (즉, 실제 크기와 함께 다른 파일에 정의되어 있음), 배열의 크기를 알 수 있을 까요? sizeof 연산자로는 안되는 것 같습니다.
Answer
질문 [*]1.24를 보기 바랍니다.



Q 6.23
배열 안에 몇 개의 요소가 있는 지 알고 싶으면 어떻게 하나요? sizeof 연산자는 배열의 크기를 byte 단위로 돌려주는데요.
Answer
전체 배열의 크기를, 한 요소의 크기로 나누면 됩니다.
  int array[] = { 1, 2, 3 };
  int narray = sizeof(array) / sizeof(array[0]);
References
[ANSI] § 3.3.3.4
[C89] § 6.3.3.4

Seong-Kook Shin
2018-05-28