[C++] 포인터와 레퍼런스

정채은·2025년 6월 2일

C++ 프로그래밍

목록 보기
4/16

📌포인터

🔹포인터(Pointer)란?

  • 메모리 주소를 저장하는 변수

즉, 정수를 담는 int나 부동 소수점을 담는 double 처럼 어떤 변수의 값이 아니라, 그 변수의 주소(메모리 위치)를 가리키는 변수이다.

왜 사용할까?

일반 변수에서 대입 연산(=)을 하면 변수에 있는 값이 그대로 복사된다. 이 복사를 할 때에는 비용이 든다.

배열의 경우에는 동일한 타입의 변수가 여러개 있는 것과 같으므로 비용이 커질 수도 있다.
예를 들어, 정수형 배열을 선언했을 때 크기를 1000만으로 만들 수 있다.

크기가 커지면 복사 비용이 꽤 부담스러울 수 있습니다.
이러한 복사 비용 때문에 C++에서는 값을 직접 복사하는 방식 대신 변수의 주소를 가리켜서 동일한 데이터에 접근할 수 있도록 할 수 있게 해준다.

이때 활용할 수 있는 것이 바로 포인터이다.

추가적으로 내가 알아본 건

1) 동적 메모리 할당을 하기 위해

정적인 배열은 크기를 컴파일 타임에 알아야 한다.

int size;
cin >> size;
int* arr = new int[size];  // 동적으로 배열 생성

2) 함수에서 원본 데이터를 수정하려고 할 때

void changeValue(int* p) {
    *p = 999;
}

int main() {
    int x = 5;
    changeValue(&x);  // 주소 전달
    cout << x;        // 999
}

→ 일반 변수 전달은 복사본이지만, 포인터를 넘기면 원본을 직접 수정 가능!

3) 배열을 다루기 위해

배열 이름 자체가 포인터처럼 작동하므로, 배열과 포인터는 밀접한 관계가 있다.

int arr[3] = {1, 2, 3};
int* p = arr;

cout << *(p + 1);   // 2
cout << p[2];       // 3

포인터 선언과 사용

int a = 10;
int* p = &a;  // p는 a의 주소를 저장함
  • int* p : int형 데이터를 가리키는 포인터
  • &a : 변수 a의 주소 (address-of 연산자)
  • *p : 포인터 p가 가리키는 주소에 있는 값 (역참조, dereferencing)

변수의 값 간접적으로 접근 or 수정

int a = 10;
int* p = &a; // a의 주소 저장

cout << "변경 전 a: " << a << endl;

*p = 20; // 포인터를 이용하여 값 변경

cout << "변경 후 a: " << a << endl;

/*
출력 결과:
변경 전 a: 10
변경 후 a: 20
*/

포인터와 배열

배열 이름의 의미

배열 이름은 배열의 시작 주소를 가지고 있다.


int arr[3] = {10, 20, 30};
int* p = arr;  // 배열 이름은 배열의 시작 주소

cout << *p;      // 10
cout << *(p+1);  // 20
cout << p[2];    // 30 (배열처럼 사용 가능!)

포인터로 배열처럼 접근할 수 있음 (p[i])

포인터 배열과 배열 포인터

포인터 배열은 포인터를 원소로 갖는 배열이다.
예를 들어, int* ptrArr[4];는 크기가 4이고, 각 원소가 int*인 배열이다.

배열 포인터는 배열 전체를 가리키는 포인터이다.
즉 단일 변수가 아닌 배열 통째를 가리키는 변수이다.
보통 다차원 배열을 제어할 때 많이 사용한다.

포인터 배열 vs 배열 포인터

#include <iostream>
using namespace std;

// 포인터 배열과 배열 포인터의 차이점 확인
int main() {
    int x = 1, y = 2, z = 3;
    int* ptrArr[3] = { &x, &y, &z }; // 포인터 배열 (각 원소가 int* 타입)
    
    int arr[3] = { 10, 20, 30 };
    int (*ptr)[3] = &arr; // 배열 포인터 (배열 전체를 가리킴)

    // 포인터 배열을 통한 접근
    cout << "*ptrArr[0]: " << *ptrArr[0] << endl; // 1
    cout << "*ptrArr[1]: " << *ptrArr[1] << endl; // 2
    cout << "*ptrArr[2]: " << *ptrArr[2] << endl; // 3

    // 배열 포인터를 통한 접근
    cout << "(*ptr)[0]: " << (*ptr)[0] << endl; // 10
    cout << "(*ptr)[1]: " << (*ptr)[1] << endl; // 20
    cout << "(*ptr)[2]: " << (*ptr)[2] << endl; // 30

    return 0;
}

  
/*
출력 결과:
*ptrArr[0]: 1
*ptrArr[1]: 2
*ptrArr[2]: 3
(*ptr)[0]: 10
(*ptr)[1]: 20
(*ptr)[2]: 30
*/

포인터 vs 배열 정리

항목포인터 (int* p)배열 (int arr[5])
주소 이동p++ 가능arr++ 불가능
크기포인터 자체의 크기전체 배열 크기
크기 변경동적 할당으로 가능고정 크기만 가능
선언 위치런타임에 new로 할당 가능컴파일 시 정해짐

포인터와 함수

함수에 포인터를 넘기면 원본 값을 수정할 수 있음 (Call by address)

void change(int* p) {
  *p = 100;
}

int main() {
  int x = 10;
  change(&x);  // x의 주소를 넘김
  cout << x;   // 100
}

동적 메모리 할당

int* p = new int;     // int 공간 하나 동적 생성
*p = 123;
delete p;             // 메모리 해제

int* arr = new int[5];  // int 배열 동적 생성
delete[] arr;           // 배열 해제

new와 delete는 힙 메모리를 다루는 연산자이다.


📌레퍼런스

🔹레퍼런스(reference) 란?

  • 다른 변수의 별명(alias) 을 만드는 기능.

포인터를 사용하면 주소값을 직접 다루어야 하므로 복잡해질 수 있다. 이 문제를 완화하기 위해 C++에서는 변수에 또 다른 이름을 부여하는 ‘레퍼런스’ 문법을 도입했다.

레퍼런스는 일반 변수와 거의 동일하게 사용할 수 있다. 그러나 내부적으로는 해당 변수를 직접 가리켜 주는 역할을 한다.

레퍼런스는 특정 변수에 대한 별명을 부여하는 것이다.
한 번 특정 변수의 레퍼런스를 연결하면, 이후로는 마치 그 변수가 두 개의 이름을 갖는것과 같다.


왜 사용할까?

1) 함수에서 값을 직접 수정하고 싶을 때

void change(int& x) {
  x = 999;
}

int a = 10;
change(a);
cout << a;  // 999

→ 복사본이 아닌 원본을 수정하게 된다.

2) 함수 호출 시 성능을 높이기 위해 (특히 큰 객체)

void print(const string& s) {
  cout << s << endl;
}
  • string은 무겁기 때문에 복사보다 참조 전달이 빠르고 효율적이다.

  • const를 붙이면 함수 안에서 수정 불가하다는 것도 보장됨.


포인터와 레퍼런스의 차이점

1️⃣ 선언과 초기화 시점이 다르다.
포인터는 선언 후, 나중에 = 연산자를 통해 가리킬 대상을 변경할 수 있다.
반면에 레퍼런스는 선언과 동시에 초기화해야 하며, 초기화 이후에는 다른 대상에 재연결할 수 없다.

2️⃣ 레퍼런스는 항상 다른 변수와 연결되어 있기 때문에 NULL이 없다.
반면에 포인터는 유효한 대상이 없음을 나타내기 위해 NULL 혹은 nullptr을 가질 수 있다.

3️⃣ 간접 참조 문법의 유무.
포인터는 주소값을 담으므로 접근할 때는 * 연산을 사용하고 주소를 가져올 때는 & 연산을 사용한다.
하지만 레퍼런스는 변수 자체의 별명이므로 일반 변수와 연산하는 방법이 동일하다.

항목레퍼런스 (int& r)포인터 (int* p)
NULL 가능 여부❌ 항상 유효한 변수 참조✅ NULL 포인터 가능
재할당 가능 여부❌ 다른 변수로 변경 불가✅ 다른 주소 할당 가능
사용법변수처럼 사용*, & 필요
주로 쓰는 상황함수 인자, 반환값 등동적 할당, 배열 처리 등

어떤 걸 써야 할까?

상황추천 방식이유
값을 수정해야 한다✅ 레퍼런스문법이 간단하고 안전함
nullptr을 전달하고 싶다✅ 포인터null 체크로 선택적으로 동작 가능
동적 할당을 다룬다✅ 포인터포인터가 필수적임
배열을 다룬다 (C 스타일)✅ 포인터배열은 포인터처럼 전달됨
C++스럽게 안전한 코드✅ 레퍼런스명확하고 직관적임

상수 레퍼런스

레퍼런스에 상수 제약을 걸어서 읽기 전용으로 사용할 수 있다.
상수 레퍼런스를 사용하면 값을 복사하지 않고도 기존 변수를 보호할 수 있다.

예를 들어, const int& cref = x; 하면 복사 과정 없이 x의 값을 읽을 수는 있지만 x값을 수정할 수는 없다.

// 상수 레퍼런스를 사용하여 변수를 보호하는 예제
int main() {
  int x = 100;
  const int& cref = x; // x를 읽기 전용으로 참조

  cout << "cref: " << cref << endl; // 100

  // cref = 200; // ❌ 오류 발생! 상수 레퍼런스는 값을 변경할 수 없음

  x = 200; // 원본 변수 x는 변경 가능
  cout << "x 변경 후 cref: " << cref << endl; // 200

/*
출력 결과:
cref: 100
x 변경 후 cref: 200
*/

profile
누군가에게 추억을 만들어주는 그날까지

0개의 댓글