[Day9] C++ Pointer & Reference(2)

베리투스·2025년 8월 14일

TIL: Today I Learned

목록 보기
17/93

지난번에 포인터와 레퍼런스의 개념을 익혔고, 이제는 실전 코드로 제대로 다뤄보았다! 💪 포인터를 이용해 배열을 자유자재로 다루는 포인터 연산부터, 포인터 사용 시 가장 조심해야 할 메모리 접근 오류와 안전장치 nullptr까지 직접 코드를 짜며 경험해 보았다. 레퍼런스를 사용했을 때 코드가 얼마나 간결하고 안전해지는지도 체감할 수 있었다. 코드로 그 차이를 명확하게 정리했다.


📌 목표

  • 포인터의 역참조(*)와 주소 연산(&)을 코드로 구현할 수 있다.
  • 포인터 연산(산술 연산)의 원리를 이해하고 배열에 활용할 수 있다.
  • 초기화되지 않은 포인터의 위험성과 nullptr의 필요성을 안다.
  • 코드 레벨에서 포인터와 레퍼런스의 사용법 차이를 비교할 수 있다.

💻 코드

1. 포인터 기본 연산 (값 변경 및 접근)

포인터의 가장 강력한 기능은 '간접적으로' 다른 변수의 값을 읽고 수정하는 것이다.

#include <iostream>

int main() {
    int num = 10;
    int* ptr = &num; // ptr은 num의 주소를 가리킴

    std::cout << "변경 전 num의 값: " << num << std::endl;
    
    // 포인터를 이용해 num의 값을 변경
    *ptr = 20; // ptr이 가리키는 주소로 가서 값을 20으로 변경
    
    std::cout << "변경 후 num의 값: " << num << std::endl;
    std::cout << "ptr이 가리키는 값: " << *ptr << std::endl;

    return 0;
}
// 출력 결과:
// 변경 전 num의 값: 10
// 변경 후 num의 값: 20
// ptr이 가리키는 값: 20

2. 포인터와 배열 (포인터 연산)

배열의 이름은 사실 그 배열의 첫 번째 요소의 주소를 가리키는 포인터 상수이다. 이 성질을 이용하면 포인터로 배열 요소를 순회할 수 있다.

  • ptr + 1의 의미: ptr의 주소값에 1을 더하는 게 아니라, ptr이 가리키는 타입의 크기만큼 주소를 이동시키는 것이다. int*라면 4바이트, char*라면 1바이트 뒤로 이동한다.
#include <iostream>

int main() {
    int arr[3] = {100, 200, 300};
    int* ptr = arr; // 배열 이름 arr은 첫 번째 요소(&arr[0])의 주소와 같음

    // 포인터 연산으로 배열 요소 접근
    std::cout << "첫 번째 요소: " << *ptr << std::endl;
    std::cout << "두 번째 요소: " << *(ptr + 1) << std::endl;
    std::cout << "세 번째 요소: " << *(ptr + 2) << std::endl;
    
    // 배열 인덱스 표현과 동일하게 동작함
    std::cout << "ptr[1]의 값: " << ptr[1] << std::endl; // *(ptr + 1)과 같음

    return 0;
}
// 출력 결과:
// 첫 번째 요소: 100
// 두 번째 요소: 200
// 세 번째 요소: 300
// ptr[1]의 값: 200

3. 포인터의 위험성 & nullptr

초기화되지 않은 포인터는 쓰레기 값을 주소로 가지기 때문에, 이를 역참조하면 프로그램이 비정상 종료될 수 있다. 이를 '댕글링 포인터' 문제라고도 한다. 따라서 포인터가 아무것도 가리키지 않을 때는 nullptr로 초기화하는 것이 안전하다.

#include <iostream>

int main() {
    int* unsafe_ptr; // 초기화되지 않음. 위험!
    // *unsafe_ptr = 10; // 실행 시 오류 발생 가능성이 매우 높음

    int* safe_ptr = nullptr; // C++11부터는 nullptr 사용을 권장

    if (safe_ptr == nullptr) {
        std::cout << "포인터가 아무것도 가리키지 않습니다." << std::endl;
    }
    
    // 이후 유효한 주소를 할당하여 사용
    int a = 42;
    safe_ptr = &a;
    
    if (safe_ptr != nullptr) {
        std::cout << "이제 포인터는 값을 가리킵니다: " << *safe_ptr << std::endl;
    }

    return 0;
}

4. 코드 비교: 포인터 vs 레퍼런스

두 코드의 차이점을 느껴보면, 레퍼런스를 사용했을 때 코드가 훨씬 직관적이고 간결해진다는 것을 알 수 있었다.

#include <iostream>

// 값을 1 증가시키는 함수
// 1. 포인터 사용
void increment_ptr(int* p) {
    if (p != nullptr) { // 항상 nullptr인지 체크하는 습관!
        (*p)++; // 괄호 필수!
    }
}

// 2. 레퍼런스 사용
void increment_ref(int& r) {
    // nullptr 체크가 필요 없음. 레퍼런스는 항상 유효한 대상을 가리킴.
    r++;
}

int main() {
    int a = 5;
    int b = 5;

    increment_ptr(&a); // 주소를 넘겨줘야 함
    increment_ref(b);  // 변수 자체를 넘겨주면 됨

    std::cout << "포인터로 증가시킨 값: " << a << std::endl;
    std::cout << "레퍼런스로 증가시킨 값: " << b << std::endl;

    return 0;
}
// 출력 결과:
// 포인터로 증가시킨 값: 6
// 레퍼런스로 증가시킨 값: 6

⚠️ 실수

  • 포인터 연산자 우선순위를 몰라서 실수를 많이 했다. *ptr++라고 썼더니, 값이 증가하는 게 아니라 포인터의 주소가 다음으로 이동해 버렸다. 😭 값 자체를 증가시키려면 (*ptr)++ 처럼 괄호로 묶어 역참조를 먼저 수행해야 한다는 것을 깨달았다.
  • 함수에 값을 변경하기 위해 변수를 넘겼는데, 포인터가 아닌 일반 변수로 넘겨서 값이 변경되지 않는 경험을 했다. void func(int val) 이렇게 말이다. C++ 함수는 기본적으로 '값에 의한 호출(Call by Value)'이라 복사본이 전달된다는 사실! 원본을 바꾸려면 포인터나 레퍼런스를 써야 했다.

✅ 핵심 요약

구분포인터 사용법 (int* ptr = &val;)레퍼런스 사용법 (int& ref = val;)
값 접근*ptr (역참조 연산자 * 필요)ref (일반 변수처럼 바로 사용)
값 변경*ptr = 20;ref = 20;
함수 전달func(&val); (주소를 전달)func(val); (변수를 그대로 전달)
안전성항상 nullptr인지 확인하는 습관이 필요.nullptr이 될 수 없어 상대적으로 안전함.
주요 용도동적 메모리 할당, 배열/이터레이터, 가리키는 대상이 바뀔 수 있을 때함수 인자/반환 값, 객체 별명 등 단순하고 안전한 참조가 필요할 때
profile
Shin Ji Yong // Unreal Engine 5 공부중입니다~

0개의 댓글