int main()
{
int Left = 1; // Lvalue
int Right = 1; // Lvalue
Left + Right; // Rvalue
// 새로이 4byte 소모 => 결과값도 어딘가에 존재하기는 하지만 명시적 이름은 없다
} // 총 12byte(Left, Right, 결과값) 소모
무언가 존재한다면 반드시 위치가 있어야 한다. ⇒ 모든 것은 메모리를 소모하기 때문
Lvalue Type
⇒ 명시적 이름(주소, 포인터)이 있어서 메모리에 접근이 가능함
⇒ 왼쪽에 올 수 있는 타입
Rvalue Type
⇒ 위치를 특정할 수는 없지만 분명히 존재함, 주로 연산의 결과값
⇒ 오른쪽에만 올 수 있는 타입
int Test()
{
return 10; // 리턴!
}
int main()
{
int Left = 1;
// 위치 : main 함수 실행 스택메모리 (그 안에서도 정확한 위치 특정 가능)
// 크기 : 4byte
// 형태 : 정수
// 값 : 1
Left = Test(); // 10
} // 총 8byte(Left, Test()의 리턴값) 소모
함수 속 return
⇒ 함수를 즉각 종료시키고, 리턴할 값을 외부로 보낸다.
⇒ 리턴값은 한 번에 한 개로 정해져 있다.
void ParameterStart(int Value)
{
Value = 99999; // (2) Value의 값 : 99999
} // (3) Value를 포함한 ParameterStart() 메모리 삭제
int main()
{
int TestValue = 10;
ParameterStart(TestValue); // (1) Value의 값 : 10 (TestValue의 값을 복사받음)
// (4) TestValue의 값 : 10
}
인자
⇒ 함수가 실행될 때 인자 자리에 들어온 값을 딱 한번 복사받는다.
⇒ 지역변수이다.
변수의 주소값을 저장하기 위한 변수
void Damage(int _Hp, int _Att)
{
_Hp = _Hp - _Att;
}
int main()
{
{
int MonsterHp = 100;
Damage(MonsterHp, 10); // 이렇게 해도 MonsterHp는 변하지 않는다
// Damage() 내부의 _Hp와 MonsterHp는 완전히 다른 개체이기 때문 ⇒ 주소가 다르다!
}
}
int main()
{
int Value = 0;
// 위치 : 100번지(라고 가정하자)
// 크기 : 4byte
// 형태 : 정수
// 값 : 0
int* ValuePtr = &Value;
// 위치 : 120번지(라고 가정하자) => 포인터도 반드시 위치를 가지고 있어야 한다!!!
// 크기 : 8byte
// 형태 : int 주소값
// 값 : 100번지
int** ValuePtrPtr = &ValuePtr;
// 위치 : 150번지(라고 가정하자)
// 크기 : 8byte
// 형태 : int* 주소값
// 값 : 120번지
}
⇒ 자료형 뒤에 *
을 붙이면, 해당 자료형의 포인터형이 된다. (int*
, bool*
, etc.)
⇒ 주소값은 무조건 정수이다.
⇒ 포인터를 위한 연산자와 문법이 별도로 존재한다.
⇒ 특정 주소에 직접적으로 접근할 수 있어 위험하기 때문
int* ValuePtr = Value; // 불가능
int* ValuePtr = &Value; // 가능
⇒ int*
에는 일반적인 정수가 아닌, 번지수를 대입해줘야 한다
💡 16진수
int BinValue = 0b 00000000 00000000 00000000 00000000; // 2진수 int HexValue = 0x 00 00 00 00; // 16진수
__int64 Address000 = 0x00000046104ffa14; // e.g.
⇒ 주소값은 16진수로 표현된다.
⇒ 주소값은 엄청 큰 숫자까지 가능해서, 4byte인 int
로는 담을 수가 없어 __int64
를 사용했다.
int main()
{
int Value = 0;
int* ValuePtr = Value; // (1) 불가능
bool Check = Value; // (2) 손실 발생!
}
자료형이 다르면 기본적으로 대입이 안된다.
1. 안된다고 생각하고, 하지 않도록 주의해야 한다!!!
2. 하지만 사실 될 때도 있긴 하다... (암시적 형변환)
char => 1byte 문자형 0b00000000
bool => 1byte 논리형 0b00000000
⇒ 이런 식으로 어짜피 byte로 표현되기 때문에 C와 C++은 자료형에 크게 의미를 두지 않는다...
⇒ 혹시 하게 된다면 자료형이 다름에도 대입했다는 점을 인지하고, 손실과 변형을 감수해야 한다.
⇒ 하지만 포인터는 절대로 할 수 없도록 막아두었다.
// <C스타일 형변환>
int Value = 10;
bool Check = (bool)Value; // true
Value = (int)Check; // 1 => 손실과 변형 발생
// 이걸로 해도 문제는 없지만, C++에서는 사용하지 않는 편
// 아래의 방식을 사용하자
// <C++ 스타일 형변환>
// 값형 => 일반적인 변수
// 참조형 => 포인터, 레퍼런스
// Static Cast : 값 <-> 값 => 바이트 크기만 다른 값형태
int Value = 100;
bool Check = static_cast<bool>(Value); // true
Value = static_cast<int>(Check); // 1
// Reinterpret Cast : 값 <-> 참조
int Value0 = 100;
int Value1 = 999;
int* Ptr = &Value0;
__int64 Address0 = Ptr; // 불가능
__int64 Address1 = reinterpret_cast<__int64>(Ptr); // 가능
__int64 Address2 = reinterpret_cast<__int64>(&Value0); // 가능
__int64 Address3 = reinterpret_cast<__int64>(&Value1); // 가능
// Value0의 주소값보다 Value1의 주소값이 더 크다
형변환이 필요할 땐 반드시 명시적 형변환을 하자!
int*
변수 앞에 *
을 붙이면 ⇒ *(아스테리스크)
를 하나 뺀다 ⇒ 가리키는 값 그 자체가 된다!
void Damage(int* _Hp, int _Att)
{
*_Hp = *_Hp - _Att;
// _Hp
// 위치 : 80번지
// 크기 : 8byte
// 형태 : int*
// 값 : 100번지
// _Att
// 위치 : 88번지
// 크기 : 4byte
// 형태 : int
// 값 : 20
}
int main()
{
int MonsterHp = 200;
// 위치 : 100번지
// 크기 : 4byte
// 형태 : int
// 값 : 200
int* MonsterHpPtr = &MonsterHp;
// 위치 : 120번지
// 크기 : 8byte
// 형태 : int*
// 값 : 100번지
*MonsterHpPtr = 50; // MonsterHp = 50;
Damage(&MonsterHp, 20); // MonsterHp = 30;
}
‘사용하지 않는 포인터는 0으로 값을 넣어두자’는 약속
// C 스타일
int* Ptr = 0;
*Ptr = 100; // 불가능 (액세스 위반)
// C++ 스타일
int* Ptr = nullptr;
// C++에서 0은 정수이기 때문에, ‘비어있는 포인터’라는 의미로 혼용되면 안된다고 여겨 nullptr이라는 상수가 생겼다
⇒ 0번지, nullptr_t
형
⇒ 주소값을 찾아야 하거나, 아직 가리킬 대상을 정하지 못한 경우에 사용
⇒ 0의 주소값을 가진 것을 사용하려 하면 에러가 발생한다 (null reference exception)
상시 *
이 붙은 상태로 사용하는 포인터
void Damage(int* _MonsterHp, int _Att)
{
*_MonsterHp = *_MonsterHp - _Att;
}
void DamageRef(int& _MonsterHp, int _Att)
{
_MonsterHp = _MonsterHp - _Att;
}
int main()
{
{
int MonsterHp = 100;
int* Ptr = &MonsterHp;
*Ptr = 200;
*Ptr = 300;
}
{
int MonsterHp = 100;
int& Ref = MonsterHp;
// int& Ref; => 이런 선언은 불가능
Ref = 200;
Ref = 300;
}
{
int MonsterHp = 100;
Damage(&MonsterHp, 30); // MonsterHp = 70
DamageRef(MonsterHp, 30); // MonsterHp = 40
}
}
⇒ 포인터는 null일 수 있지만, 레퍼런스는 무조건 참조할 대상이 존재할 때만 사용할 수 있다.
{
int Value0 = 10;
int Value1 = 20;
int* Ptr = &Value0;
*Ptr = 1000; // Value0 = 1000
Ptr = &Value1;
*Ptr = 2000; // Value1 = 2000
Ptr = nullptr;
}
{
int Value0 = 10;
int Value1 = 20;
int& Ref = Value0;
Ref = 1000; // Value0 = 1000
Ref = Value1; // Value0 = 20
Ref = 2000; // Value0 = 2000
// Value1의 값은 변경되지 않는다
}
⇒ 포인터는 가리키는 대상을 중간에 변경할 수 있지만, 레퍼런스는 한번 지정하면 대상이 변경되지 않는다.
💡
sizeof()
연산자
특정 자료형의 바이트 크기를 확인할 수 있는 연산자int Value; int IntSize = 0; int PtrSize = 0; IntSize = sizeof(int); // 4 IntSize = sizeof(Value); // 4 PtrSize = sizeof(int*); // 8
int Value = 0;
int* Ptr = &Value;
int SizePtrValue = sizeof(Ptr); // 8
int& Ref = Value;
int SizeRefValue = sizeof(Ref); // 4
int SizePtrValue = sizeof(*Ptr); // 4
int SizeRefValue = sizeof(int); // 4
// 셋 다 동일한 의미나 마찬가지
⇒ 포인터는 8byte이지만, 레퍼런스는 4byte이다.
⇒ Ref는 *
이 붙은 int*
의 형태이기 때문에, int
와 동일
int[]
(int 배열형)으로 사용할 수 있다.
int MonsterHps[5] = {11, 22, 33, 44, 55};
int MonsterHp1 = MonsterHps[0];
int MonsterHp2 = MonsterHps[1];
int MonsterHp3 = MonsterHps[2];
int MonsterHp4 = MonsterHps[3];
int MonsterHp5 = MonsterHps[4];
int* Ptr = &MonsterHps[0]
int& Ref = MonsterHps[0]; // MonsterHps[i] == int&
__int64 Address0 = reinterpret_cast<__int64>(&MonsterHps[0]);
__int64 Address1 = reinterpret_cast<__int64>(&MonsterHps[1]);
__int64 Address2 = reinterpret_cast<__int64>(&MonsterHps[2]);
__int64 Address3 = reinterpret_cast<__int64>(&MonsterHps[3]);
__int64 Address4 = reinterpret_cast<__int64>(&MonsterHps[4]);
// 모든 변수는 붙어있으므로, 주소를 살펴보면 4씩 떨어져있다!
int MonsterHps[5] = {11, 22, 33, 44, 55};
int* Ptr = MonsterHps;
int MonsterHp1 = Ptr[0];
int MonsterHp2 = Ptr[1];
int MonsterHp3 = Ptr[2];
int MonsterHp4 = Ptr[3];
int MonsterHp5 = Ptr[4];
// 위와 동일한 의미
MonsterHp1 = *(Ptr + 0);
MonsterHp2 = *(Ptr + 1);
MonsterHp3 = *(Ptr + 2);
MonsterHp4 = *(Ptr + 3);
MonsterHp5 = *(Ptr + 4);
__int64 Address0 = reinterpret_cast<__int64>(Ptr + 0); // 100번지(라고 가정)
__int64 Address0 = reinterpret_cast<__int64>(Ptr + 1); // 104번지
__int64 Address0 = reinterpret_cast<__int64>(Ptr + 2); // 108번지
__int64 Address0 = reinterpret_cast<__int64>(Ptr + 3); // 112번지
__int64 Address0 = reinterpret_cast<__int64>(Ptr + 4); // 116번지
// Ptr의 주소값 + (sizeof(int) * i)
⇒ 배열로 사용 가능한 문법은, 포인터를 이용해서도 사용 가능하다.
int MonsterHps[5] = {11, 22, 33, 44, 55};
int* Ptr = MonsterHps;
int ArrSize = sizeof(MonsterHps); // 20
int PtrSize = sizeof(Ptr); // 8
// 크기가 다르다
⇒ 하지만 포인터와 배열이 같은 것은 절대 아니다.
// Q) 함수의 인자는 기본적으로 지역번수이지만, 차이점이 있다. 주소를 확인하여 차이점을 알아내보자.
void Test(int _Value0, int _Value1, int _Value2, int _Value3)
{
__int64 add0 = reinterpret_cast<__int64>(&_Value0); // 887335418080
__int64 add1 = reinterpret_cast<__int64>(&_Value1); // 887335418088
__int64 add2 = reinterpret_cast<__int64>(&_Value2); // 887335418096
__int64 add3 = reinterpret_cast<__int64>(&_Value3); // 887335418104
// int인데도 8byte씩 떨어져있다!
// 가장 큰 기본자료형이 8byte이기 때문
/*
이렇게도 가능하지만 좀 더 번거로운 방식이다
int* ptr0 = &_Value0;
int* ptr1 = &_Value1;
int* ptr2 = &_Value2;
int* ptr3 = &_Value3;
add0 = reinterpret_cast<__int64>(ptr0);
add1 = reinterpret_cast<__int64>(ptr1);
add2 = reinterpret_cast<__int64>(ptr2);
add3 = reinterpret_cast<__int64>(ptr3);
*/
}
int main()
{
Test(0, 0, 0, 0);
}
📢 코딩스탠다드
- 전역변수와 지역변수는 이름을 구분하여 작성하자
- 변수에는 반드시 초기값을 설정하자
- 함수의 인자 앞에는 언더바를 붙이자 (new!)
- 포인터를 초기화할 때 절대 0을 사용하지 말고, nullptr을 사용하자 (new!)
- 경로, 함수명, 변수명에 한글은 절대 쓰지 말자 (new!)
- 프로젝트 생성 시 바탕화면은 절대 피하자 (new!)