- 배열의 이름과 &연산자
- 이중 포인터
- 1차원 배열과 포인터
- 2차원 배열과 포인터
- Q&A
- 마치며
([모두의 코드] 작성자분께서 이번 강좌가 제일 어려운 내용일 수 있다고 하시네요....나 자신 화이팅 😂)
지난 시간에 배운 내용을 떠올려보겠습니다.
포인터
: 메모리 상에 특정 데이터가 저장된 주소값을 가리키는 변수
&연산자
: 메모리 상에 특정 데이터가 저장된 주소값을 가져옴
*연산자
: 메모리 상에 특정 주소값에 저장된 데이터를 가져옴
배열은 배열, 포인터는 포인터이다.(같은 것이 아니다)
단, sizeof
와 &연산자와 함께 쓰일 때를 제외하면, 배열의 이름은 첫 번째 원소를 가리킨다.
arr[i]
와 같은 표현은 컴파일러에 의해 *(arr + i)
로 변환된다.
앞서 계속해서 배열의 이름은,
sizeof함수와 &연산자를 함께 사용하는 경우를 제외하고는,
암묵적으로 포인터로 변환이 된다고 배웠습니다.
아래 예시를 참고하세요.
#include <stdio.h>
int main() {
int arr[3] = { 1, 2, 3 };
printf("%d\n", sizeof(*arr));
printf("%d", sizeof(arr));
}
[Result]
4
12
sizeof함수를 이용하면 배열 전체의 크기를 반환하기 때문인거는 지난 시간에 배웠는데,
&연산자는 왜 그렇죠?
결론부터 말씀드리면, 배열의 이름과 &연산작 함께 쓰이면,
배열의 첫번째 원소의 주소를 가리키는 포인터가 아닌,
전체 배열을 가리키는 포인터로 변환됩니다.
다음 예제를 봅시다.
#include <stdio.h>
int main() {
int arr[3] = { 1, 2, 3 };
for (int i = 0; i < 3; i++) {
printf("arr[%d] : %p\n", i, &arr[i]);
}
printf("\n");
printf("arr : %p\n", arr);
printf("&arr : %p\n\n", &arr);
printf("arr + 1 : %p\n", arr + 1);
printf("&arr + 1 : %p", &arr + 1);
}
[Result]
arr[0] : 0000002739D0F7A8
arr[1] : 0000002739D0F7AC
arr[2] : 0000002739D0F7B0
arr : 0000002739D0F7A8
&arr : 0000002739D0F7A8
arr + 1 : 0000002739D0F7AC
&arr + 1 : 0000002739D0F7B4
위의 예시에서,
우선 int형 배열 arr
의 각 원소의 주소값을 출력했습니다.
이후에 arr
과 &arr
의 주소값을 각각 출력했습니다.
그런데 arr[0]
과 주소값이 동일합니다.
그러면 &연산자를 사용하든 말든 똑같은 거 아닌가요??
아닙니다!!
사실 어떻게 변수와 변수의 &가 어떻게 같겠어요...
사실 두 놈은 주소는 같지만 다른 Type의 주소입니다.
그래서 위의 코드에서 arr + 1
과 &arr + 1
의 주소값을 출력했습니다.
오잉 값이 다르네요.
만약 두 개가 동일하다면 똑같은 값이 나와야 정상이죠?
그래서 결과를 보니
arr + 1
은 arr[0]
과 주소값이 4만큼 차이나고,
&arr + 1
은 arr[0]
과 주소값이 12만큼 차이납니다.
즉, arr + 1
은 int형 배열 arr
내에서 연산이 된 것이고,
&arr + 1
은 int형 배열 arr
만큼 연산이 이루어진 것이죠.
결론적을 말씀드리면,
arr
은 배열의 첫번째 원소를 가리키는 포인터고,
&arr
은 크기가 3인 배열 전체를 가리키는 포인터입니다.
2차원 배열에서도 마찬가지입니다.
arr[5][4]
라는 배열에서,
배열의 이름 arr
은 크기가 4인 첫번째 배열 가리키는 포인터로 변환됩니다.
그러나 &arr
은 5 * 4 배열 전체를 가리킵니다.
- 배열 이름과 sizeof함수 / &연산자
- 배열 이름 : 첫번째 원소를 가리키는 포인터로 암묵적으로 변환
- sizeof : 전체 배열의 크기를 반환
- &연산자: 전체 배열을 가리키는 포인터로 반환
말 그대로, 이중으로 참조하는 포인터입니다.
아래의 예시는,
int를 가리키는 포인터를 가리키는 포인터입니다.
int **p
끔찍한 혼종처럼 생겼죠!
#include <stdio.h>
int main() {
int a = 1;
int* pa;
int** ppa;
pa = &a;
ppa = &pa;
printf("a : %d\n", a);
printf("*pa : %d\n", *pa);
printf("**ppa : %d", **ppa);
return 0;
}
[Result]
a : 1
*pa : 1
**ppa : 1
위의 예시를 보면
우선, 포인터 pa
는 변수 a
의 주소값을 참조하고 있습니다.
즉, *pa
는 a
와 동일하죠.
그리고, 이중포인터 ppa
는 포인터 pa
의 주소값을 참조하고 있습니다.
즉, *ppa
는 pa
와 동일하죠.
따라서 **ppa
는 a
와 동일합니다.
#include <stdio.h>
int main() {
int arr[3] = { 1,2,3 };
int* p;
/* p = &arr[0]와 동일 */
p = arr;
printf("arr[1] : %d \n", arr[1]);
printf("p[1] : %d \n", p[1]);
return 0;
}
[Result]
arr[1] : 2
p[1] : 2
위의 예제를 살펴보면,
arr[1]
은 컴파일러에 의해 *(arr + 1)
이 되어 2
를 출력합니다.
p[1]
도 마찬가지로 *(p + 1)
이 되어 2
를 출력합니다.
포인터에 1을 더하게 되면,
p
에 저장된 주소값에 1이 더해지는 것이 아니라,
1 * (포인터가 가리키는 Type의 크기)
가 더해집니다.
위의 경우에는 int형을 가리키므로, 4가 더해지겠죠.
#include <stdio.h>
int main() {
int arr[10] = {100, 98, 97, 95, 89, 76, 92, 96, 100, 99};
int* parr = arr;
int sum = 0;
while (parr - arr <= 9) { //..??
sum += (*parr);
parr++;
}
printf("내 시험 점수 평균 : %d \n", sum / 10);
return 0;
}
원본글에 있던 예제 중 하나입니다.
전 이 while (parr - arr <=9)
부분이 헷갈렸는데요,
생각해보니 쉬웠습니다.
parr
은 while문에서 증감연산자를 통해 계속해서 증가합니다.
(parr + 0) ~ (parr + 9)까지죠.
arr
은 sizeof함수나 &연산자와 쓰이지 않았으므로, 암묵적으로 배열의 첫번째 원소를 가리킵니다.
따라서 parr - arr
은
= (parr + n) - (arr)
= (arr + n) - (arr)
로써 사용할 수 있습니다.
while문을 10번 수행하게 되면,
parr
은 마지막 원소은 arr[9]
를 가리키므로,
parr - arr
은 9
가 됩니다.
(이해하는데 오래 걸렸습니다 🤣)
2차원 배열은 1차원 배열이 여러 개가 있다고 생각하면 됩니다.
하지만, 실제 컴퓨터 메모리 구조는 1차원이기 때문에 항상 선형으로 존재합니다.
#include <stdio.h>
int main() {
int arr[2][3];
printf("arr[0] : %p \n", arr[0]);
printf("&arr[0][0] : %p \n", &arr[0][0]);
printf("arr[1] : %p \n", arr[1]);
printf("&arr[1][0] : %p \n", &arr[1][0]);
return 0;
}
[Result]
arr[0] : 0x7ffda354e530
&arr[0][0] : 0x7ffda354e530
arr[1] : 0x7ffda354e53c
&arr[1][0] : 0x7ffda354e53c
위의 예제에서는,
arr[0]
과 arr[0][0]
의 주소값은 동일하고,
arr[1]
과 arr[1][0]
의 주소값이 동일합니다.
즉, 1차원 배열과 마찬가지로
sizeof나 &연산자와 사용하지 않을 경우,
arr[0]
은 arr[0][0]
을 가리키는 포인터로 암묵적으로 Type 변환이 됩니다.
그렇다면 2차원 배열의 이름을 포인터에 전달하기 위해서는 포인터의 타입을 어떻게 정의해아할까요?
arr[0][0]
은 int형,
arr[0]
은 int *형,
그러면 int **형이 되어야 하지 않을까요?
그러나 아닙니다!
바로 배열을 가리키는 포인터, '배열 포인터'로 접근해야 합니다.
그 이유에 대해 살펴봅시다.
다음을 실행해 보면 컴파일 오류가 등장합니다.
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int **parr;
parr = arr;
printf("arr[1][1] : %d \n", arr[1][1]);
printf("parr[1][1] : %d \n", parr[1][1]);
return 0;
}
그 이유는 parr[1][1]
에서, 이상한 메모리 공간의 값에 접근했기 때문입니다.
왜 그럴까요?
parr[1][1]
은 *(*(parr + 1) +1
과 동일한 문장입니다.
우선 내부의 parr + 1
을 봅시다.
parr
은 int*형을 가리키는 포인터이므로,parr + 1
을 하면 8바이트가 증가합니다.
(포인터의 크기는 주소값을 저장하므로, 64비트에서는 8바이트입니다.)
따라서, arr
배열의 세 번째 원소인 3
을 가리킵니다.
그 다음에, *(parr + 1) + 1
을 하면,
int형이므로 4바이트만큼 증가해서, 7
이 됩니다.
즉, index 범위를 벗어나게 됩니다.
그렇다면 어떻게 해야하나요?
우선, int arr[10]
이라는 배열에서 x
번째 원소의 주소값을 알아봅시다.
시작주소를 arr
이라고 한다면 arr[x]
의 주소값은
가 됩니다.
그렇다면 2차원 배열 int arr[a][b]
는요?
int arr[a][b]
는 int arr[b]
짜리 배열이 a
개 존재하는 형태입니다.
따라서 arr[x][0]
의 주소값은 x
번째의 int arr[b]
짜리 배열이 됩니다.
그렇다면 해당 주소는 입니다.
그렇다면 arr[x][y]
의 주소값은
가 됩니다.
그렇다면 해당 주소값을 정확히 알기 위해서는
x, y
뿐만 아니라 b
도 알아야 합니다.
즉, 2차원 배열을 가리키는 포인터를 통해서, 원소들에 정확히 접근하기 위해서는
b
의 값가 포인터 타입에 명시되어야만 접근할 수 있습니다.
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int(*parr)[3]; // 괄호를 꼭 붙이세요
parr = arr; // parr 이 arr 을 가리키게 한다.
printf("parr[1][2] : %d , arr[1][2] : %d \n", parr[1][2], arr[1][2]);
return 0;
}
[Result]
parr[1][2] : 6 , arr[1][2] : 6
위의 예시에서는 int(*parr)[3];
로 2차원 배열의 원소에 접근 가능합니다.
우선 1번 조건(가리키는 원소의 크기)은 int
로 만족합니다.
그리고 2번 조건은 3
으로 알 수 있습니다.
즉, parr
은 크기가 3인 배열 전체를 가리키는 포인터입니다.
- 배열 포인터의 구성
(배열의 형) (*포인터 이름)[2차원 배열의 열 개수];
포인터 배열은 말 그대로 포인터들의 배열입니다.
앞서 설명한
<배열 포인터>는 배열을 가리키는 포인터이면,
<포인터 배열>은 포인터를 원소로 가지는 배열입니다.
배열은 배열이고, 포인터는 포인터이므로,
두 놈은 사실 별 관계가 없습니다.
#include <stdio.h>
int main() {
int *arr[3];
int a = 1, b = 2, c = 3;
arr[0] = &a;
arr[1] = &b;
arr[2] = &c;
printf("a : %d, *arr[0] : %d \n", a, *arr[0]);
printf("b : %d, *arr[1] : %d \n", b, *arr[1]);
printf("b : %d, *arr[2] : %d \n", c, *arr[2]);
printf("&a : %p, arr[0] : %p \n", &a, arr[0]);
return 0;
}
[Reuslt]
a : 1, *arr[0] : 1
b : 2, *arr[1] : 2
b : 3, *arr[2] : 3
&a : 0x7ffe8a2fa4e4, arr[0] : 0x7ffe8a2fa4e4
위의 예시에서 arr
의 각 원소들은 포인터로써,
각 원소들은 a, b, c
를 각각 가리키고 있습니다.
-
...정말 어려웠습니다😢
사실 지난 시간에 배운 내용은 그나마 쉽게 이해할 수 있었는데,
이번 파트는 이해하는 것 자체도 정말 어려웠네요.
혼동이 많이 됩니다.
공부하면서 하나하나 음미하며 이해하는 건 정말 오랜만이네요.
특히나, 배열과 포인터는 정말......
배열 포인터는 정말 이해하기가 쉽지 않았습니다.
하지만 원본 작성자분께서도 무수히 많은 자료를 접하고 공부하셔서 나아지셨다고 하니,
저도 그런 과정이 필요하겠죠?
노력하겠습니다...😊
[Reference] : 위 글은 다음 내용을 참고, 인용하여 만들어졌습니다.
- 전반적 내용
: '모두의 코드' 중 '씹어먹는 C 언어' 중 '<12 - 3. 포인터는 영희이다! (포인터)>`- 배열 이름과 &연산자 (&연산자가 이해되지 않는 분들께 추천)
: https://www.geeksforgeeks.org/whats-difference-between-array-and-array-for-int-array5/