[C++ 기초] 바이트 패딩, 클래스의 메모리, const 위치에 따른 의미, 선언과 구현 분리, 레퍼런스 추가 예시

라멘커비·2023년 12월 26일
0

CPP 입문

목록 보기
10/25
post-thumbnail

지난 코드의 문제점 해결해보기

🌀메모리

  • 빈 클래스의 크기는 1바이트이다. 스트룹선생님이 그렇게 만드신거임.
    (이유 : 인간의 편의 + 0바이트를 허용하면 많은 문법이 무너짐.)
    함수는 클래스의 크기에 영향을 주지 않는다.
class A {

};

class B {
public:
	void Function(){

	}
};

int main() {
	printf_s("A Size = %lld\n", sizeof(A)); // 1 바이트
	printf_s("B Size = %lld\n", sizeof(B)); // 1 바이트
	return 0;
}

  • (아래 코드) 빈 클래스가 0바이트였다면 A 100개짜리 배열인 NewA도 0바이트이고, NewA + 2를 해도 번지 이동이 되지 않는다.
A NewA[100];
A* NewA;
NewA + 2;

🌀바이트 패딩

바이트 패딩은 메모리 정렬법이다.

바이트 패딩 규칙

  1. 전 멤버변수 중에 가장 큰 바이트를 가진 기본자료형을 찾는다.
  2. 어떤 바이트의 변수가 나오든지 그 가장 큰 바이트로 할당한다.
  3. 그 다음 바이트가 2, 4 혹은 8바이트 안에 함께 들어갈 수 있는 변수라면 그 뒤로 채운다.
    (다시 그 안에서 가장 큰 바이트로 채운다.)
  4. 1번으로 돌아간다.

바이트 패딩에 따른 클래스 크기 예시

class C {
public:
	bool Test;
};

class D {
public:
	bool Test;
	int Test0;
};

class E {
public:
	bool Test0; 
	int Test1;
	bool Test2;
};

class F {
public:
	bool Test0;
	bool Test1;
	int Test2;
};

class G {
public:
	bool Test0;
	bool Test1;
	__int64 Test2;
};

class H {
public:
	G Test0; //G 크기 16
	F Test1; //F 크기 8
};

class Test0 {
	bool Test;
};
class Test1 {
	__int64 Test;
};
class I {
public:
	Test0 Test0; 
	Test1 Test1; 
};

class J {
public:
	bool Test0;
	int Test1;
	__int64 Test2;
};

int main() {
	printf_s("C Size = %lld\n", sizeof(C)); // 1 바이트, 비어있는 클래스의 크기가 1이라고 거기에 더해지는 것이 아니다.
	printf_s("D Size = %lld\n", sizeof(D)); // 8 바이트, bool 4(1바이트인데 3바이트 패딩), int 4로 측정된 것
	printf_s("E Size = %lld\n", sizeof(E)); // 12 바이트
	printf_s("F Size = %lld\n", sizeof(F)); // 8 바이트
	printf_s("G Size = %lld\n", sizeof(G)); // 16 바이트
	printf_s("H Size = %lld\n", sizeof(H)); // 24 바이트
	printf_s("I Size = %lld\n", sizeof(I)); // 16 바이트
	printf_s("J Size = %lld\n", sizeof(J)); // 16 바이트


	F NewF = F();
	__int64 Address0 = reinterpret_cast<__int64>(&NewF.Test0); //100번지
	__int64 Address1 = reinterpret_cast<__int64>(&NewF.Test1); //101번지
	__int64 Address2 = reinterpret_cast<__int64>(&NewF.Test2); //104번지
	return 0;
}

바이트 패딩 설명(클래스 E와 F 차이)

  • 클래스 E는 아래 모양새처럼 패딩이 생겨서 크기로 12바이트가 나왔다.
class E {
public:
	bool Test0; 
	int Test1;
	bool Test2;
};

bool 1바이트
패딩 1
패딩 1
패딩 1
int 4
	바
	이
	트
bool 1바이트
패딩 1
패딩 1
패딩 1
  • 클래스 F는 E와 변수의 순서가 다르기 때문에 패딩이 비교적 적게 생긴다. 그래서 클래스 F는 크기가 8바이트이다.
class F {
public:
	bool Test0;
	bool Test1;
	int Test2;
};

bool 1바이트
bool 1바이트
패딩 1
패딩 1
int 4
	바
	이
	트

🌀클래스 메모리

  • 클래스 포인터로 쓸때는 -> 사용
  • 아래 코드에서 두 Damage함수는 문법적으로는 차이가 있지만 결과적으로 차이가 없다.
  • 위의 멤버 함수가 사실 아래의 전역 함수와 똑같이 인식된다. 첫번째 인자에 자신을 호출한 객체의 포인터들어간다.
  • this를 사용할 때 거의 생략해서 쓰지만 존재를 잊으면 안된다.
class Player {
public:
	int Att = 10;
	int Hp = 100;
	void Damage(int _Damage) {
		Hp -= _Damage;
	}
};
// 위아래 두 Damage함수는 문법적으로는 차이가 있지만 결과적으로 차이가 없다.
void Damage(Player* _this, int _Damage) {
	// 방어코드
	if (nullptr == _this) {
		return;
	}
	// 클래스 포인터로 쓸때는 -> 사용
	_this->Hp = _Damage;
}
  • 이 코드와 같은 느낌(원리)이다.
    __thiscall : 함수 호출 규약 중 하나. 멤버함수에 자동으로 붙음. 이거때문에 첫번째 인자로 this 포인터 들어간다. 우리가 신경쓸필요 X
    //포인터 뒤 const이기 때문에 자기자신의 주소값을 바꿀 수 없다.
class Player {
public:
	int Att = 10;
	int Hp = 100;
	void __thiscall Damage(Player* const _this, int _Damage) {
		_this->Hp -= _Damage;
		//this->Hp -= _Damage;
		//포인터 뒤 const이기 때문에 자기자신의 주소값을 바꿀 수 없다.
	}
};
  • 들어온 Player 객체가 자기 자신인지 아닌지 확인할 방법이 없다. (NewPlayer1객체를 넣어줬더니 NewPlayer1의 Hp가 깎였다.)
    근데 지금은 일부러 _this를 명시해둔거라 신경 쓸 일은 아님. this는 따로 언제나 존재.

🌀const 붙이는 위치

const 자료형 const * const 다 가능

//세 개 다 동일한 의미가 됨..
int const Value0 = 20;
const int Value1 = 20; // <- 쌤은 이것만 사용
const int const Value2 = 20;
	int Test = 0;
	// 위치 180번지
	// 크기 4
	// 형태 int 
	// 값 0

	const int* const Value1 = &Test;
	// 위치 200번지
	// 크기 8
	// 형태 const int* const
	// 값 180번지
	
	//180번지의 값을 100으로 바꿔라
	*Value1 = 200; // 안됨 (const int)* const 괄호부분때문에 안됨
	//200번지의 값을 0번지로 바꿔라
	Value1 = nullptr; // 안됨 const int(* const) 괄호부분때문에 안됨
  • const int* Ptr : 값을 못바꿈
  • int* const Ptr : 주소를 못바꿈
  • 함수 뒤 const : 함수 내 어떤 변수의 값도 못바꿈

🌀헤더 정리

전처리기 -> 컴파일러 -> 어셈블러 -> 링커

  • 링커의 역할 : 선언과 구현을 이어줌.!

선언과 구현

  • 선언과 구현을 분리하면 inline이 되지 않는다. 보통 Get함수나 수학관련 클래스처럼 간단한 함수는 선언과 구현을 분리하지 않는다.

  • 선언의 의미 : 이런 함수가 있을 것이니 믿고 호출해도 된다. 일단 호출해두면 나중에 구현과 연결될 것이라는 의미. (함수뿐만 아니라 다른것도됨) 선언은 많아도 상관없다.
    선언자체가 없으면 "식별자를 찾을 수 없습니다" 라는 에러가 발생한다. (헤더 include했는지 확인도 필수)

  • 구현 : 선언된 함수의 내용이 적힌 곳. 링커는 이 구현부를 선언부와 연결. 선언이 있는데 구현이 없으면 "함수에서 참조되는 확인할 수 없는 외부 기호" 에러가 발생한다.

#include <iostream>

// 전처리기 -> 컴파일러 -> 어셈블러 -> 링커
// 링커의 역할 : 선언과 구현을 이어줌.!

// 선언
// 선언의 의미 : 이런 함수가 있을 것이니 믿고 호출해도 된다. 일단 호출해두면 나중에 구현과 연결될 것이라는 의미. (함수뿐만 아니라 다른것도됨)
// 선언은 많아도 상관없다.
void FightDamage(int* _Hp, int _Att);
void FightDamage(int* _Hp, int _Att);
void FightDamage(int* _Hp, int _Att);

class Monster {
public:
	int Hp;
	void Damage(int _Att) {
		FightDamage(&Hp, _Att);
	}
};

class Player {
public:
	int Hp;
	void Damage(int _Att) {
		FightDamage(&Hp, _Att);
	}
};

// 구현
// 링커는 이 구현부를 선언부와 연결.
// 선언이 있는데 구현이 없으면 에러
void FightDamage(int* _Hp, int _Att) {
	*_Hp -= _Att;
}

int main() {
	Player NewPlayer = Player();
	Monster NewMonster = Monster();

	NewPlayer.Damage(10);

	return 0;
}

클래스의 선언과 구현

  • 클래스 함수의 선언과 분리는 클래스 바깥쪽에서 이루어진다. 클래스 외부에서 구현되는 선언과 구현의 분리는 함수를 구현할 때 FullName(범위 확인 연산자:: 포함, 클래스이름::함수이름)으로 구현해줘야 한다.
    함수의 구현을 나중에 하는 것의 이점이 더 많기 때문에 보통 선언은 (main함수 기준으로 위, 아래)위쪽에 전부 남기고 구현만 아래쪽에 몰아 넣는 형태가 된다.
class Player {
public:
	int Hp;
	void Damage(int _Att) {
		FightDamage(&Hp, _Att);
	}
	// 선언
	void TestPlayerRender();
};
// 구현
void Player::TestPlayerRender() {

}
  • 헤더에는 #pragma once를 꼭 넣어야 한다.
    #pragma once : 헤더 중복 방지 전처리기. #include로 같은 헤더를 몇번 반복해도 한 번만 해준다. 중복 방지 안 하면 같은 클래스가 여러개 정의되는 것이다.

  • int2같은 근본적인 클래스는 선언과 구현을 분리하지 않는다. (모두가 나를 사용하지만 나는 모두를 사용하지 않는 느낌의 클래스)

저번에 만든 'ConsoleGame'의 선언과 구현을 분리해보기

(내일 더 설명해야 함)
예시로 Player 클래스의 선언과 구현 분리 코드.

  • Player.h
    (간단한 Get함수는 헤더에 냅두어봤다.. 쌤은 따로 하심.)
// 헤더에는 아래의 #pragma once를 꼭 넣어야 합니다.
#pragma once
#include "Math.h"
#include <conio.h>

// 헤더에는 선언만 놓습니다.
class Player
{
public:
	Player();
	Player(const int2& _StartPos, char _RenderChar);

	inline int2 GetPos()
	{
		return Pos;
	}

	inline char GetRenderChar()
	{
		return RenderChar;
	}

	void Update();
	void SetBulletFire(bool* _IsFire);

private:
	int2 Pos = { 0, 0 };
	char RenderChar = '@';
	bool* IsFire = nullptr;
};
  • Player.cpp
#include "Player.h"
Player::Player()
{
}

Player::Player(const int2& _StartPos, char _RenderChar)
	: Pos(_StartPos), RenderChar(_RenderChar)
{
}

void Player::Update()
{
	int Value = _getch();

	switch (Value)
	{
	case 'a':
	case 'A':
	{
		if ((Pos + Left).X != 0)
		{
			Pos += Left;
		}
		break;
	}
	case 'd':
	case 'D':
	{
		if ((Pos + Right).X != (ScreenX - 2))
		{
			Pos += Right;
		}
		break;
	}
	case 'w':
	case 'W':
	{
		if ((Pos + Up).Y != 0)
		{
			Pos += Up;
		}
		break;
	}
	case 's':
	case 'S':
	{
		if ((Pos + Down).Y != (ScreenY - 1))
		{
			Pos += Down;
		}
		break;
	}
	case 'q':
	case 'Q':
	{
		if (nullptr != IsFire)
		{
			*IsFire = true;
		}
		// IsFire = true;
	}
	default:
		break;
	}

	int a = 0;
}

void Player::SetBulletFire(bool* _IsFire)
{
	if (nullptr == _IsFire)
	{
		return;
	}

	IsFire = _IsFire;
}

선생님은 기본 소스파일 필터, 헤더파일 필터 쓰지 않으심. ..

🌀(레퍼런스 추가 예시)

  • 레퍼런스는 무조건 초기화를 피할 수 없다.
	//int& Ref; 에러남
    //레퍼런스는 무조건 초기화를 피할 수 없다.
    //즉 int&를 사용했다면 int가 하나 필요함.
	int Value = 0;
	int& Ref = Value; 
  • 클래스 내 레퍼런스
class Test {
public:
	// 리터럴 초기화는 멍청한 코드.
	//int Value = 0;
	//int& Ref = Value;
};
  • 아래처럼 사용할 수 있다.
class Test {
public:
	int& Ref; // 이것도 위험함
	Test(int& _Ref)
		:Ref(_Ref) {

	}
};

int main() {
	int Value = 20;
	Test NewTest = Test(Value);
	return 0;
}
  • 레퍼런스는 참조(초기화) 대상을 바꿀 수 없음.
	int Value0 = 0;
	int& Ref = Value0;

	int Value1 = 2;
	Ref = Value1;

	Ref = 50; // Value0이 50으로 바뀜!
profile
일단 시작해보자

0개의 댓글

관련 채용 정보