포인터의 주소
포인터 자신의 주소를 출력해 봅니다.
포인터도 변수이므로 자신의 메모리 공간과 주소가 따로 있습니다.
p는 「저기」라고 적어 둔 쪽지와 같은데,
그 쪽지도 어딘가에 놓여 있어야 합니다.
#include <stdio.h>
int main(void) {
int num = 60;
int *p = #
printf("num의 값 %d\n", num);
printf("p에 저장된 값 %p\n", (void *) p); // num의 주소
printf("p 자신의 주소 %p\n", (void *) &p); // 다른 주소다
return 0;
}주소가 두 개 출력됩니다
p에는 num의 주소가 저장되어 있고,
&p는 p 자신의 주소입니다.
둘은 서로 다른 주소입니다.
변수 앞에 주소 연산자 &를 붙이면 그 변수의 주소를 얻습니다.
포인터도 변수이니 예외가 아닙니다.
특별한 규칙이 새로 생긴 것이 아니라 이미 있는 규칙을 한 번 더 적용한 것뿐입니다.
그렇다면 &p로 얻은 주소도 어딘가에 저장할 수 있어야 합니다.
주소를 저장하는 변수가 포인터이니, 포인터의 주소를 저장하는 포인터도 있어야 합니다.
스스로 확인
p가 num을 가리키게 한 뒤 p = &other;로 바꾸면, &p로 얻는 주소도 함께 달라질까요?
별표를 하나 더
역참조를 한 번 더 하면 됩니다.
int *p가 「int를 가리키는 포인터」였으니,
int **q는 「int를 가리키는 포인터를 가리키는 포인터」입니다.
변수 이름 q에서 왼쪽으로 읽어 나가면 그대로 뜻이 됩니다.
이런 포인터를 이중 포인터(double pointer)라고 부릅니다.
#include <stdio.h>
int main(void) {
int num = 60;
int *p = #
int **q = &p; // p의 주소를 저장한다
printf("num = %d\n", num); // 60
printf("*p = %d\n", *p); // 60 한 번 역참조한다
printf("**q = %d\n", **q); // 60 두 번 역참조한다
printf("p = %p\n", (void *) p);
printf("*q = %p\n", (void *) *q); // p에 저장된 것과 같은 주소
**q = 99; // 두 번 역참조해 num을 바꾼다
printf("num = %d\n", num); // 99
return 0;
}| 식 | 값 | 읽는 방법 |
|---|---|---|
q |
p의 주소 |
역참조하지 않으면 q에 저장된 주소 그대로 |
*q |
num의 주소 |
한 번 역참조하면 p에 저장된 값이 나온다 |
**q |
60 |
두 번 역참조하면 num의 값이 나온다 |
별표 하나가 한 걸음입니다
역참조할 횟수만큼 별표를 붙인다고 보면 헷갈리지 않습니다.
num까지는 두 번 역참조해야 하니 **q이고,
p까지는 한 번이면 되니 *q입니다.
값을 읽을 뿐 아니라 바꿀 수도 있습니다.
**q = 99;는 두 번 역참조해 num의 값을 바꿉니다.
*q = &other;라고 쓰면 한 번만 역참조해 p에 저장된 주소를 바꿉니다 —
별표의 개수가 접근하는 대상을 정합니다.
별표 세 개, 네 개도 문법에 맞습니다
int ***r도 선언할 수 있습니다. 다만 실제로 쓰는 일은 드뭅니다.
단계가 늘수록 코드를 읽는 사람이 따라가기가 급격히 어려워지고,
그만큼 여러 단계가 필요하다면 구조를 다르게 설계하는 편이 나은 경우가 많기 때문입니다.
스스로 확인
*q = NULL;을 실행하면 무엇이 NULL이 될까요? num은 어떻게 될까요?
포인터를 바꾸는 함수
이중 포인터가 실제로 필요해지는 경우입니다.
메모리를 할당하는 일을 함수에 맡기고 싶다고 해 봅시다.
함수 안에서 malloc을 호출하고, 그 반환값(주소)을 호출한 쪽의 포인터가 받아야 합니다.
그런데 void make(int *out)이라고 선언하고 함수 안에서 out = malloc(…)을 실행하면
바뀌는 것은 사본뿐입니다 — C의 인자 전달은 값에 의한 전달이라, out은 호출한 쪽 포인터의 값을 복사해 받은 매개변수이기 때문입니다.
규칙은 언제나 같습니다
「함수 안에서 어떤 변수를 바꾸고 싶으면 그 변수의 주소를 전달한다」입니다.
int 변수를 바꾸고 싶으면 int *를 전달합니다.
이번에 바꾸고 싶은 것은 int * 포인터이니 그 주소, 곧 int **를 전달합니다.
#include <stdio.h>
#include <stdlib.h>
void make(int **out, int n) { // 결과를 저장할 포인터의 주소를 전달받는다
*out = (int *) malloc(n * sizeof(int));
if (*out == NULL) {
return;
}
for (int i = 0; i < n; i = i + 1) {
(*out)[i] = (i + 1) * 10; // 괄호를 빼면 뜻이 달라진다
}
}
int main(void) {
int *data = NULL;
make(&data, 5); // data의 주소를 전달한다
if (data == NULL) {
printf("메모리를 할당하지 못했습니다.\n");
return 1;
}
for (int i = 0; i < 5; i = i + 1) {
printf("%d ", data[i]); // 10 20 30 40 50
}
printf("\n");
free(data);
return 0;
}&data를 전달하고 *out에 대입합니다
make(&data, 5)는 data의 주소를 전달합니다.
함수 안의 *out = malloc(…)은 그 주소에 있는 변수, 곧 호출한 쪽의 data에 할당한 메모리의 주소를 저장하므로
호출한 쪽의 data가 바뀝니다.
data를 NULL로 초기화하고 시작한 것도 눈여겨보세요.
초기화하지 않은 포인터에는 알 수 없는 주소가 들어 있으므로, 함수가 data를 바꾸지 못하고 끝나는 경우가 생기면 호출한 쪽이 실패를 알아볼 방법이 없기 때문입니다.
(*out)[i]의 괄호를 빼면 안 됩니다
*out[i]라고 쓰면 대괄호의 우선순위가 역참조 연산자보다 높아
*(out[i])로 해석됩니다.
out을 포인터의 배열로 보고 인덱스 i인 요소를 역참조하라는 뜻이 되어
i가 1 이상이면 전혀 다른 메모리 위치에 접근하게 됩니다.
먼저 out을 역참조해 data를 얻은 다음, data가 가리키는 메모리에서 인덱스 i인 요소에 접근해야 하므로
(*out)[i]처럼 괄호로 순서를 정해 줍니다.
두 식 모두 컴파일되므로 컴파일러가 알려 주지 않습니다.
스스로 확인
사용을 마친 메모리를 해제하는 일까지 함수에 맡긴다면, 그 함수도 int **를 전달받아야 할까요? 무엇을 바꾸고 싶은지로 생각해 보세요.
행마다 따로 할당한 표
크기를 실행 중에 정하는 표를 만들어 봅니다.
2차원 배열은 행과 열의 개수를 미리 정해야 합니다. 크기를 실행 중에 정해야 한다면 동적으로 할당해야 하는데, 여기서는 표를 행마다 나누어 할당하므로 할당도 두 단계로 합니다.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int rows = 3;
int cols = 4;
int **grid = (int **) malloc(rows * sizeof(int *)); // 행을 가리킬 포인터 세 개
if (grid == NULL) {
return 1;
}
for (int r = 0; r < rows; r = r + 1) {
grid[r] = (int *) malloc(cols * sizeof(int)); // 행마다 「따로」 할당한다
if (grid[r] == NULL) {
for (int k = 0; k < r; k = k + 1) { // 앞서 할당한 행들을 해제한다
free(grid[k]);
}
free(grid);
return 1;
}
}
for (int r = 0; r < rows; r = r + 1) {
for (int c = 0; c < cols; c = c + 1) {
grid[r][c] = (r + 1) * 10 + c;
}
}
for (int r = 0; r < rows; r = r + 1) {
for (int c = 0; c < cols; c = c + 1) {
printf("%4d", grid[r][c]);
}
printf("\n");
}
printf("0번 행 %p\n", (void *) grid[0]);
printf("1번 행 %p\n", (void *) grid[1]); // 이어져 있지 않을 수 있다
for (int r = 0; r < rows; r = r + 1) { // 각 행을 「먼저」 해제한다
free(grid[r]);
}
free(grid); // grid를 먼저 해제하면 각 행의 주소를 잃는다
return 0;
}먼저 「행을 가리킬 포인터」를, 그다음 각 행을 할당합니다
malloc(rows * sizeof(int *))은 정수를 저장할 메모리가 아니라
「int를 가리키는 포인터」 세 개를 저장할 메모리를 할당합니다.
sizeof(int)가 아니라 sizeof(int *)를 쓰는 이유입니다.
그다음 행마다 malloc을 한 번씩 더 호출해
반환된 주소를 grid[0]·grid[1]·grid[2]에 저장합니다.
모두 마치면 grid[r][c]처럼 2차원 배열과 똑같이 쓸 수 있습니다.
2차원 배열과 겉모습만 같습니다
int a[3][4]는 요소 열두 개가 빈틈없이 이어져 있습니다.
그런데 위 프로그램을 실행해 보면 0번 행과 1번 행의 주소가 대개 바로 이어지지 않습니다.
행마다 따로 할당했으니 서로 다른 곳에 흩어져 놓일 수 있기 때문입니다.
그래서 포인터 하나로 처음부터 끝까지 이어서 읽는 방법은 여기서 통하지 않습니다.
「요소 몇 개마다 다음 행이 시작되는지」라는 규칙 자체가 없습니다 —
각 행이 어디에 있는지는 grid[r]에 저장된 주소가 알려 줍니다.
그 대신 행마다 길이를 다르게 할당할 수도 있습니다.
해제하는 순서가 정해져 있습니다
각 행을 먼저 해제하고 grid를 나중에 해제합니다.
grid를 먼저 해제하면 각 행의 주소가 저장되어 있던 메모리가 해제되어
각 행을 해제할 방법이 없어집니다.
중간에 할당에 실패했을 때도 마찬가지입니다.
위 코드는 실패한 행 앞까지 할당해 둔 행을 하나씩 해제하고 나서 grid를 해제하고 끝냅니다.
할당한 메모리가 여럿이면 실패하는 경로에서도 빠짐없이 해제해야 합니다.
스스로 확인
행마다 길이가 다른 표(0번 행은 요소 두 개, 1번 행은 요소 다섯 개)를 만들려면 무엇만 바꾸면 될까요? 그 길이들은 어디에 저장해 두어야 할까요?
핵심 정리
포인터에도 주소가 있다
포인터도 변수이므로 &p로 자신의 주소를 얻을 수 있다.
새 규칙이 아니라 있던 규칙을 한 번 더 적용한 것이다.
별표 하나가 한 걸음
**q는 두 번 역참조해 값에, *q는 한 번 역참조해
포인터 p에 접근한다. 별표의 개수가 접근하는 대상을 정한다.
바꾸려면 그 변수의 주소를
int 변수를 바꾸려면 int *, 포인터를 바꾸려면 int **를 전달한다.
함수가 메모리를 할당해 호출한 쪽의 포인터에 그 주소를 저장해 줄 때 쓰인다.
겉모습만 2차원 배열
행마다 따로 할당하므로 행들이 이어져 있다는 보장이 없다.
대신 행마다 길이를 다르게 할 수 있다.
각 행을 먼저, grid를 나중에 해제한다.
확인 퀴즈
배운 내용을 확인해 봅시다.
1. int num; int *p = # int **q = &p;일 때 *q의 값은?
2. 함수가 호출한 쪽의 포인터 변수를 바꾸게 하려면 무엇을 전달해야 하는가?
3. int **grid로 만든 표가 int a[3][4]와 다른 점은?