🎯 메모리 주소 표현
변수를 선언하면 해당 변수의 크기만큼 메모리에 공간이 할당 되고 그 공간에 데이터를 저장하게 됩니다.
변수에 메모리 주소가 부여되면, 해당 주소를 이용해서 공간을 정확하게 찾을 수 있습니다.
주소는 바이트 단위로 부여가 되는데, 메모리 주소는 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 = #
*pnum = 200;
++(*pnum);
std::cout << num << std::endl;
std::cout << &num << std::endl;
std::cout << pnum << std::endl;
▶️ 포인터의 강제형변환
int num = 100;
int* pnum = #
float* pFNum = ( float* )#
*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 = #
void* pVoid = #
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 = #
// 이런식으로 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 = #
*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 = #
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 = #
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 = #
int* pNum2 = pNum;
// 일반 변수의 메모리 주소를 넣을 수 없음
//int** ppNum = #
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;
}
'개발개발 > c++' 카테고리의 다른 글
🍯 꿀팁 : 성능 테스트를 위한 시간 계산코드 (0) | 2025.01.09 |
---|---|
c++ 문자열 함수, typedef (0) | 2024.12.30 |
c++의 참조변수 (reference variable) (0) | 2024.12.29 |
템플릿 - 비타입 인자, 템플릿 특수화, 템플릿 가변인자 (0) | 2024.12.26 |
STL과 벡터 (0) | 2024.12.24 |
댓글