포인터와 배열

iwtkmn_0219·2022년 7월 21일
0

C 언어

목록 보기
2/2
post-thumbnail

0. 들어가기

포인터로 배열을 다루기 위해서는 C언어에서 배열에 대한 정보를 어떤식으로 저장하고 다루는지 알아야만 합니다. 이에 대한 지식이 없어도 충분히 다룰 수 있지만, 지식이 없는 상태로 배열을 포인터로 다루다간 언젠가 이질감이 들지도 모릅니다. 그때서야 이질감을 지우기 위해 서핑을 통해 둘러봐도 이질감이 도저히 지워지지 않을수도 있습니다. 생각지도 않았던 C언어의 처리방식 때문일수도 있기 때문입니다. 그래서 배열에 대한 처리방식을 상태로 포인터로 배열을 다루기 시작하면 이를 이해하는 데에도 도움이 되겠지만, C언어 자체를 이해함으로써 메모리를 직접 관리하는 C의 장점을 다방면으로 활용할 수도 있습니다.

1. 배열은 어떤식으로 저장되는가?

C언어에서 배열도 다른 변수들과 마찬가지로 특정 주소에 저장됩니다. 따라서 배열도 마찬가지로 포인터로 접근할 수 있습니다. 그러나 배열은 다른 변수들과는 다르게 배열 내부에는 ‘여러 개’의 데이터가 담겨 있습니다. 지난 포스팅에서 다루었지만 컴퓨터는 1바이트(8 비트)를 기준으로 데이터를 저장합니다만 C에서는 과연 어떻게 배열을 저장할까요?

int arr[5];  // -66061304;

arr이라는 배열이 생성되어 있습니다. 다만, 배열 자체가 저장되어있는 것이 아니라 arr의 위치(주소)이 저장되어있습니다. 실제로 부가적인 것들을 붙이지 않고 arr만을 printf를 통해 출력했을 때 위의 주석처럼 이상한 숫자를 출력하는 것을 확인할 수 있는데 해당 숫자는 메모리 공간에서 arr이 저장된 주소값입니다. 심지어 아래와 같이 &arr을 출력해도 동일한 값을 출력합니다.

int arr[5];
printf("arr: %d\n", arr);  // -66061304;
printf("&arr: %d\n", &arr);  // -66061304;

지금 당장은 익숙하지 않아 이해가 느릴수도 있으니 이번 포스팅에서는 &arr이라고 작성하겠지만, arr도 마찬가지로 주소값이라는 것을 기억해야합니다.

2. 1차원 배열에서의 인덱싱

arr의 1번째 인덱스에 접근해보겠습니다. 이전의 그림에서 한 칸은 1 바이트를 의미합니다. 배열에 접근하는 방식은 다음과 같습니다.

우선 접근하고자 하는 인덱스인 ‘1’에 배열의 자료형인 정수의 크기(바이트 단위)인 ‘4’를 곱해줍니다. 이렇게 구한 값인 4를 arr의 주소값에 더해줍니다. 해당 위치가 배열에 해당하는 값을 저장하는 공간입니다. 만약 &arr의 값이 -66061304였다면, &arr[1]의 값은 -66061300입니다.

위에서 설명한 방식으로 데이터를 저장하면 사실 다른 데이터와 다른점이 없습니다. 다른 데이터들과 마찬가지로 배열도 메모리공간에 데이터를 저장합니다. 단지 저장하는 공간을 연속해서 사용하며, 배열만의 방식으로 특정해준다는 아이디어가 첨가되었습니다.

💡 기존에 사용하던 방식에 ‘데이터를 더 효율적으로 다룰 수 있는 아이디어’를 더한 것을 “자료구조”라고 합니다. 자료구조 중 가장 단순한 형태가 우리가 지금까지 배운 배열입니다. 배열에 추가적으로 아이디어를 더해서 새로운 자료구조를 만들어낼 수도 있으며 실제로 그러한 자료구조가 존재합니다. (스택, 큐 등)

이러한 원리로 인해서 아래와 같은 코드도 작성할 수 있습니다.

int main(void) {
		int arr[5];
	
		for (int i = 0; i < 5; i++) {
				scanf("%d", arr + i);
		}
	
		for (int i = 0; i < 5; i++) {
				printf("%d ", arr[i]);
		}
}

원래 scanf함수는 값을 저장하고자 하는 주소값을 불러왔습니다. 그러나 arr + i라는 수식을 통해 우리가 직접 저장공간을 정해줄 수 있습니다. (위에서 언급했듯 arr&arr는 동일한 역할을 수행합니다.)

위의 코드에서 이해가 안되는 부분이 있다면 정상입니다. 분명 arr에 정수형을 더했다면 오류가 생겨야만합니다. 왜냐하면 정수는 4바이트씩 사용하기 때문에 arr에 0을 더한 메모리부분은 값을 수정할 수 있다고 해도, arr에 1을 더한 메모리 부분은 값을 수정하는 것이 불가능하다고 생각되기 때문입니다. 그러나 포인터와 포인터가 아닌 데이터에 대한 연산은 다음과 같이 이루어집니다.

pointer + not_pointer = pointer + (not_pointer sizeof(pointer))

위와 같은 이유때문에 arr+i에서 자동적으로 배열의 다음값을 가져올 수 있습니다. 예제를 들어보도록 하겠습니다. (절대 다음과 같이 코드를 작성해서는 안됩니다. 이해를 돕기 위한 코드임을 미리 알려드립니다.)

int i = 1;
int* p = 0;
printf("%d", p + i);

위에서 출력되는 값은 4입니다. p + i가 자동적으로 p + (i sizeof(p))의 연산을 수행하였기 때문입니다.

int i = 1;
char* p = 0;
printf("%d", p + i);

위에서 출력되는 값은 1입니다. char의 크기는 1바이트이기 때문입니다.

1차원 배열에서의 인덱싱 방법을 순서대로 나열해보도록 하겠습니다.

  1. 배열의 주소를 받아온다.
  2. 접근하고자 하는 인덱스에 자료형의 크기를 곱한다.
  3. 배열의 주소에 곱한 값을 더해준다.

3. 배열은 사실

우리가 배열의 특정 위치에 접근하고 싶다면 arr[i]와 같이 인덱스를 통해 접근하였습니다. 그러나 우리가 여기서 사용하는 대괄호도 연산의 일종에 해당합니다. arr[i]는 *(arr + i)을 의미합니다. arr은 포인터, i는 정수이므로 더 자세하게 풀어서 작성하면, *(arr + (i * sizeof(*arr)))라고 쓸 수 있습니다.
따라서 만약 누군가에게 빅엿을 선사하고 싶다면 i[arr]이라고 작성해줍시다. 그러나 정말 원한을 크게 산 사람이 아니라면 이런 짓은 하지 맙시다. (1[arr]arr[1]은 동일한 코드)

사실 이러한 사실을 몰라도 배열을 사용하는데에는 큰 문제가 없습니다. 그러나 포인터를 다루게 되고 추후에 동적할당이라는 기능을 잘 사용하기 위해서는 이러한 내용이 도움이 될 것이라고 생각합니다.

4. 다차원 배열에서의 인덱싱

1차원배열과 동일한 원리로 다차원배열에도 접근할 수 있습니다.

int arr[5][5];
arr[3][4] = 0;
552 556 560 564 568
572 576 580 584 588
592 596 600 604 608
612 616 620 624 628
632 636 640 644 648

보기 쉽게 주소값을 나열해두었습니다. 1차원 배열과 마찬가지로 &arr에는 552라는 0번째 행, 0번째 열의 값이 저장되어 있습니다. 이 값에 접근하고자 하는 행의 인덱스 3과 열의 크기를 곱한후 더해줍니다. 이때 열의 크기는 4 x 5(정수형의 크기(바이트 단위))입니다. 이 값을 더해주면 612라는 값을 얻을 수 있는데 이 값은 3번째 행, 0번째 열의 값입니다. 해당 값에 접근하고자 하는 열의 인덱스 4와 정수형의 크기 4를 곱한 12를 더해주면 628를 얻을 수 있습니다.

2차원 배열에서의 인덱싱 방법을 순서대로 나열해보도록 하겠습니다.

  1. 배열의 주소를 받아온다.
  2. 접근하고자 하는 행의 인덱스에 열의 크기를 곱한후 더한다.
  3. 더한 값에 접근하고자 하는 열의 인덱스와 자료형의 크기를 곱한후 더한다.

0개의 댓글