본문 바로가기
개발개발/c++

c++ 포인터, void 포인터, 다중포인터

by 유잉유잉유잉 2024. 12. 31.
728x90

 

🎯 메모리 주소 표현 

변수를 선언하면 해당 변수의 크기만큼 메모리에 공간이 할당 되고 그 공간에 데이터를 저장하게 됩니다. 


변수에 메모리 주소가 부여되면, 해당 주소를 이용해서 공간을 정확하게 찾을 수 있습니다.

 

주소는 바이트 단위로 부여가 되는데, 메모리 주소는 16진수로 표현됩니다.


x86으로 개발시 32비트 체제를 사용하기 때문에 메모리의 주소는 32비트를 차지하게  됩니다.

16진수 1자리는 2진수 4자리를 사용하기 때문에 32비트는 16진수 8자리를 표현할 수 있습니다.

주소를 표현하는데 4byte가 사용됩니다.


x64로 개발시 64비트 체제를 사용하기 때문에 메모리의 주소는 64비트를 차지하게 됩니다. 

16진수 16자리를 표현할 수 있습니다.

주소를 표현하는데 8byte가 사용됩니다.

 

 

 

 

🎯 포인터

  • 메모리 주소를 저장하기 위한 변수타입 입니다.

 

  • 모든 변수 타입들은 포인터타입 변수를 생성할 수 있습니다.

 

  • 개발 환경에 따라 다른 크기로 메모리 주소를 저장하게 됩니다.
    • x86 : 4바이트
    • x64 : 8바이트
  • 참조 : 포인터 변수가 메모리 주소를 가지게 되는것

 

  • 역참조 : 가지고 있는 메모리 주소에 접근하는것

 

  • 포인터 선언시 타입을 동일하게 선언해야 합니다.
    • int변수의 메모리 주소를 저장하기 위해서는 int의 포인터 타입으로 선언해야 합니다.

 

  • 변수 앞에 &을 붙여주게 되면 해당 변수의 메모리 주소값을 반환합니다.
int num = 100;

std::cout << num << std::endl;
std::cout << &num << std::endl;

  • 64비트로 만들어졌기 때문에 메모리 주소가 16진수 16자리로 표현된것을 확인하실 수 있습니다. 

 

 

▶️ 포인터 변수의 선언

  • 변수명 앞에  * 를 붙여서 선언합니다.
  • 포인터 변수에 일반 변수의 주소(&변수)를 대입하여 사용합니다
  • 포인터 변수가 역참조 (포인터 변수가 저장한 주소의 변수의 값)하여 값을 변경할땐 포인터 변수명 앞에 * 을 붙이고 사용합니다.
  • 중간에 참조대상 변경이 가능합니다.
    int num = 100;
    int num2 = 300;
    int* pnum = &num2;
    pnum = &num;

    *pnum = 200;
    ++(*pnum);

    std::cout << num << std::endl;
    std::cout << &num << std::endl;
    std::cout << pnum << std::endl;

 

 

 

▶️ 포인터의  강제형변환

int num = 100;
int* pnum = &num;
float* pFNum = ( float* )&num;

*pFNum = 22.7355f;

std::cout << num << std::endl;
std::cout << &num << std::endl;
std::cout << pnum << std::endl;
std::cout << pFNum << std::endl;
std::cout << *pFNum << std::endl;

num의 값이 엉뚱한 값이 나오는것을 확인하실 수 있습니다.

int형의 경우 부호비트 외에는 모두 정수를 표현하는데 쓰이지지만

float형은 부동소수점 타입이라 지수부(소수점 단위 표현)가 따로 있기 때문입니다

소수점 단위를 표현하기 위한 부분이 int형에서 잘못 계산되기 때문에 큰 수가 나오게 됩니다. 

가급적이면 타입을 맞춰서 사용하길 추천드립니다.

특히 void 포인터 사용할 때 이런 문제가 자주 발생합니다. 

 

 

 

 

▶️ void 포인터

  • 어떤 타입의 변수라도 메모리 주소 저장이 가능합니다.
  • 역참조가 불가능 합니다
    • int 포인터일 경우 int 변수의 크기인 4byte만큼의 역참조를 하면 된다는 정보를 가지고 있습니다.
    • void는 타입 정보가 없기 때문에 역참조가 불가능합니다.
int num = 100;
float num2 = 0.1f;
int* pnum = &num;
void* pVoid = &num;
pVoid = &num2;

*pnum = 200;
++*pnum;

std::cout << num << std::endl;
std::cout << &num << std::endl;
std::cout << pnum << std::endl;
std::cout << pVoid << std::endl;

 

void 포인터도 형변환을 통한 역참조가 가능합니다. 

int num = 100;
void* pVoid = &num;

// 이런식으로 int pointer에 int포인터형으로 형변환 한 void 포인터를 옮길 수 있음
int* pTest = ( int* )pVoid;

*( ( int* )pVoid ) = 50;

std::cout << num << std::endl;

 

 

 

‼️void 포인터 형에서 형변환시 주의사항

float	Number1 = 3.14f;
void* pNumberVoid = &Number1;

*( ( int* )pNumberVoid ) = 9999;

std::cout << "Number1 = " << Number1 << std::endl;

잘못된 타입으로 형변환할 경우 원치않은 값이 나올 수 있습니다.

 

 

 

‼️함수에서 포인터 사용시 주의사항 : 댕글링포인터

int* GetIntAddress()
{
    int	NumberFunc = 500;

    return &NumberFunc;
}

int main()
{
    int* pNumAddr = GetIntAddress();

    std::cout << "pNumAddr = " << pNumAddr << std::endl;

    return 0 ;
}

NumberFunc 변수는 이 함수가 호출될때 메모리가 생성되고 이 함수의 호출이 종료될 때 메모리가 제거가 됩니다.


여기서 이 변수의 주소를 반환하게 되면 이 함수가 종료될때 반환된 주소는 이미 제거된 메모리가 되므로 반환된 주소를 이용하여 역참조를 시도할 시 문제가 발생합니다.


이렇게 제거된 메모리 주소를 가지고 있는 포인터를 댕글링포인터라고 합니다.

개발시 댕글링포인터를 가지지 않도록 주의해야합니다

 

 

 

 

▶️ 배열에 포인터 사용하기

int array[5] = {};

std::cout << array << std::endl;

cout을 통해 배열 array를 출력하면 array의 주소 (시작주소)가 나옵니다

이 예제를 통해 배열의 이름은 이 배열이 할당된 메모리의 시작 주소임을 알 수 있습니다.

 

 

 

int array[5] = {};
int* pArray = array;

pArray[0] = 33;

std::cout << *pArray << std::endl;

int형 포인터에 array는 배열의 주소이니 할당을 하였습니다.

배열과 동일하게 pArray에선 인덱스를 통한 접근이 가능합니다. 

pArray도 array와 동일하게 배열의 시작 주소를 가지고 있으므로

인덱스를 이용하여 동일한 배열에 접근이 가능한 것 입니다.

 

 

▶️ 구조체형 포인터

struct FMonster
{
	char name[32];
    int level;
}

int main()
{
	FMonster monster;
    FMonster * pMonster = &monster;
    
    // 에러 : .level 왼쪽에는 클래스/구조체/공용 구조체가 있어야 합니다.
    //*pMonster.Level = 30;
}

구조체형 포인터도 선언이 가능합니다.

그런데 위 예시에서 pMonster가 level에 접근하려 할 때 에러가 납니다.

-> * (역참조 연산자)와 . 은 연산자 우선순위에 의해 생기는 에러입니다.

역참조보다 .이 우선순위가 높기 때문에 발생합니다.

(*pMonster).level = 30;
pMonster->level = 40;

위처럼 괄호를 통해 우선순위를 정해주거나 또는

화살표(->)를 이용하여 접근이 가능합니다

 

 

 

▶️ 포인터 연산 : +, -

포인터 연산은 포인터 변수가 배열의 주소를 알고 있을 경우 주로 활용됩니다.

int arr[10] = {};
int* pArr = arr;

std::cout << pArr << std::endl;
std::cout << pArr + 1 << std::endl;

두번째 주소는 첫번째 주소에서 변수의 타입인 int형의 크기(4byte)만큼 값이 더해진 상태입니다 

 

int arr[10] = {};
int* pArr = arr;
bool* pBoolArr = ( bool* ) arr;

std::cout << pArr << std::endl;
std::cout << pArr + 1 << std::endl;

std::cout << pBoolArr << std::endl;
std::cout << pBoolArr + 1 << std::endl;

bool 포인터의 경우 포인터에 1을 더했을 때, bool의 크기인 1byte 만큼 크기가 늘어난 것을 확인하실 수 있습니다.

 

위 예제에서 알 수 있듯이

포인터 연산은 포인터 변수 타입의 메모리 크기 만큼 증가 혹은 감소하게 됩니다.

 

배열 또한 타입에 따라 크기가 할당된 동일한 주소 구조로 이뤄 졌기에

인덱스를 통한 접근이 가능하다는것을 알 수 있습니다.

std::cout << pArr[2] << std::endl;
std::cout << *(pArr + 2 )  << std::endl;

두개의 문장은 동일하게 arr배열의 2번 배열에 접근합니다.

 

 

포인터 연산은 +와 -만 지원됩니다.

 

 

 

 

▶️ const 포인터

int arr[3] = {};

const int * pArr = arr;

// pArr[0] = 1; // 값을 바꾸려 했기 때문에 에러

변수타입 왼쪽에 const를 붙일 경우 참조하는 대상의 값 변경이 불가능한 포인터가 만들어집니다. 

변경하지 않고 사용하는것은 가능합니다.

참조하는 대상을 바꾸는것도 가능합니다.

 

 

int num = 0;
int const * ptr = &num;

 *ptr = 2; // [에러!]

변수타입과 * 사이에 const가 붙었을 경우 

참조하는 대상의 값 변경이 불가능한 포인터가 만들어집니다. 

 

 

int arr[3] = {};

int * const pArr = arr;

int arr2[3] = {};

// pArr = arr2; // 에러

포인터 변수이름 왼쪽에 const를 붙일 경우

참조 대상을 변경이 불가능한 포인터가 만들어지게 됩니다.

참조하는 대상의 값을 변경하는것은 가능합니다.

 

 

 

const int * const pArr = arr;

변수 타입의 양쪽에 const가 붙을 경우,

참조대상도 참조하는 대상의 값도 변경이 불가능한 포인터가 만들어집니다.

 

 

 

▶️ 문자열에서 const char 포인터의 활용

// 문자열 리터럴 (""안에 들어간 문자열)은 const char * 타입을 가지게 된다.

// 타입이 맞지 않아서 에러
//char * Text = "문자열";

const char * Text = "문자열";

 

 

▶️ 포인터 타입의 배열도 선언이 가능

int * ptrArr[10] = {};

std::cout << ptrArr << std::endl;
std::cout << ptrArr + 1 << std::endl;

8만큼 늘어난 이유는 포인터는 변수의 "주소"를 저장하기 때문입니다.

x64 기준으로 주소의 크기는 8byte기 때문에 주소를 증가시킬 경우 8씩 증가하게 됩니다.

 

 

 

▶️ 포인터 레퍼런스 : *&

int	num		= 1;
int&	refNum		= num;
int*	pNum		= &num;
int**	ppNum		= &pNum;
int*&	prNum		= pNum;
int*	(&prNum2)	= pNum;

std::cout << "num : " << num << std::endl;
std::cout << "&num : " << &num << std::endl << std::endl;

std::cout << "pNum : " << pNum << std::endl;
std::cout << "*pNum : " << *pNum << std::endl;
std::cout << "&pNum : " << &pNum << std::endl;
std::cout << "pNum type : " << typeid( pNum ).name() << std::endl << std::endl;

std::cout << "ppNum : " << ppNum << std::endl;
std::cout << "*ppNum : " << *ppNum << std::endl;
std::cout << "&ppNum : " << &ppNum << std::endl;
std::cout << "ppNum type : " << typeid( ppNum ).name() << std::endl << std::endl;

std::cout << "prNum : " << prNum << std::endl;
std::cout << "*prNum : " << *prNum << std::endl;
std::cout << "&prNum : " << &prNum << std::endl;
std::cout << "prNum type : " << typeid( prNum ).name() << std::endl << std::endl;

위 예제에서 int *& prNum은 int형 포인터의 레퍼런스입니다. 

int*&	prNum		= pNum;

prNum은 pNum의 또다른 별칭이라고 생각하면 이해가 쉽습니다.

실제로 prNum은 pNum과 동일하게 사용 가능합니다.

prNum과 prNum2도 동일한 표현입니다.

 

 

int	num		= 1;
int*	pNum		= &num;
int*&	prNum		= pNum;

*pNum = 20;

std::cout << "num : " << num << std::endl;
std::cout << "*pNum : " << *pNum << std::endl;
std::cout << "*prNum : " << *prNum << std::endl;

*prNum = 30;

std::cout << "num : " << num << std::endl;
std::cout << "*pNum : " << *pNum << std::endl;
std::cout << "*prNum : " << *prNum << std::endl;

 

 

 

 

▶️ 함수의 인자로 포인터의 레퍼런스 사용 

void GetAddress( int** ppPtr )
{
	*ppPtr = ( int* )0x00467700;
}

void GetAddress2( int*& ppPtr )
{
	ppPtr = ( int* )0x00467700;
}

void main3()
{
	int* ptr = nullptr;
	GetAddress( &ptr );

	int* ptr2 = nullptr;
	GetAddress2( ptr );
}

이중포인터처럼 사용이 가능하나 헷갈릴 수 있기 때문에 

포인터의 레퍼런스보단 이중포인터를 매개변수로 사용하기를 권장드립니다.

 

 

 

▶️ 다중포인터

  • 일반 포인터 : 일반 변수의 메모리 주소를 담아놓는 변수
  • 이중 포인터 : 일반 포인터 변수의 메모리 주소를 담아놓는 변수
  • 삼중 포인터 : 이중 포인터 변수의 메모리 주소를 담아놓는 변수 
#include <iostream>

int main()
{
    int		num		= 10;
    int		num2	= 20;
    int*	pNum	= &num;
    int*	pNum2	= pNum;
    // 일반 변수의 메모리 주소를 넣을 수 없음
    //int** ppNum	= &num;
    int**	ppNum	= &pNum;
    int***	pppNum	= &ppNum;

    std::cout << "num = " << num << std::endl;
    std::cout << "num2 = " << num2 << std::endl;

    std::cout << "num addr = " << &num << std::endl;		// 000000689332F844
    std::cout << "num2 addr = " << &num2 << std::endl;		// 000000689332F864

    std::cout << "pNum = " << pNum << std::endl;			// 000000689332F844
    std::cout << "*pNum addr = " << *pNum << std::endl;		// 10
    std::cout << "&pNum addr = " << &pNum << std::endl;		// 000000689332F888

    std::cout << "ppNum  = " << ppNum << std::endl;		// 000000689332F888
    std::cout << "*ppNum  = " << *ppNum << std::endl;	// 000000689332F844
    std::cout << "**ppNum = " << **ppNum << std::endl;	// 10
    std::cout << "&ppNum addr = " << &ppNum << std::endl;	// 000000689332F8C8

    // pNum은 num변수의 메모리 주소를 가지고 있기 때문에
    // 역참조를 통해 num변수의 값을 바꿀 수 있다.
    *pNum = 500;

    // ppNum은 pNum 포인터 변수의 메모리주소를 가지고 있기 때문에
    // 역참조를 통해 pNum포인터 변수가 가지고 있는 메모리주소를 다른 주소로 변경할 수도 있다.
    *ppNum = &num2;
}

 

 

 

 

 

728x90

댓글