C++ 참조(Reference) 정리
1. C++ 참조(Reference)란?
참조(Reference)는 변수의 별칭(Alias) 으로, 원본 변수와 같은 메모리 주소를 공유합니다.
1
2
3
4
| int x = 10;
int& ref = x; // x를 참조하는 ref 선언
ref = 20; // x의 값도 20으로 변경됨
std::cout << x; // 출력: 20
|
ref와 x는 같은 메모리 공간을 공유하므로 ref를 변경하면 x도 변경됩니다.
2. 참조의 기본 특징
- 한 번 참조하면 변경 불가 →
int& ref = x; 이후 ref를 다른 변수에 참조할 수 없음. - 초기화가 반드시 필요 →
int& ref; 는 오류 발생! (참조는 nullptr이 될 수 없음). - 메모리 주소 공유 → 참조는 원본 변수와 같은 주소를 가짐.
1
2
3
| int x = 10;
int& ref = x;
std::cout << &x << " == " << &ref << std::endl; // 같은 주소
|
3. 참조의 종류
🔹 (1) 일반 참조 (Lvalue Reference)
1
2
3
| int x = 10;
int& ref = x;
ref = 30; // x도 30으로 변경됨
|
🔹 (2) 상수 참조 (Const Reference)
1
2
| const int x = 10;
const int& ref = x; // x를 읽기 전용으로 참조
|
🔹 (3) Rvalue 참조 (Move Reference, &&)
1
2
| int&& rref = 10; // 임시 값(10)을 참조
std::cout << rref; // 10
|
- Rvalue 참조(
&&)를 사용하면 불필요한 복사를 방지하여 성능 최적화 가능.
4. 참조 vs 포인터 비교
| 구분 | 참조 (&) | 포인터 (*) |
|---|
| NULL 가능 여부 | ❌ 불가능 | ✅ 가능 (nullptr) |
| 초기화 여부 | ✅ 반드시 초기화 필요 | ❌ 필요 없음 |
| 변경 가능 여부 | ❌ 한 번 설정하면 변경 불가 | ✅ 가리키는 주소 변경 가능 |
| 역참조 필요 여부 | ❌ 불필요 (직접 사용) | ✅ 필요 (*ptr) |
1
2
3
4
5
6
| int x = 10;
int& ref = x; // 참조
int* ptr = &x; // 포인터
*ptr = 20; // x = 20
ref = 30; // x = 30
|
5. 참조 vs 포인터: 언제 사용해야 할까?
🔹 참조를 사용하는 것이 좋은 경우
(1) 함수 인자로 원본 데이터를 수정할 때
1
2
3
4
5
6
| void modify(int& num) { num += 10; }
int main() {
int x = 10;
modify(x);
std::cout << x; // 20
}
|
(2) 함수에서 불필요한 복사를 방지할 때
1
2
3
| void print(const std::string& text) { std::cout << text << std::endl; }
std::string str = "Long Text";
print(str); // 복사 없이 원본 사용
|
(3) 특정 변수를 별칭(alias)로 사용할 때
1
2
3
4
| int x = 100;
int& alias = x; // x의 별칭
alias = 200;
std::cout << x; // 200
|
(4) Rvalue를 활용하여 성능 최적화할 때 (이동 시멘틱)
1
2
| void process(std::string&& str) { std::cout << "Processing: " << str << std::endl; }
process("Temporary String");
|
🔹 포인터를 사용하는 것이 좋은 경우
(1) 참조를 변경해야 할 때
1
2
3
| int a = 10, b = 20;
int* ptr = &a;
ptr = &b; // ✅ 가능
|
(2) nullptr을 허용해야 할 때
1
| int* ptr = nullptr; // ✅ 가능
|
(3) 동적 메모리 할당을 할 때 (new/delete)
1
2
| int* ptr = new int(10);
delete ptr;
|
(4) 배열을 가리킬 때
1
2
3
| int arr[3] = {10, 20, 30};
int* ptr = arr;
ptr++;
|
(5) 함수에서 변수의 주소를 저장해야 할 때
1
2
3
4
| void modifyPointer(int** ptr) {
static int y = 20;
*ptr = &y;
}
|
6. 참조 vs 포인터 비교 정리
| 사용 목적 | 참조 (&) | 포인터 (*) |
|---|
| 값을 변경해야 하는 경우 | ✅ 사용 가능 | ✅ 사용 가능 |
| 참조하는 대상 변경 가능 여부 | ❌ 불가능 | ✅ 가능 |
NULL(nullptr) 가능 여부 | ❌ 불가능 | ✅ 가능 |
불필요한 복사 방지 (const &) | ✅ 사용 가능 | ❌ 불필요한 복사 발생 가능 |
동적 메모리 할당 (new/delete) | ❌ 불가능 | ✅ 가능 |
배열을 다룰 때 (ptr++) | ❌ 불가능 | ✅ 가능 |
7. 결론
- 참조(
&): 안전한 별칭, 함수 인자로 사용할 때, 복사 방지(const &) - 포인터(
*): 가리키는 대상 변경 가능, nullptr 허용, 동적 메모리 할당, 배열 처리 - Rvalue 참조(
&&): 이동 연산자(move semantics)를 활용하여 성능 최적화
C++ 참조(Reference)와 포인터(Pointer)의 성능 비교
1. 참조와 포인터는 성능이 동일할까?
C++에서 참조(&)와 포인터(*)는 모두 변수의 주소를 활용하여 간접적으로 데이터를 다루지만, 로우레벨에서 완전히 동일하게 동작한다고 볼 수는 없습니다.
대부분의 경우 참조는 포인터와 유사한 기계어(어셈블리 코드)로 변환되므로 성능 차이는 미미하지만, 특정 상황에서는 차이가 발생할 수도 있습니다.
2. 참조와 포인터의 로우레벨(어셈블리) 동작 비교
🔹 C++ 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
| void modify(int& ref) { // 참조 사용
ref = 20;
}
void modifyPtr(int* ptr) { // 포인터 사용
*ptr = 20;
}
int main() {
int x = 10;
modify(x); // 참조 함수 호출
modifyPtr(&x); // 포인터 함수 호출
}
|
🔹 GCC 컴파일 후 어셈블리 코드 (x86-64)
modify(int&):
mov DWORD PTR [rdi], 20 ; 참조를 포인터처럼 다룸
ret
modifyPtr(int*):
mov DWORD PTR [rdi], 20 ; 포인터를 역참조하여 값 변경
ret
- 참조(
int& ref)와 포인터(int* ptr)의 어셈블리 코드는 동일하게 변환됨. - 즉, 함수 인자로 참조를 사용해도 내부적으로는 포인터와 동일한 방식으로 처리됨.
- 따라서 단순한 변수 참조는 성능 차이가 없음.
3. 성능 차이가 발생할 수 있는 경우
(1) 최적화 관점: 참조가 더 빠를 수 있음
컴파일러는 참조가 절대 nullptr이 될 수 없음을 보장하므로, 최적화에서 유리할 수 있습니다.
1
2
3
4
5
6
7
8
9
| void modify(int& ref) {
ref += 10; // 참조는 null-check 불필요
}
void modifyPtr(int* ptr) {
if (ptr) { // 포인터는 null-check 필요
*ptr += 10;
}
}
|
- 컴파일러는 참조가 항상 유효한 변수임을 보장하기 때문에, 불필요한
nullptr 체크를 제거하고 최적화할 수 있음. - 반면, 포인터는
nullptr 체크가 필요할 수도 있어, 약간의 성능 차이가 발생할 가능성이 있음.
(2) 참조의 한계: 참조를 변경할 수 없음
1
2
3
4
| int x = 10, y = 20;
int& ref = x; // ref는 x를 참조
ref = y; // ❌ ref가 y를 참조하는 것이 아니라, x의 값을 y로 변경
|
- 참조는 한 번 특정 변수에 연결되면 변경할 수 없음.
- 이 때문에 가변적인 주소를 다뤄야 할 경우 포인터가 필수적이며, 성능 면에서 더 유리할 수도 있음.
(3) 객체 지향 프로그래밍(OOP)에서 참조가 더 유리한 경우
클래스의 멤버 변수로 포인터를 사용할 경우, 동적으로 할당된 객체를 추적하는데 추가 비용이 발생할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class MyClass {
private:
int& ref; // 참조 (추적 비용 없음)
int* ptr; // 포인터 (NULL 체크 필요)
public:
MyClass(int& r, int* p) : ref(r), ptr(p) {}
void update() {
ref += 10; // ✅ 직접 접근 (빠름)
if (ptr) { // ❌ NULL 체크 필요 (성능 저하 가능)
*ptr += 10;
}
}
};
|
- 클래스 내부에서 참조를 사용하면 NULL 체크가 불필요하여 성능이 향상될 가능성이 있음.
- 반면, 포인터를 사용할 경우 NULL인지 체크해야 하는 비용이 추가될 수 있음.
(4) 멀티스레딩 환경에서 포인터가 더 유리한 경우
멀티스레딩 환경에서는 참조보다는 포인터를 사용하는 것이 안전할 수도 있음.
1
2
3
4
5
6
7
8
9
| void threadFunction(int& ref) {
ref += 1; // 🚨 데이터 레이스 발생 가능
}
void threadFunction(int* ptr) {
if (ptr) {
*ptr += 1; // ✅ 더 안전함
}
}
|
- 참조는
nullptr을 가질 수 없어 NULL 체크를 할 수 없기 때문에, 스레드가 예기치 않게 동작할 수 있음. - 멀티스레딩 환경에서는 안전성을 고려하여 포인터를 선호하는 경우가 있음.
4. 참조 vs 포인터: 성능 차이 정리
| 구분 | 참조 (&) | 포인터 (*) |
|---|
| 어셈블리 변환 | ✅ 포인터와 동일 | ✅ 동일 |
| 최적화 가능성 | ✅ NULL 체크 불필요 | ❌ NULL 체크 필요 가능성 |
| 가변성 | ❌ 변경 불가능 | ✅ 변경 가능 |
| 멀티스레드 안정성 | 🚨 위험 가능성 있음 | ✅ NULL 체크 가능 |
5. 결론
- 일반적인 성능은 동일하지만, 최적화 여부, 가변성, 스레드 안전성 등에 따라 차이가 발생할 수 있음.
- 컴파일러가 최적화를 잘 수행하면 성능 차이는 거의 없지만, 참조는 불필요한 NULL 체크를 제거할 수 있어 약간의 최적화 가능성이 있음.
- 멀티스레드 환경에서는 포인터가 더 안전할 수도 있음.