[C] 포인터와 배열

Gyuwon Lee·2022년 6월 16일
1

42 Seoul 7기

목록 보기
6/6
post-thumbnail

📢 42서울 7기 라피신을 대비하여 모두의 코드 '씹어먹는 C 언어' 강좌의 내용을 정리한 게시물로, 이전 블로그로부터 옮겨 온 글입니다. (원문)

1. 배열과 포인터

먼저, 배열이란 간단히 다음과 같았다:

  • 변수가 여러개 모인 것
  • 메모리 상에 연속되게 놓여 있는 원소(변수)들의 집합에 이름붙인 것

여기서 2차원, 3차원 등 고차원 배열도 만들 수 있었지만 결국 이것들도 메모리 상에서는 1차원 평면 위에 연속되게 놓여 있는 형태가 된다.

/* 배열의 존재 상태? */
#include <stdio.h>
int main() {
  int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  int i;

  for (i = 0; i < 10; i++) {
    printf("arr[%d] 의 주소값 : %p \n", i, &arr[i]);
  }
  return 0;
}

위 코드를 컴파일해보면, arr[0] 부터 arr[9]까 지 각 원소들의 주소값이 4씩 증가하며 연속적으로 위치하고 있음을 확인할 수 있다.

연속적?

바로 앞에서 우리는 포인터에 + 1 을 해주면 (ex. int* pa = &a; int* pb = pa + 1) 그 다음 값에 접근할 수 있음을 확인했다. 그 말인 즉슨, 배열의 첫 번째 원소의 주소값을 포인터에 넣어 준다면 그 포인터에 + 1 , + 2 를 해주는 것으로 그 다음 값, 다다음 값에 접근할 수 있다는 뜻이다.

포인터는 자신이 가리키는 데이터의 '형'의 크기를 곱한 만큼 덧셈을 수행한다. 즉 p 라는 포인터가 int a; 를 가리킨다면 p + 1 을 할 때 p 의 주소값에 사실은 1*4 가 더해지고, p + 3 을 하면 p 의 주소값에 3 * 4 인 12 가 더해진다는 것이다.

그러면 한번 배열의 원소를 가리키는 포인터를 만들어서 이를 확인해보자:

#include <stdio.h>

int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
int* parr = &arr
int i;

for (i = 0; i < 10; i++>)
printf("arr[%d]의 주소값: %p", i, &arr[i]);
printf("(parr + %d) 의 값 : %p ", i, (parr + i));

if (&arr[i] == (parr + i)) {
    printf( " [일치하쥬?]\n");
} else {
    printf( " [앗...불일치하쥬?]\n");
}

위 코드를 컴파일해보면 &arr[i], 즉 배열의 i 번째 원소의 실제 주소값과 첫 번째 원소의 주소로부터 i 만큼 떨어진 메모리의 주소(parr + i) 가 일치한다는 것을 확인할 수 있다. parrint 형이므로 + i 를 하면 주소값에는 사실상 4*i 가 더해지게 되는 것이다.

즉, 배열의 주소값을 저장하는 포인터를 만들고, 그 포인터에 연산자 * 를 사용하여 그 주소에 해당하는 실제 값을 의미하도록 만드는 것은 배열의 특정 원소에 접근하는 것과 동일한 역할을 하게 된다.


1) 배열의 이름

앞서 배열의 주소값을 포인터에 전해주기 위해 아래와 같은 코드를 사용했다.

int* parr = &arr

그랬더니 포인터 parr 에 배열의 첫 번째 원소의 주소값이 전해졌다. (그래서 parr + i 를 통해 이후 원소들에 순차적으로 접근했었다)

#include <stdio.h>
int main() {
  int arr[3] = {1, 2, 3};

  printf("arr 의 정체 : %p \n", arr);
  printf("arr[0] 의 주소값 : %p \n", &arr[0]);

  return 0;
}

위 코드를 컴파일하면, 배열의 이름인 arr 과 배열의 첫 번째 원소의 주소인 &arr[0] 이 동일한 값을 나타내고 있음을 알 수 있다.

따라서 배열에서 배열의 이름은 배열의 첫 번째 원소의 주소값을 나타내고 있다는 사실을 알 수 있다. 그렇다면 배열의 이름이 배열의 첫 번째 원소를 가리키는 포인터라고 할 수 있을까?


📌 배열은 배열, 포인터는 포인터

먼저 한 마디로 정리하자면, 배열 이름은 변경 불가능한 값이고 포인터는 변수로서 변경 가능한 값이다.

#include <stdio.h>
int main() {
  int arr[3] = {1, 2, 3};
  int* parr = arr;

  printf("Sizeof(arr) : %d \n", sizeof(arr));
  printf("Sizeof(parr) : %d \n", sizeof(parr));
}

위 코드를 컴파일해보면 배열과 포인터는 본질적으로 크기 면에서 애초에 다르다는 것을 알 수 있다. 배열인 arr는 12바이트(int 원소가 3개이므로 4*3) 즉 배열의 실제 크기가 나온다. 반면 포인터 parr 는 (64비트 컴퓨터의 경우) 8바이트다.

즉 배열의 이름( arr , 배열 그 자체)과, 첫 번째 원소의 주소값( parr , 배열의 시작 주소)은 엄밀히 다른 것이다.

그렇다면 도대체 왜 두 값을 출력 했을 때 같은 값이 나올까?

그 이유는 C 언어 상에서 배열의 이름이 sizeof 연산자나 주소값 연산자( & )와 사용될 때를 빼면, 배열의 이름을 사용할 때 암묵적으로 첫 번째 원소를 가리키는 포인터타입 변환되기 때문이다.

그러나, 이 때에 배열의 이름은 그 값을 바꿀 수 없는 상수 형태의 포인터이다. 따라서 int arr[3]; arr++ 같이 배열 이름에 직접 증가나 감소 연산자를 사용할 수는 없다. 물론 *(arr + i)는 된다. 하지만 arr 의 값 즉 원본을 수정할 수는 없다. 배열의 시작 위치가 만약 변경된다면 배열의 원소 전체가 흐트러지게 되므로 메모리의 이상한 공간에 잘못 접근하게 될 수도 있는 노릇이다.


2) [] 연산자

사실 포인터 연산자는 세 가지로, *&, 마지막으로 []도 있다. 앞서 보았듯 이 포인터 연산자들은 변수가 위치한 메모리의 주소를 구하거나( & ), 포인터 변수가 갖는 주소에 저장된 값을 구하는 데( * ) 쓰인다.

스택오버플로우를 살짝 찾아보자.

e1[e2]   means   *(e1+e2)

라고 한다. 또한

A postfix expression followed by an expression in square brackets [] is a subscripted designation of an element of an array object. The definition of the subscript operator [] is that E1[E2] is identical to (*((E1)+(E2))). Because of the conversion rules that apply to the binary + operator, if E1 is an array object (equivalently, a pointer to the initial element of an array object) and E2 is an integer, E1[E2] designates the E2-th element of E1 (counting from zero).

라고도 한다. 즉, 후위 연산자[] 는 배열 첨자 연산자로 배열의 특정한 원소를 나타내는 데 사용하는 기호가 맞는데, 그렇게 특정 원소를 나타낼 수 있는 원리가 마치 포인터와 같이 해당 원소에 접근하는 것이다.

C 언어 상에서 E1[E2]*((E1)+(E2)) 와 같다. 이 때 이항 연산자 + 의 타입 변환 규칙에 따라, E1 이 배열이고 E2 가 정수라면 E1(E2)*((E1)+(E2))배열 E1E2 번째 요소주소에 저장되어 있는 실제 값을 가져오는 것이다.

그러니까 사실 E1[E2]E2[E1] 이나 괄호 안에서 + 로 연결되면 순서가 상관없기 때문에 동일한 결과를 나타낸다. 즉 arr[3] 이나 3[arr] 이나 똑같다.

profile
하루가 모여 역사가 된다

0개의 댓글