나만의 작은 도서관
[C++] 포인터(Pointer) 본문
포인터(Pointer)란?
포인터는 메모리 상에 위치한 특정한 데이터의 (시작) 주소값을 저장하는 변수를 말한다.
포인터 선언
// [포인터 타입]* [포인터 이름]
int* p [= nullptr];
참조(&) 연산자와 역참조(*) 연산자
- 참조(&): 변수 앞에 '&'를 붙이면 값 대신 주소를 반환한다.
- 역참조(*): 변수 앞에 '*'를 붙이면 변수가 저장 중인 주소값에 저장된 값을 반환한다. 역참조는 포인터 변수만 사용할 수 있다.
int a = 10;
int* ptr = &a;
cout << &a << '\n'; // 변수 a의 주소 반환(참조)
cout << *ptr << '\n'; // ptr의 저장중인 주소값(변수 a)에 저장된 값(10)을 반환(역참조)
포인터와 멤버 데이터
포인터 변수의 타입이 struct 또는 class인 경우, 멤버 데이터를 꺼낼 땐 화살표('->')를 사용한다.
#include <iostream>
using namespace std;
struct A {
void func1() {cout << "func1" << '\n';};
int var = 10;
};
int main(void){
A a;
A *ptr = &a;
// [포인터 변수 이름]->[멤버 데이터 이름]
ptr->func1(); // 실행 결과: func1
ptr->var; // 10
return 0;
}
포인터의 특징
포인터는 고정 크기
- 포인터 변수는 타입에 상관없이 4바이트(32비트 환경) 또는 8바이트(64비트 환경) 고정 크기를 가진다.
- 32비트 주소를 사용한다면 RAM은 4GB까지만 사용 가능하다. (32 bits = 10^9 * 4)
포인터 타입을 적어주는 이유
- 포인터 변수는 데이터의 시작 주소만 저장하고 있기 때문에, 데이터를 온전히 읽기 위해선 데이터가 어디까지 저장되어 있는지 알아야 한다.
- 이를 위해 포인터 타입을 적어줌으로써 읽어야 할 메모리의 크기를 명시한다.
int a = 10;
int* ptr = &a; // ptr의 주소값이 0x10이라면, 0x13까지 읽는다.(int 타입은 4바이트)
void 포인터
- void는 모든 타입의 포인터 변수를 받을 수 있는 포인터 타입이다.
- void 포인터는 주소값을 저장하고 있지만, 주소가 가리키는 값이 어떤 타입인지 모른다.
- 역참조를 통해 값을 읽기 위해선 명시적 캐스팅을 해야 한다.
- 임의의 변수를 void 포인터로 캐스팅하면 주소를 얻을 수 있다. (ex. 함수 주소)
#include <iostream>
using namespace std;
void func() {
cout << "Hello World" << '\n';
}
int main(void){
int a = 10;
void* p = &a;
cout << *(int*)p << endl; // void* -> int* 캐스팅 후, 역참조
cout << (void*)func << endl; // func 함수의 주소를 반환
return 0;
}
포인터의 덧셈과 뺄셈
- (ptr + n) 또는 (ptr - n)과 같이, 포인터 변수에 정수값을 더하거나 빼면 (포인터 타입 * n) 만큼 주소가 이동한다. 이는 정적 배열에서 index로 이동할 때 활용된다.
int a;
int* ptr_a = &a;
cout << ptr_a << '\n';
cout << ptr_a + 1 << '\n';
int arr[4] = {1, 2, 3, 4};
for(int i = 0; i < 4; i++)
{
cout << *(arr + i) << ' ';
}
실행 결과
0x61fe0c
0x61fe10
1 2 3 4
포인터와 const
- 일반 변수와 다르게, 포인터는 const를 어디에 붙이느냐에 따라 의미가 달라진다.
- const int* p : 역참조값에 const 속성이 부여된다.
- int* const p : 포인터 변수에 const 속성이 부여된다.
int num1 = 100;
int num2 = 200;
// 포인터 변수 주소값 변경
{
int* const p1 = &num1;
const int* p2 = &num1;
const int* const p3 = &num1;
p1 = &num2; /*식이 수정할 수 있는 lvalue여야 합니다.*/
p2 = &num2; /*가능*/
p3 = &num2; /*식이 수정할 수 있는 lvalue여야 합니다.*/
}
// 역참조값 변경.
{
int* const p1 = &num1;
const int* p2 = &num1;
const int* const p3 = &num1;
*p1 = num2; /*가능*/
*p2 = num2; /*식이 수정할 수 있는 lvalue여야 합니다.*/
*p3 = num2; /*식이 수정할 수 있는 lvalue여야 합니다.*/
}
다중 포인터
- 역참조를 여러 번 할 수 있는 포인터이다. 대입 시 역참조(*)를 할 수 있는 횟수 = 참조(&)한 횟수가 되어야 한다.
- 보통은 '*' 개수가 하나 적은 포인터의 주소를 넣어준다.
int* ptr1;
int** ptr2 = &ptr1;
포인터 배열, 배열 포인터
- 포인터 배열 : 포인터가 들어있는 배열
- 배열 포인터 : 배열을 가리키는 포인터
int (*ptr_arr)[3]; // 배열 포인터 : ptr_arr -> [int][int][int]
int *ptr_arr[3]; // 포인터 배열 : [int *][int *][int *]
int arr[3] = {1, 2, 3};
int (*ptr_arr)[3] = &arr;
int *ptr_arr2[3] = {&arr[0], &arr[1], &arr[2]};
개인적인 이해 방식
- 배열 '[ ]' 위치를 앞으로 당겨서 보면 이해가 쉽다.
int *ptr_arr[3] => (int* [3]) ptr_arr; // int* 타입의 데이터 3개를 담은 배열
int (*ptr_arr)[3] => (int [3]) *ptr_arr; // int 타입의 데이터 3개를 담은 배열을 가리키는 포인터
int*** ptr_arr[3] => (int*** [3]) ptr_arr; // int*** 타입의 데이터 3개를 담은 배열
int** (*ptr_arr)[3] => (int** [3]) *ptr_arr; // int** 타입의 데이터 3개를 담은 배열을 가리키는 포인터
포인터 표기에 대한 고찰
- 포인터를 선언할 때 "int* p" 스타일과 "int *p" 스타일이 있다. 어떤 스타일로 사용하든 결과는 같지만, 각각의 스타일로 표기했을 때 서로 다른 의도를 남길 수 있다.
- int* p : "포인터 타입"임을 강조하는 스타일. 레퍼런스(int&) 선언 스타일과 일관되게 사용하기 좋다. (개인적인 생각)
- int *p : "포인터 변수"임을 강조하는 스타일. 포인터 변수를 여러 개 선언 시 실수를 줄일 수 있다.
int *p, *q, *r;
int *p, q, r; // q나 r은 포인터가 아닌 int타입의 변수임
참고 자료
'C++ > 문법 및 메소드(STL)' 카테고리의 다른 글
[C++][STL] 반복자(Iterator) (0) | 2025.03.13 |
---|---|
[C++] 레퍼런스(Reference) (0) | 2025.03.13 |
[C++][Keyword] 열거형(enum, enum class) (0) | 2025.02.19 |
[C++][Class] struct와 class의 차이 (0) | 2025.02.19 |
[C++][Keyword] 정적 변수(static 키워드) (0) | 2025.02.18 |