본문 바로가기
Game DevTip/C++

11. C++ 참조자에 대하여

by LIKE IT.라이킷 2025. 7. 15.

 

참조란 무엇인가?

실생활 비유로 이해하기

  • 본명: 김철수
  • 별명: 철수, 철수야, 수야
  • 특징: 어떤 별명으로 불러도 같은 사람을 가리킴

C++의 참조와 비교시

  • 원본 변수: 실제 메모리에 저장된 변수
  • 참조 변수: 원본 변수의 또 다른 이름 (별명)
  • 특징: 참조로 접근하든 원본으로 접근하든 완전히 같은 변수

포인터 vs 참조 비교

특징 포인터 참조

개념 메모리 주소를 저장 변수의 별명
선언 int* ptr int& ref
초기화 나중에 가능 선언과 동시에 필수
재할당 다른 변수를 가리킬 수 있음 불가능
null 값 가능 (nullptr) 불가능
메모리 별도의 메모리 공간 차지 메모리 공간 차지 안함
연산 포인터 연산 가능 불가능

참조의 기본 사용법

기본 문법

#include <iostream>
using namespace std;

int main() {
    int original = 100;
    int& ref = original;  // ref는 original의 별명

    cout << "=== 초기 상태 ===" << endl;
    cout << "original: " << original << endl;
    cout << "ref: " << ref << endl;
    cout << "original 주소: " << &original << endl;
    cout << "ref 주소: " << &ref << endl;  // 같은 주소!

    // 참조를 통한 값 변경
    ref = 200;
    cout << "\\n=== ref = 200 후 ===" << endl;
    cout << "original: " << original << endl;  // 200으로 변경됨
    cout << "ref: " << ref << endl;

    // 원본을 통한 값 변경
    original = 300;
    cout << "\\n=== original = 300 후 ===" << endl;
    cout << "original: " << original << endl;
    cout << "ref: " << ref << endl;  // 300으로 변경됨

    return 0;
}

참조의 특별한 성질

#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int b = 20;

    // 참조 생성
    int& refA = a;
    cout << "refA = " << refA << endl;  // 10

    // 참조는 다른 변수를 참조할 수 없음
    // int& refA = b;  // 오류! 이미 선언된 참조

    // 새로운 참조 생성
    int& refB = b;
    cout << "refB = " << refB << endl;  // 20

    // 참조의 참조도 가능
    int& refRefA = refA;
    refRefA = 99;
    cout << "a = " << a << endl;  // 99

    return 0;
}

함수에서의 참조 활용

1. 참조로 매개변수 전달

#include <iostream>
using namespace std;

// 값 전달 (Pass by Value)
void incrementValue(int num) {
    num++;
    cout << "함수 내부 num: " << num << endl;
}

// 참조 전달 (Pass by Reference)
void incrementReference(int& num) {
    num++;
    cout << "함수 내부 num: " << num << endl;
}

// 포인터 전달 (Pass by Pointer)
void incrementPointer(int* num) {
    (*num)++;
    cout << "함수 내부 *num: " << *num << endl;
}

int main() {
    int value = 10;

    cout << "=== 원본 값 ===" << endl;
    cout << "value: " << value << endl;

    cout << "\\n=== 값 전달 테스트 ===" << endl;
    incrementValue(value);
    cout << "호출 후 value: " << value << endl;  // 변경되지 않음

    cout << "\\n=== 참조 전달 테스트 ===" << endl;
    incrementReference(value);
    cout << "호출 후 value: " << value << endl;  // 변경됨

    cout << "\\n=== 포인터 전달 테스트 ===" << endl;
    incrementPointer(&value);
    cout << "호출 후 value: " << value << endl;  // 변경됨

    return 0;
}

2. 함수 반환값으로 참조 사용

#include <iostream>
using namespace std;

// 전역 변수 (예시용)
int globalVar = 100;

// 참조 반환 함수
int& getGlobalRef() {
    return globalVar;
}

// 위험한 참조 반환 (지역 변수)
int& dangerousFunction() {
    int localVar = 50;
    // return localVar;  // 위험! 지역 변수의 참조 반환
    return globalVar;    // 안전한 대안
}

int main() {
    cout << "=== 참조 반환 함수 ===" << endl;
    cout << "globalVar: " << globalVar << endl;

    // 참조를 반환받아 직접 수정
    int& ref = getGlobalRef();
    ref = 200;
    cout << "ref 수정 후 globalVar: " << globalVar << endl;

    // 반환된 참조를 직접 사용
    getGlobalRef() = 300;
    cout << "직접 수정 후 globalVar: " << globalVar << endl;

    return 0;
}

참조와 포인터 변환

참조와 포인터 간의 관계

#include <iostream>
using namespace std;

int main() {
    int number = 42;

    // 참조 생성
    int& ref = number;

    // 참조의 주소 = 포인터
    int* ptrFromRef = &ref;

    // 포인터의 역참조 = 참조
    int& refFromPtr = *ptrFromRef;

    cout << "number: " << number << endl;
    cout << "ref: " << ref << endl;
    cout << "*ptrFromRef: " << *ptrFromRef << endl;
    cout << "refFromPtr: " << refFromPtr << endl;

    // 모두 같은 값 출력
    cout << "\\n=== 주소 비교 ===" << endl;
    cout << "&number: " << &number << endl;
    cout << "&ref: " << &ref << endl;
    cout << "ptrFromRef: " << ptrFromRef << endl;
    cout << "&refFromPtr: " << &refFromPtr << endl;

    return 0;
}

실용적인 참조 활용 예제

1. 배열 원소의 참조

#include <iostream>
using namespace std;

int main() {
    int arr[5] = {10, 20, 30, 40, 50};

    // 배열 원소의 참조 생성
    int& firstElement = arr[0];
    int& middleElement = arr[2];

    cout << "=== 초기 배열 ===" << endl;
    for (int i = 0; i < 5; i++) {
        cout << "arr[" << i << "] = " << arr[i] << endl;
    }

    // 참조를 통한 배열 수정
    firstElement = 99;
    middleElement = 88;

    cout << "\\n=== 수정된 배열 ===" << endl;
    for (int i = 0; i < 5; i++) {
        cout << "arr[" << i << "] = " << arr[i] << endl;
    }

    return 0;
}

2. 구조체와 참조

#include <iostream>
#include <string>
using namespace std;

struct Student {
    string name;
    int age;
    double gpa;
};

// 구조체를 참조로 받는 함수
void printStudent(const Student& student) {
    cout << "이름: " << student.name << endl;
    cout << "나이: " << student.age << endl;
    cout << "학점: " << student.gpa << endl;
}

// 구조체를 참조로 받아 수정하는 함수
void updateGPA(Student& student, double newGPA) {
    student.gpa = newGPA;
    cout << student.name << "의 학점을 " << newGPA << "로 업데이트했습니다." << endl;
}

int main() {
    Student student1 = {"김철수", 20, 3.5};

    cout << "=== 초기 학생 정보 ===" << endl;
    printStudent(student1);

    cout << "\\n=== 학점 업데이트 ===" << endl;
    updateGPA(student1, 4.0);

    cout << "\\n=== 업데이트된 학생 정보 ===" << endl;
    printStudent(student1);

    return 0;
}

참조 사용 시 주의사항

1. 참조는 반드시 초기화해야 함

// 올바른 사용
int number = 10;
int& ref = number;  // 선언과 동시에 초기화

// 잘못된 사용
// int& ref;  // 오류! 초기화 없는 참조 선언

2. 참조는 재할당 불가능

int a = 10;
int b = 20;
int& ref = a;  // ref는 a의 별명

// ref = b;  // 이것은 재할당이 아니라 a에 b의 값을 대입
ref = b;     // a의 값이 20으로 변경됨

3. 상수 참조 (const reference)

#include <iostream>
using namespace std;

int main() {
    int number = 100;

    // 일반 참조
    int& ref = number;
    ref = 200;  // 가능

    // 상수 참조
    const int& constRef = number;
    // constRef = 300;  // 오류! 상수 참조는 수정 불가

    cout << "number: " << number << endl;
    cout << "constRef: " << constRef << endl;

    // 리터럴에 대한 상수 참조 (특별한 경우)
    const int& literalRef = 42;  // 가능!
    cout << "literalRef: " << literalRef << endl;

    return 0;
}

언제 참조를 사용할까?

참조를 사용하는 경우

  1. 함수 매개변수: 큰 객체를 복사하지 않고 전달
  2. 함수 반환값: 체이닝이나 직접 수정이 필요할 때
  3. range-based for 루프: 컨테이너 원소에 직접 접근
  4. 별명: 긴 변수명을 간단하게 만들 때

포인터를 사용하는 경우

  1. 동적 메모리 할당: new와 delete 사용
  2. null 값 필요: 유효하지 않은 상태 표현
  3. 재할당 필요: 다른 객체를 가리켜야 할 때
  4. 포인터 연산: 배열 순회나 복잡한 주소 계산

정리

핵심 개념 요약

  1. 참조: 변수의 별명, 안전하고 간편함
  2. 초기화: 선언과 동시에 반드시 초기화
  3. 재할당: 불가능, 한 번 정해지면 끝
  4. 메모리: 별도 공간 차지 안함
  5. 함수: 매개변수와 반환값으로 효율적 사용

참조 vs 포인터 선택 가이드

  • 참조 선택: 안전하고 간단한 별명이 필요할 때
  • 포인터 선택: 유연성과 동적 할당이 필요할 때
반응형

댓글