Chapter 12. Pointers and Arrays

지환·2021년 12월 7일

12.1 Pointer Arithmetic

pointer가 array element를 가리킬 때, arithmetic 연산이 가능하다.(array 가리키지 않으면 UB)

아래 세가지 연산만 허용
1. Adding an integer to a pointer : pa[i]를 가리킨다면, p+j == a[i+j]
2. Subtracting an integer from a pointer : pa[i]를 가리킨다면, p-j == a[i-j]
3. Subtracting one pointer from another : array elements에 따른 distant로 계산된다.(음수 나올 수도 있음) (피연산자가 같은 array의 element를 가리켜야 함.)

Comparing Pointers

relational operator(<, <=, >, >=)
equality operator(==, !=)

Pointers to Compound Literals

int *p = (int []){1, 2, 3};

12.2 Using Pointers for Array Processing

#define N 10
~~~
for (~~; p < &a[N]; ~~)
~~~

이런식으로 &a[N]을 적어도 perfectly safe
왜냐하면 저 값은 계산하지 않기 때문이다.

Processing array 하는 데 있어 두가지 방법이 있다.
1. array subscripting
2. pointer arithmetic
사실 둘을 같게 볼 수 있다. (a[i] == a+i == i[a])

둘 중 뭐가 더 빠른가?
옛날엔 2번이 빨랐지만, 요즘은 1번이 더 낫다.
그냥 둘 다 배우고, 둘 중 본인 프로그램에 맞는 더 자연스러운 방식으로 작성해라.

12.3 Using an Array Name as a Pointer

Pointer arithmentic에서 array와 pointer의 관계가 드러난다.
하지만 다른 중요한 key relationship이 있는데 바로,
"The name of an array can be used as a pointer to the first element in the array."
즉, 배열의 이름은 첫번째 elements의 포인터
a == &a[0] (일차원 배열이라 가정)
그렇다고 해서 array name에 다른 pointer 값을 assign 할 순 없음(array name은 상수로 보는게 맞을듯)

From K&R
"By definition, the value of a variable or expression of type array is the address of element zero of the array."
(여기서 array type의 expressions도 address로 해석된다는걸 알아야 2차원 배열 포인터 p에 대해 *(*(p+1)+3) 같은 연산이 이해되네. *(p+1)도 배열 type의 expression이니까 해당 배열의 첫 element 주소인 것)

Array Arguments

함수에 array name이 pass될 때, 이는 항상 pointer로 간주된다.
따라서,

1. 원래 그냥 arugment라면 copy되기 때문에 그 값의 변경이 영향을 주지 않았지만, array의 경우 값을 수정하면 원래 array에도 영향을 준다.
따라서 이를 방지하려면,

int func(const int a[], int n)
{
  ~~
}

식으로 const를 넣어서 선언해야한다.

2. array argument를 넘기는데 걸리는 시간은 array size에 달려있지않다. 왜냐하면 array에 대한 복사는 일어나지 않기 때문이다.

3. array parameter는 원한다면 pointer로 선언해도 상관없다.

int func(int *a, int n)
{
  ~~
}

여기서 주의할 점이, parameter에선 array나 pointer나 그게 그거지만, 일반 변수를 선언할
때는 둘을 당연히 구분해 줘야 한다.

4. silice해서 pass할 수도 있다. a를 넘기면 a[0]부터 시작하겠지만, &a[1]을 넘겨서 넘겨받은 함수는 그게 시작점인줄 알게 할 수 있다.


12.4 Pointers and Multidimensional Arrays

Processing the Elements of a Multidimensional Array

C에서는 two-dimensional array를 row-major order로 저장하기 때문에, 이를 이용해서 two-dimensional을 one-dimensional로 간주하여 볼 수 있다.

int *p;
for (p = &a[0][0]; p <= &a[NUM_ROWS-1][NUM_COLS-1]; p++)
  *p = 0;

하지만 이렇게 이차원 배열을 일차원으로 대하면 readability에 안좋다.(오래된 컴파일러에선 속도가 좀 빠를수도 있지만, 최근 컴파일러에선 딱히 그렇지도 않음)

Processing the Rows of a Multidimensional Array

한 row에 대해서만 진행

p = &a[i][0];

p = a[i];

a가 이차원 배열이라면, 둘은 같은 expression이다.
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

들어가기전에 2차원 배열에서의 type을 설명하자면,
int a[10][20];
이라고 했을때,
a의 type : int (*) [20] (int ** 아님)
a[0]의 type : int *
a[0][0]의 type : int
크게보면 a 배열은 원소 10개짜리 배열인데, 각 원소가 int [20] 배열인 것
(10개의 1차원 배열 각각 안에 또 1차원 배열이 있는 구조)
따라서 a는 크게 보면 1차적으로 1차원 배열이다.(int [20]이 원소인 1차원 배열)
위에서 정의할때, "배열의 이름은 첫번째 element의 포인터"라고 했다.
근데 이 이차원 배열에서 첫번째 element는 (1차원으로 생각한다면) int [20]짜리 큰 덩어리다.
그러므로 그 첫번째 element에 대한 포인터는 int (*) [20]이 맞다.
좀 이상해보일 순 있지만, 그냥 일차원 배열도 element type에 따라 char인지 int인지 그 크기가 갈린다.
이처럼 2차원 배열은 row한 덩어리를 element로 보기 때문에(p.269 아래에 그렇게 말함),
각 row의 길이까지 크기로 고려한다는건 별 다를게 없다.

한 column에 대해서만 진행

int a[NUM_ROWS][NUM_COLS], (*p)[NUM_COLS], i;
for (p = &a[0]; p < &a[NUM_ROWS]; p++)         //p = &a[0] 대신, p = a 도 가능
  (*p)[i] = 0;

Using the Name of a Multidimensianal Array as a Pointer

바로 위에서 한거랑 같은 말이다.
int a[NUM_ROWS][NUM_COLS];
했을 때, aa[0][0]의 포인터가 아니라, aa[0]의 포인터다.
a를 이차원 배열이 아니라, 일단은 커다란 1차원 배열로 보기 때문이다.
따라서 a의 type은 int (*) [NUM_COLS] 이다.

이차원 배열을 일차원 배열처럼 사용하게 함수로 넘겨주려면,
func(a, ~);
식으로 일차원처럼 이름만 띡 넘기면 안된다.
왜냐하면 type이 일치하지 않기때문이다.
func(a[0], ~);
처럼 넘겨줘야 한다. (a[0]의 type은 1차원 배열 이름처럼 int * 이다.)


12.5 Pointers and Variable-Length Arrays (C99)

그냥 위랑 똑같이 배운대로 쓰면 된다. (2차원일 경우는 잘 적용하며)
근데 VLA를 가리키는 포인터는 변수에 따라 값이 바뀌므로, 이 포인터를 variably modified type을 가졌다고 한다.

variably modified type은 무조건 함수 안이나 함수 prototype에서 선언돼야 한다.


Q&A

a[i] == *(a+i) == *(i+a) == i[a]

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

parameter에서 *aa[] 중에 뭐가 더 낫나?
: 어떤 사람은 a[]가 배열을 argument로 넘겨준다는걸 확실히 알려주므로 a[]를 선택한다. 하지만 어떤 사람은 *a가 더 정확하다고 *a를 쓴다. 왜냐하면 사실 넘겨주는건 array 전체가 아니라 포인터뿐이기 때문이다. 또 어떤 사람은 함수 내에서 쓰임에 따라 둘을 바꿔 적는다.(pointer arithmetic을 쓰냐, subscripting을 쓰냐에 따라 적는다.) 실제로 a[]보다 *a가 더 보편적이므로 *a가 더 낫다.

!주의
만약 int a[10][20]; 을 함수 인자로 넘겨받으려면 그냥 int ** 이라고 해선 안된다.
잘생각해보자. a의 type은 int * [20] 이다. 1차원 배열일땐 그냥 간단하게 int를 가리켰기때문에 괜찮았지만, 이차원은 int 20칸을 가리킨다는 것을 주의해야한다.(그렇게 정의돼있지 않다면 a[1][3] 같은 원소 접근이 애초에 안됐을것. 왜냐하면 a[1]을 했을때 int 한칸을 가는게 아니라 int 20칸을 가야함.)
그렇기때문에 이런 a를 함수 인자로 받으려면 func(int a[][20]){~} 이라고 정의하거나 func(int (*a)[20]){~} 이라고 정의해야한다.

하지만 void func(int **a){~} 같은 경우처럼 이차원 배열이 아닌 이중포인터를 넘겨받는다면, int *a[] 라고 써도 상관없다. 본질을 잘 파악해야한다.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

사실, name of array는 pointer가 아니다. C compiler가 필요에 따라 array name을 pointer로 바꾸는 것일 뿐이다.
sizeof operator를 적용시켜보면 차이를 알 수 있다. array는 array 길이가 다 나오는 반면, pointer는 그냥 그 주소를 저장하기 위한 byte 수만 나올 뿐이다.

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ

2차원 배열을 1차원 배열처럼 다루는건 몇몇 컴파일러에선 작동하지 않는다.
bounds-checking이 작동하면 한 row를 넘어가면 error로 간주할 수 있다.
pa[0][0]를 가리키게 했을 때, p는 결국 a[0]라는 1차원 배열을 가리키는게 되므로 그렇다.

0개의 댓글