Skip to content

C++ 포인터와 참조 완전 정복

포인터와 참조는 모두 다른 객체를 간접적으로 접근하는 수단이지만, 설계 철학과 제약이 다릅니다.

특성포인터 (T*)참조 (T&)
null 가능 여부가능 (nullptr)불가 (반드시 유효한 객체 필요)
재지정 가능 여부가능 (다른 주소 대입)불가 (선언 시 바인딩 고정)
산술 연산가능 (ptr++, ptr + n)불가
역참조 연산자*ptr, ptr->불필요 (. 연산자 사용)
배열 표현자연스러움부적합
주 용도동적 할당, 선택적 소유, 배열함수 파라미터, 별칭

#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;
}
#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;
}

참조는 선언 시 반드시 초기화해야 하며, 이후 다른 객체를 참조하도록 변경할 수 없습니다.

#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;
}

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 int* → int(를) const(인 상태로) 가리키는 포인터
int* const → const(고정된) 포인터, int를 가리킴
const int* const → const(고정된) 포인터, const int를 가리킴

이중 포인터(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;
}
#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;
}

#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;
}

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;
}
#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;
}
#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 = 주소 불변, 위치로 구분
이중 포인터함수 내에서 포인터 자체를 수정할 때 사용
함수 포인터함수를 값처럼 저장·전달, 콜백 패턴의 기반