즉, 정수를 담는 int나 부동 소수점을 담는 double 처럼 어떤 변수의 값이 아니라, 그 변수의 주소(메모리 위치)를 가리키는 변수이다.
일반 변수에서 대입 연산(=)을 하면 변수에 있는 값이 그대로 복사된다. 이 복사를 할 때에는 비용이 든다.
배열의 경우에는 동일한 타입의 변수가 여러개 있는 것과 같으므로 비용이 커질 수도 있다.
예를 들어, 정수형 배열을 선언했을 때 크기를 1000만으로 만들 수 있다.
크기가 커지면 복사 비용이 꽤 부담스러울 수 있습니다.
이러한 복사 비용 때문에 C++에서는 값을 직접 복사하는 방식 대신 변수의 주소를 가리켜서 동일한 데이터에 접근할 수 있도록 할 수 있게 해준다.
이때 활용할 수 있는 것이 바로 포인터이다.
추가적으로 내가 알아본 건
정적인 배열은 크기를 컴파일 타임에 알아야 한다.
int size;
cin >> size;
int* arr = new int[size]; // 동적으로 배열 생성
void changeValue(int* p) {
*p = 999;
}
int main() {
int x = 5;
changeValue(&x); // 주소 전달
cout << x; // 999
}
→ 일반 변수 전달은 복사본이지만, 포인터를 넘기면 원본을 직접 수정 가능!
배열 이름 자체가 포인터처럼 작동하므로, 배열과 포인터는 밀접한 관계가 있다.
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)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*인 배열이다.
배열 포인터는 배열 전체를 가리키는 포인터이다.
즉 단일 변수가 아닌 배열 통째를 가리키는 변수이다.
보통 다차원 배열을 제어할 때 많이 사용한다.
#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
*/
| 항목 | 포인터 (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는 힙 메모리를 다루는 연산자이다.
포인터를 사용하면 주소값을 직접 다루어야 하므로 복잡해질 수 있다. 이 문제를 완화하기 위해 C++에서는 변수에 또 다른 이름을 부여하는 ‘레퍼런스’ 문법을 도입했다.
레퍼런스는 일반 변수와 거의 동일하게 사용할 수 있다. 그러나 내부적으로는 해당 변수를 직접 가리켜 주는 역할을 한다.
레퍼런스는 특정 변수에 대한 별명을 부여하는 것이다.
한 번 특정 변수의 레퍼런스를 연결하면, 이후로는 마치 그 변수가 두 개의 이름을 갖는것과 같다.
void change(int& x) {
x = 999;
}
int a = 10;
change(a);
cout << a; // 999
→ 복사본이 아닌 원본을 수정하게 된다.
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
*/