Ch11 introduced pointers, and how they're used as function arguments, values returned by the functions.
포인터가 배열의 원소를 가리킬 때, 포인터에 덧셈과 뺄셈 연산을 할 수 있다.
포인터와 배열의 관계는 C를 마스터하는데 critical한 부분이다. 그러나 배열을 다루는데 포인터를 써야 하는 가장 큰 이유 - 효율성- 은 예전만큼 중요하지 않은데, 컴파일러들이 더 발전했기 때문이다.
12.1 pointer arithmetic and how pointers can be compared using the relational and equality operators
12.2 how we can use pointer arithmetic for processing array elements
12.3 array name can serve as a pointer to the array's first element
12.4 how the topcis of first three sections apply to multidimensional arrays
12.5 wrap-up, by exploring the relationship between pointers and variable-length arrays (C99)
12.1 Pointer Arithmetic
int a[10], *p;
라고 정의 되어 있을 때,
p = &a[0];
=> p가 a[0]을 가리키게 한다.
그림으로 보면 위와 같다.
*p = 5; 라고 하면, a[0]자리에 5가 들어간다.
포인터에 pointer arithmetic (or address arithmetic)을 수행함으로써 배열의 다른 원소에 접근할 수 있다.
pointer arithmetic에는 다음 세가지 종류가 있다.
1. Adding an integer to a pointer
2. Subtracting an integer from a pointer
3. Subtracting one pointer from another
각각의 연산을 살펴보자. 다음부터 나올 예시에서는 다음 선언이 유효하다고 가정한다.
int a[10], *p, *q, i;
Adding an Integer to a Pointer
포인터 p의 값에 정수 j를 더하면, p가 가리키는 원소에서 j자리만큼 뒤의 원소를 가리킨다.
만약 p가 a[i]를 가리킨다면, p + j는 a[i+j]를 가리킨다(물론 a[i+j]가 있다는 전제 하이다).
p = &a[2];
q = p + 3;
p += 6;
Subtracting an Integer from a Pointer
p가 a[i]를 가리킬 때, p - j 는 a[i-j]를 가리키게 된다.
p = &a[8];
q = p - 3;
p -= 6;
Subtracting One Pointer from Another
포인터끼리 뺄셈을 수행했을 때, 그 결과는 포인터 간의 거리(measured in array elements)이다.
p가 a[i]를 가리키고 q가 a[j]를 가리킬 때, p - q는 i - j와 같다.
p = &a[2];
q = &a[5];
i = p - q; /* i is 3 */
i = q - p; /* i is -3 */
※ 배열의 원소를 가리키지 않는 포인터에 연산을 수행하는 것은 undefined behavior를 일으킨다. 나아가, 한 포인터에서 다른 포인터를 빼는 것도 두 포인터가 같은 배열의 원소를 가리키지 않으면 undefined behavior를 일으킨다.
Comparing Pointers
관계 연산자(<, <=, >, >=)와 동등 연산자(==)를 통해 복수의 포인터를 비교할 수 있다.
두 포인터에 관계 연산자를 사용하는 것은 두 포인터가 같은 배열에 있는 원소를 가리킬 때만 의미가 있다.
비교의 결과는 두 원소의 상대적인 포지션의 결과에 따른다. 예를 들어,
p = &a[5];
q = &a[1];
이면, p<=q의 값은 0이고 p>=q의 값은 1이다.
12.2 Using Pointers for Array Processing
Pointer arithmetic을 통해 배열의 원소에 반복적으로 증가하는 포인터 변수를 통해 하나하나 접근할 수 있다.
#define N 10
...
int a[N], sum, *p;
...
sum = 0;
for(p = &a[0]; p < &a[N]; p++)
sum += *p;
포인터를 사용하지 않고도 쉽게 loop문을 subscripting (a[i])을 이용해 쓸 수 있다.
subscripting 대신 pointer arithmetic을 써야 한다는 주장의 가장 큰 근거는 실행 속도가 더 빠르다는 것인데, 이는 implementation에 따라 다르다. 어떤 컴파일러는 subscripting을 사용할 때 더 나은 코드가 되기도 한다.
Combining the * and ++ Operators
배열의 원소를 다루는 statement에서 흔하게 * (indirection)과 ++ 연산자를 혼합해서 사용한다.
간단한 예로 한 배열 원소에 값을 저장하고, 다음 원소로 넘어가는 경우를 생각하자.
array subscripting 사용시,
a[i++] = j;
p가 배열 원소를 가리키고 있다면, 그에 대응하는 statement는
*p++ = j;
postfix ++연산자는 *보다 우선순위가 빠르기 때문에, 컴파일러는 위 식(expression)을 다음과 같이 본다.
*(p++) = j;
p++의 값은 p이다(postfix ++연산자를 사용하고 있는데, p의 값은 다음 expression(여기서는 j)이 evaluated되기 전까지는 증가하지 않는다).
따라서 *(p++)의 값은 *p, 즉 p가 가리키는 object이다.
(*p)++는 p가 가리키는 object의 값을 반환한 후, 그 object의 값을 1 증가시킨다(p 자체는 변하지 않는다).
Expression |
Meaning |
*p++ or *(p++) |
Value of expression is *p before increment; increment p later |
(*p)++ |
Value of expression is *p before increment; increment *p later |
*++p or *(++p) |
Increment p first; value of expression is *p after increment |
++*p or ++(*p) |
Increment *p first; value of expression is *p after increment |
4개의 표현은 모두 사용하지만, *p++를 더 자주 사용한다. 이 표현은 loop에서 유용하다.
for (p = &a[0]; p < &a[N]; p++)
sum += *p;
대신에
p = &a[0];
while (p < &a[N])
sum += *p++;
라고 쓸 수 있다.
10.2에서 사용했던 stack에서 integer variable이었던 top을 바꿔 보자.
int *top_ptr = &contents[0];
void push(int i)
{
if (is_full())
stack_overflow();
else
*top_ptr++ = i;
}
int pop(void)
{
if (is_empty())
stack_underflow();
else
return *--top_ptr;
}
*top_ptr-- 이 아닌, *--top_ptr이라고 쓴 것에 유의할 것. pop 함수에서 top_ptr을 먼저 1 감소시키고, 그다음 그것이 가리키는 값을 얻기를 원하기 때문이다.
12.3 Using Array Name as a Pointer
배열의 이름은 배열 첫번째 원소를 가리키는 포인터로 쓰일 수 있다.
int a[10];
*a = 7; /* stores 7 in a[0] */
*(a+1) = 12; /* stores 12 in a[1] */
일반적으로 a + i는 &a[i] (a의 i번째 원소를 가리키는 포인터)와 같고, *(a + i) 는 a[i] (원소 i)와 같다.
배열의 이름을 포인터로 쓸 수 있기 때문에 배열 안을 도는 루프문을 더 간단하게 표현할 수 있다.
for (p = &a[0]; p < &a[N]; p++)
sum += *p;
대신
for (p = a; p < a + N; p++)
sum += *p;
배열의 이름이 포인터로 쓰이기는 하지만, 새로운 값을 assign할 수는 없다. 배열의 이름을 포인터 변수에 복사해서 그 포인터 변수에 값을 assign할 수는 있다.
/* Reverses a series of numbers (pointer version) */
#include <stdio.h>
#define N 10
int main(void)
{
int a[N], *p;
printf("Enter %d numbers: ", N);
for (p = a; p < a + N; p++)
scanf("%d", p);
printf("In reverse order:");
for (p = a + N - 1; p >= a; p--)
printf(" %d", *p);
printf("\n");
return 0;
}
Array Arguments (Revisited)
함수로 전달되었을 때, 배열의 이름은 항상 포인터로 간주된다.
일반적인 변수가 함수로 전달될 때, 그 값은 복사되어 전달된다. 파라미터에 대한 어떤 변동도 원래 변수에는 영향을 끼치지 않는다. 반면, 배열이 argument로 사용되었을 때는 배열이 복사되지 않고, 함수 내에서의 값의 변동에 보호되지 않는다. 만약 배열 파라미터가 바뀌지 않고 그대로 남아있기를 원한다면, 함수 선언부에 const를 넣어 준다.
int find_largest(const int a[], int n)
만약 const가 있다면, 컴파일러는 함수의 body에 배열의 원소에 assignment가 있는지를 체크한다.
배열의 크기는 배열을 함수로 pass 하는 시간에 영향을 끼치지 않는다. 왜냐하면 배열의 복사본이 만들어지지 않기 때문이다.
배열 파라미터는 원한다면 포인터로 선언되어도 된다.
int find_largest(int *a, int n)
a를 포인터로 선언하는 것은 배열로 선언하는 것과 동일하다. 컴파일러는 그 둘을 동일하게 취급한다.
배열을 파라미터로 갖는 함수에는 배열의 "slice" - a sequence of conseucutive elements 가 전달될 수 있다. 예를 들어 b라는 배열의 b[5], ..., b[14] 중 가장 큰 원소를 찾고 싶다면,
largest = find_largest(&b[5], 10);
Using a Pointer as an Array Name
배열의 이름을 포인터로 사용할 수 있다면, 포인터를 마치 배열인 것처럼 subscript해서 사용할 수 있는가? 답은 Yes이다.
#define N 10
...
int a[N], i, sum=0, *p = a;
...
for (i = 0; i < N; i++)
sum += p[i];
컴파일러는 p[i]를 *(p+i)와 같게 취급한다(perfectly legal use of pointer arithmetic).
12.4 Pointers and Multidimensional Arrays
포인터는 일차원 배열의 원소를 가리킬 수 있을 뿐 아니라, 다차원 배열의 원소도 가리킬 수 있다. 이 섹션에서는 이차원 배열을 다루지만, 더 높은 차원의 배열에도 적용할 수 있다.
Processing the Elements of a Multidimensional Array
8.2에서 살펴보았듯이 2차원 배열은 row-major order로 저장된다.
이 점을 포인터와 연관지어 이용해 볼 수 있다. 포인터 p를 반복적으로 증가하면서, 2차원 배열의 모든 원소에 접근할 수 있다.
int a[NUM_ROWS][NUM_COLS];
int row, col;
...
for (row = 0; row< NUM_ROWS; row++)
for (col = 0; col < NUM_COLS; col++)
a[row][col] = 0;
이것이 지금까지 배운 nested for loops로 이차원 배열의 모든 원소에 접근하는 방법이다. 그러나 배열 a를 일차원 배열로 바라보면(실제로 그렇게 저장됨), 이중 for 문을 하나의 for 문으로 표현할 수 있다.
int *p;
...
for (p = &a[0][0]; p <= &a[NUM_ROWS-1][NUM_COLS-1]; p++)
*p = 0;
이러한 방법은 대부분의 C 컴파일러에서 제대로 작동한다. 가독성을 다소 떨어트리기는 하지만, 오래된 컴파일러에서는 이러한 방식이 효율성 측면에서 더 우월하다. 그러나 최근의 많은 컴파일러에서는 속도의 차이가 거의 없거나 아예 없는 경우가 많다.
Processing the Rows of a Multidimensional Array
배열 i행의 첫번째 원소에 접근하는 포인터는,
p = &a[i][0]; 또는 p = a[i];
배열의 한 행 전체에 접근하는 코드
int a[NUM_ROWS][NUM_COLS], *p, i;
...
for (p = a[i]; p < a[i] + NUM_COLS; p++)
*p = 0;
Processing the Columns of a Multidimensional Array
배열이 열이 아니라 행으로 저장되기 때문에 열에 접근하는 것은 좀 더 복잡하다.
int a[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS], i;
...
for (p = &a[0]; p < &a[NUM_ROWS]; p++)
(*p)[i] = 0;
(*p) [NUM_COLS] : NUM_COLS만의 길이를 가진 정수 배열의 포인터(괄호 없이 *p[NUM_COLS]라고 하면 포인터로 구성된 배열을 의미한다)
p++ : p가 다음 행을 가리키게 함
(*p) [i]: *p는 a의 한 행을 가리키므로, (*p) [i]는 그 행의 i열을 가리킨다(괄호 없이 *p[i]라고 하면 *(p[i])를 의미한다.
Using the Name of a Multidimensional Array as a Pointer
일차원 배열의 이름이 포인터로 쓰일 수 있는 것과 마찬가지로, 차원에 관계 없이 모든 배열은 그 이름이 포인터로 쓰일 수 있다. 그러나 좀더 조심해서 다뤄야 한다.
int a[NUM_ROWS][NUM_COLS];
위와 같은 배열이 있을 때, a는 a[0][0]에 대한 포인터가 아닌 a[0]의 포인터이다.
C의 관점에서는 배열 a를 이차원 배열이 아닌 일차원 배열(원소가 일차원 배열인)로 본다.
a가 포인터로 쓰였을 때, a의 type은 int (*) [NUM_COLS]이다(길이가 NUM_COLS인 배열의 포인터).
a가 a[0]을 가리킨다는 사실을 이용하면 이차원 배열의 원소를 다루는 루프를 간단하게 만들 수 있다.
for (p = &a[0]; p < &a[NUM_ROWS]; p++)
(*p)[i] = 0;
// 대신
for (p = a; p < a + NUM_ROWS; p++)
(*p)[i] = 0;
이것을 다르게 이용하면, 다차원 배열을 일차원 배열인 것처럼 C를 '속이는' 경우에 편리하다.
int a[NUM_ROWS][NUM_COLS];
...
largest = find_largest(a[0], NUM_ROWS * NUM_COLS);
largest = find_largest(a, NUM_ROWS * NUM_COLS); /*** WRONG ***/
마지막 줄은, 포인터 a의 타입은 int (*)[NUM_COLS]이지만 find_largest가 expect하는 것은 int * 이므로 틀렸다.
Q&A 해석