포스트

[C++] 참조

[C++] 참조

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
  • refx는 같은 메모리 공간을 공유하므로 ref를 변경하면 x도 변경됩니다.

2. 참조의 기본 특징

  1. 한 번 참조하면 변경 불가int& ref = x; 이후 ref를 다른 변수에 참조할 수 없음.
  2. 초기화가 반드시 필요int& ref;오류 발생! (참조는 nullptr이 될 수 없음).
  3. 메모리 주소 공유 → 참조는 원본 변수와 같은 주소를 가짐.
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 체크를 제거할 수 있어 약간의 최적화 가능성이 있음.
  • 멀티스레드 환경에서는 포인터가 더 안전할 수도 있음.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.