본문 바로가기
C++/문법

C++ 기본문법(Const, Class)

by 뇌 속의 통 2024. 11. 25.

const

뒤의 값을 상수 즉, 바꿀 수 없도록 설정하는 키워드

 

const char* Message = "Hello, World!";

-> Message 변수 값을 변경할 수 없다.

위 코드에서 Hello, World!는 특정 변수 값이 아닌 코드 영역(롬)에 저장되있는 값을 가져와 Message에 대입하는 것이므로 const를 사용하여 코드 영역 데이터가 수정되지 않도록 해야한다.

 

const 자료형 함수명() {};

-> 함수가 반환하는 값을 변경할 수 없다.

 

자료형 함수명(const int* 변수명){};

->포인터가 가르키는 값을 변경할 수 없다.

 

자료형 함수명(int* const 변수명){};

->포인터의 값(주소)를 변경할 수 없다.

 

Const는 보통 값을 가져오되 해당 값이 수정되거나 바뀌어서는 안되는 데이터인 경우 사용된다.

 

Class

 

Class는 생성될 때 생성자(Constructor), 소멸될 때 소멸자(Destructor)가 호출되는데 따로 구현하지 않으면 기본 생성자, 소멸자가 호출되게 된다.

 

우리는 우리의 의도를 정확하게 전달하는 것이 중요하기 때문에 함수 내용이 없더라도 생성자와 소멸자를 만드는 것이 좋다.

 

class FMonster
{
	public : 
			FMonster(); // 생성자
			~FMonster(); // 소멸자

}

생성자나 소멸자도 Header에서 선언 후 cpp file에서 구현한다.

 

만약 포인터로 선언한다면 생성자, 소멸자는 호출되지 않는다.

해당 자료형을 가르킬 포인터를 만들었을 뿐, 실제 객체를 만들지 않았기 때문이다.

 

또한 new를 통해 동적 할당을 하는 경우 delete 키워드를 사용하여 객체를 메모리에서 해제해야지 소멸자가 발동된다. 소멸되지 않으면 소멸자는 호출되지 않는다.

 

생성자에서는 반드시 멤버 변수 초기화를 해야하며, 아래와 같이 초기화 리스트 생성자로 사용할 수도 있다.

FMonster::FMonster() : X(10), Y(20) 
{

}

 

생성자와 소멸자 호출 순서를 보자면 아래와 같다.

부모 생성자 → 자식 생성자 → 자식 소멸자 → 부모 소멸자

메모리를 생각해보면 맨처음 메모리가 잡힐때 부모내 함수 & 변수가 잡히고 그 다음 자녀의 변수& 함수가 메모리에 잡히게 된다.

그러면 부모 → 자식순으로 생성자가 호출이 될 수 밖에 없다.

또한, 부모 class가 먼저 소멸하게되면 사실상 메모리가 연속적으로 구현되어 있을텐데 부모 class 뒤에 붙어 있는 자식 class 부분의 주소를 알수가 없게된다.

 

int Myint;

 

위와 같은 변수를 auto, 자동 변수라고 한다.

 

생성자는 인자에 따라 overloading할 수 있지만, 소멸자는 어차피 없어지는 것뿐이기 때문에 하나밖에 없다.

아무런 인자가 없는 것을 기본 생성자라고 한다.

 

Class내에서 멤버 변수, 멤버 함수를 접근지정자로 보호하거나 공개할 수 있다.

접근지정자는 Public, Private, Protected 3가지가 있으며 각각 아래와 같다.

 

Public : 자녀 접근가능(상속), 외부인 접근가능

Protected : 자녀 접근가능(상속), 외부인 접근불가

Private : 자녀 접근불가(상속하지 않음), 외부인 접근불가

 

이렇게 Class 내 멤버 함수, 변수를 접근지정자로 보호하는 것을 캡슐화(encapuslation)라고 한다.

 

encapuslation하는 이유는 내가 만든 Class의 중요한 데이터를 여러명이서 협업하는 경우 누구나 접근하지 못하도록 하여 프로그램 설계 시 휴먼 에러를 줄이는데 효과적이다.

 

멤버 변수는 보통 private, 중요한 함수도 private한다.

노출된 함수는 interface라고 하며, 우리가 흔히 말하는 인터페이스의 개념과 동일하다.

즉 모두 객체 지향(물건 지향)적 시각이다.

 

Class로 물건(객체)를 만들어 내고 그 객체는 interface를 통해 조작하는 것이다.

앞서 말했듯 Class의 전제는 내가 설계하여 타인에게 사용하도록 주는 것이다.

 

이렇게 우리가 멤버 변수를 private하게 되면 실제 data에 뭐가 들었는지 보고, 함수를 통해 사용할 수 있어야하는데 이를 Get, Set함수라고 한다.

 

class FMonster
{
private :
			int X;
			int Y;
public :
			int GetX();
			int GetY();
			void SetX();
			void Sety();
}

 

Get 함수를 accessor, Set 함수를 mutator라고 한다.

 

각 class들의 공통기능을 묶어서 부모 class를 만드는 것을 is a 다른 말로 추상화한다고 한다.

부모 class와 자식 class로 설계하는 이유는 수정 등 유지보수가 편리하기 때문이다.

 

실제로 모두 같은 Move 함수를 수정해야 하는 경우 부모 class에서 수정하면 되지만, 부모-자식 class로 설계되어 있지 않다면 하나하나 찾아가면서 수정해야한다. 성능에 이점이 있거나 꼭 이렇게 설계해야하는 것은 아니다.

 

맵에 고블린 1~3, 슬라임 1~3, 멧돼지 1~3가 있다고 가정해보자.

각 몬스터는 1~3마리 사이의 랜덤 값을 갖게 된다. 어떻게 구현을 해야할까?

 

srand(time(0))

int MonsterCount = rand() % 3 + 1;

FGoblin Goblins[3];
FSlime Slimes[3];
FWildBoar WildBoars[3];

for(int i = 0; i < MonsterCount; ++i)
{
	Goblins[i].Move();
}

 

위와 같이 정적으로 구현할 수 있다.

3마리 구현 후 랜덤한 값을 받아 그만큼의 몬스터만 운용하도록 설계되어 있다.

 

그러나 이 경우 1~100마리 더 나아가 1~100000마리 등 점점 늘어나면 사실상 쓰지도 않는 메모리를 모두 잡아 놓고 그 중 일부만 사용하게 되버리는 것이다.

 

이를 보완하기 위해선 동적으로 할당을 해주면 된다.

srand(time(0))

int MonsterCount = rand() % 3 + 1;

FGoblin* Goblins = new FGoblin[MonsterCount];
FSlimes* Slimes= new FSlimes[MonsterCount];
FWildBoars* WildBoars= new FWildBoars[MonsterCount];

for(int i = 0; i < MonsterCount; ++i)
{
	Goblin[i].Move();
}

delete[] Goblins;
delete[] Slimes;
delete[] WildBoars;

위와 같이 랜덤한 숫자로 각각 몬스터별 배열을 만들어 주고 이를 가르키는 포인터를 이용해준다.

 

Vector를 이용하면 더 쉽게 작성할 수 있다.

srand(time(0))

int MonsterCount = rand() % 3 + 1;


vector<FGoblin*> Goblins;

for(int i = 0; i < MonsterCount; ++i)
{
	Goblin[i] = new FGoblin[MonsterCount];
}
for(int i = 0; i < MonsterCount; ++i)
{
	delete Goblins[i];
}

Goblins.clear();

 

대신, 포인터 배열을 구성하여 각 포인터가 몬스터 배열을 가르키도록 설정해주어야 한다.

왜냐하면 실제로 객체를 만든 것이 아니라 처음 vector를 이용하여 만든 것은 FGoblin을 가르키는 포인터 변수를 만든 것 뿐이기 때문이다. 포인터 변수는 주소값을 저장하는 변수이지 해당 몬스터 객체가 아니다.

 

그리고 해제하기 위해선 일단 각 인덱스의 객체주소를 delete를 이용하여 해제해주고 마지막으로 포인터 배열 자체를 vector 내부 함수인 clear를 이용하여 비워줘야 한다.

 

포인터 배열을 이용하는 이유가 무엇일까?

바로 관리가 편하기 때문이다.

 

부모 class가 같은 경우 부모 class형 포인터를 선언해주고 그 포인터로 자녀를 가르키게 되면 큰 이상없이 포인터가 작동하게 된다.

 

이 말은 즉, 부모 class 포인터 배열을 선언하고 각 인덱스에 하나는 고블린 배열, 하나는 멧돼지 배열 등 각기 다른 배열들을 모두 가르킬 수 있다는 뜻이다.

 

물론 부모 부분 뒤에 연속적으로 잡혀 있는 자녀 부분은 포인터 입장에선 알 수 없으니 개발자가 컨트롤해줘야 한다.

 

srand(time(0))

int MonsterCount = rand() % 3 + 1;


vector<FMonster*> Monster;

Monster = new Goblin[10];
Monster = new Slime[10];
Monster = new WildBoar[10];

for(int i = 0; i < MonsterCount; ++i)
{
	Monster[i].Move();
}

for(int i = 0; i < MonsterCount; ++i)
{
	delete Monster[i];
}

Monster.clear();

 

이렇게 하게 되면 Monster라는 배열 하나만으로 서로 다른 Monster를 컨트롤할 수 있게 된다.

 

그러면 부모 class의 Move함수와 자녀 class내 Move함수가 있을 때 자녀 class의 Move함수를 호출하고 싶다면 어떻게 해야할까? 부모 class형 포인터기 때문에 함수를 호출하면 부모 class 내 함수를 사용한다.

 

이것을 부모 class 내 함수에 virtual 키워드를 붙여 자녀 class 내에서 재정의 할 것이라는 것을 알려주고 자녀 class 내 함수에 virtual 키워드와 override 키워드를 붙여 재정의 한 것이라고 알려주면 해당 함수 호출 시 자녀 class에 있는 재정의된 함수가 호출하게 된다.

 

자녀 객체에서 부모 함수를 쓰고 싶은 경우 아래와 같이 자녀의 함수에서 부모 함수를 호출하면 된다.

void FGoblin::Move()
{
	cout << "이동" << endl;
	FMonster::Move();
}

 

virtual : 자녀 class에서 재정의 할 수도 있으니 찾아보라는 의미

override : 재정의

 

class FMonster
{
public:
		virtual void Move();
}

class FGoblin : public FMonster
{
public:
		virtual void Move() override; 
}

 

즉, 부모 Class 내 함수 중 virtual이 붙은 경우는 자녀 Class에서 업그레이드 해서 사용해도 된다는 의미다.

 

또한, 부모 Class형 포인터를 선언하여 자녀 Class형 객체를 가르키는 경우 자녀의 소멸자가 정상적으로 작동되지 않는다. 포인터 입장에서는 뒤에 뭐가 있는지 모르기 때문.

 

부모와 자녀 소멸자 앞에 모두 virtual을 붙여줌으로써, 자녀의 소멸자도 정상적으로 작동하도록 설정할 수 있다.

이처럼 부모 class형 포인터 배열을 만들어서 자녀 class 객체를 다 집어넣고 관리하는 것을 다형성이라고 한다.

 

성능이 좀 떨어지더라도 배열 하나에 다 넣고 반복문을 돌려 그때그때 맞는 걸로 골라서 사용하겠다는 의미이다.

new를 이용하여 객체를 만들때 부모 class형 객체를 만드는 경우가 발생해선 안된다.

 

왜냐하면 부모 class는 추상화한 것이지 실제로 객체를 만들어 사용하려고 한게 아니기 때문이다.

 

쉽게 생각하면 우리는 고블린, 슬라임 등을 몬스터라는 개념으로 추상화하여 배열에 묶어 사용하는 것이지, 그냥 몬스터라는 객체를 만드려고 한게 아니기 때문이다.

 

이럴때는 부모 class 내에 virtual void Draw() = 0;를 넣어놓으면 해당 class는 추상 class로 바뀌면서 객체 생성할 수 없게된다.(Instance 할 수 없다)

class FMonster
{
public:
		virtual void Draw() = 0;
}

 

하지만 virtual 즉, 자녀의 class에서 재정의할 수 있다는 의미로 사용하는 키워드기 때문에 반드시 자녀 class에서 해당 함수를 재정의하여야만 자녀 class가 추상 class로 바뀌는 것을 막을 수 있다.

 

이와 같은 함수를 순수 가상 함수라고하며, 다중 상속 문제(부모가 여러명이고 부모 내부에 모두 같은 함수가 존재하는 경우)등에서 어떤 함수를 호출해야 하는지 모르기 때문에 이런 경우에도 순수 가상 함수를 사용하여 설계해야 한다.

 

* 순수 가상 함수 선언 방법

virtual void MyFunction() = 0;

 

 

 

기타

  • <algorithm> 안에 random_shuffle(), shuffle() 함수가 모두 구현되어 있다.
  • <conio.h> _getch() 키 입력을 받아서 바로 반환하는 함수
  • cpp : C++ 확장자 .c : C확장자
  • 단축키 : 함수 + Alt + Enter : 해당 함수 구현
  • 우리가 분할구현을 하다보면 맨 상단에 pragma once라고 기재된 것을 볼수 있는데 이것은 한번만 읽으라는 뜻으로 Monster를 만든다고 할때 Monster의 자녀 Class Goblin를 읽어올때 부모 Class를 정의한다. 다른 자녀 Class인 WildBoar를 읽어올때도 부모 Class를 정의하게 되면 중복 되버린다.
  • 아래와 같이 Visual Studio에서 Class Diagram을 이용하면 설계도를 볼 수 있다.

 

'C++ > 문법' 카테고리의 다른 글

File Input Output & Delta Time  (0) 2024.11.07
Inline & Switch case문  (8) 2024.11.06
Class 와 Template  (0) 2024.10.31
C++ 기본 문법(문자열, 동적할당, 구조체)  (2) 2024.10.25
C++ 기본 문법(변수와 함수)  (0) 2024.10.23