reference: https://modoocode.com/231 (씹어먹는 C 언어)
포인터 기초
C언어에서 포인터는 다음과 같이 정의할 수 있다.
(포인터에 주소값이 저장되는 데이터의 형) *(포인터의 이름);
예를 들어 p
라는 포인터가 int
데이터를 가리키고 싶다고 하면,
int *p;
int* p;
라고 하면 올바르게 된다. 즉, 포인터 p
는 int
형 데이터의 주소값을 저장하는 변수가 된다!
포인터를 정의했으면 주소값을 넣아야 할 터, 넣기 위해 우선 &
연산자에 대해 알아보자.
AND 연산자 &
와 생긴 건 같지만, 그 친구는 두 항 사이에 쓰는 이항 연산자!
포인터에 쓰는 &
는 단항 연산자!
&변수
형태로 사용하면 그 변수의 주소를 알 수 있다.
그렇다면 반대로, 해당 주소값의 데이터를 가져오고 싶다면? *
p = &a;
*p = 3;
위와 같이 3을 담고 있는 변수 a의 주소를 가리키는 p가 있을 때, *p는 a와 동일한 의미❗
포인터도 엄연한 변수이기에 특정한 메모리 공간을 차지한다는 것도 잊지말자!
포인터에는 시작 주소만이 담겨 있다.포인터가 주소값만 보관하는데 왜 굳이 타입이 필요할까? 어차피 주소값은 32 비트 시스템에서 항상 4 바이트이고, 64 비트 시스템에서는 8 바이트 인데 그냥
pointer
라는 타입을 만들어버리면 안될까?
int
, char
과 같은 데이터형이 아닌 pointer
라는 타입으로 선언하면, 포인터는 메모리에서 얼마만큼을 읽어들여야 하는 지 알 길이 없다. 하지만 int *p
와 같이 선언한다면, 이 포인터는 int 데이터를 가리키니까 시작점에서 4바이트만 읽어들이면 되겠구나! 를 알 수 있다는 것.상수 포인터
const int* pa = &a;
*pa = 3; // 올바르지 않은 문장
pa = &b; // 올바른 문장
포인터 pa가 가리키는 값은 바뀌면 안된다!
int* const pa = &a;
*pa = 3; // 올바른 문장
pa = &b; // 올바르지 않은 문장
포인터 pa가 바뀌면 안된다!
const int* const pa = &a;
*pa = 3; // 올바르지 않은 문장
pa = &b; // 올바르지 않은 문장
둘다 const -> pa와 pa가 가리키는 값 둘 다 바뀌면 안된다!
포인터의 덧셈
/* 포인터의 덧셈 */
#include <stdio.h>
int main() {
int a;
int* pa;
pa = &a;
printf("pa 의 값 : %p \n", pa);
printf("(pa + 1) 의 값 : %p \n", pa + 1);
return 0;
}
실행 결과
pa 의 값 : 0x7ffd6a32fc4c
(pa + 1) 의 값 : 0x7ffd6a32fc50
분명 포인터에 + 1을 하라고 명령했으나, 두 수의 차이는 다름아닌 4! 이유가 뭘까?
바로 int
데이터형을 가리키는 포인터이기 때문이다. int
는 4바이트니까… 아니 근데 왜 4가 더해지거나 빠지는건데….
배열은 변수가 여러개 모인 것으로 생각할 수 있다 라고 공부했다. 여기에 또다른 놀라운 특성이 있는데, 그건 바로 배열들의 각 원소는 메모리 상에 연속되게 놓인 다는 점이다.
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
이라는 배열을 정의한다면 메모리 상에서 다음과 같이 나타납니다.
int arr[10]
의 한 원소는 int
형 변수이기 때문에, 4바이트를 차지한다.
#include <stdio.h>
int main() {
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int* parr;
parr = &arr[0];
printf("arr[3] = %d , *(parr + 3) = %d \n", arr[3], *(parr + 3));
return 0;
}
실행 결과
arr[3] = 4 , *(parr + 3) = 4
parr + 3
을 수행하면, arr[3]
의 주소값이 되고, 거기에 *
를 붙여주면 *
의 연산자의 역할이 '그 주소값에 해당하는 데이터를 의미해라!' 라는 뜻이므로 *(parr + 3)
은 arr[3]
과 동일하다!
#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 의 정체 : 0x7fff1e868b1c
arr[0] 의 주소값 : 0x7fff1e868b1c
arr
과 arr[0]
의 주소값이 동일하다. 그렇다면 배열에서 배열의 이름은 배열의 첫 번째 원소의 주소값을 나타내고 있다는 사실을 알 수 있다. 어? 그렇다면…
배열의 이름이 배열의 첫 번째 원소를 가리키는 포인터인가? ❌
C 언어 상에서 배열의 이름이 sizeof
연산자나 주소값 연산자(&
)와 사용될 때 (예를 들어 &arr
) 경우를 빼면, 배열의 이름을 사용시 암묵적으로 첫 번째 원소를 가리키는 포인터로 타입 변환되기 때문에, 배열의 시작 원소와 주소값이 동일하게 나온 것.
arr
이 sizeof
랑도, 주소값 연산자랑도 사용되지 않았기에, arr
은 첫 번째 원소를 가리키는 포인터로 타입 변환되었기에, &arr[0]
와 일치하게 된다.
a[3] : 4
*(a+3) : 4
[]
라는 연산자가 쓰이면 자동적으로 위 처럼 형태로 바꾸어서 처리하게 됩니다. 즉, 우리가 arr[3]
이라 사용한 것은 사실 *(arr + 3)
으로 바뀌어서 처리가 된다.
고급 개념들
#include <stdio.h>
int main() {
int a;
int *pa;
int **ppa;
pa = &a;
ppa = &pa;
a = 3;
printf("a : %d // *pa : %d // **ppa : %d \n", a, *pa, **ppa);
printf("&a : %p // pa : %p // *ppa : %p \n", &a, pa, *ppa);
printf("&pa : %p // ppa : %p \n", &pa, ppa);
return 0;
}
실행 결과
a : 3 // *pa : 3 // **ppa : 3
&a : 0x7ffd26a79dd4 // pa : 0x7ffd26a79dd4 // *ppa : 0x7ffd26a79dd4
&pa : 0x7ffd26a79dd8 // ppa : 0x7ffd26a79dd8
2차원 배열의 [] 연산자
/* 정말로? */
#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;
}
실행 결과
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]
을 가리키는 포인터로 암묵적으로 타입 변환되고, arr[1]
은 arr[1][0]
을 가리키는 포인터로 타입 변환된다라는 뜻이다.
int arr[]
에서 arr
과 &arr[0]
는 그 자체로는 완전히 다른 것이였던 것처럼 2 차원 배열 int arr[][]
에서 arr[0]
과 &arr[0][0]
와 다릅니다. 다만 암묵적으로 타입 변환 시에 같은 것으로 변할 뿐입니다. 따라서 sizeof
를 사용하였을 경우 2 차원 배열의 열의 개수를 계산할 수 있습니다.int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
printf("전체 크기 : %d \n", sizeof(arr));
printf("총 열의 개수 : %d \n", sizeof(arr[0]) / sizeof(arr[0][0]));
printf("총 행의 개수 : %d \n", sizeof(arr) / sizeof(arr[0]));
}
실행 결과
전체 크기 : 24
총 열의 개수 : 3
총 행의 개수 : 2
sizeof
연산자의 경우 포인터로 타입 변환을 시키지 않기 때문에 sizeof(arr[0])
는 마치 sizeof
에 1 차원 배열을 전달한 것과 같습니다. 그리고 그 크기 (3) 을 알려주겠지요. 그리고 sizeof(arr[0][0])
을 하게 된다면 int
의 크기인 4 를 리턴하게 되어서 총 열의 개수를 알 수 있게 됩니다./* (배열의 형) */ (*/* (포인터 이름) */)[/* 2 차원 배열의 열 개수 */];
// 예를 들어서
int (*parr)[3];
크기가 3인 배열을 기리키는 포인터와 동일한데, 어떻게 2차원 배열을 가리킬 수 있는걸까? 이게 말이 되는게, 1 차원 배열에서 배열의 이름이 첫 번째 원소를 가리키는 포인터로 타입 변환이 된 것 처럼, 2 차원 배열에서 배열의 이름이 첫 번째 행 을 가리키는 포인터로 타입 변환이 되어야 합니다. 그리고 그 첫 번째 행은 사실 크기가 3 인 1 차원 배열이지요!포인터 배열은 말 그대로 포인터들의 배열
포인터 배열은 정말로 배열이고, 배열 포인터는 정말로 포인터
/* 포인터배열*/
#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;
}
실행 결과
a : 1, *arr[0] : 1
b : 2, *arr[1] : 2
b : 3, *arr[2] : 3
&a : 0x7ffe8a2fa4e4, arr[0] : 0x7ffe8a2fa4e4
배열의 형을 int, char
등등으로 하듯이, 배열의 형을 역시 int*
으로도 할 수 있다.
다시말해, 배열의 각각의 원소는 int
를 가리키는 포인터 형으로 선언된 것!