C, C++/C++ 언어 / / 2024. 4. 29.

[C++] 포인터의 개념 및 사용하는 이유

포인터의 정의

포인터란 객체의 메모리 주소를 저장하는 변수입니다.

여기서 객체란 변수, 배열, 구조체, 클래스, 함수 등 메모리에 저장되는 모든 객체를 말합니다.

 

포인터의 정의에서 가장 중요한 것은 변수라는 것입니다.

여기서 시작하는 것이 가장 쉬운 것 같습니다.

int A;
int* ptrA;

첫 번째 줄은 int형 변수 A이고, 두 번째 줄은 int형 포인터입니다.

둘 다 변수이고, 단지 ptrA에는 int형 메모리 주소를 담고 있다는 것을 알려주려고 *를 붙인 것뿐이죠.

 

참고로 포인터 변수라고들 하는데 이것은 번역상의 불필요한 설명을 많이들 쓰게 된 경우로, 포인터와 포인터 변수는 같은 것을 말하는 것입니다.

 

포인터의 선언과 초기화

 

포인터를 선언하기 위해선 [ 데이터형* 변수명 ]의 형식을 가집니다.

여기서 쓰인 *는 연산자가 아니고 포인터임을 표시하기 위한 기호입니다.

int* pInt;		// 정수형 객체의 주소를 저장
char* pChar;    	// char형 객체의 주소를 저장
float* pFloat;      	// float형 객체의 주소를 저장

 

그런데, int형 포인터에 int형 변수의 메모리 주소를 어떻게 넣을까요?

주소 연산자 &를 사용합니다.

int A = 0;
int* pInt = &A;

int* pInt2 = NULL;

여기서 변수 A 앞의 &주소 연산자입니다. 변수 A의 메모리 주소를 알려주기 때문에, 포인터에 메모리 주소를 입력할 수 있게 됩니다.

마지막 줄은 pInt2 포인터에 0을 대입합니다. 메모리 주소가 대입되지 않았다는 의미입니다. 말 그대로 초기화죠.

 

포인터의 사용 목적

포인터를 왜 사용하는 걸까요?

 

다음과 같은 세 가지 주요 목표 때문에 C와 C++에서 광범위하게 사용되고 있습니다.

  • 힙에 새로운 객체를 할당하기 위해서
  • 다른 함수에게 함수를 전달하기 위해서
  • 배열과 다른 데이터 구조 상의 요소들에 접근하기 위해서

위의 말이 한 번에 와닿지 않을 수도 있지만, 차차 무슨 얘긴지 알게 될 것입니다.

 

한마디로, 포인터가 가리키는 주소에 있는 객체를 읽고 사용하기 위해서 포인터를 사용한다고 정리할 수 있습니다.

 

포인터는 역참조(dereference) 연산자를 통해서, 가리키고 있는 객체의 값을 읽을 수 있습니다.

int A = 10;
int* pA = &A;	// A의 포인터

int B = *pA;	// pA를 통해서 A의 값을 역참조

위의 코드에서 pA는 변수 A의 주소를 가리키는 포인터입니다.

이 포인터 pA에 * (역참조 연산자)를 사용하면 포인터 pA가 가리키는 객체에 접근할 수 있습니다. 객체에서 값을 읽어낼 수 있을 뿐만 아니라, 변경이 가능한 경우 값을 변경할 수도 있습니다.

마지막 줄과 같이 *pA는 포인터 pA가 가리키는 A의 값에 접근할 수 있고, 이 값은 10이므로 B는 10이 되는 것입니다. 

 

하지만, 위의 예는 아주 형편없다고 할 수 있습니다. 왜냐하면 변수 B에 10을 넣고 싶으면 B에 바로 A를 대입하는 것이 정상이기 때문이죠. 전혀 포인터의 필요성을 전혀 느낄 수 없습니다.

 

그래서 바로 현실적인 예제를 보여드리겠습니다.

#include <iostream>
using namespace std;

void FuncA ( int A ){
    A += 10;
}

void FuncB ( int* pA ){
    *pA += 10;
}

int main(){

    int C = 3;
    int* pC = &C;	// C의 포인터 (C를 가리키는 포인터)
    
    FuncA(C);
    cout << C << "\n";	// 3이 출력됨
    
    FuncB(pC);
    cout << C << "\n";	// 13이 출력됨
    
    return 0;
}

이 예제의 결과를 처음 보는 분은 알 듯 모를 듯한 기분이 들지도 모르겠습니다.

 

자세히 설명하자면, FuncA를 호출할 때 인자 A는 C의 값을 복사할 뿐이고 A에 10을 더하고 난 뒤, 함수를 벗어나면 인자 A는 사라집니다. 함수 내의 변수는 함수 범위 내에서만 효력이 있고, 다른 함수로 넘어가면 효력이 없기 때문입니다.

그래서 C의 값은 3으로 값이 변경되지 않습니다.

 

마찬가지로 FuncB를 호출할 때 포인터 pA는 pC의 값(메모리 주소)을 복사할 뿐입니다. 그런데 이번에는 pA가 가리키는 객체(메모리 주소)가 pC가 가리키는 객체인 C와 같다는 점이 다르죠. 

이러한 포인터 pA에 역참조 연산을 써서 C의 값을 바꾸게 되면  FuncB 함수 외부에 있는 객체 C의 값을 바꾸게 됩니다.

 

C언어를 시작할 때 많이들 배우는 scanf 함수 사용법을 이해할 때가 왔습니다.

#include <iostream>

int main() {

    int a, b, c;
    int* pa = &a;	// a의 포인터
    int* pb = &b;	// b의 포인터

    scanf("%d %d %d", pa, pb, &c);	// &c는 c의 메모리 주소

    printf("%d %d %d\n", a, b, c);

    return 0;
}

포인터 사용의 전형적인 예입니다.

scanf  함수의 인자로 변수 a, b, c의 포인터를 넘기고 있습니다. scanf 함수 내부에선 역참조 연산자를 사용하여 a, b, c의 값을 사용자의 입력 값으로 바꾸고 있죠.  

 

new, delete 연산자

이번에는 힙(heap)에 변수를 할당하는 경우, 포인터가 사용되는 법을 알아보겠습니다.

 

메모리에 객체를 할당할 때 사용되는 연산자는 new입니다.

new는 객체 또는 객체 배열을 할당하고 초기화하려고 시도하고 개체에 적합한 형식의 포인터를 반환합니다.

 

반대로 메모리 상에 할당되었던 객체를 삭제하기 위해 사용되는 연산자는 delete입니다.

인수는 new 연산자를 통해 이전에 할당되었던 메모리에 대한 포인터입니다.

 

int* pInt = new int;

*pInt = 3;	// 역참조를 통해 할당된 메모리에 접근

delete pInt;

위의 예는 메모리 상에 할당된 int객체를 포인터를 통해서 사용하는 법을 보여주고 있습니다.

 

포인터에 대한 +, ++ 연산자

다음 코드를 컴파일하면 "error: cannot convert 'int*' to 'bool*' in initialization"라는 문구를 만나게 됩니다.

int a = 10;
int* pInt = &a;		// ok

bool* pBool = &a;	// error

위 문구는 정수형 데이터 주소를 bool형 포인터에 담아서 사용하지 말라는 뜻입니다. C언어에서는 권장하지 않는다는 뜻이죠. 왜일까요?

 

위의 int형 포인터 pInt에 증감연산자++를 사용하면 pInt가 저장하고 있는 메모리 주소는 어떻게 될까요? 

마찬가지로 pInt에서 pInt+1 하면 메모리 주소가 1이 증가될까요?

정답은

포인터증감 연산자를 사용하면 데이터 타입의 크기만큼 메모리 주소를 증감시킵니다.

 

입니다.

그리고 결과적으로 그다음의 데이터를 가리키게 됩니다.

실제로 어떻게 되는지 알아봅시다.

#include <iostream>
using namespace std;

int main() {

    int a = 10;
    int* pInt = &a;
    
    bool* pBool = (bool*)&a;	// bool 데이터 타입으로 강제 변환
    
    cout << "pInt의 주소 " << pInt << "\n";
    cout << "pInt+1의 주소 " << pInt+1 << "\n";
    
    pInt++;
    cout << "++연산 후의 pInt의 주소 " << pInt << "\n";

    cout << "pBool의 주소 " << pBool << "\n";
    cout << "pBool+1의 주소 " << pBool+1 << "\n";
}

출력:

pInt의 주소 0x61fe0c
pInt+1의 주소 0x61fe10
++연산 후의 pInt의 주소 0x61fe10
pBool의 주소 0x61fe0c
pBool+1의 주소 0x61fe0d

 

결과를 보다시피, +, ++ 연산자는 데이터 타입의 크기만큼 메모리 주소를 증가시킵니다.

연산자가 이렇게 동작하는 것은 int형 포인터에 대해 ++연산자가 메모리 주소를 1 증가시키는 것은 아무런 의미가 없기 때문입니다. 그래서 ++연산자는  그다음 int 데이터의 주소를 가리키도록 메모리 주소를 4만큼 증가시킵니다.

 

int arr[3] = { 1, 2, 3 };
int* pInt = &arr[0];

int b = *(pInt+1);	// 다음 배열 멤버

int 배열은 int 데이터가 메모리 상에 연속적으로 위치하고 있습니다. 그래서 pInt 포인터에 int의 크기인 4를 더하면 pInt가 가리키는 멤버의 다음 멤버를 가리키게 되고 그 값을 가져올 수 있습니다.

 

같은 의미로 bool형 포인터에서는 ++연산자가 메모리 주소를 1 증가시킵니다.

 

이번에는 실제적인 예를 들어보겠습니다.

#include <iostream>
using namespace std;

struct studant{
    string name;
    int math;
    int history;
};

int main() {

    studant st[3] = { {"aa", 85, 90}, {"bb34", 95, 93}, {"ccrte", 87, 90} };

    studant* pSt = &st[0];	// 첫 번째 멤버의 포인터
    
    for(int i = 0; i < 3; i++){
        
        studant* pMember = pSt+i;	// i번째 멤버의 포인터
        
        cout << pMember->math << "\n";	// ->는 구조체의 멤버 연산자
    }
    
    return 0;
}

구조체 studant 타입의 포인터 pSt에 ++ 연산자를 사용하면 studant의 데이터의 크기를 몰라도 다음 studant 데이터를 가리키고 있는 것을 알게 되는 것이죠.

 

다른 함수에게 함수를 전달하기

다른 함수에게 함수를 전달하는 방법으로 "함수 포인터"라는 것을 사용합니다.

함수 포인터는 함수 객체를 가리키는 포인터의 일종으로, 이것에 대하여 설명할 내용이 좀 있습니다.

다른 글에서 따로 정리하는 것이 좋겠습니다.

 

 

정리

  • 포인터는 메모리 상의 객체를 읽고, 사용하기 위해 사용하는 변수이다.
  • 주소 연산자 &는  객체의 메모리 주소를 알려준다.
  • 역참조 연산자 *를 사용함으로써 포인터가 가리키는 객체를 사용할 수 있다.

이 세 가지만 기억하시면 나머지를 차차 따라오게 될 것입니다.

 

이 글과 관련된 글들

포인터로 배열에 접근하기

함수 포인터 개념 및 사용하는 이유

void 포인터의 개념과 사용법

 

 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유