C언어 (10) 포인터와 배열
포인터에 대해 간단히 보았는데, 활용에 대해서는 아직 잘 모르겠다.
포인터의 특성을 좀 더 보고, 배열과 연관지어 보며 어떻게 사용하는지 알아보자.
다중 포인터
1
int** ppnum;
이런 포인터는 존재할 수 있을까?
포인터 변수도 주소를 가지고, 8바이트 또는 4바이트의 크기를 가진 변수이다.
그렇다면 포인터 변수의 주소를 저장하는 포인터 변수도 만들 수 있지 않을까?
1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
int main() {
int num = 10;
int* p1 = #
int** p2 = &p1;
int*** p3 = &p2;
printf("%p %p %p\n", p1, p2, p3);
printf("%d %d %d %d", num, *p1, **p2, ***p3);
return 0;
}
코드로 보면 p1은 num의 주소를 가진다. p2는 p1의 주소를 가진다. p3는 p2의 주소를 가진다.
결과를 보면 *p2는 num의 주소를 가지고, **p2는 num의 값을 가진다.
즉 ***p3 == **p2 == *p1 = num이다.
이런 짤도 있는데 p2에서 int형 변수 num에 접근하려면 2번 참조해야 할 것이다.
그러면 참조 연산자인 *을 2번 써주면 된다.
1
2
int* p1 = #
int* pp1 = &p1;
물론 이렇게 그냥 포인터 변수에도 다른 포인터 변수의 주소를 저장할 수 있다.
당연한 이야기지만 이러면 pp1을 통해서 num의 값을 간접 참조하는 것을 불가능하다.
*pp1으로 참조하는 값은 &num이기 때문이다.
포인터의 연산
포인터 변수는 주소값이 들어있는 변수이다. 여기에 더하기와 빼기도 가능할까?
가능하지만 일반적인 변수에 대한 연산과는 조금 다르게 증가하고 감소한다.
char형 주소에 +1을 하면 1 증가, int형 주소에 +1하면 4 증가, double형 주소에 +1하면 8이 증가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main() {
char c;
int a;
double d;
char* pc = &c;
int* pa = &a;
double* pd = &d;
printf("%p %p %p\n", pc, pc+1, pc-1);
printf("%p %p %p\n", pa, pa+1, pa-1);
printf("%p %p %p\n", pd, pd+1, pd-1);
return 0;
}
1
2
3
00000007202FFDF0 00000007202FFDF1 00000007202FFDEF
00000007202FFDF4 00000007202FFDF8 00000007202FFDF0
00000007202FFDF8 00000007202FFE00 00000007202FFDF0
이렇게 포인터가 가리키는 변수의 크기에 따라 증가하고 감소한다.
이중포인터로 예제를 하나 더 봐보자.
1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main() {
int a=10;
int* pa = &a;
int** ppa = &pa;
printf("%p %p %p\n", pa, pa + 1, pa + 2);
printf("%p %p %p\n", ppa, ppa + 1, ppa + 2);
return 0;
}
1
2
0000006A688FFBE8 0000006A688FFBEC 0000006A688FFBF0
0000006A688FFBE0 0000006A688FFBE8 0000006A688FFBF0
pa가 가리키는 변수는 int이므로 4씩 증가한다.
ppa가 가리키는 변수는 int*이므로 8씩 증가한다.
그러면 포인터 변수에 곱셈이나 나눗셈이 가능할까? 포인터끼리 더하거나 곱하는 것도 가능할까?
1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main() {
int a=10;
int b;
int* pa = &a;
int* pb = &b;
printf("%p %p %p %p %p\n", pa, pa * 2, pa / 2, pa + pb, pa*pb, pa-pb);
return 0;
}
위의 코드를 실행하고 하려면 곱하기, 나누기가 안된다는 것을 알 수 있다.
왜 이것을 지원하지 않을까? 생각하면 주소의 값*2를 한다는 것에 무슨 의미가 있을까?
주소라는 것은 컴퓨터가 임의로 할당하는 것이므로 주소의 값*2 라는 연산은 아무런 의미가 없다.
만약 주소의 값*2한 곳에 무슨 값이 있다고 하더라도 그 값과 원래 주소의 값과는 아무런 연관이 없다.
같은 이유로 나누기도 안되고 포인터간의 더하기, 곱하기도 불가능하다.
포인터 값 2개를 더해서 나온 결과는 값 2개와는 아무런 연관이 없기 때문이다.
그런데 예외적으로 포인터간의 빼기는 지원하고 결과도 잘 나온다.
그 이유를 바로 밑에서 알아보자.
포인터와 배열
위의 내용들이 포인터와 배열과의 연관을 설명하기 위해서 한 이야기들이다.
이전에 했던 배열 설명을 보면 이렇게 말을 했다.
1
배열이란 같은 자료형을 가진 연속된 메모리 공간으로 이루어진 자료구조이다
배열의 원소의 주소는 각각의 자료형의 크기만큼 차이가 난다.
char형 배열에서는 1, int형 배열에서는 4. 이렇게 원소 주소마다 차이가 있다.
그런데 포인터의 덧셈과 뺄셈에서도 딱 이렇게 값이 변화한다.
int형 포인터에 +1을 하면 4가 증가하고, char형 포인터에 +1을 하면 1이 증가한다.
1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main() {
int arr[3] = { 10, 20, 30 };
int* pa = &arr[0];
printf("%p %p %p\n", &arr[0], &arr[1], &arr[2]);
printf("%p %p %p\n", pa, pa+1, pa+2);
printf("\n");
printf("%d %d %d\n", arr[0], arr[1], arr[2]);
printf("%d %d %d\n", *pa, *(pa + 1), *(pa + 2));
return 0;
}
1
2
3
4
5
0000009478B6FDB0 0000009478B6FDB4 0000009478B6FDB8
0000009478B6FDB0 0000009478B6FDB4 0000009478B6FDB8
10 20 30
10 20 30
결과는 arr이 int형 배열이기 때문에 4씩 차이난다.
여기서 중요하게 볼 것은 포인터 변수와 연산을 통해 배열의 원소에 접근할 수 있다는 것이다.
배열의 시작 주소
배열의 이름은 배열의 시작 주소이다.
1
2
3
4
5
6
7
#include<stdio.h>
int main() {
int arr[3] = { 1, 1, 1 };
int* pa = arr;
printf("%p %p %p %p %p\n", &arr, &arr[0], arr, arr+0, pa);
return 0;
}
이 코드를 실행해보면 5개의 값은 전부 같다. &arr과 arr도 같고, arr과 &arr[0]도 같다.
arr이라는 배열의 이름은 배열의 시작 주소인 &arr[0]과 같은 값을 가진다는 것이다.
그러면 arr+1은 어떻게 될까? &arr+1과 같은 의미이고 이는 &arr[0]에 4가 증가한 값이다.
1
2
3
4
5
6
7
#include<stdio.h>
int main() {
int arr[3] = { 1, 1, 1 };
int* pa = arr;
printf("%p %p\n", arr+1, &arr[1]);
return 0;
}
코드를 실행해 확인해보면 arr+1 == &arr[1]이다.
그러면 arr+i는 arr[i]의 주소를 가지는 것이고, *(arr+i)로 arr[i]의 값을 가져올 수 있다.
arr + 0의 의미
1
2
3
4
5
6
#include<stdio.h>
int main() {
int arr[3] = { 1, 1, 1 };
printf("%d %d %d\n", sizeof(arr), sizeof(arr+0), sizeof(&arr[0]));
return 0;
}
1
12 8 8
코드를 실행해보면 각각 12, 8, 8이 나온다.
arr은 int형 변수가 3개가 모여있는 배열이기에 4*3 = 12의 크기를 가진다.
하지만 arr+0은 arr 배열의 0번째 요소의 주소를 의미한다.
그렇기에 포인터 변수와 같은 8의 크기를 가지게 되는 것이다.
arr과 arr+0을 %p를 통해 출력하면 같은 값을 가지지만 sizeof로 보면 크기가 다르다.
arr은 배열 전체이지만, arr+0은 arr[0]의 주소를 가리키는 포인터 연산이다.
그렇기에 배열의 이름인 arr과 arr+0은 엄밀히 구분하면 다르다.
하지만 컴파일러에서 배열 이름을 sizeof나 &같은 특정 연산자와 함께 사용하는 것이 아니라면
일반적으로 배열의 이름은 배열의 시작 원소를 가리키는 포인터로 변환해서 사용한다.
그렇기에 %p를 통해 출력하면 같은 결과가 나오는 것이다.
포인터 간의 뺄셈을 지원하는 이유
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
int* ptr1 = &arr[1]; // arr[1]의 주소
int* ptr2 = &arr[4]; // arr[4]의 주소
printf("%p %p\n", ptr2, ptr1);
int diff = ptr2 - ptr1;
printf("포인터 간의 차이: %d\n", diff); // 3 출력 (arr[4]와 arr[1] 사이의 원소 차이)
return 0;
}
배열의 요소간의 주소 차이는 (배열의 자료형)*(사이의 요소 개수)이다.
1
2
000000891317F8A0 000000891317F894
포인터 간의 차이: 3
실제 주소의 차이는 12지만, 포인터간의 연산은 포인터가 가리키는 객체에 관련이 있다.
그러므로 12/sizeof(int) = 3이므로 diff에는 3이 저장이 된다.
배열에서 포인터 간 뺄셈을 사용하면 사이의 원소의 개수를 알 수 있다.
헷갈리지만 꼭 알아두자.
내용이 길어져서 글을 2개로 나눴다.
중요한 것은 배열의 이름은 배열의 시작 주소와 같은 값을 가지고 있다!