[C++] Pointer 개념, 연산에 대해 알기

2022. 6. 13. 00:35♣ C++

이번 포스팅에선 C++의 포인터에 대해 알아보도록 하겠습니다.

포인터 : 다른 변수, 혹은 그 변수의 메모리 공간 주소를 가리키는 변수

즉, 특정 변수가 저장되어 있는 메모리 주소

 

개발 환경 : VSCode, Windows 10

 

 

 

 

 

기본 개발 세팅은 다음과 같습니다.

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

int main()

{    
    // pointer
    int num1 = 10;
    int num2 = 4;
    cout << "pointer 예시 \n";
    return 0;
}

 

 

 

 

 

포인터 (Pointer)


포인터란? 메모리의 주소 값을 저장하는 변수입니다.

char형 변수가 문자를 저장하고, int형 변수가 정수를 저장하듯이, pointer는 주소를 저장합니다.

 

 

http://www.tcpschool.com/cpp/cpp_arrayPointer_pointerIntro

TCP School에서 가져왔습니다.

 

 

 

 

[포인터 선언]

 

타입 *포인터 이름 = &변수이름;
or
타입 *포인터 이름 = 주소값;

 

포인터는 위와 같은 방식으로 선언됩니다. 

밑은 예제입니다.

int main()
{    
    int num1 = 5;
    int *ptr_1 = &num1;
    cout << "num1 의 주소는 "<< ptr_1 << " 입니다.";
}

 

결과

num1 의 주소는 0x61ff08 입니다.

 

 

 

 

[포인터 연산자]

포인터를 선언하고 활용함에 있어, 2개의 주요 연산자가 존재합니다.

 

1. 주소 연산자(&) : 변수의 이름 앞에 사용하여, 해당 변수의 주소 값을 반환합니다.

int *ptr_1 = &num1;

 

2. 참조 연산자(*) : 포인터 이름, 주소 앞에 사용하며 포인터가 가리키는 주소에 저장된 값을 반환합니다.

cout << "num1 의 주소에 저장된 값은 "<< *ptr_1 << " 입니다.";


num1 의 주소에 저장된 값은 5 입니다.

*포인터 이름을 사용한 결과, 주소에 저장된 값이 반환된 것을 확인할 수 있습니다.

 

 

 

 

 

기초적인 포인터 개념으로 코딩해보자!

 

int main()
{    
    double num1 = 3.14556456;
    int num2 = 1234;
    double *ptr_1 = &num1;
    int *ptr_2 = &num2;

    printf("num1 의 포인터 변수의 크기는 %d\n", sizeof(ptr_1));
    cout << "num1 의 주소값은 " << ptr_1 << endl; 
    cout << "num1 의 값은 " << *ptr_1 << endl; 
    cout << "num2 의 주소값은 " << ptr_2 << endl; 
    cout << "num2 의 값은 " << *ptr_2 << endl; 
}

 

결과

num1 의 포인터 변수의 크기는 4
num1 의 주소값은 0x61ff00
num1 의 값은 3.14556
num2 의 주소값은 0x61fefc
num2 의 값은 1234

 

쉽게 생각하면 됩니다. 포인터는 변수의 주소값을 가진 변수입니다.

[*포인터]는 주소값에 저장된 변수를, [포인터]는 주소값을 반환합니다.

 

 

 

 

 

 

 

 

포인터 연산


그저 주소를 가리키는 변수인데, 웬 연산?이라는 생각이 들 수 있습니다.

포인터는 그저 주소를 가리키기 위해 존재하는 것이 아닙니다. 나중에 배울 '메모리 동적 할당'을 위해 쓰입니다. 메모리를 원하는 주소에 할당하고, 해제하는 작업을 후에 하게 될 것입니다. 그때 원활하게 주소를 다루려면 당연히 관련 연산 작업도 이해해야 할 것입니다. 

 

포인터 연산은 그 시작점입니다. 

 

포인터 연산의 특징은 다음과 같습니다. 

1) 포인터는 값을 증거/감소시키는 등의 제한된 연산만 가능합니다.

2) 포인터끼리의 연산(덧셈, 곱셈, 나눗셈)은 아무 의미가 없습니다.

3) 포인터끼리의 뺄셈은 두 포인터 사이의 거리를 의미합니다.

4) 포인터에 정수는 더하고 뺄 수 있지만, 실수는 허용되지 않습니다.

5) 포인터끼리 대입하거나 비교할 수 있습니다.

 

 

 

[타입별 포인터 연산]

포인터는 타입마다 연산에 따른 변화가 다릅니다.

만약 int형 타입의 포인터에 대하여, 1을 더하면 int형 메모리 크기(4)만큼 이동합니다.

 

 

 

int main()
{    
    int num2 = 1234;
    int *ptr_2 = &num2;
    int *ptr_3;
    cout << ptr_2;
    cout << "\n";
    ptr_3 = ptr_2 + 1;
    cout << ptr_3;
}

 

결과

0x61ff04
0x61ff08

ptr_2에 1을 더한 결과, 0x61ff04 -> 0x61ff08로 변한 것을 확인할 수 있습니다. int형 메모리 크기만큼 움직인 것에 주목해야 합니다. 실수형은 어떨까요?

 

 

int main()
{    
    double num2 = 1.234;
    double *ptr_2 = &num2;
    double *ptr_3;
    cout << ptr_2;
    cout << "\n";
    ptr_3 = ptr_2 + 1;
    cout << ptr_3;
}

 

결과

0x61ff00
0x61ff08

예상대로 실수형 메모리 크기인 8만큼 이동했음을 확인할 수 있습니다.

이렇듯이 포인터는 연산한다고 다 똑같은 결과가 나오는 것이 아닙니다. 타입마다 메모리 크기가 다르기에, 연산 결과도 모두 다름을 인지해야 합니다.

 

 

 

 

 

[포인터와 배열]

이 둘은 언뜻 봐서는 연관이 없어 보입니다. 하지만 미묘한 관계성이 있습니다.

어떤 부분에선 서로를 대체할 수도 있습니다. 배열의 이름을 포인터처럼 사용할 수 있으며, 포인터를 배열의 이름으로 사용할 수도 있습니다. 

 

C++에서는 배열의 이름이 주소로 해석됩니다. 해당 배열의 첫 요소의 주소와 같습니다. 

즉, array의 주소 값 = array의 첫번째 요소인 1의 주소값

 

int main()
{    
    int array[] = {1,2,3,4,5};
    int *ptr_1 = array;
	
    // array[i] 로도 가능하지만, 여기선 포인터도 배열처럼 쓰일 수 있음을 보여주고자 한다.
    for (int i=0; i < 5; i++){
        cout << i << " 번째 요소의 값은 " << ptr_1[i] << " 입니다.\n";
    }
}

 

결과

0 번째 요소의 값은 1 입니다.
1 번째 요소의 값은 2 입니다.
2 번째 요소의 값은 3 입니다.
3 번째 요소의 값은 4 입니다.
4 번째 요소의 값은 5 입니다.

포인터를 배열에 대입한 결과, 포인터도 배열과 유사하게 기능함을 볼 수 있습니다. arr[i]를 통해, 배열에서 직접 값을 불러왔어도 똑같은 결과가 나왔을 것입니다. 

다만, 배열과 포인터의 크기 계산에선 차이점을 보입니다. 

 

 

int main()
{    
    int array[] = {1,2,3,4,5};
    int *ptr_1 = array;

    cout << sizeof(array) << endl;
    cout << sizeof(ptr_1);
}

 

결과

20
4

배열을 sizeof() 했을 때는 배열의 크기를 반환했습니다. 하지만 포인터를 sizeof() 했을 때는 포인터 변수 자체의 크기를 출력했습니다. 

 

 

 

 

 

[배열의 포인터 연산]

 

array[n] = *(array + n)

 

다음은 배열의 이름을 포인터처럼 사용하는 예제입니다. 

포인터 연산을 통해 배열의 요소에 접근합니다.

 

int main()
{    
    int array[] = {1,2,3,4,5};
    // 배열의 이름으로 포인터 연산을 통해 배열 요소에 접근
    for (int i=0; i<5; i++){
        cout << i+1 << " 번째 배열 요소의 주소값은 " << *(array + i) << " 입니다." << endl;
    }
}

 

결과

1 번째 배열 요소의 주소값은 1 입니다.
2 번째 배열 요소의 주소값은 2 입니다.
3 번째 배열 요소의 주소값은 3 입니다.
4 번째 배열 요소의 주소값은 4 입니다.
5 번째 배열 요소의 주소값은 5 입니다.

 

 

 

처음 포인터에 대입한 배열의 주소 값은 배열의 첫 번째 주소와 같았습니다. 

그 배열에 포인터 연산처럼 +2를 해줌으로써 배열의 3번째 요소를 나타낸 것이라 할 수 있습니다. 

 

 

 

 

 

지금까지 포인터 개념과 연산하는 방법을 살펴보았습니다. 

다음 포스팅에선 포인터의 존재 이유, 바로 메모리 동적 할당에 대해 다뤄보도록 하겠습니다.