포인터와 배열
배열 이름이 첫 요소 주소로 해석되는 원리와 포인터 산술을 익히고 배열 자체와 포인터 변수의 차이를 구분합니다.
1차원 배열에서 배열 이름은 대부분의 식에서 첫 원소를 가리키는 포인터로 바뀐다고 했습니다. 그래서 포인터로 배열의 원소를 가리키고, 포인터에 정수를 더해 다음 원소로 옮겨 갈 수 있습니다. 하지만 배열 이름이 곧 포인터는 아니며, 이 차이를 놓치면 sizeof나 함수 매개변수에서 엉뚱한 결과를 얻습니다.
이 글은 배열이 포인터로 바뀌는 규칙, 포인터에 정수를 더하고 빼는 포인터 산술, 배열과 포인터 변수의 차이, C 스타일 문자열과 2차원 배열에서의 포인터를 다룹니다. 예제의 크기는 int가 4바이트, 포인터가 8바이트인 Windows x86-64의 GCC를 기준으로 합니다.
배열에서 포인터로의 변환
배열 이름은 포인터 변수가 아니라 배열 객체입니다. 다만 sizeof나 단항 &의 피연산자일 때 등을 빼면, 대부분의 식에서 배열은 첫 원소를 가리키는 포인터로 바뀝니다. 이것을 배열에서 포인터로의 변환(array-to-pointer conversion)이라고 합니다.
#include <iostream>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;
std::cout << std::boolalpha << (p == &arr[0]) << '\n';
std::cout << *p << ' ' << p[1] << '\n';
std::cout << sizeof(arr) << ' ' << sizeof(*arr) << ' '
<< sizeof(*&arr) << '\n';
}true
10 20
20 4 20int* p = arr;에서 arr은 첫 원소의 주소로 바뀌므로 p == &arr[0]은 참이고, 포인터도 p[1]처럼 인덱스로 원소에 접근할 수 있습니다. 반면 sizeof(arr)에서는 변환이 일어나지 않아 배열 전체의 20바이트가 나옵니다. *arr은 첫 원소인 int여서 4바이트이고, &arr은 배열 전체를 가리키는 int (*)[5] 타입이므로 *&arr은 다시 배열 전체인 20바이트입니다.
&arr과 &arr[0]은 같은 위치에서 시작하지만 타입이 다릅니다. arr + 1은 다음 int로 4바이트 이동하지만, &arr + 1은 배열 하나 크기인 20바이트를 건너뛴 위치를 가리킵니다. 주소의 값이 같다고 타입까지 같은 것은 아닙니다.
포인터 산술
포인터에 정수를 더하면, 그 정수에 가리키는 타입의 크기를 곱한 만큼 이동합니다. int*에 1을 더하면 다음 int 원소를 가리키게 되며, 이것을 포인터 산술(pointer arithmetic)이라고 합니다.
#include <iostream>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int* p = numbers;
std::cout << *p << ' ';
++p;
std::cout << *p << ' ';
p += 2;
std::cout << *p << ' ';
p -= 3;
std::cout << *p << ' ' << p[2] << '\n';
for (int* current = numbers; current < numbers + 5; ++current) {
std::cout << *current << ' ';
}
std::cout << '\n';
}10 20 40 10 30
10 20 30 40 50p는 numbers[0]에서 시작해, ++p로 numbers[1]의 20, p += 2로 numbers[3]의 40, p -= 3으로 다시 numbers[0]의 10을 가리킵니다. p[2]는 *(p + 2)와 같은 뜻이어서 30입니다. 아래쪽 루프는 인덱스 대신 포인터를 한 칸씩 옮기며, numbers + 5에 닿으면 멈춥니다.
같은 배열을 가리키는 두 포인터를 빼면 그 사이의 원소 개수가 나오며, 결과의 타입은 <cstddef>의 std::ptrdiff_t입니다. 포인터 산술과 크기 비교는 같은 배열 안에서만 의미가 있습니다. 마지막 원소 바로 다음 위치인 numbers + 5는 만들어서 비교에 쓸 수 있지만, 그 위치에는 원소가 없으므로 간접 참조할 수 없습니다.
더 깊이: 서로 다른 배열의 포인터를 비교하려면
<와 >로 포인터를 비교한 결과가 정해져 있는 것은 같은 배열의 원소나 그 끝 다음 위치, 같은 객체의 멤버처럼 서로 관련 있는 위치를 가리킬 때입니다. 관련 없는 두 객체를 가리키는 포인터를 <로 비교하면 결과는 지정되지 않은(unspecified) bool 값이어서, 어느 쪽이 크다고 기대할 수 없습니다. 연속된 가상 주소 공간을 쓰는 많은 구현에서는 일관된 결과가 나오지만, 포인터의 모든 비트가 주소가 아닌 환경도 있어 표준은 이를 요구하지 않습니다.
대신 표준은 프로그램마다 구현이 정하는 포인터의 엄격한 전순서(strict total order)가 있다고 정하고, std::less<T*>나 std::greater<T*> 같은 표준 함수 객체가 그 순서를 따르게 했습니다. 그래서 std::set<Node*>처럼 서로 관련 없는 객체의 주소를 키로 쓰는 컨테이너도 기본 비교인 std::less로 올바르게 동작합니다. 주소를 직접 정렬해야 한다면 내장 < 대신 std::less<T*>{}를 비교 함수로 넘기며, 이 규칙은 cppreference의 비교 연산자 문서에 정리되어 있습니다.
현재 위치에서 세는 인덱스
포인터의 인덱스는 배열의 처음이 아니라, 포인터가 지금 가리키는 위치에서 셉니다. 아래 코드는 p를 둘째 원소에 두고 p[2]에 대입한 뒤, 배열 쪽 이름인 numbers[3]으로 읽어 같은 원소인지 확인합니다. 함께 만든 end는 마지막 원소 바로 다음 위치를 가리키는데, 이 위치는 비교하거나 빼는 데만 쓰고 *end처럼 간접 참조하면 안 됩니다.
#include <iostream>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int* p = numbers + 1;
int* end = numbers + 5;
p[2] = 400;
std::cout << numbers[3] << ' ' << (end - numbers) << ' '
<< *(end - 1) << '\n';
}400 5 50아래 그림처럼 p는 인덱스 1을 가리키므로 p[2]는 *(p + 2), 즉 numbers[3]이고 그 값이 400으로 바뀝니다. end는 마지막 원소 바로 다음 위치이므로 end - numbers는 원소 개수 5이고, end - 1은 마지막 원소 50을 가리킵니다. 포인터 값만으로는 배열의 길이를 알 수 없으며, 여기서 5를 얻은 것은 시작과 끝을 따로 알고 있기 때문입니다.
C++ 배열-포인터 변환과 문맥 계약
numbers의 실제 값 10/20/30/40/50에서 p=numbers+1은 index 1, p[2]=400은 index 3을 바꾼다. one-past는 값 슬롯으로 추가하지 않는다.
ARRAY OBJECT · CONTEXT · DECAY · RANGE
배열 이름은 포인터가 아니라, 문맥에 따라 포인터로 변환되는 배열 식이다
int arr[5]는 다섯 원소 배열 객체를 선언합니다. 같은 시작 주소여도 문맥에 따라 타입과 이동 단위가 달라집니다.
int arr[5] = {10, 20, 30, 40, 50};
arr의 선언 타입은 int[5]입니다. 배열은 포인터 값을 저장하는 변수가 아니며, 다른 주소를 대입할 수 없는 이유도 “상수 포인터”라서가 아니라 내장 배열이 대입 가능한 객체가 아니기 때문입니다.
이 문맥에서 배열 변환이 일어나는가?
연산자를 보기 전에 배열 이름을 무조건 int*로 읽지 않습니다. 먼저 array-to-pointer conversion이 적용되지 않는 문맥인지 판정합니다.
sizeof(arr)
배열 전체 타입을 유지하므로 결과는 5 × sizeof(int)입니다. 특정 구현에서 int가 4바이트라고 가정한 숫자를 언어 규칙으로 고정하지 않습니다.
&arr → int (*)[5]
배열 전체를 가리킵니다. &arr + 1의 이동 단위는 int[5] 하나이며, 배열 바로 뒤의 one-past 위치가 됩니다.
arr → int*
&arr[0]과 같은 첫 요소 포인터가 됩니다. 이때 arr + 1은 다음 int 요소를 가리킵니다.
| 표현 | 이 식에서의 타입 | 한 칸 이동 | 저장된 길이·재지정 |
|---|---|---|---|
arr | 일반 식에서 int*로 변환 | sizeof(int) | 변환된 포인터 값에는 길이가 없고, 배열 객체 자체에는 다른 주소를 대입할 수 없습니다. |
&arr[0] | int* | sizeof(int) | 첫 요소의 주소이며 배열 소유권을 새로 만들지 않습니다. |
&arr | int (*)[5] | int[5] 하나 | 배열 전체의 주소이며 출력되는 주소 표기는 첫 요소 주소와 같을 수 있습니다. |
p | 별도 int* 객체, arr로 초기화 | sizeof(int) | p는 재지정 가능하고 sizeof(p)는 포인터 객체의 크기입니다. |
arr → int*
일반 식에서는 첫 요소 포인터로 변환되고 한 칸은 int 한 요소입니다. 변환된 값에는 길이가 없으며 배열 객체 자체에는 다른 주소를 대입할 수 없습니다.
&arr[0] → int*
첫 요소의 주소입니다. 한 칸은 int 한 요소이며 배열 소유권은 새로 만들지 않습니다.
&arr → int (*)[5]
배열 전체의 주소입니다. 한 칸은 int[5] 하나이며 첫 요소 주소와 같은 위치로 표시될 수 있습니다.
int* p = arr
p는 재지정할 수 있는 별도 포인터 객체입니다. 한 칸은 int 한 요소이고 sizeof(p)는 포인터 객체의 크기입니다.
int a[] 매개변수의 타입은 int*
함수 안의 sizeof(a)는 원래 배열 전체가 아니라 포인터 매개변수의 크기입니다. 요소 개수, 읽기·쓰기 범위, 소유권은 이 구문에 들어 있지 않으므로 별도 인수와 API 계약으로 전달해야 합니다.
포인터 산술과 비교는 같은 배열의 요소 또는 one-past를 전제로 합니다. one-past는 계산할 수 있지만 역참조할 수 없고, 실제 원소에서만 읽고 씁니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 실제 numbers의 5개 원소를 배열 영역에 표시한다. p가 index 1을 가리키는 관계와 p[2]가 index 3에 400을 쓰는 접근 관계는 서로 다른 두 간선이다. 각 원소의 최종값은 10,20,30,400,50이며 one-past는 읽을 원소가 없는 끝 위치로 따로 설명한다. 모바일도 같은 5개 원소/현재 대상 1/상대 접근 3/끝 위치 읽기 금지를 유지한다. 물리 주소나 sizeof int가 4라고 가정하지 않는다. |
| 노드·연결선 라벨 | numbers는 5개 원소 배열 |
| 노드·연결선 라벨 | 대상 index 1 |
| 노드·연결선 라벨 | 5개 원소 뒤 위치는 별도의 값 슬롯이 아님 |
| 노드·연결선 라벨 | index 5 · 읽기 금지 |
| 노드·연결선 라벨 | 현재 대상 index 1 |
| 노드·연결선 라벨 | 현재 위치에서 2칸 |
| 노드·연결선 라벨 | 현재 대상 1 + 상대 이동 2 = index 3 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 배열과 기준 주소 numbers=[10,20,30,40,50]에서 p는 배열 인덱스 1을 가리킨다. |
|
|
| 상대 인덱스 쓰기 p[2]에 쓰면 현재 p 기준 두 칸 뒤인 numbers[3]이 바뀐다. |
|
|
| 배열 결과 나머지 원소와 one-past 경계를 확인한다. |
|
|
C++ 배열-포인터 변환과 문맥 계약
numbers의 실제 값 10/20/30/40/50에서 p=numbers+1은 index 1, p[2]=400은 index 3을 바꾼다. one-past는 값 슬롯으로 추가하지 않는다.
ARRAY OBJECT · CONTEXT · DECAY · RANGE
배열 이름은 포인터가 아니라, 문맥에 따라 포인터로 변환되는 배열 식이다
int arr[5]는 다섯 원소 배열 객체를 선언합니다. 같은 시작 주소여도 문맥에 따라 타입과 이동 단위가 달라집니다.
int arr[5] = {10, 20, 30, 40, 50};
arr의 선언 타입은 int[5]입니다. 배열은 포인터 값을 저장하는 변수가 아니며, 다른 주소를 대입할 수 없는 이유도 “상수 포인터”라서가 아니라 내장 배열이 대입 가능한 객체가 아니기 때문입니다.
이 문맥에서 배열 변환이 일어나는가?
연산자를 보기 전에 배열 이름을 무조건 int*로 읽지 않습니다. 먼저 array-to-pointer conversion이 적용되지 않는 문맥인지 판정합니다.
sizeof(arr)
배열 전체 타입을 유지하므로 결과는 5 × sizeof(int)입니다. 특정 구현에서 int가 4바이트라고 가정한 숫자를 언어 규칙으로 고정하지 않습니다.
&arr → int (*)[5]
배열 전체를 가리킵니다. &arr + 1의 이동 단위는 int[5] 하나이며, 배열 바로 뒤의 one-past 위치가 됩니다.
arr → int*
&arr[0]과 같은 첫 요소 포인터가 됩니다. 이때 arr + 1은 다음 int 요소를 가리킵니다.
| 표현 | 이 식에서의 타입 | 한 칸 이동 | 저장된 길이·재지정 |
|---|---|---|---|
arr | 일반 식에서 int*로 변환 | sizeof(int) | 변환된 포인터 값에는 길이가 없고, 배열 객체 자체에는 다른 주소를 대입할 수 없습니다. |
&arr[0] | int* | sizeof(int) | 첫 요소의 주소이며 배열 소유권을 새로 만들지 않습니다. |
&arr | int (*)[5] | int[5] 하나 | 배열 전체의 주소이며 출력되는 주소 표기는 첫 요소 주소와 같을 수 있습니다. |
p | 별도 int* 객체, arr로 초기화 | sizeof(int) | p는 재지정 가능하고 sizeof(p)는 포인터 객체의 크기입니다. |
arr → int*
일반 식에서는 첫 요소 포인터로 변환되고 한 칸은 int 한 요소입니다. 변환된 값에는 길이가 없으며 배열 객체 자체에는 다른 주소를 대입할 수 없습니다.
&arr[0] → int*
첫 요소의 주소입니다. 한 칸은 int 한 요소이며 배열 소유권은 새로 만들지 않습니다.
&arr → int (*)[5]
배열 전체의 주소입니다. 한 칸은 int[5] 하나이며 첫 요소 주소와 같은 위치로 표시될 수 있습니다.
int* p = arr
p는 재지정할 수 있는 별도 포인터 객체입니다. 한 칸은 int 한 요소이고 sizeof(p)는 포인터 객체의 크기입니다.
int a[] 매개변수의 타입은 int*
함수 안의 sizeof(a)는 원래 배열 전체가 아니라 포인터 매개변수의 크기입니다. 요소 개수, 읽기·쓰기 범위, 소유권은 이 구문에 들어 있지 않으므로 별도 인수와 API 계약으로 전달해야 합니다.
포인터 산술과 비교는 같은 배열의 요소 또는 one-past를 전제로 합니다. one-past는 계산할 수 있지만 역참조할 수 없고, 실제 원소에서만 읽고 씁니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 실제 numbers의 5개 원소를 배열 영역에 표시한다. p가 index 1을 가리키는 관계와 p[2]가 index 3에 400을 쓰는 접근 관계는 서로 다른 두 간선이다. 각 원소의 최종값은 10,20,30,400,50이며 one-past는 읽을 원소가 없는 끝 위치로 따로 설명한다. 모바일도 같은 5개 원소/현재 대상 1/상대 접근 3/끝 위치 읽기 금지를 유지한다. 물리 주소나 sizeof int가 4라고 가정하지 않는다. |
| 노드·연결선 라벨 | numbers는 5개 원소 배열 |
| 노드·연결선 라벨 | 대상 index 1 |
| 노드·연결선 라벨 | 5개 원소 뒤 위치는 별도의 값 슬롯이 아님 |
| 노드·연결선 라벨 | index 5 · 읽기 금지 |
| 노드·연결선 라벨 | 현재 대상 index 1 |
| 노드·연결선 라벨 | 현재 위치에서 2칸 |
| 노드·연결선 라벨 | 현재 대상 1 + 상대 이동 2 = index 3 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 배열과 기준 주소 numbers=[10,20,30,40,50]에서 p는 배열 인덱스 1을 가리킨다. |
|
|
| 상대 인덱스 쓰기 p[2]에 쓰면 현재 p 기준 두 칸 뒤인 numbers[3]이 바뀐다. |
|
|
| 배열 결과 나머지 원소와 one-past 경계를 확인한다. |
|
|
*end처럼 끝 위치를 간접 참조하거나, numbers + 6처럼 끝 위치를 넘어가는 포인터를 만드는 것은 정의되지 않은 동작입니다. 표준 라이브러리의 반복자도 이처럼 시작과 마지막 다음 위치의 두 지점으로 범위를 나타내며, 시작은 포함하고 끝은 포함하지 않는 이 범위를 반열린 범위(half-open range)라고 합니다.
배열과 포인터 변수의 차이
배열과 포인터는 비슷하게 쓰이지만 서로 다른 것입니다.
| 특징 | 배열 int arr[5] | 포인터 int* p |
|---|---|---|
| 정체 | 원소 다섯 개를 소유하는 객체 | 주소 하나를 담는 객체 |
sizeof | 배열 전체, 여기서 20 | 포인터 하나, 여기서 8 |
| 다른 주소 대입 | 불가능, arr = p;는 오류 | 가능, p = &arr[2]; |
| 배열끼리 대입 | arr1 = arr2;는 오류 | 포인터끼리는 대입 가능 |
| 길이 정보 | 타입에 들어 있음 | 없음 |
함수 매개변수의 int a[]는 int* a로 바뀌므로, 함수 안의 sizeof(a)는 배열 전체가 아니라 포인터의 8바이트입니다. 배열을 함수에 넘길 때는 길이를 따로 넘기거나, 시작과 끝 포인터를 넘기거나, std::span을 써서 길이가 함께 전달되게 합니다.
C 스타일 문자열과 char*
C 스타일 문자열은 널 문자로 끝나는 char 배열이므로, char*로 그 시작을 가리킬 수 있습니다. 아래 코드의 text는 배열 greeting의 첫 글자를 가리키며, 같은 포인터를 std::cout과 std::strlen에 넘기고 인덱스로 글자 하나를 바꿉니다. greeting은 문자열 리터럴의 글자를 복사해 만든 배열이므로, 그 글자를 바꾸어도 됩니다.
#include <cstring>
#include <iostream>
int main() {
char greeting[] = "Hello World";
char* text = greeting;
std::cout << text << '\n';
std::cout << (text + 6) << '\n';
std::cout << std::strlen(text) << '\n';
text[0] = 'h';
std::cout << greeting << '\n';
}Hello World
World
11
hello Worldstd::cout은 char*를 받으면 주소가 아니라, 그 위치부터 널 문자까지의 문자열로 출력합니다. 그래서 text + 6은 인덱스 6의 W부터 출력해 World가 됩니다. text[0] = 'h'는 greeting 배열의 첫 글자를 바꿉니다. char*를 문자열로 출력하거나 strlen에 넘기려면, 그 포인터가 널 문자로 끝나는 글자 배열을 가리켜야 합니다.
2차원 배열과 포인터
다차원 배열의 int matrix[2][3]은 int[3] 행 두 개를 가진 배열입니다. 그래서 matrix는 대부분의 식에서 첫 행을 가리키는 int (*)[3]로 바뀌고, matrix[0]은 첫 행의 첫 원소를 가리키는 int*로 바뀝니다.
#include <iostream>
int main() {
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
std::cout << matrix[0][1] << ' ' << *(matrix[0] + 1) << '\n';
std::cout << (*(matrix + 1))[0] << ' ' << *(*(matrix + 1) + 2) << '\n';
}2 2
4 6matrix[0] + 1은 int*에 1을 더한 것이므로 같은 행의 다음 원소 2를 가리킵니다. matrix + 1은 행 포인터에 1을 더한 것이므로 int[3] 하나만큼 건너뛰어 둘째 행을 가리키고, 그 행의 0번 원소가 4, 2번 원소가 6입니다. 아래 그림처럼 포인터가 한 칸 움직일 때 int 하나만큼인지, 행 하나만큼인지는 포인터의 타입이 정합니다.
2D ARRAY · ROW TYPE · ELEMENT TYPE · STRIDE
타입에 따라 행·열 이동 폭이 다르다
int matrix[2][3]는 int[3] 행이 둘인 배열입니다. 선언·포인터 타입을 나누면 행·열 이동 규칙이 선명합니다.
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int matrix[2][3]
첫 행은 1 · 2 · 3, 둘째 행은 4 · 5 · 6으로 초기화합니다.
matrix는 int[2][3], matrix[0]은 int[3] 타입입니다. 두 식은 많은 문맥에서 각각 첫 행 포인터와 첫 요소 포인터로 변환됩니다.
| 행 | 열 0 | 열 1 | 열 2 | 행 식의 타입 |
|---|---|---|---|---|
matrix[0] | matrix[0][0] = 1 | matrix[0][1] = 2 | matrix[0][2] = 3 | 배열 int[3], 일반 식에서 int* |
matrix[1] | matrix[1][0] = 4 | matrix[1][1] = 5 | matrix[1][2] = 6 | 배열 int[3], 일반 식에서 int* |
matrix[0]
열 0 · 1 · 2의 값은 차례로 1 · 2 · 3입니다. 일반 식에서는 int*로 변환됩니다.
matrix[1]
열 0 · 1 · 2의 값은 차례로 4 · 5 · 6입니다. 일반 식에서는 int*로 변환됩니다.
matrix → int (*)[3]
일반 식에서 첫 행을 가리키는 포인터로 변환됩니다. matrix에 1을 더하면 int[3] 한 행만큼 이동해 두 번째 행을 가리키고, 그 행의 첫 값은 4입니다.
matrix[0] → int*
일반 식에서 첫 요소 포인터가 됩니다. matrix[0] + 1은 int 한 원소만큼 이동해 값 2를 가리킵니다.
| 표현 | 변환 뒤 타입 | 이동 단위 | 역참조 결과 |
|---|---|---|---|
matrix + 1 | int (*)[3] | 행 하나, 즉 sizeof(int[3]) | *(matrix + 1)은 두 번째 행 배열입니다. |
matrix[0] + 1 | int* | 요소 하나, 즉 sizeof(int) | *(matrix[0] + 1)은 값 2입니다. |
matrix[r][c] | 최종 요소는 int | 먼저 행, 다음 열 | *(*(matrix + r) + c)와 같은 요소를 나타냅니다. |
matrix + 1
타입은 int (*)[3]이고 한 행만큼 이동합니다. 역참조하면 두 번째 행 배열입니다.
matrix[0] + 1
타입은 int*이고 한 요소만큼 이동합니다. 역참조한 값은 2입니다.
matrix[r][c]
행과 열을 차례로 선택한 int 요소입니다. *(*(matrix + r) + c)와 같습니다.
같은 주소여도 타입에 따라 이동 폭이 다릅니다. 선언 타입과 변환된 포인터 타입을 구분해 대상 한 단위 이동으로 읽습니다.
원소가 행 우선으로 이어져 있더라도, 첫 행의 int*를 행의 끝 너머로 옮겨 둘째 행의 원소를 읽는 것은 올바른 접근이 아닙니다. 둘째 행의 원소는 matrix[1]에서 시작하는 포인터로 접근합니다.
정리
- 배열 이름은 포인터가 아니지만,
sizeof와&의 피연산자일 때 등을 빼면 첫 원소를 가리키는 포인터로 바뀝니다. &arr은 배열 전체를 가리키는 포인터여서, 주소가arr과 같아도 타입과 이동 폭이 다릅니다.- 포인터에 정수를 더하면 가리키는 타입의 크기만큼 이동하며,
p[i]는*(p + i)와 같습니다. - 포인터 산술은 같은 배열 안에서만 의미가 있고, 마지막 다음 위치는 만들 수 있지만 간접 참조할 수 없습니다.
- 배열은 대입할 수 없고
sizeof가 배열 전체를 알려 주지만, 포인터는 길이를 알지 못하므로 함수에는 길이를 함께 넘깁니다. - 2차원 배열에서
matrix + 1은 행 하나,matrix[0] + 1은 원소 하나만큼 이동합니다.
연습 문제
1. 시작 위치 바꾸기
relative_index.cpp에서 p의 초깃값만 numbers + 2로 바꾸면 출력은 무엇인가요?
해설 보기
p[2]는 이제 numbers[4]이므로 마지막 원소가 400으로 바뀌고, numbers[3]은 40 그대로입니다. end - numbers는 여전히 5이고, *(end - 1)은 바뀐 마지막 원소 400입니다. 출력은 40 5 400입니다.
2. 이동하는 바이트 수
int arr[5]에서 arr + 1과 &arr + 1은 각각 arr의 시작에서 몇 바이트 떨어진 위치를 가리키나요? int는 4바이트입니다.
해설 보기
arr + 1은 int*에 1을 더한 것이므로 4바이트 떨어진 arr[1]을 가리킵니다. &arr + 1은 int (*)[5]에 1을 더한 것이므로 배열 하나 크기인 20바이트 떨어진 위치, 즉 배열 전체의 바로 다음 위치를 가리킵니다. 이 위치는 만들 수는 있지만 간접 참조할 수 없습니다.
3. 시작과 끝으로 넘긴 범위
다음 함수에 sum(numbers, numbers + 5)와 sum(numbers + 1, numbers + 3)을 넘기면 각각 무엇을 돌려주나요? numbers는 {10, 20, 30, 40, 50}입니다.
int sum(const int* first, const int* last) {
int total = 0;
for (const int* p = first; p != last; ++p) {
total += *p;
}
return total;
}해설 보기
첫 호출은 모든 원소를 더해 150을 돌려줍니다. 둘째 호출은 numbers[1]부터 numbers[3] 앞까지, 즉 20과 30만 더해 50을 돌려줍니다. 끝 포인터가 가리키는 원소는 포함하지 않는 반열린 범위이며, 이 방식이면 배열의 일부 구간도 같은 함수로 다룰 수 있습니다.
다음 글 const 포인터와 포인터 상수에서는 const의 위치에 따라 가리키는 값을 바꿀 수 없는 포인터와, 가리키는 대상을 바꿀 수 없는 포인터를 구별합니다.