C++ 포인터와 참조 완전 정복
개요 — 포인터 vs 참조 비교
Section titled “개요 — 포인터 vs 참조 비교”포인터와 참조는 모두 다른 객체를 간접적으로 접근하는 수단이지만, 설계 철학과 제약이 다릅니다.
| 특성 | 포인터 (T*) | 참조 (T&) |
|---|---|---|
| null 가능 여부 | 가능 (nullptr) | 불가 (반드시 유효한 객체 필요) |
| 재지정 가능 여부 | 가능 (다른 주소 대입) | 불가 (선언 시 바인딩 고정) |
| 산술 연산 | 가능 (ptr++, ptr + n) | 불가 |
| 역참조 연산자 | *ptr, ptr-> | 불필요 (. 연산자 사용) |
| 배열 표현 | 자연스러움 | 부적합 |
| 주 용도 | 동적 할당, 선택적 소유, 배열 | 함수 파라미터, 별칭 |
1. 포인터 기초
Section titled “1. 포인터 기초”1.1 포인터 선언과 역참조
Section titled “1.1 포인터 선언과 역참조”#include <iostream>
int main(){ int value = 42; int* ptr = &value; // ptr은 value의 주소를 저장
std::cout << "value = " << value << "\n"; // 42 std::cout << "&value = " << &value << "\n"; // 주소값 (예: 0x7ffd...) std::cout << "ptr = " << ptr << "\n"; // 동일한 주소 std::cout << "*ptr = " << *ptr << "\n"; // 42 (역참조)
*ptr = 100; // 포인터를 통해 원본 수정 std::cout << "value after = " << value << "\n"; // 100
return 0;}1.2 nullptr과 포인터 안전 사용
Section titled “1.2 nullptr과 포인터 안전 사용”#include <iostream>
struct Node{ int Data; Node* Next;
explicit Node(int data) : Data(data), Next(nullptr) {}};
void PrintNode(const Node* node){ // 역참조 전 항상 null 체크 if (node == nullptr) { std::cout << "null\n"; return; } std::cout << "Data: " << node->Data << "\n";}
int main(){ Node* head = new Node(10); head->Next = new Node(20);
PrintNode(head); // Data: 10 PrintNode(head->Next); // Data: 20 PrintNode(nullptr); // null
// 해제 후 nullptr 대입 (Dangling Pointer 방지) delete head->Next; head->Next = nullptr; delete head; head = nullptr;
return 0;}2. 참조(lvalue reference)
Section titled “2. 참조(lvalue reference)”2.1 참조의 특성
Section titled “2.1 참조의 특성”참조는 선언 시 반드시 초기화해야 하며, 이후 다른 객체를 참조하도록 변경할 수 없습니다.
#include <iostream>
int main(){ int a = 10; int b = 20;
int& ref = a; // ref는 a의 별칭 ref = 100; // a가 100으로 바뀜 (ref 자체가 다른 곳을 가리키는 게 아님)
std::cout << "a = " << a << "\n"; // 100 std::cout << "ref = " << ref << "\n"; // 100
// ref = b; 는 "ref가 b를 가리키도록" 변경하는 것이 아니라 // "a에 b의 값(20)을 대입" 하는 것 ref = b; std::cout << "a = " << a << "\n"; // 20 (b의 값이 a에 복사됨)
return 0;}2.2 함수 파라미터에서 참조 활용
Section titled “2.2 함수 파라미터에서 참조 활용”#include <iostream>#include <string>
// 값 전달: 복사본을 수정 → 원본 영향 없음void IncreaseByValue(int n){ n += 10; // 복사본 수정}
// 참조 전달: 원본을 직접 수정void IncreaseByRef(int& n){ n += 10; // 원본 수정}
// const 참조: 복사 없이 읽기 전용으로 전달 (큰 객체에 효율적)void PrintName(const std::string& name){ std::cout << "Name: " << name << "\n"; // name = "Other"; // 컴파일 에러: const 참조는 수정 불가}
int main(){ int x = 5; IncreaseByValue(x); std::cout << x << "\n"; // 5 (변화 없음)
IncreaseByRef(x); std::cout << x << "\n"; // 15
std::string playerName = "Alice"; PrintName(playerName); // 복사 없이 전달
return 0;}3. const 포인터 구분
Section titled “3. const 포인터 구분”const의 위치에 따라 의미가 완전히 달라집니다. 이 부분이 가장 많이 혼동됩니다.
#include <iostream>
int main(){ int a = 10; int b = 20;
// 1. 포인터 to const: 가리키는 값을 변경 불가, 포인터 자체는 변경 가능 const int* p1 = &a; // *p1 = 100; // 컴파일 에러: 값 수정 불가 p1 = &b; // OK: 포인터 자체는 재지정 가능
// 2. const 포인터: 포인터 자체를 변경 불가, 가리키는 값은 변경 가능 int* const p2 = &a; *p2 = 100; // OK: 값 수정 가능 // p2 = &b; // 컴파일 에러: 포인터 재지정 불가
// 3. const 포인터 to const: 둘 다 변경 불가 const int* const p3 = &a; // *p3 = 200; // 컴파일 에러 // p3 = &b; // 컴파일 에러
std::cout << *p1 << "\n"; // 20 std::cout << *p2 << "\n"; // 100 std::cout << *p3 << "\n"; // 100
return 0;}const 포인터 읽기 요령
Section titled “const 포인터 읽기 요령”오른쪽에서 왼쪽으로 읽으면 됩니다.
const int* → int(를) const(인 상태로) 가리키는 포인터int* const → const(고정된) 포인터, int를 가리킴const int* const → const(고정된) 포인터, const int를 가리킴4. 이중 포인터
Section titled “4. 이중 포인터”4.1 이중 포인터 기본
Section titled “4.1 이중 포인터 기본”이중 포인터(T**)는 포인터의 주소를 저장합니다. 함수 내부에서 포인터 자체를 수정해야 할 때 사용합니다.
#include <iostream>
// 포인터를 함수 내부에서 새로운 주소로 변경void AllocateBuffer(int** ppBuffer, int size){ *ppBuffer = new int[size]; for (int i = 0; i < size; ++i) { (*ppBuffer)[i] = i; }}
int main(){ int* buffer = nullptr; AllocateBuffer(&buffer, 5);
for (int i = 0; i < 5; ++i) { std::cout << buffer[i] << " "; // 0 1 2 3 4 } std::cout << "\n";
delete[] buffer; buffer = nullptr;
return 0;}4.2 2D 배열과 이중 포인터
Section titled “4.2 2D 배열과 이중 포인터”#include <iostream>
int main(){ const int rows = 3; const int cols = 4;
// 동적 2D 배열 (포인터 배열 방식) int** matrix = new int*[rows]; for (int i = 0; i < rows; ++i) { matrix[i] = new int[cols]; for (int j = 0; j < cols; ++j) { matrix[i][j] = i * cols + j; } }
// 출력 for (int i = 0; i < rows; ++i) { for (int j = 0; j < cols; ++j) { std::cout << matrix[i][j] << "\t"; } std::cout << "\n"; }
// 해제 (역순) for (int i = 0; i < rows; ++i) { delete[] matrix[i]; } delete[] matrix; matrix = nullptr;
return 0;}5. 함수 포인터
Section titled “5. 함수 포인터”5.1 함수 포인터 선언과 호출
Section titled “5.1 함수 포인터 선언과 호출”#include <iostream>
int Add(int a, int b) { return a + b; }int Mul(int a, int b) { return a * b; }int Sub(int a, int b) { return a - b; }
int main(){ // 함수 포인터 선언: 반환타입 (*변수명)(파라미터타입들) int (*operation)(int, int) = nullptr;
operation = Add; std::cout << "Add: " << operation(3, 4) << "\n"; // 7
operation = Mul; std::cout << "Mul: " << operation(3, 4) << "\n"; // 12
operation = Sub; std::cout << "Sub: " << operation(3, 4) << "\n"; // -1
return 0;}5.2 함수 포인터 배열과 콜백 패턴
Section titled “5.2 함수 포인터 배열과 콜백 패턴”#include <iostream>#include <string>
// 콜백 함수 타입using EventCallback = void (*)(const std::string& eventName);
void OnClick(const std::string& eventName){ std::cout << "[Click] " << eventName << "\n";}
void OnHover(const std::string& eventName){ std::cout << "[Hover] " << eventName << "\n";}
// 함수 포인터를 받아 나중에 호출 (콜백)class Button{public: void SetClickCallback(EventCallback callback) { m_OnClick = callback; }
void Click(const std::string& name) { if (m_OnClick) { m_OnClick(name); } }
private: EventCallback m_OnClick = nullptr;};
int main(){ Button btn; btn.SetClickCallback(OnClick); btn.Click("StartButton"); // [Click] StartButton
btn.SetClickCallback(OnHover); btn.Click("ExitButton"); // [Hover] ExitButton
return 0;}6. 실전 혼동 케이스 정리
Section titled “6. 실전 혼동 케이스 정리”6.1 포인터 배열 vs 배열 포인터
Section titled “6.1 포인터 배열 vs 배열 포인터”int main(){ int a = 1, b = 2, c = 3;
// 포인터 배열: int*를 원소로 갖는 배열 int* ptrArray[3] = { &a, &b, &c }; std::cout << *ptrArray[1] << "\n"; // 2
// 배열 포인터: int[3] 배열 전체를 가리키는 포인터 int arr[3] = { 10, 20, 30 }; int (*arrayPtr)[3] = &arr; std::cout << (*arrayPtr)[2] << "\n"; // 30
return 0;}6.2 포인터 vs 참조 선택 기준
Section titled “6.2 포인터 vs 참조 선택 기준”#include <string>
// 참조를 사용해야 하는 경우: null이 될 수 없고, 재지정이 필요 없을 때void AppendSuffix(std::string& text, const std::string& suffix){ text += suffix;}
// 포인터를 사용해야 하는 경우: null일 수 있는 선택적 파라미터void PrintIfNotNull(const std::string* text){ if (text) { std::cout << *text << "\n"; }}
// 반환 타입: 항상 유효한 객체면 참조, null 가능성 있으면 포인터const std::string& GetNameRef(const Player& player){ return player.Name; // 항상 유효}
const std::string* FindPlayer(const std::string& name){ // 찾지 못하면 nullptr 반환 가능 return nullptr;}6.3 const 참조와 임시 객체
Section titled “6.3 const 참조와 임시 객체”#include <string>#include <iostream>
std::string GetGreeting() { return "Hello, World!"; }
int main(){ // const 참조는 임시 객체(rvalue)에 바인딩 가능 // 임시 객체의 수명이 참조의 수명만큼 연장됨 const std::string& ref = GetGreeting(); std::cout << ref << "\n"; // OK
// 비-const 참조는 임시 객체에 바인딩 불가 // std::string& badRef = GetGreeting(); // 컴파일 에러
return 0;}| 개념 | 핵심 요약 |
|---|---|
| 포인터 | 주소 저장, null 가능, 재지정 가능, 산술 연산 지원 |
| 참조 | 별칭, null 불가, 재지정 불가, 초기화 필수 |
| const 포인터 | const T* = 값 불변, T* const = 주소 불변, 위치로 구분 |
| 이중 포인터 | 함수 내에서 포인터 자체를 수정할 때 사용 |
| 함수 포인터 | 함수를 값처럼 저장·전달, 콜백 패턴의 기반 |