[C++ 기초] 함수 포인터, 가상함수 테이블, 동적 할당(1)

라멘커비·2024년 1월 3일
0

CPP 입문

목록 보기
15/25
post-thumbnail

함수 포인터

🥕함수 포인터 메모리

  • 다음은 A클래스를 상속받은 B클래스(아무 내용 없음)의 내부 내용이다.

    virtual이 없는 클래스의 내부에는 내용이 없고 그냥 부모와 자신이 이어져있다는 내용만 있다.
  • 다음은 virtual 멤버 함수를 가진 FightUnit클래스를 상속받은 Player클래스 객체의 내용이다.

virtual이 존재하는 클래스는 vfptr이라는 가상함수 포인터라는 것을 가지고 있다는 것을 알 수 있다.
그래서 FightUnit의 크기가 8바이트이다.

🥕가상함수 테이블

그런데 virtual 함수가 여러 개여도 8바이트이다.
클래스에 virtual 함수가 여러 개면 생성될 때 생성자에서 자동으로 함수 포인터의 배열로 만들고, 그 배열의 포인터를 들고 있기 때문이다.
void(**vfptr)()가 된다. 그래서 virtual 함수가 여러 개여도 클래스의 크기는 8바이트가 된다. 그리고 그 함수 포인터의 배열이 가상함수 테이블이다.

// 내부적으로 이런 느낌
void(*vfptrArr[2])()
=
{
    FightUnitStatusRender,
    FightUnitDamage
}

void(**vfptr)() = vfptrArr;

만약 FightUnitDamage()함수를 사용하면 해당 코드가 vfptr[1]() 로 대체된다.
만약 자식 클래스에서 virtual 함수를 오버라이드하면 자식 클래스의 생성자가 실행될 때 그 함수가 있던 vfptr[1]을 자식의 함수로 바꾼다.

// 자식 클래스의 함수로 변경
void(*vfptrArr[2])()
=
{
    FightUnitStatusRender,
    PlayerDamage
}

함수 포인터의 배열의 포인터 (진짜임)

  • 함수 포인터 (Value0, Value1)
  • 그 함수 포인터들을 갖고 있는 배열 (ArrPtr)
  • 그 배열의 주소를 들고 있는 포인터 (Ptr2D)
    • 포인터는 얼마나 화려하고 복잡하든 8바이트이다. Ptr2D의 크기는 8바이트.

이것들의 메모리맵을 보면 이해에 도움이 된다.

~~

정리)
1. 가상함수 테이블이란 함수 포인터의 배열의 첫번째 주소이다.
2. 함수 2중 포인터라고 할 수 있다.
3. 생성자에서 가상함수 테이블의 값들을 채워준다.

enum

사용자 정의 자료형이다. 정수형 상수를 정의하는 자료형이다.

job

0 전사
1 마법사
2 궁수

DamageType

0 마법 데미지
1 물리 데미지

직업, 데미지 타입과 같은 데이터를 문자열로 직접 표현하는 것은 메모리가 많이 사용되고 실수할 가능성이 높다. 숫자로 대체하여 쓰더라도 직관적이지 않아 어려움이 있다. 그럴 때 enum을 사용하면 굿.

🥕enum 사용 방법

enum은 직관적으로 쓰면서도 손쉽게 int가 될 수 있다.
숫자를 명시해주지 않으면 자동으로 0부터 채운다. 숫자를 직접 지정할 수 있다.
몇개만 명시하면 명시하지 않은 부분은 자연스럽게 위쪽 EnumType + 1씩 된다. (하지만 안 씀)

enum Job {
    Fighter,    //Job::Fighter = 0
    Mage,
};
int main()
{
    // 이렇게 사용 가능
    Job Fighter = Job::Fighter;

    // 형변환으로 확인, 이렇게 사용하지는 말라고 하심
    int FighterInt = Job::Fighter;  // 0
    int MageInt = Job::Mage;        // 1

    return 0;
}

🥕enum class (권장)

enum class는 사용할 때 FullName으로 사용한다. DamageType::PDamage

얘는 문법적으로 형변환을 막았다. 그래서 선생님은 이걸 더 선호하신다. 선생님 표현으로 형변환은 '악'.
강제 형변환은 static_cast로 할 수 있지만 안 쓸 예정.

enum class DamageType {
    PDamage,
    MDamage,
};

enum보다 enum class가 더 좋은? 이유

using

typedef랑 거의 완전히 같은 문법인 using.
typedef가 문법적으로 아름답지 못하다 생각해서(연산자도 없고 덜 명시적이라서) using도 만들었다고 한다.

// 자료형에 별명 붙여주는 게 typedef였음
typedef int int32;

// Job이라는 사용자 정의 자료형
enum class Job {
	Fighter,
};

// 가능
typedef Job JobType;

// typedef가 문법적으로 아름답지 못하다 생각해서 using도 만듦, typedef랑 다른 게 없다.
using myint = int;

namespace

한 회사에 UI 담당하는 사람과 Monster 담당하는 사람이 있다고 하자. 몬스터가 죽으면 장비나 아이템을 떨군다. 떨어진 아이템을 먹으면 인벤토리로 들어가야 한다.

  • 몬스터 담당자가 class Item을 만든다.
  • UI 담당자가 class Item을 만든다.
    => 안 됨!

이름이 겹치는 것을 막기 위해 만든 것이 바로 namespace이다. 접두사같은 기능. "내 클래스를 사용하려면 접두사를 붙여라" 라는 의미.

namespace UI {
	class Item {

	};
}

namespace Play {
	class Item {

	};
}

각각 UI::Item, Play::Item이 된 것이다.

using namespace UI; : 이름에 UI를 사용하지 않아도 알아서 연결해주는 문법.
선생님이 극혐하시는 문법이다. 선생님은 namespace를 사용한다면 무조건 풀네임으로 사용하신다.

선생님 말씀) 이걸 사용해야 할 때는 보통 이름을 붙여주고 싶다기 보다는 작업을 나눌 때 사용하는 느낌이다.

cout

cout의 앞에 붙는 std:: 가 바로 namespace이다. 스탠다드의 줄임말로 C++ 언어차원에서 지원하는 기본 기능이다.
cout은 화면에 글자를 출력해준다.

🥕cout 만들어보기

cout의 정체는 전역 객체였던 것이다.

// 헤더 (.h)
namespace std {
	class MyStream {
	public:
		void operator <<(const char* _Text) {
			printf_s(_Text);
		}
	};
	// 전역 객체
	extern MyStream mycout;
}

// (.cpp)
std::MyStream mycout;

int main() {
	std::mycout << "Hello World!\n";
}

동적 할당, 정적 할당

🥕정적 할당 (= 정적 바인딩)

  • 메모리를 할당할 때 이미 정해진 크기대로밖에 만들 수 없고 수정이 불가능한 메모리 사용 방식을 정적 바인딩이라고 한다. 지금까지 해온 메모리 바인딩이 정적 바인딩이다.
  • 정적 바인딩은 플레이 중간에 메모리의 크기를 수정할 수 없다.
  • 보통 스택과 데이터영역에 바인딩된다.
    • 전역변수 : 데이터영역
      * 지역변수 : 스택영역
    • 상수 : 코드영역

우리 게임에 마을이 100개라고 할 때 아래처럼 배열로 만들면 한 번에 100개의 마을을 모두 만들게 된다. (불필요함) 내 플레이어의 눈에 보이는 주변만 처리하면 된다.

class Zone {
	// 마을
};

int main()
{
	Zone Arr[100]; // X
}

🥕동적 할당 (= 동적 바인딩)

  • 동적 할당은 힙 메모리를 사용하는 문법이다.
  • 메모리의 크기를 운영체제가 관리해주지만 거의 램의 크기만큼 할당할 수 있다. 문법도 간단하다.

new 연산자

동적으로 무언가를 만들어야 할 때 사용한다.
new 자료형() 으로 사용한다.
new 연산자는 자료형의 포인터를 반환한다.

코드메모리맵

어떤 때는 만들고 어떤 때는 지울 수 있다는 점이 동적 할당의 핵심이다.
아래와 같이 활용할 수 있다.

	Zone* CurZone = nullptr;

	if (플레이어가 들어왔다면) {
		CurZone = new Zone();
	}
  • 동적 할당은 런타임 도중에 할당할 크기를 변경할 수 있다.
	int MonsterCount = 10;
	int PlayerLevel = 30;

	if (PlayerLevel > 20)
	{
		MonsterCount = 30;
	}

	Monster* NewMonster = new Monster[MonsterCount];

	delete[] NewMonster;

delete

delete로 동적 할당한 메모리를 삭제할 수 있다.
C++에서는 delete를 안 하면 무한히 생성할 수도 있다. (주의)

int* Ptr = new int(10);
delete Ptr;
  • 배열로 할당/삭제하는 방법
    delete[] 사용.
Monster* NewMonster = new Monster[10];
delete[] NewMonster;
  • 포인터를 delete하면 그 포인터가 가리키는 곳의 메모리가 해제된다.

	Monster* NewMonster = new Monster[MonsterCount]; // NewMonster의 위치 :500번지
	Monster* NewMonsterArr = NewMonster;			 // NewMonsterArr의 값 : 500번지

	delete NewMonsterArr;		// 500번지 메모리 해제

Leak

Leak이란 new를 하고 delete를 하지 않은 메모리를 말한다.
C++ 프로그래밍에 심각한 문제를 발생시킬 수 있으므로 leak은 무조건 잡아야 한다.

_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF); : 릭 체크 코드, 프로그램 시작부분에 놓는 게 좋다. 앞으로 무조건 사용.

class Monster {
	int Att;
	int Hp;
};
int main()
{
    // leak 체크 코드
	_CrtSetDbgFlag(_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF);

	Monster* NewMonster = new Monster();
	//delete NewMonster; delete를 안 해봄.
}
  • 8바이트 leak 나온 모습 (Monster에 변수 8바이트만큼 있어서)

과제

🥕2가지 과제

(모든 과제에서 StringCount 사용했다.)

StringEqual 함수 만들기

enum class StringReturn
{
    Equal,
    NotEqual
};

int StringCount(const char* const _Ptr)
{
    int Count = 0;
    while (_Ptr[Count])
    {
        ++Count;
    }
    return Count;
}

StringReturn StringEqual(const char* const _Left, const char* const _Right)
{
    int LeftCount = 0;
    int RightCount = 0;
    LeftCount = StringCount(_Left);
    RightCount = StringCount(_Right);
    if (LeftCount != RightCount) {
        return StringReturn::NotEqual;
    }
    else {
        // 글자 수가 같으면 진짜 같은지 체크
        for (int i = 0; i < LeftCount; i++) {
            if (_Left[i] != _Right[i]) {
                //하나라도 다른 게 있으면 바로 NotEqual 리턴
                return StringReturn::NotEqual;
            }
        }
        // 여기까지 왔다면 모두 같은 것이므로 Equal 리턴
        return StringReturn::Equal;
    }
}
  • main() 내
StringReturn NewReturn1 = StringEqual("AAA", "AAAA");       // NotEqual
StringReturn NewReturn2 = StringEqual("ABC123", "ABC123");  // Equal

StringAdd 함수 만들기

마지막에 _Dest[LeftCount + RightCount] = 0;로 문자열의 마지막을 표시해야 _Dest에 이미 문자열이 있었어도 의도대로 만들 수 있다.

void StringAdd(char* _Dest, const char* const _Left, const char* const _Right)
{
    int LeftCount = 0;
    int RightCount = 0;
    LeftCount = StringCount(_Left);
    RightCount = StringCount(_Right);
    for (int i = 0; i < LeftCount; i++) {
        _Dest[i] = _Left[i];
    }
    for (int i = LeftCount; i < LeftCount + RightCount; i++) {
        _Dest[i] = _Right[i - LeftCount];
    }
    _Dest[LeftCount + RightCount] = 0;
    return;
}
  • main() 내
char Arr[100] = {};
StringAdd(Arr, "hello", "world");	//helloworld

🥕추가 과제 (안 해도 됨)

StringConstains 함수 만들기

  • 있는지 없는지 확인, 있다면 몇개 있는지 확인
int StringContains(const char* const _Dest, const char* const _Find)
{
    int DestCount = StringCount(_Dest);
    int FindCount = StringCount(_Find);
    int Result = 0;

    for (int i = 0; i < DestCount; i++) {
        bool CheckFlag = true;
        for (int j = 0; j < FindCount; j++) {
            if (_Dest[i + j] != _Find[j]) {
                CheckFlag = false;
            }
        }
        if (CheckFlag == true) {
            Result++;
        }
    }
    return Result;
}
  • main() 내
int Result1 = StringContains("ababcccccabab", "ab");    // 4
int Result2 = StringContains("ababcccccabab", "c");     // 5

각 함수랑 같은 것들

  • StringCount 함수는 strlen 함수와 같음
  • StringEqual 함수는 strcmp로 가능
	StringReturn NewReturn1 = StringEqual("AAA", "AAAA");       // NotEqual
	StringReturn NewReturn2 = StringEqual("ABC123", "ABC123");  // Equal
	int NewReturn3 = strcmp("ABC123", "ABC123");                // 0
	int NewReturn4 = strcmp("ABC123", "ABC");                   // 1
  • StringAdd 함수는 sprintf_s 함수로 같은 기능 사용 가능
    char ArrTest[100];
    sprintf_s(ArrTest, "%s%s", "AAAA", "BBBB");
  • StringContains 함수는 strcmp에 포함될 것
profile
일단 시작해보자

0개의 댓글

관련 채용 정보