[TIL] 23-12-14

Lev·2023년 12월 14일
0

📓TIL Archive

목록 보기
4/33

함수 (이어서)

리턴값

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;
}

nullptr

‘사용하지 않는 포인터는 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);
}

📢 코딩스탠다드

  1. 전역변수와 지역변수는 이름을 구분하여 작성하자
  2. 변수에는 반드시 초기값을 설정하자
  3. 함수의 인자 앞에는 언더바를 붙이자 (new!)
  4. 포인터를 초기화할 때 절대 0을 사용하지 말고, nullptr을 사용하자 (new!)
  5. 경로, 함수명, 변수명에 한글은 절대 쓰지 말자 (new!)
  6. 프로젝트 생성 시 바탕화면은 절대 피하자 (new!)
profile
⋆꙳⊹⋰ 𓇼⋆ 𝑻𝑰𝑳 𝑨𝑹𝑪𝑯𝑰𝑽𝑬 ⸝·⸝⋆꙳⊹⋰

0개의 댓글