배열 요소에 접근하는 방법
1. 대괄호( [ ] )
기본적인 배열의 요소의 접근은 배열의 이름 오른쪽에 대괄호를 치면 됩니다.
int a[3] = {1, 2, 3};
printf("%d\n", a[0]);
하지만 이런 방법 외에도 포인터를 통해 배열 요소에 접근할 수 있습니다.
2. 포인터 변수의 역참조 연산자( * )
기본적으로 배열의 이름은 그 자체로 해당 배열의 시작 주소와 같습니다.
다만 이 시작 주소로부터 특정 배열에 접근할 때는 고려할 사항이 있습니다.
우선 배열은 다차원 배열이 될 수 있고, 차원 수에 따라 역참조 연산자( * )를 몇번 붙여야 하는지 알 수 있습니다.
예를 들어 다음과 같이 1차원 배열이 있다고 해보겠습니다.
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 1차원이므로 1차원 포인터 변수 사용!
위 배열의 이름인 arr는 배열의 시작 주소를 가지고 있고,
1차원 배열이므로 이 주소를 담을 수 있는 포인터 변수도 1차원이어야 합니다.
따라서 n차원이면 n차원 포인터 변수를 사용해 배열의 주소를 담을 수 있습니다.
이제 원래 주제였던 배열 요소 접근으로 가보겠습니다.
특정 n차원 배열의 요소에 접근하기 위해서는 역참조 연산자를 n번 붙여 요소에 접근 할 수 있습니다.
따라서 1차원 배열에 대한 요소의 접근은 다음과 같이 이루어집니다.
int firstElement = *(a);
int secondElement = *(a + 1);
int thirdElement = *(a + 2);
...
배열의 이름에 특정 값을 직접 더하면, n차원 포인터에 대한 주소의 증감과 같아집니다.
이제 더 복잡한 2차원으로 넘어가겠습니다.
int b[2][2] = {{1, 2}, {3, 4}};
위 배열은 2차원입니다.
따라서 배열의 주소를 담을 수 있는 포인터 변수도 2차원이어야 합니다.
1차원 배열의 예시처럼 접근하면 어떻게 될까요?
printf("%d\n", *(b + 1)); // ???
예상한 값은 b[0][1]에 있는 값이었지만, 출력은 이상한 값이 나왔을 것입니다.
2차원 배열인데도 불구하고 역참조( * )를 한번밖에 하지 않았기 때문에 주소값이 나왔기 때문입니다.
그럼 위 예시에서 역참조를 한번 더 해서 나온 값은 어디에 있는 값일까요?
printf("%d\n", **(b + 1));
출력해보면 b[1][0]에 있는 값이라는 것을 알 수 있습니다.
다른 예시로 **(b), **(b+1)을 출력하면, 모두 b[0][0], b[1][0]과 같이
세로 방향으로만 접근하고 있다는 것을 알 수 있습니다.
그럼 가로 방향은 어떻게 접근해야 할까요?
우선 아까 실패했던, *(b+1)을 살펴보겠습니다.
이 연산으로부터 나오는 값은 b[1][0]의 주소값과 같다는 것을 알 수 있습니다.
printf("%d\n", *(b + 1) == &b[1][0]); // 1
잠깐 생각해보면 1차원 배열일 때는, (b + 1)과 &b[1]이 같았다는 것을 떠올릴 수 있습니다.
b도 같을까요?
printf("%d\n", *(b + 1) == &b[1]); // 1
b도 마찬가지로 접근하고 있음을 알 수 있습니다.
뭔가 한가지 패턴이 보입니다.
대괄호로 접근하는 것은 포인터로 접근하는 방법으로 딱 한가지 패턴으로 바뀌게 됩니다.
a[x] $\rightarrow$ *(a + x)
패턴을 정리하면 다음과 같습니다.
1. a[x]
대괄호의 왼쪽에 있는 변수와 대괄호 안에있는 값을 괄호로 먼저 묶고, $\rightarrow$ (a + x)
마지막으로 역참조 연산자를 맨 왼쪽에 붙이면서 마무리 합니다. $\rightarrow$ *(a + x)
2. *(...)[x]
괄호는 우선순위가 제일 높으므로 괄호를 대상으로 먼저 묶어야합니다. $\rightarrow$ ((...) + x)
마지막으로 역참조 연산자를 맨 왼쪽에 붙이면서 마무리 합니다. $\rightarrow$ *((...) + x)
만약 대괄호가 더 남아있다면, 패턴으로 생성된 결과를 괄호로 묶어주어야 합니다.
괄호를 치지 않으면, 연산자의 우선순위때문에 의도한 대로 동작하지 않기 때문입니다.
따라서
1번패턴의 경우 (*(a + x))[y][z]...
2번 패턴의 경우 (*((...) + x))[y][z]...
와 같이 바뀌게 됩니다.
패턴 적용하기
이 패턴을 b[0][1]에 적용해보겠습니다.
먼저 맨 왼쪽에 있는 대괄호부터 시작합니다.
1. b[0][1] $\rightarrow$ (*(b + 0))[1]
$\rightarrow$ 포인터 연산으로 바꿔준뒤,
2. (*(b + 0))[1] $\rightarrow$ *(*(b + 0) + 1)
$\rightarrow$ 1번에서 사용했던 괄호는 더이상 필요없습니다. 의미 없는 괄호이기 때문입니다.
결과로 나온 식을 해석해보겠습니다.
1. b에 0을 더한 값을 참조했기 때문에 이는 0번째 행을 의미합니다.
2. 0번째 행에서 1을 더하고 이를 참조하게 되면 0번째 행에서 1번째 열을 의미하게 됩니다.
3. 대괄호와 포인터 연산을 같이 사용
이제 같이 한번 사용해보겠습니다.
다음 두 연산은 같은 값을 출력할까요?
int a[2][2] = {{1, 2}, {3, 4}};
printf("%d\n", *a[1]); // ???
printf("%d\n", (*a)[1]); // ???
첫번째 출력은 a[1][0]이고, 두번째 출력은 a[0][1]을 출력합니다.
첫번째 식을 풀어보면 **(a + 1) 이고, 두번째 식은 *((*a) + 1) 이기 때문입니다.
두번째 식의 괄호 안쪽을 대괄호 방식으로 만들면 a[0][1]와 같아진다고 해석할 수 도 있습니다.
'언어 > C' 카테고리의 다른 글
[C언어] 주소에 대한 swap 구현 (0) | 2022.06.13 |
---|---|
[C언어] 함수 포인터 (0) | 2022.06.12 |
[C언어] Dangling Pointer와 Memory Leak (0) | 2022.06.12 |