스파르타내일배움-unreal

[스파르타코딩클럽] 게임개발자를 위한 C++ - 포인터와 레퍼런스

ye0n-j 2025. 5. 30. 20:32

* 포인터와 레퍼런스 문법을 익히자


1. 포인터

변수들은 특정한 값을 담았다. int는 정수, double은 부동 소수점을 담았다.

포인터도 변수. 값을 담는 것이 아닌 변수의 주소값을 담는다. 메모리의 주소값을 알면 해당 공간을 직접 제어할 수 있다.

예를 들어 int* p와 같이 선언된 p는 정수를 가리키는 포인터로서 int 변수의 메모리 주소를 저장할 수 있다.

 

1) 값을 담는 일반 변수

일반 변수에서 대입 연산(=)을 하면 변수에 있는 값이 그대로 복제된다. 하나의 변수를 다른 변수에 대입하면 샐운 메모리 공간에 동일한 값이 복제된다. 따라서 복사 이후에도 두 변수는 서로 독립적인 공간을 가지므로, 한 쪽 값을 변경해도 다른 쪽에는 영향이 없다.

 

B에 값을 할당하고 A에 B값을 대입하는 상황에서 A의 값을 변경해도 B의 값은 변경되지 않는다. 독립된 메모리 공간이기 때문에

 

[ 일반 변수의 대입 ]

#include <iostream>
using namespace std;

int main() {
    int a = 10;  // 변수 a 선언 및 초기화
    int b = a;   // 변수 b에 a의 값을 복사

    cout << "초기값 - a: " << a << ", b: " << b << endl;

    b = 20; // b의 값 변경

    cout << "변경 후 - a: " << a << ", b: " << b << endl;

    return 0;
}

/*
출력 결과:
초기값 - a: 10, b: 10
변경 후 - a: 10, b: 20
*/

[ 배열의 대입 ]

#include <iostream>
using namespace std;

int main() {
    int arr1[3] = {1, 2, 3};
    int arr2[3];

    // 배열 전체를 한 번에 대입하는 것은 불가능
    // arr2 = arr1;  // 컴파일 오류 발생

    // 개별 요소 복사
    for (int i = 0; i < 3; i++) {
        arr2[i] = arr1[i];
    }

    // arr2 변경 후 확인
    arr2[0] = 100;

    cout << "arr1[0]: " << arr1[0] << ", arr2[0]: " << arr2[0] << endl;

    return 0;
}

/*
출력 결과:
arr1[0]: 1, arr2[0]: 100
*/

 

2 ) 복사에는 비용이 있다.

A=B의 의미를 다시 상기했을 때 B의 있는 내용을 전부 A에 복사한다는 의미이고 이 복사를 할 때 복사 비용이 든다.

예를 들어, 문서 하나를 받고, 동일하게 타이핑 하는 업무를 받았다고 가정했을 때, 4장으로 되어있는 문서와 4000장으로 되어있는 문서를 각각 타이핑 할 때 완료하기 위해 필요한 시간을 다를 것이다.

변수도 이와 유사하다. 각 변수마다 값을 표현하기 위해 사용되는 공간의 크기가 다르다. 그렇다면 복사하는 데에 필요한 비용도 다르다.

문자형은 1바이트, 정수형은 4바이트, 부동 소수점은 8바이트, 배열의 경우는 동일한 타입의 변수가 여러개 있는 것과 같으므로 비용이 커질 수도 있다. 크기가 커지만 이러한 복사 비용이 꽤 부담스러워질 수 있고 이러한 복사 비용 때문에 C++에서는 값을 직접 복사하는 대신 변수의 주소를 가리켜서 동일한 데이터에 접근할 수 있도록 한다.

바로, 포인터를 활용해서

값을 알고 싶을 때 굳이 복사를 할 필요 없이 주소값에 접근하여 값만 가지고 오는 행위가 적절할 것이다.

[값 복사비용 (기본 변수)]

#include <iostream>
using namespace std;

int main() {
    int a = 10;   // 4바이트 크기의 변수
    int b = a;    // 변수 a의 값을 b에 복사 (4바이트 비용 발생)

    cout << "a: " << a << ", b: " << b << endl;

    b = 20; // b의 값만 변경

    cout << "변경 후 a: " << a << ", b: " << b << endl;

    return 0;
}

/*
출력 결과:
a: 10, b: 10
변경 후 a: 10, b: 20
*/

[값 복사 비용(배열)]

#include <iostream>
using namespace std;

int main() {
    const int SIZE = 1000000; // 1,000,000개의 정수 (약 4MB)
    int arr1[SIZE]; 
    int arr2[SIZE];

    // 배열 복사 (매우 높은 복사 비용)
    for (int i = 0; i < SIZE; i++) {
        arr2[i] = arr1[i];
    }

    cout << "배열 복사 완료" << endl;

    return 0;
}

 

3 ) 포인터 변수의 연산

포인터의 모든 연산은 주소값과 관련되어 있다. 아래 두 가지 연산은 오직 포인터 변수만 가능하다.

-1. 변수의 주소값을 담을 수 있다.

-2. 담고 있는 주소값에 해당되는 메모리에 있는 값을 읽거나 수정할 수 있다.

A=B를 하면 B에 있는 값이 A에 그대로 저장된다. 여기서 주목할 것. 값이 그대로 저장된다는 것.

따라서 값이 아닌 주소값을 정보로 제공하겠다는 것을 알려야 하고 이때 사용하는 연산자가 & 연산자 이다.

A=&B를 하게되면 A에 B의 주소값이 들어간다.

[변수의 주소값 저장]

#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int* p = &a; // 변수 a의 주소를 포인터 p에 저장

    cout << "변수 a의 값: " << a << endl;
    cout << "변수 a의 주소: " << &a << endl;
    cout << "포인터 p의 값(저장된 주소): " << p << endl;

    return 0;
}

/*
출력 결과:
변수 a의 값: 10
변수 a의 주소: 0x61ff08 (주소는 실행할 때마다 다를 수 있음)
포인터 p의 값(저장된 주소): 0x61ff08
*/

 

4) 값 대신 주소 담기

- 우리는 변수의 크기가 커지면 많은 복사 비용이 필요하다는 것을 알았다. 그럼에도 불구하고 해당 변수의 정보가 필요한 경우 효율적인 방법은 무엇일까?

-> 주소값을 담는다. 하나의 방법은 값 전체를 복사하는 것이 아닌 변수의 시작 주소를 알려주는 것. 주소값은 고유하기 때문에 이를 알고 있다면 실제 변수의 위치에 접근할 수 있다. 

예를 들어 arr 이라는 배열이 선언이 되었고 시작 주소가 22 라고 할 때, 포인터 변수는 값 대신 변수의 주소값을 담는다. (22를 담는다)

[ 포인터를 이용한 배열 접근]

#include <iostream>
using namespace std;

int main() {
    int arr[3] = {10, 20, 30};
    int* p = arr; // 배열의 이름은 배열의 시작주소를 가지고 있어서 & 연산자가 필요 없다
    
    cout << "p가 가리키는 값: " << *p << endl;
    cout << "p+1이 가리키는 값: " << *(p + 1) << endl;
    cout << "p+2이 가리키는 값: " << *(p + 2) << endl;

    return 0;
}

/*
출력 결과:
p가 가리키는 값: 10
p+1이 가리키는 값: 20
p+2이 가리키는 값: 30
*/

5) 포인터 변수의 구성요소

-1. 메모리에 변수 3개가 있다. x는 정수형(4바이트), y와 z는 문자형(1바이트)이다.

-2. 포인터 변수가 2개 있고, 하나의 x의 시작주소는 200, 하나는 y의 시작주소 204를 담고 있다.

여기서 시작 주소가 200인것은 알았지만 정수 변수의 X의 크기(4바이트)는 알 수 없다. X에 저장된 값을 제대로 읽으려면 시작 주소부터 4바이트를 일거야 하므로 포인터에는 변수의 크기를 파악할 수 있는 타입 정보가 함께 필요하다.

 

따라서 포인터는 주소 값 뿐만 아니라 가리키는 변수의 타입도 중요하다. 아래의 2가지 정보가 있어야 한다.

-1. 변수의 시작주소.

-2. 변수의 타입 (변수의 크기를 알기 위해서 필요)

C++ 에서는 이를 위해 타입 정보를 포함한 포인터 변수를 사용한다. 포인터 변수는 선언 시 데이터 형 뒤에 *을 붙여 포인터임을 표시한다.

예를 들어, int* ptr이라고 선언하면 ptr은 정수를 가리키는 포인터가 된다. 변수의 시작 주소 값을 담고, 해당 변수 타입이 정수라는 걸 알 수 있으므로 제대로 값을 읽어올 수 있다.

[포인터 변수의 타입과 크기]

#include <iostream>
using namespace std;

int main() {
    int x = 3;
    char y = 'A';

    int* ptr1 = &x;  // 정수형 변수 x의 주소 저장
    char* ptr2 = &y; // 문자형 변수 y의 주소 저장

    cout << "ptr1이 가리키는 값: " << *ptr1 << endl;
    cout << "ptr2가 가리키는 값: " << *ptr2 << endl;

    return 0;
}

/*
출력 결과:
ptr1이 가리키는 값: 3
ptr2가 가리키는 값: A
*/

[포인터를 이용한 주소 확인]

#include <iostream>
using namespace std;

int main() {
    int x = 3;
    char y = 'A';

    int* ptr1 = &x;
    char* ptr2 = &y;

    cout << "x의 주소: " << &x << ", ptr1: " << ptr1 << endl;
    cout << "y의 주소: " << (void*)&y << ", ptr2: " << (void*)ptr2 << endl;

    return 0;
}

/*
출력 결과 (주소는 실행할 때마다 다름):
x의 주소: 0x61ff00, ptr1: 0x61ff00
y의 주소: 0x61ff04, ptr2: 0x61ff04
*/

[포인터 연산과 데이터 크기]

#include <iostream>
using namespace std;

int main() {
    int x = 3;
    char y = 'A';

    int* ptr1 = &x;
    char* ptr2 = &y;

    cout << "ptr1: " << ptr1 << ", ptr1 + 1: " << ptr1 + 1 << endl;
    cout << "ptr2: " << ptr2 << ", ptr2 + 1: " << ptr2 + 1 << endl;

    return 0;
}

/*
출력 결과 (주소는 실행할 때마다 다름):
ptr1: 0x61ff00, ptr1 + 1: 0x61ff04  (int는 4바이트)
ptr2: 0x61ff04, ptr2 + 1: 0x61ff05  (char는 1바이트)
*/

[포인터를 이용한 값 읽기]

#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int* p = &a; // 변수 a의 주소를 저장

    cout << "포인터 p가 가리키는 값: " << *p << endl; // p가 가리키는 변수 a의 값 출력

    return 0;
}

/*
출력 결과:
포인터 p가 가리키는 값: 10
*/

[포인터를 이용한 값 변경]

#include <iostream>
using namespace std;

int main() {
    int a = 10;
    int* p = &a; // a의 주소 저장

    cout << "변경 전 a: " << a << endl;

    *p = 20; // 포인터를 이용하여 값 변경

    cout << "변경 후 a: " << a << endl;

    return 0;
}

/*
출력 결과:
변경 전 a: 10
변경 후 a: 20
*/

 

6 ) 포인터 변수의 역참조

- 포인터가 주소만 저장하면 무슨 의미가 있을까?

-> 포인터를 제대로 활용하려면 해당 주소에 있는 실제 값을 읽고 수정할 수 있어야 한다. 포인터는 주소를 다루는 특성 때문에 산술 연산 역시 주소를 제어하는 방식으로 동작한다. 이를 위해 역참조 연산자(*)를 사용한다. 역참조 연산자는 주소값을 따라가서 값을 읽거나 쓸 수 있도록 하는 연산자이다.

A라는 포인터 변수가 있다면, A를 출력하면 주소값이 나오고, *A를 출력하면 해당 메모리에 있는 값이 나온다.

-1. 정수형 포인터 변수 ptr과 정수형 변수 xr가 선언되었고, x의 값은 3이다.

-2. ptr = &x, ptr이라는 포인터 변수에 x의 주소값 저장. x의 시작 주소 저장 (200이라고 가정)

-3. ptr을 출력하면 200이 출력된다. 내부에 주소값인 200이 저장되어 있기 때문에

-4. *ptr을 출력하면 3이 출력된다.(역참조연산) ptr이 담고있는 주소값 200을 따라간 후, 해당 주소로부터 4바이트를 읽으면 3이라는 값이 나오기 때문에 (4바이트를 읽는 이유는 정수형이라고 이미 선언을 해주었기 때문에 알 수 있다)

-5. char 변수 y 추가, ptr=&x의 경우는 x도 정수형 변수이고 ptr도 정수형 포인터로 타입이 일치했기 때문에 문제가 없었으나 pt=&y의 경우는 y가 문자형이기 때문에 주소값을 담으려고 시도하면 에러가 발생한다.

-6. 메모리 값을 출력하는 것 뿐만 아니라 값도 변경할 수 있다. *ptr=40으로 ptr이 담고있는 주소값으로 가서 값을 40으로 변경한다.

 

[ 포인터와 변수의 관계 ]

#include <iostream>
using namespace std;

int main() {
    int x = 3;    // 정수형 변수 x 선언
    int* ptr = &x; // 포인터 ptr에 x의 주소 저장

    cout << "x의 값: " << x << endl;
    cout << "x의 주소: " << &x << endl;
    cout << "ptr의 값(저장된 주소): " << ptr << endl;
    cout << "*ptr이 가리키는 값: " << *ptr << endl;

    return 0;
}

/*
출력 결과:
x의 값: 3
x의 주소: 0x61ff00 (주소는 실행할 때마다 다름)
ptr의 값(저장된 주소): 0x61ff00
*ptr이 가리키는 값: 3
*/

[ 다른 타입의 변수를 가리키려 할 때 발생하는 문제 ]

#include <iostream>
using namespace std;

int main() {
    int x = 3;
    char y = 'A';

    int* ptr = &x; // 정상적인 포인터 할당
    // ptr = &y; // 오류 발생: char형 변수를 int* 포인터에 저장 불가능

    return 0;
}

[각각 올바른 포인터 타입을 사용하는 포인터]

#include <iostream>
using namespace std;

int main() {
    int x = 3;
    char y = 'A';

    int* intPtr = &x; // 정수형 포인터
    char* charPtr = &y; // 문자형 포인터

    cout << "*intPtr: " << *intPtr << endl;
    cout << "*charPtr: " << *charPtr << endl;

    return 0;
}

/*
출력 결과:
*intPtr: 3
*charPtr: A
*/

[포인터 연산]

#include <iostream>
using namespace std;

int main() {
    int x = 3;
    char y = 'A';

    int* ptr1 = &x;
    char* ptr2 = &y;

    cout << "ptr1: " << ptr1 << ", ptr1 + 1: " << ptr1 + 1 << endl;
    cout << "ptr2: " << (void*)ptr2 << ", ptr2 + 1: " << (void*)(ptr2 + 1) << endl;

    return 0;
}

/*
출력 결과 (주소는 실행할 때마다 다를 수 있음):
ptr1: 0x61ff00, ptr1 + 1: 0x61ff04  (int는 4바이트)
ptr2: 0x61ff04, ptr2 + 1: 0x61ff05  (char는 1바이트)
*/

[잘못된 포인터 접근]

#include <iostream>
using namespace std;

int main() {
    int* p; // 초기화되지 않은 포인터

    // *p = 10;  // 실행 시 오류 발생 가능: 메모리 접근 오류 
    // 예기치 못한 에러로 종료

    return 0;
}

[nullptr을 이용한 안전한 포인터 사용]

#include <iostream>
using namespace std;

int main() {
    int* p = nullptr; // 포인터를 안전하게 초기화

    if (p == nullptr) {
        cout << "포인터가 아직 어떤 변수도 가리키지 않습니다." << endl;
    }

    return 0;
}

/*
출력 결과:
포인터가 아직 어떤 변수도 가리키지 않습니다.
*/

 

7 ) 배열 이름의 의미

배열도 변수이다. 따라서 변수의 기본적인 특성들을 가지고 있다. (배열은 주소값을 가지고 있는 특이한 변수다)

- 1. 배열의 이름은 배열의 시작 주소를 가지고 있다.

 - 2. 값을 저장할 수 있다.

일반 변수와 차이가 있다면 배열은 여러 개의 값을 저장할 수 있기에 배열만이 갖는 추가적인 특성(변수를 묶어서 관리)이 있다.

 - 1. 인덱스를 통한 임의 접근이 가능한 이유는 배열은 메모리가 연속적으로 할당되기 때문이다.

 - 2. 임의 접근이 가능하려면 배열 이름에 저장된, 배열의 시작 주소를 알아야 한다. 이러한 이유로 배열 자체가 담고있는 시작 주소를 변경할 수 없다.

 - 3. 배열 이름은 주소값을 담고 있기 때문에, 기존 포인터와 마찬가지로 * 연산자를 활용해서 해당 주소에 있는 값을 확인할 수 있다.

 - 4. 값을 읽는 것 뿐아니라, 기존 포인터와 마찬가지로 * 연산자를 활용해서 값을 넣을 수도 있다.

* buf[k]=*(buf+k)

 

8 ) 배열과 포인터의 차이

배열의 이름은 사용될 때 대부분 포인터로 암시적 형 변환되어 동작한다. 예를 들어, int arr[4];가 있을 때 arr는 배열 전체를 의미한다. 하지만 식이나 인자로 사용되면 int* 형으로 변환되어 배열의 첫 번째 원소 주소로 해석된다.

이 주소값은 변경할 수 있다.

- 1. 배열 이름은 주소값을 담고 있지만, 이 주소값 대신 다른 주소값을 할당할 수 없다.
      - 배열 이름이 담고 있는 주소값은 특별해서 이 주소값이 없으면 기준점이 없어져 임의 접근이 불가하다. 아배열 이름에 ㄷ른 주소를 대입하려는 경우에 에러가 발생한다.

 - 2. 변수의 크기가 다르다. 

      - 포인터 변수의크기는 타입과 무관하게 운영체제에서 관리하는 메모리 주소의 크기. 배열의 크기는 배열 원소 타입의 크기에 개수를 곱한 것. 요약하자면, 여러 개의 데이터를 관리하기 위해 포인터가 가지고 있는 연산이 제공되는 변수라고 보면 될 것

 

[ 배열의 기본 개념과 인덱스를 통한 접근]

#include <iostream>
using namespace std;

// 배열의 선언, 초기화 및 인덱스를 통한 접근 예제
int main() {
    int arr[5] = {10, 20, 30, 40, 50}; // 크기가 5인 배열 선언 및 초기화

    // 배열 요소 접근 (인덱스를 이용한 접근)
    cout << "arr[0]: " << arr[0] << endl; // 10
    cout << "arr[2]: " << arr[2] << endl; // 30
    cout << "arr[4]: " << arr[4] << endl; // 50

    // 배열의 시작 주소 확인
    cout << "배열의 시작 주소: " << arr << endl;
    cout << "첫 번째 요소의 주소: " << &arr[0] << endl;

    return 0;
}

/*
출력 결과 예시:
arr[0]: 10
arr[2]: 30
arr[4]: 50
배열의 시작 주소: 0x... (메모리 주소)
첫 번째 요소의 주소: 0x... (메모리 주소)
*/

[ 포인터를 이용한 배열 접근 ]

#include <iostream>
using namespace std;

// 배열 이름을 포인터처럼 활용하는 예제
int main() {
    int arr[3] = {100, 200, 300};
    int *ptr = arr; // 배열 이름은 배열의 시작 주소를 가리킴

    cout << "*ptr: " << *ptr << endl;     // 100 (arr[0] 값)
    cout << "*(ptr+1): " << *(ptr+1) << endl; // 200 (arr[1] 값)
    cout << "*(ptr+2): " << *(ptr+2) << endl; // 300 (arr[2] 값)

    return 0;
}

/*
출력 결과:
*ptr: 100
*(ptr+1): 200
*(ptr+2): 300
*/

[ 배열 이름을 변경할 수 없는 이유 ]

#include <iostream>
using namespace std;

// 배열 이름을 포인터처럼 활용하는 예제
int main() {
    int arr[3] = {100, 200, 300};
    int *ptr = arr; // 배열 이름은 배열의 시작 주소를 가리킴

    cout << "*ptr: " << *ptr << endl;     // 100 (arr[0] 값)
    cout << "*(ptr+1): " << *(ptr+1) << endl; // 200 (arr[1] 값)
    cout << "*(ptr+2): " << *(ptr+2) << endl; // 300 (arr[2] 값)

    return 0;
}

/*
출력 결과:
*ptr: 100
*(ptr+1): 200
*(ptr+2): 300
*/

 

9) 포인터 배열과 포인터

포인터에 대한 배열과 관련된 특징들

1 ) 포인터 배열은 포인터를 원소로 같는 배열, 예를 들어, int* ptrArr[4];는 크기가 4이고 원소가 int*인 배열이다.

2 ) 배열 포인터는 배열 전체를 가리키는 포인터. 즉 단일 변수가 아닌 배열 통째를 가리키는 변수. 보통 다차원 배열을 제어할 때 많이 사용

 

[ 포인터 배열 ]

#include <iostream>
using namespace std;

// 포인터 배열: 포인터를 원소로 갖는 배열
int main() {
    int a = 10, b = 20, c = 30;
    int* ptrArr[3] = { &a, &b, &c }; // 포인터 배열 선언 및 초기화

    // 포인터 배열을 이용하여 값 출력
    cout << "*ptrArr[0]: " << *ptrArr[0] << endl; // 10
    cout << "*ptrArr[1]: " << *ptrArr[1] << endl; // 20
    cout << "*ptrArr[2]: " << *ptrArr[2] << endl; // 30

    return 0;
}

/*
출력 결과:
*ptrArr[0]: 10
*ptrArr[1]: 20
*ptrArr[2]: 30
*/

[ 배열 포인터 ]

#include <iostream>
using namespace std;

// 배열 포인터: 배열 전체를 가리키는 포인터
int main() {
    int arr[3] = { 100, 200, 300 };
    int (*ptr)[3] = &arr; // 배열 포인터 선언

    // 배열 포인터를 이용하여 배열 요소 접근
    cout << "(*ptr)[0]: " << (*ptr)[0] << endl; // 100
    cout << "(*ptr)[1]: " << (*ptr)[1] << endl; // 200
    cout << "(*ptr)[2]: " << (*ptr)[2] << endl; // 300

    return 0;
}

/*
출력 결과:
(*ptr)[0]: 100
(*ptr)[1]: 200
(*ptr)[2]: 300
*/

[ 포인터 배열 vs 배열 포인터 ]

#include <iostream>
using namespace std;

// 포인터 배열과 배열 포인터의 차이점 확인
int main() {
    int x = 1, y = 2, z = 3;
    int* ptrArr[3] = { &x, &y, &z }; // 포인터 배열 (각 원소가 int* 타입)
    
    int arr[3] = { 10, 20, 30 };
    int (*ptr)[3] = &arr; // 배열 포인터 (배열 전체를 가리킴)

    // 포인터 배열을 통한 접근
    cout << "*ptrArr[0]: " << *ptrArr[0] << endl; // 1
    cout << "*ptrArr[1]: " << *ptrArr[1] << endl; // 2
    cout << "*ptrArr[2]: " << *ptrArr[2] << endl; // 3

    // 배열 포인터를 통한 접근
    cout << "(*ptr)[0]: " << (*ptr)[0] << endl; // 10
    cout << "(*ptr)[1]: " << (*ptr)[1] << endl; // 20
    cout << "(*ptr)[2]: " << (*ptr)[2] << endl; // 30

    return 0;
}

/*
출력 결과:
*ptrArr[0]: 1
*ptrArr[1]: 2
*ptrArr[2]: 3
(*ptr)[0]: 10
(*ptr)[1]: 20
(*ptr)[2]: 30
*/

[ 배열 포인터의 잘못된 예시 ]

#include <iostream>
using namespace std;

// 배열 포인터의 잘못된 사용 예제
int main() {
    int arr[3] = {10, 20, 30};
    int (*ptr)[3] = &arr; // 배열 포인터 선언

    // 배열 포인터는 단일 요소를 직접 가리킬 수 없음
    // ptr = arr; // 오류 발생: 배열의 첫 번째 요소 주소와 다름

    return 0;
}

/*
컴파일 오류 메시지 예시:
error: invalid conversion from 'int*' to 'int (*)[3]'
*/

 

10  ) 포인터 연산

포인터는 주소값을 담는다. 따라서 산술 연산 시, 일반적인 수치 연산이 아닌 메모리 주소의 이동으로 해석 된다.

ptr+1을 하게되면 ptr이 담고 있는 주소값에 대한 연산이 수행된다. 포인터의 타입에 따라 해당 타입 변수의 크기만큼 담고 있는 주소를 증가시킨다.

 -1. ptr+1 을 실행하면 ptr이 가리키는 주소에서 한 단위 메모리 주소가 이동. 이 한단위 라는 것은 포인터 자료형 크기에 따라서 결정

 -2. (*ptr)+1 ptr이 가리니는 변수의 값을 1증가 시킨다

 -3. *(ptr+1) == ptf[1] 과 동일. 배열 인덱스 연산자 []는 내부적으로 포인터 연산을 통해 구현되어 있음

 

2. 레퍼런스

포인터를 사용하면 주소값을 직접 다루어야 하기 때문에 복잡해질 수 있다. 이 문제를 완화하기 위해 C++에서는 변수에 또 다른 이름을 부여하는 레퍼런스 문법을 도입했고 레퍼런스는 일반 변수와 동일하게 사용할 수 있다.

그러나 내부적으로는 해당 변수를 직접 가리켜주는 역할을 한다.

레퍼런스는 특정 변수에 대한 별명을 부여하는 것으로 한 번 특정 변수의 레퍼런스를 연결하면 이후는 마치 그 변수가 두 개의 이름을 갖는 것과 같다.

선언 방법은 데이터형 뒤에 &를 붙이는 것 (int& ref=var;) 이렇게하면 ref의 값 변경시 var의 값도 변경된다.

[기본적인 레퍼런스 사용]

#include <iostream>
using namespace std;

// 레퍼런스를 활용하여 변수에 별명을 부여하는 예제
int main() {
    int var = 10;
    int& ref = var; // var의 레퍼런스 선언

    cout << "초기 값:" << endl;
    cout << "var: " << var << endl; // 10
    cout << "ref: " << ref << endl; // 10

    ref = 20; // ref를 변경하면 var도 변경됨

    cout << "ref 값을 변경한 후:" << endl;
    cout << "var: " << var << endl; // 20
    cout << "ref: " << ref << endl; // 20

    return 0;
}

/*
출력 결과:
초기 값:
var: 10
ref: 10
ref 값을 변경한 후:
var: 20
ref: 20
*/

[잘못된 레퍼런스 사용]

#include <iostream>
using namespace std;

// 잘못된 레퍼런스 선언 예제
int main() {
    int& ref; // 레퍼런스는 선언과 동시에 초기화해야 함

    return 0;
}

/*
컴파일 오류 메시지 예시:
error: 'ref' declared as reference but not initialized
*/

 

1 ) 포인트와 레퍼런스의 차이점

 -1. 선언과 초기화 시점이 다르다.

       포인터는 선언 후, 나중에 = 연산자를 통해 가라킬 대상을 변경할 수 있다.
       레퍼런스는 선언과 동시에 초기화 해야 하며 초기화 이후에는 다른 대상에 재연결 할 수 없다.

  -2. 레퍼런스는 항상 다른 변수와 연결되어 있기 때문에 NULL이 없다

       포인터는 유효한 대상이 없음을 나타내기 위해 NULL 혹은 nullptr을 가질 수 있다

   -3. 간접 참조 문법의 유무

       포인터는 주소값을 담으므로 접근할 때는 * 연산을 사용하고 주소를 가져올 때는 & 연산을 사용한다

       레퍼런스는 변수 자체의 별명임로 일반 변수와 연산하는 방법이 동일하다

[ 선언 및 초기화 차이 ]

#include <iostream>
using namespace std;

// 포인터와 레퍼런스의 선언 및 초기화 차이
int main() {
    int a = 10, b = 20;

    // 포인터는 선언 후 나중에 다른 변수를 가리킬 수 있음
    int* ptr = &a; // 포인터 선언 및 초기화
    ptr = &b; // 포인터가 다른 변수를 가리킬 수 있음

    // 레퍼런스는 선언과 동시에 초기화해야 함
    int& ref = a;
    // ref = &b; // ❌ 오류! 레퍼런스는 다른 변수에 재할당할 수 없음

    cout << "포인터 사용:" << endl;
    cout << "*ptr: " << *ptr << endl; // 20 (포인터가 b를 가리키고 있음)

    cout << "레퍼런스 사용:" << endl;
    cout << "ref: " << ref << endl; // 10 (a를 가리키고 있음)

    return 0;
}

/*
출력 결과:
포인터 사용:
*ptr: 20
레퍼런스 사용:
ref: 10
*/

[ nullptr 가능성 여부의 차이 ]

#include <iostream>
using namespace std;

// 포인터는 NULL을 가질 수 있지만, 레퍼런스는 반드시 변수와 연결되어야 함
int main() {
    int a = 42;
    int* ptr = nullptr; // 포인터는 nullptr이 가능
    ptr = &a; // 이후에 a를 가리키도록 설정 가능

    // int& ref; // ❌ 오류! 레퍼런스는 반드시 선언과 동시에 초기화해야 함
    int& ref = a; // 올바른 선언 방식

    cout << "포인터 사용:" << endl;
    cout << "ptr이 가리키는 값: " << *ptr << endl; // 42

    cout << "레퍼런스 사용:" << endl;
    cout << "ref: " << ref << endl; // 42

    return 0;
}

/*
출력 결과:
포인터 사용:
ptr이 가리키는 값: 42
레퍼런스 사용:
ref: 42
*/

[ 포인터와 레퍼런스의 간접 참조 문법 차이 ]

#include <iostream>
using namespace std;

// 포인터와 레퍼런스의 간접 참조 문법 비교
int main() {
    int x = 5;
    int* ptr = &x;  // 포인터 선언
    int& ref = x;   // 레퍼런스 선언

    cout << "포인터 접근 방법:" << endl;
    cout << "x: " << x << endl;       // 5
    cout << "*ptr: " << *ptr << endl; // 5 (포인터를 통한 간접 참조)
    cout << "ptr: " << ptr << endl;   // x의 주소값

    cout << "레퍼런스 접근 방법:" << endl;
    cout << "ref: " << ref << endl;   // 5 (레퍼런스는 그냥 변수처럼 사용 가능)

    *ptr = 10; // 포인터를 사용하여 값 변경
    cout << "포인터로 변경 후 x: " << x << endl; // 10

    ref = 20; // 레퍼런스로 값 변경
    cout << "레퍼런스로 변경 후 x: " << x << endl; // 20

    return 0;
}

/*
출력 결과:
포인터 접근 방법:
x: 5
*ptr: 5
ptr: 0x... (메모리 주소)
레퍼런스 접근 방법:
ref: 5
포인터로 변경 후 x: 10
레퍼런스로 변경 후 x: 20
*/

[ 잘못된 레퍼런스 사용 예시 ]

#include <iostream>
using namespace std;

// 잘못된 레퍼런스 사용 예제
int main() {
    int* ptr = nullptr; // 포인터는 nullptr 가능
    // int& ref = nullptr; // ❌ 오류 발생: 레퍼런스는 NULL을 가질 수 없음

    return 0;
}

/*
컴파일 오류 메시지 예시:
error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘std::nullptr_t’
*/

2 ) 상수 레퍼런스

레퍼런스에 상수 제약을 걸어서 읽기 전용으로 사용할 수 있다. 상수 레퍼런스를 사용하면 값을 복사하지 않고도 기존 변수를 보호할 수 있다. 예를 들어, const int& cref = x; 복사과정 없이 x의 값을 읽을 수는 있지만 x의 값을 수정할 수는 없다.

[ 상수 레퍼런스의 기본개념 ]

#include <iostream>
using namespace std;

// 상수 레퍼런스를 사용하여 변수를 보호하는 예제
int main() {
    int x = 100;
    const int& cref = x; // x를 읽기 전용으로 참조

    cout << "cref: " << cref << endl; // 100

    // cref = 200; // ❌ 오류 발생! 상수 레퍼런스는 값을 변경할 수 없음

    x = 200; // 원본 변수 x는 변경 가능
    cout << "x 변경 후 cref: " << cref << endl; // 200

    return 0;
}

/*
출력 결과:
cref: 100
x 변경 후 cref: 200
*/

[ 상수 레퍼런스를 잘못 사용하는 경우 ]

#include <iostream>
using namespace std;

// 잘못된 상수 레퍼런스 사용 예제
int main() {
    const int& cref; // ❌ 오류 발생! 레퍼런스는 반드시 초기화해야 함

    return 0;
}

/*
컴파일 오류 메시지 예시:
error: ‘cref’ declared as reference but not initialized
*/