포인터와 배열
포인터는 메모리 주소에 직접 접근하므로 배열 등의 자료구조를 처리하는 데 매우 유용히 사용된다.
배열의 주소
- 배열은 여러 개의 데이터가 데이터형의 크기만큼 순차적으로 메모리 공간에 할당된다.
배열 원소의 주소값 확인
#include <iostream>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++) {
std::cout << "arr[" << i << "]의 주소값: " << &arr[i] << std::endl;
}
return 0;
}
arr[0]의 주소값: 006FFB7C
arr[1]의 주소값: 006FFB80
arr[2]의 주소값: 006FFB84
arr[3]의 주소값: 006FFB88
arr[4]의 주소값: 006FFB8C
- arr의 원소의 데이터형인 int의 크기가 4byte이므로, 각각의 원소들은 메모리에 4byte씩 순차적으로 할당된다.
포인터로 배열의 원소 값 및 원소의 주소값 접근
- 포인터 변수에 배열의 첫 번째 원소의 주소값인 &arr[0]를 대입한다.
- (ptr + i)와 &arr[i]는 정확히 같은 주소값을 가리킨다.
#include <iostream>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
int* ptr = &arr[0];
for (int i = 0; i < 5; i++) {
std::cout << "arr[" << i << "]의 값: " << arr[i] << std::endl;
std::cout << "(ptr + " << i << ")가 가리키는 값: " << *(ptr + i) << std::endl << std::endl;
std::cout << "arr[" << i << "]의 주소값: " << &arr[i] << std::endl;
std::cout << "(ptr + " << i << ")의 주소값 " << ptr + i << std::endl;
std::cout << "----------------------------" << std::endl << std::endl;
}
return 0;
}
arr[0]의 값: 1
(ptr + 0)가 가리키는 값: 1
arr[0]의 주소값: 010FFAFC
(ptr + 0)의 주소값 010FFAFC
----------------------------
arr[1]의 값: 2
(ptr + 1)가 가리키는 값: 2
arr[1]의 주소값: 010FFB00
(ptr + 1)의 주소값 010FFB00
----------------------------
arr[2]의 값: 3
(ptr + 2)가 가리키는 값: 3
arr[2]의 주소값: 010FFB04
(ptr + 2)의 주소값 010FFB04
----------------------------
arr[3]의 값: 4
(ptr + 3)가 가리키는 값: 4
arr[3]의 주소값: 010FFB08
(ptr + 3)의 주소값 010FFB08
----------------------------
arr[4]의 값: 5
(ptr + 4)가 가리키는 값: 5
arr[4]의 주소값: 010FFB0C
(ptr + 4)의 주소값 010FFB0C
----------------------------
배열 이름 = 첫 번째 원소의 주소값
#include <iostream>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
std::cout << "arr[0]의 주소값: " << &arr[0] << std::endl;
std::cout << "배열명 arr이 가리키는 값: " << arr << std::endl;
return 0;
}
arr[0]의 주소값: 00AFFC7C
배열명 arr이 가리키는 값: 00AFFC7C
- 사실 배열의 이름은 첫 번째 원소의 주소값을 가리킨다. 따라서 arr과 &arr[0]이 정확히 같은 주소값을 나타낸다.
- 배열의 이름을 사용할 시 자동으로 첫 번째 원소를 가리키는 포인터 타입으로 변환된다.
- 단, 배열의 이름이 첫 번째 원소를 가리키는 포인터를 말하는 것은 아니다.
- 배열은 선언과 동시에 데이터를 저장할 연속적인 메모리 공간을 가지지만, 포인터는 데이터를 저장하는 공간을 갖는 것이 아니라, 데이터를 저장하는 메모리의 시작 주소를 저장한다. 이는 sizeof() 연산자를 통해 알 수 있다.
- 포인터는 변수이지만, 배열은 포인터 상수이다. 따라서 배열의 이름으로 다른 주소를 가리키는 행위는 불가능하며, 증감연산자의 사용도 불가능하다.
예제 1) 배열의 크기, 포인터의 크기 확인
#include <iostream>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
int* ptr = arr;
std::cout << "ptr의 크기: " << sizeof(ptr) << std::endl;
std::cout << "arr의 크기: " << sizeof(arr) << std::endl;
return 0;
}
ptr의 크기: 4
arr의 크기: 20
- sizeof(ptr)은 포인터의 크기(배열의 첫 번째 원소의 주소 크기만큼)를 나타낸다.
- 반면에 sizeof(arr)은 배열 arr의 전체 크기를 나타낸다.
- 즉, 배열의 이름 arr과 배열의 시작 주소를 나타내는 ptr은 엄연히 다른 것이다.
예제 2) 배열 이름에 증감연산자 적용하기 (오류 발생)
#include <iostream>
using namespace std;
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
int* ptr = arr;
cout << "ptr에 증감연산자 사용: " << ptr++ << endl;
cout << "arr에 증감연산자 사용: " << arr++ << endl;
return 0;
}
- 포인터는 변수이기에 증감연산자 사용이 가능하지만, 배열의 이름인 arr은 포인터 상수이기 때문에 증감연산자 사용이 불가능하다.
[ ] 연산자
- 놀랍게도 [ ]는 연산자이다.
- [ ] 연산자는 array[i]의 형태처럼 사용되며, C 및 C++에서는 자동으로 *(array + i)로 변환하여 처리한다.
- i[array] 역시 *(i + array)로 변환되기 때문에 동일한 기능을 수행하지만, 가독성이 떨어져 잘 사용되지 않는다.
#include <iostream>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++) {
std::cout << "arr[" << i << "]의 값: " << arr[i] << std::endl;
std::cout << "*(arr + " << i << ")의 값: " << *(arr + i) << std::endl << std::endl;
}
return 0;
}
arr[0]의 값: 1
*(arr + 0)의 값: 1
arr[1]의 값: 2
*(arr + 1)의 값: 2
arr[2]의 값: 3
*(arr + 2)의 값: 3
arr[3]의 값: 4
*(arr + 3)의 값: 4
arr[4]의 값: 5
*(arr + 4)의 값: 5
배열 포인터
- 배열을 가리키는 포인터
- datatype(*name)[n];으로 선언한다. ▶ 일차원 배열을 가리키는 배열 포인터에서 n은 원소의 개수이다.
- 만약 int(*parr)[5]에서 () 소괄호를 적지 않으면 int *parr[5]가 되고, 이는 int* 포인터를 원소로 5개 갖는 배열 parr이 생성된다는 의미가 된다. (이는 포인터 배열임). 따라서 배열 포인터를 생성할 때 꼭 () 소괄호를 적어주어야 한다.
#include <iostream>
int main() {
int arr[5] = { 1, 2, 3, 4, 5 };
int(*parr)[5];
parr = &arr;
for (int i = 0; i < 5; i++) {
std::cout << "arr[" << i << "]의 값: " << arr[i] << std::endl;
std::cout << "*(parr)[" << i << "]의 값: " << (*parr)[i] << std::endl << std::endl;
}
return 0;
}
arr[0]의 값: 1
*(parr)[0]의 값: 1
arr[1]의 값: 2
*(parr)[1]의 값: 2
arr[2]의 값: 3
*(parr)[2]의 값: 3
arr[3]의 값: 4
*(parr)[3]의 값: 4
arr[4]의 값: 5
*(parr)[4]의 값: 5
2차원 배열과 포인터
- 2차원 배열은 위와 같이 데이터타입의 크기만큼 메모리에 할당되어 순차적으로 저장된다.
arr[i]와 &arr[i][0]의 관계
- 1차원 배열에서 arr과 &arr[0]는 같은 값을 나타냄을 알 수 있었다. (정확히 둘이 같은 것은 아님. 컴퓨터가 암묵적으로 arr을 arr[0]을 가리키는 포인터로 타입 변환한 것임)
- 2차원 배열에서도 암묵적인 변환에 의해 arr[i]와 &arr[i][0]가 동일한 값을 나타낸다.
#include <iostream>
int main() {
int arr[3][4];
for (int i = 0; i < 3; i++) {
std::cout << "arr[" << i << "]가 나타내는 값: " << arr[i] << std::endl;
std::cout << "&arr[" << i << "][0]가 나타내는 값: " << &arr[i][0] << std::endl << std::endl;
}
return 0;
}
arr[0]가 나타내는 값: 012FF944
&arr[0][0]가 나타내는 값: 012FF944
arr[1]가 나타내는 값: 012FF954
&arr[1][0]가 나타내는 값: 012FF954
arr[2]가 나타내는 값: 012FF964
&arr[2][0]가 나타내는 값: 012FF964
2차원 배열에서 행과 열의 개수 구하기
- 배열은 포인터와 달리 그 자체로 배열 전체의 크기를 담고 있기 때문에 sizeof() 연산자를 이용하여 행과 열의 개수를 구할 수 있다.
- 앞서 sizeof(arr)은 배열이 차지하고 있는 메모리의 전체 크기를 나타냄을 알 수 있었다.
- sizeof(arr[0])은 arr[0](행)가 차지하고 있는 메모리의 전체 크기를 나타낸다.
#include <iostream>
int main() {
int arr[3][4];
std::cout << "전체 크기: " << sizeof(arr) << std::endl;
std::cout << "총 원소의 개수: " << sizeof(arr) / sizeof(int) << std::endl;
std::cout << "총 행의 개수: " << sizeof(arr) / sizeof(arr[0]) << std::endl;
std::cout << "총 열의 개수: " << sizeof(arr[0]) / sizeof(arr[0][0]) << std::endl;
return 0;
}
전체 크기: 48
총 원소의 개수: 12
총 행의 개수: 3
총 열의 개수: 4
2차원 배열의 배열 포인터
- 2차원 배열 arr이 존재할 때, arr을 가리키는 포인터를 int**로 만든다고 착각할 수 있다.
- (왜냐하면, 1차원 배열에서는 int*를 사용하여 arr을 가리키는 포인터를 만들었기 때문이다.)
Ex) int**로 arr을 가리킬 때 (오류 발생)
#include <iostream>
int main() {
int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
int** parr;
parr = arr;
std::cout << arr[0][1];
std::cout << parr[0][1];
return 0;
}
- int **을 이용하여 2차원 배열을 가리키는 포인터를 생성하였을 때 오류가 발생한다.
- 2차원 배열은 메모리 상에서 행과 열로 저장되는 것이 아니라, 일렬로 저장된다. (이전 2차원 배열 그림 참고)
- 따라서, 2차원 배열에서 열에 해당하는 값을 포인터가 알지 못하면 포인터 parr은 다음 행의 데이터가 위치한 메모리 주소가 어디인지 알 수 없다.
- 2차원 배열을 가리킬 때 배열 포인터를 사용한다. 아래의 예시들을 보면서 이를 알아보자.
Ex) arr과 arr + 1
#include <iostream>
int main() {
int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
std::cout << "arr의 값: " << arr << std::endl;
std::cout << "arr + 1의 값: " << arr + 1 << std::endl;
return 0;
}
arr의 값: 006FFDB8
arr + 1의 값: 006FFDC8
- arr은 2차원 배열의 시작주소를 나타낸다.
- arr + 1과 arr의 차이는 16이다. (16진수) ▶ 이는 배열의 열이 차지하는 메모리 크기와 동일하다. 4byte(int) * 4 = 16
- 즉, arr + 1은 다음 열인 arr[0][1]의 시작주소를 나타내는 것이 아닌, 2번째 행인 arr[1]의 시작주소를 나타낸다.
- 이처럼 2차원 배열을 가리키는 포인터를 생성하기 위해서는 열의 크기를 정확히 명시할 필요가 있다.
Ex) 배열 포인터에 열의 개수를 명시
#include <iostream>
int main() {
int arr[3][4] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} };
int(*parr)[4];
parr = arr;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
std::cout << "arr[" << i << "][" << j << "]: " << arr[i][j] << ", ";
std::cout << "parr[" << i << "][" << j << "]: " << parr[i][j] << std::endl;
}
}
return 0;
}
arr[0][0]: 1, parr[0][0]: 1
arr[0][1]: 2, parr[0][1]: 2
arr[0][2]: 3, parr[0][2]: 3
arr[0][3]: 4, parr[0][3]: 4
arr[1][0]: 5, parr[1][0]: 5
arr[1][1]: 6, parr[1][1]: 6
arr[1][2]: 7, parr[1][2]: 7
arr[1][3]: 8, parr[1][3]: 8
arr[2][0]: 9, parr[2][0]: 9
arr[2][1]: 10, parr[2][1]: 10
arr[2][2]: 11, parr[2][2]: 11
arr[2][3]: 12, parr[2][3]: 12
- 2차원 배열의 열의 개수를 정확히 명시함으로써 2차원 배열을 가리키는 포인터를 생성할 수 있다.
- parr은 크기가 4인 배열을 가리키는 포인터이다. ▶ 2차원 배열의 이름이 첫 번째 행을 가리키는 포인터로 타입 변환된 것이다.
- [4]는 해당 포인터에서 1을 증가시킬 시 주소값은 4만큼 증가한다는 것을 의미한다. ▶ 열의 개수를 명시
다차원 배열 포인터 (3차원 이상)
- 2차원 배열 포인터에서 차원의 개수만 늘려주면 된다.
#include <iostream>
int main() {
int arr[2][3][4] = { {{1, 2, 3, 4}, {1, 2, 3, 4}, {1, 2, 3, 4}}, {{5, 6, 7, 8}, {5, 6, 7, 8}, {5, 6, 7, 8}} };
int(*parr)[3][4];
parr = arr;
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 4; k++) {
std::cout << "arr[" << i << "][" << j << "]" << "[" << k << "]: " << arr[i][j][k] << ", ";
std::cout << "parr[" << i << "][" << j << "]" << "[" << k << "]: " << parr[i][j][k] << std::endl;
}
}
}
return 0;
}
arr[0][0][0]: 1, parr[0][0][0]: 1
arr[0][0][1]: 2, parr[0][0][1]: 2
arr[0][0][2]: 3, parr[0][0][2]: 3
arr[0][0][3]: 4, parr[0][0][3]: 4
arr[0][1][0]: 1, parr[0][1][0]: 1
arr[0][1][1]: 2, parr[0][1][1]: 2
arr[0][1][2]: 3, parr[0][1][2]: 3
arr[0][1][3]: 4, parr[0][1][3]: 4
arr[0][2][0]: 1, parr[0][2][0]: 1
arr[0][2][1]: 2, parr[0][2][1]: 2
arr[0][2][2]: 3, parr[0][2][2]: 3
arr[0][2][3]: 4, parr[0][2][3]: 4
arr[1][0][0]: 5, parr[1][0][0]: 5
arr[1][0][1]: 6, parr[1][0][1]: 6
arr[1][0][2]: 7, parr[1][0][2]: 7
arr[1][0][3]: 8, parr[1][0][3]: 8
arr[1][1][0]: 5, parr[1][1][0]: 5
arr[1][1][1]: 6, parr[1][1][1]: 6
arr[1][1][2]: 7, parr[1][1][2]: 7
arr[1][1][3]: 8, parr[1][1][3]: 8
arr[1][2][0]: 5, parr[1][2][0]: 5
arr[1][2][1]: 6, parr[1][2][1]: 6
arr[1][2][2]: 7, parr[1][2][2]: 7
arr[1][2][3]: 8, parr[1][2][3]: 8
포인터 배열
- 이것은 진짜로 포인터들의 배열이다. (배열 포인터는 말그대로 배열을 가리키는 포인터임)
- datatype *arr[n] 또는 datatype* arr[n] 으로 선언한다. ▶ n은 저장할 포인터의 개수
// int형 포인터 배열
int *arr[n];
int* arr[n];
포인터 배열 생성
#include <iostream>
int main() {
int a = 1, b = 2, c = 3, d = 4;
int* ptr[4];
ptr[0] = &a;
ptr[1] = &b;
ptr[2] = &c;
ptr[3] = &d;
std::cout << "a: " << a << ", *ptr[0]: " << *ptr[0] << ", &a: " << &a << ", &ptr[0]: " << ptr[0] << std::endl;
std::cout << "b: " << b << ", *ptr[1]: " << *ptr[1] << ", &b: " << &b << ", &ptr[1]: " << ptr[1] << std::endl;
std::cout << "c: " << c << ", *ptr[2]: " << *ptr[2] << ", &c: " << &c << ", &ptr[2]: " << ptr[2] << std::endl;
std::cout << "d: " << d << ", *ptr[3]: " << *ptr[3] << ", &d: " << &d << ", &ptr[3]: " << ptr[3] << std::endl;
return 0;
}
a: 1, *ptr[0]: 1, &a: 0058FC54, &ptr[0]: 0058FC54
b: 2, *ptr[1]: 2, &b: 0058FC48, &ptr[1]: 0058FC48
c: 3, *ptr[2]: 3, &c: 0058FC3C, &ptr[2]: 0058FC3C
d: 4, *ptr[3]: 4, &d: 0058FC30, &ptr[3]: 0058FC30