Priv's Blog
포인터와 배열, 포인터 산술 본문
1. C++의 배열
이제 C++ 내부에서 배열을 어떻게 다루는지 알아보겠습니다.
정수형 변수에 1이라는 값을 더하면, 변수의 값이 1만큼 올라갑니다.
하지만 포인터 변수는 다르죠.
포인터 변수에 1이라는 값을 더하면 메모리 주소가 달라집니다.
만약 포인터 변수의 타입이 double이라면, double은 8byte 크기이므로, 값이 더해졌을 때 8byte만큼 올라갈 것입니다.
#include <iostream>
using namespace std;
int main() {
double* dp = new double;
cout << dp << endl; // 0x600003878030
dp += 1;
cout << dp << endl; // 0x600003878038
return 0;
}
그렇다면 C++에서 포인터가 배열을 가리킨다면 어디를 가리킬까요?
위와 같이 단순히 하나의 값만 다루는 변수라면 문제가 없지만, 다들 아시다시피 배열을 여러 개의 값을 연속적으로 저장해 다루는 방식의 자료구조입니다.
이를 직접 알아보려면 포인터가 배열을 가리키도록 한 뒤, 주소를 출력해 보면 됩니다.
#include <iostream>
using namespace std;
int main() {
double ap[3] = {1, 2, 3};
double* dp = ap;
cout << dp << endl; // 0x16bc53560
cout << &ap[0] << endl; // 0x16bc53560
return 0;
}
위 코드를 실행해서 알 수 있듯이, C++에서 배열을 가리키는 포인터는 그 배열의 0번째 요소의 주소를 저장합니다.
그 포인터 변수에 +1을 한다면, 위에서 언급한 것처럼 메모리 주소가 올라가겠죠.
위 코드에서는 double 타입이므로, 8byte가 올라갈 것입니다.
그렇게 된다면 배열의 1번째 주소를 저장하게 되므로, ap[1]을 dp가 가리키는 상태가 됩니다.
즉, 인덱스의 값이 1만큼 올라간 것과 같습니다.
2. 역참조
포인터는 특정한 메모리 주소를 이용해 '참조'하는 역할을 수행합니다.
어느 메모리 주소에 참조할 수 있다면, 그 메모리 주소에 담긴 값이 무엇인지도 알 수 있죠.
그 값을 조작하는 것도 물론 가능합니다.
이처럼 데이터가 저장된 주소에 접근하여 그 데이터를 조작하는 것을 '역참조'라고 합니다.
역참조는 편리하긴 하지만, 이 기능을 무분별하게 사용하면 데이터에 대한 신뢰도가 떨어질 수 있어서 주의가 필요합니다.
어디서 어떤 코드가 이 데이터의 주소에 접근해 왜 다른 값으로 변경했는지 이해하지 못하고 있다면, 그 데이터를 쉽게 사용할 수 없게 되죠.
어떤 값으로 바뀔지 모르기 때문입니다.
역참조는 * 기호를 사용합니다.
포인터 변수에 메모리 주소를 저장하기 위해서는 & 기호를, 그 메모리 주소에 저장된 데이터에 접근하기 위해서는 * 기호를 사용한다고 이해하면 됩니다.
#include <iostream>
using namespace std;
int main() {
double num = 1.5;
double* pnum = #
cout << "num: " << num << endl;
cout << "pnum: " << pnum << endl;
cout << "*pnum: " << *pnum << endl;
*pnum += 2;
cout << endl;
cout << "num: " << num << endl;
cout << "pnum: " << pnum << endl;
cout << "*pnum: " << *pnum << endl;
return 0;
}
/* RESULT
num: 1.5
pnum: 0x16f083570
*pnum: 1.5
num: 3.5
pnum: 0x16f083570
*pnum: 3.5
*/
당연하지만 아직 초기화되지 않은 변수에 접근해서 역참조를 해서는 안됩니다.
아직 메모리 블록이 생성도 되지 않았는데, 그 주소에 참조하여 값을 변조한다는 것은 말이 안 되겠죠.
또한 포인터를 이용해 배열에 저장된 값을 조작하는 것도 역참조와 같은 개념입니다.
3. 포인터 산술
포인터 변수를 가지고 산술을 해봅시다.
이때도 포인터는 메모리 주소를 저장하는 것임을 기억하셔야 합니다.
#include <iostream>
using namespace std;
int main() {
int tacos[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int* pt = tacos;
int* pe = &tacos[9];
cout << pt << endl;
cout << pe << endl;
pt += 1;
cout << pt << endl;
cout << pe << endl;
pe -= 1;
cout << pt << endl;
cout << pe << endl;
return 0;
}
/* RESULT
0x16cee3550
0x16cee3574
0x16cee3554
0x16cee3574
0x16cee3554
0x16cee3570
*/
4. 배열의 동적 바인딩과 정적 바인딩
배열의 크기를 정해주고 시작한다면 정적 바인딩에 해당합니다.
정적 바인딩을 통해 한 번 정해진 배열의 크기는 컴파일 타임에 결정되므로 절대 바뀔 수 없습니다.
하지만 new [ ]를 이용해 동적 바인딩을 한다면 이야기가 달라집니다.
동적 바인딩을 통해 만들어진 배열의 크기는 프로그램이 실행되는 도중인 런 타임에 결정됩니다.
그러므로 우리가 원하는 때에 배열의 크기를 설정해 줄 수 있습니다.
#include <iostream>
using namespace std;
int main() {
int tacos[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int size;
cin >> size;
int * pizza = new int[size];
cout << pizza << endl;
delete[] pizza;
return 0;
}
위 코드에서 tacos 배열은 정적 바인딩, pizza 배열은 동적 바인딩을 통해 구현되었습니다.
5. 배열과 포인터 표기법
배열과 포인터는 사실상 사촌 관계입니다.
포인터 개념이 없다면 배열이라는 개념도 존재하기 어렵죠.
이전에 포인터 변수가 배열을 참조할 때, 배열의 0번째 값의 주소를 저장한다고 했습니다.
또한 그 포인터 변수에 1을 더하면, 배열의 1번째 값의 주소를 참조하는 것으로 바뀐다고도 했습니다.
이는 배열과 포인터를 표기할 때 방법이 아래와 같이 두 가지라는 것을 의미합니다.
tacos[0] == *tacos
tacos[1] == *(tacos + 1)
실제로 코드를 짤 때 이 두 가지 방법을 모두 사용할 수 있습니다.
#include <iostream>
using namespace std;
int main() {
int* tacos = new int[4];
*tacos = 0;
tacos[1] = 1;
tacos[2] = 2;
*(tacos + 3) = 3;
cout << *tacos << endl;
cout << *(tacos + 1) << endl;
cout << tacos[2] << endl;
cout << tacos[3] << endl;
return 0;
}
수고하셨습니다!
'Dev. Study Note > C++' 카테고리의 다른 글
C++의 함수 프로토타입 (0) | 2024.06.26 |
---|---|
포인터 기초 개념과 new 키워드 (0) | 2024.05.13 |
상속의 타입 (0) | 2024.02.13 |
2D Array Row, Column (0) | 2024.01.23 |
friend 키워드 (0) | 2024.01.10 |