나만의 작은 도서관

[C++] 포인터(Pointer) 본문

C++/문법 및 메소드(STL)

[C++] 포인터(Pointer)

pledge24 2025. 3. 13. 03:01

포인터(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타입의 변수임

 


참고 자료

https://modoocode.com/23

https://modoocode.com/24

https://modoocode.com/25