SH1R0_HACKER

Chapter03 클래스의 기본 본문

Programming Language/C++

Chapter03 클래스의 기본

SH1R0_HACKER 2020. 10. 18. 15:28

 

Part 02 객체지향의 도입

 

Chapter03 클래스의 기본

03-01 C++에서의 구조체

< 문제 03-1 [구조체 내에 함수정의하기] >

#include <iostream>
using namespace std;

struct Point // 2차원 평면상에서의 좌표를 표현할 수 있는 구조체
{
	int xpos;
	int ypos;

	void MovePos(int x, int y) // 점의 좌표이동
	{
		xpos += x;
		ypos += y;
	}

	void AddPoint(const Point& pos) // 점의 좌표증가
	{
		xpos += pos.xpos;
		ypos += pos.ypos;
	}

	void ShowPosition() // 현재 x, y 좌표정보 출력
	{
		cout << "[" << xpos << ", " << ypos << "]" << endl;
	}
};

int main(void)
{
	Point pos1 = { 12, 4 };
	Point pos2 = { 20, 30 };

	pos1.MovePos(-7, 10);
	pos1.ShowPosition();	// [5, 14] 출력

	pos1.AddPoint(pos2);
	pos1.ShowPosition();	// [25, 44] 출력
	return 0;
}

 

< 구조체 안에 enum 상수의 선언 >

- C언어에서 배운 열거형(enumeration)을 복습해보자.

열거형(enumeration)이란 변수가 가질 수 있는 값들을 나열해놓은 자료형이다. 열거형도 구조체처럼 사용자가 새로운 자료형을 정의하는 방법의 하나이다. 하나의 예로, 요일을 나타내는 열거형을 정의하여 보자.

enum days { SUN, MON, TUE, WED, THU, FRI, SAT };

 

열거형 days 안에 들어 있는 상수들은 0에서 시작하여 1씩 증가하는 정수값으로 자동으로 설정된다.

즉 SUN은 0이고 MON은 1이다.

 

- 이제 C++로 돌아오자.

아래의 코드는 자동차 레이싱 게임의 캐릭터 스텟을 나타내는 코드이다.

구조체안에 함수를 선언하였고, CAR_CONST라는 이름공간을 새로 만들어 #define을 대체하였다.

#include <iostream>
using namespace std;

namespace CAR_CONST // CAR_CONST 이름공간 안에 구조체 Car에서 사용하는 상수들을 모아두었다.
{
	enum
	{
		ID_LEN = 20,
		MAX_SPD = 200,
		FUEL_STEP = 2,
		ACC_STEP = 10,
		BRK_STEP = 10,
	};
}

struct Car
{
	char gamerID[CAR_CONST::ID_LEN]; // 상수 ID_LEN의 접근을 위해서 이름공간 CAR_CONST를 지정하고 있다.
	int fuelGauge;
	int curSpeed;

	void ShowCarState();
	void Accel();
	void Break();
};

void Car::ShowCarState()
{
	cout << "소유자ID: " << gamerID << endl;
	cout << "연료량: " << fuelGauge << "%" << endl;
	cout << "현재속도: " << curSpeed << "km/s" << endl << endl;
}
void Car::Accel()
{
	if (fuelGauge <= 0)
		return;
	else
		fuelGauge -= CAR_CONST::FUEL_STEP;

	if ((curSpeed + CAR_CONST::ACC_STEP) >= CAR_CONST::MAX_SPD)
	{
		curSpeed = CAR_CONST::MAX_SPD;
		return;
	}

	curSpeed += CAR_CONST::ACC_STEP;
}
void Car::Break()
{
	if (curSpeed < CAR_CONST::BRK_STEP)
	{
		curSpeed = 0;
		return;
	}

	curSpeed -= CAR_CONST::BRK_STEP;
}

int main(void)
{
	Car run99 = { "run99",100,0 };
	run99.Accel();
	run99.Accel();
	run99.ShowCarState();
	run99.Break();
	run99.ShowCarState();

	Car sped77 = { "sped77",100,0 };
	sped77.Accel();
	sped77.Break();
	sped77.ShowCarState();
	return 0;
}

 


03-2 클래스(Class)와 객체(Object)

- 클래스(Class)와 구조체(struct)의 차이는 키워드를 struct가 아닌 class를 사용한 것이 코드상에서 유일한 차이이다.

 

< 접근제어 지시자(접근레어 레이블) >

● Public : 어디서든 접근허용

● protected : 상속관계에 놓여있을 때, 유도 클래스에서의 접근허용

● private : 클래스 내(클래스 내에 정의된 함수)에서만 접근허용

 

아래의 예시는 접근제어 지시자 Public과 private를 이용한 코드이다.

#include <iostream>
#include <cstring>
using namespace std;

namespace CAR_CONST
{
	enum
	{
		ID_LEN=20, MAX_SPD=200, FUEL_STEP=2,
		ACC_STEP=10, BRK_STEP=10
	};
}

class Car // struct를 대신해서 class 선언이 삽입되었다. 따라서 이는 클래스의 정의에 해당한다.
{
private: // 접근제어 지시자중 하나인 private이 선언되었으므로, 
		 // 이어서 등장하는 변수와 함수는 private에 해당하는 범위 내에서(클래스 내에서만) 접근이 가능하다.
	char gamerID[CAR_CONST::ID_LEN];
	int fuelGauge;
	int curSpeed;
public: // 이어서 등장하는 변수와 함수는 public에 해당하는 범위 내에서(어디서든) 접근이 가능하다.
	void InitMembers(char* ID, int fuel); // 클래스 안에 선언된 변수의 초기화를 목적으로 정의된 함수
	void ShowCarState();
	void Accel();
	void Break();
};

void Car::InitMembers(char* ID, int fuel) // 클래스 안에 선언된 변수의 초기화를 목적으로 정의된 함수
{
	strcpy(gamerID, ID);
	fuelGauge = fuel;
	curSpeed = 0; // 변수 CurSpeed는 무조건 0으로 초기화 되도록 정의하였다.
}
void Car::ShowCarState()
{
	cout << "소유자ID: " << gamerID << endl;
	cout << "연료량: " << fuelGauge << "%" << endl;
	cout << "현재속도: " << curSpeed << "km/s" << endl << endl;
}
void Car::Accel()
{
	if (fuelGauge <= 0)
		return;
	else
		fuelGauge -= CAR_CONST::FUEL_STEP;

	if ((curSpeed + CAR_CONST::ACC_STEP) >= CAR_CONST::MAX_SPD)
	{
		curSpeed = CAR_CONST::MAX_SPD;
		return;
	}
	curSpeed += CAR_CONST::BRK_STEP;
}
void Car::Break()
{
	if (curSpeed < CAR_CONST::BRK_STEP)
	{
		curSpeed = 0;
		return;
	}
	curSpeed -= CAR_CONST::BRK_STEP;
}

int main(void)
{
	Car run99;
	run99.InitMembers("run99", 100); // 초기화를 목적으로 InitMembers 함수를 호출하고 있다.
	run99.Accel();
	run99.Accel();
	run99.Accel();
	run99.ShowCarState();
	run99.Break();
	run99.ShowCarState();
	return 0;
}

 

1. 접근제어 지시자 A가 선언되면, 그 이후에 등장하는 변수나 함수는 A에 해당하는 범위 내에서 접근이 가능하다.

2. 그러나 새로운 접근제어 지시자 B가 선언되면, 그 이후로 등장하는 변수나 함수는 B에 해당하는 범위 내에서 접근이 가능하다.

3. 함수의 정의를 클래스 밖으로 빼도, 이는 클래스의 일부이기 때문에, 함수 내에서 private으로 선언된 변수에 접근이 가능하다.

4. 키워드 struct를 이용해서 정의한 구조체(클래스)에 선언된 변수와 함수에 별도의 접근제어 지시자를 선언하지 않으면, 모든 변수와 함수는 public으로 선언된다.

5. 키워드 class를 이용해서 정의한 클래스에 선언된 변수와 함수에 별도의 접근제어 지시자를 선언하지 않으면, 모든 변수와 함수는 private으로 선언된다.

 

 

< 용어정리 : 객체(Object), 멤버변수, 멤버함수 >

멤버변수 : 클래스를 구성하는(클래스 내에 선언된) 변수를 가리킨다.

멤버함수 : 클래스를 구성하는(클래스 내에 정의된) 함수를 가리킨다.

 

즉 , 위의 예제에서 Car 클래스를 구성하는 '멤버변수'는 다음과 같다.

char gamerID[CAR_CONST::ID_LEN];
int fuelGauge;
int CurSpeed;

 

그리고 Car 클래스를 구성하는 '멤버함수'는 다음과 같다.

void InitMembers(char * ID, int fuel); void ShowCarState(); void Accel(); void Break();

void InitMembers(char * ID, int fuel);
void ShowCarState();
void Accel();
void Break();

03-3 객체지향 프로그래밍의 이해

- C++는 객체지향 언어이다. 객체는 영어로 Object (사물, 또는 대상)이다. 즉, Object는 우리 주변에 존재하는 물건 (연필, 나무, 지갑, 돈 등등)이나 대상 (철수, 친구, 선생님 등등) 전부를 의미한다. 지금부터 다음 상황을 시뮬레이션 하는 프로그램을 구현한다고 가정해보자.

 

" 과일장수에게 두 개의 사과를 구매했다!"

 

이 문장에 삽입되어있는 객체의 종류는 나(me), 과일장수, 사과

객체지향 프로그래밍은 현실에 존재하는 사물과 대상, 그리고 그에 따른 행동을 있는 그대로 실체화 시키는 형태의 프로그래밍이다.

'나'와 '과일장수'라는 객체를 생성하여 다음의 행동을 실체화시켜 보겠다.

 

"나는 과일장수에게 2,000원을 주고 두 개의 사과를 구매했다."

 

사과도 객체로 실체화시킬 수 있으나, 코드의 간결성을 위해서 '나'와 '과일장수'만 객체화시키도록 하겠다.

프로그램상에서 바라보는 과일장수의 관점은 '과일의 판매'에 있다. 따라서 프로그램상에서 바라보는 과일장수는 다음과 같은 형태이다.

 

1. 과일장수는 과일을 팝니다.

2. 과일장수는 사과 20개, 오렌지 10개를 보유하고 있습니다.

3. 과일장수의 과일판매 수익은 현재까지 50,000원 입니다.

 

첫 번째는 과일장수의 '행동(behavior)'을 의미한다.

두 번째와 세 번째는 과일장수의 '상태(state)'를 의미한다.

이처럼 객체는 하나 이상의 상태 정보(데이터)와 하나 이상의 행동(기능)으로 구성이 되며, 상태정보는 변수를 통해서 표현이 되고(변수에 상태 정보를 저장할 수 있으므로), 행동은 함수를 통해서 표현이 된다.

 

먼저 과일장수의 상태 정보를 변수로 표현해보자. (이 과일장수는 사과만 판매한다고 가정한다)

int numOfApples; // 보유하고 있는 사과의 수
int myMoney; // 판매수익

 

이번에는 과일장수의 행위인 과일의 판매를 함수로 표현해보자.

int SaleApples(int money)	// 사과 구매액이 함수의 인자로 전달
{
	int num = money / 1000;	// 사과가 개당 1,000원이라고 가정
	numOfApples -= num;	// 사과의 수가 줄어들고,
	myMoney += money;	// 판매 수익이 발생한다.
	return num;	// 실제 구매가 발생한 사과의 수를 반환
}

 

이렇게 해서 과일장수 객체를 구성하게 되는 변수 함수가 마련되었으니, 이제 이들을 묶어서 객체로 실체화하는 일만 남았다.

 

객체를 생성하기에 앞서 객체의 생성을 위한 '틀(mod)'을 먼저 만들어야 한다. 위에서 마련해놓은 함수와 변수를 이용해서 틀을 만들면 다음과 같은 형태가 된다.

class FruitSeller
{
private:
    // 변수 선언
	int APPLE_PRICE;   // 사과의 가격
	int numOfApples;   // 보유하고 있는 사과의 수
	int myMoney;   // 판매 수익
public:
    // 함수 정의
	int SaleApples(int money)	// 사과 구매액이 함수의 인자로 전달
	{
		int num = money / 1000;	// 사과가 개당 1,000원이라고 가정
		numOfApples -= num;	// 사과의 수가 줄어들고,
		myMoney += money;	// 판매 수익이 발생한다.
		return num;	// 실제 구매가 발생한 사과의 수를 반환
	}
};

 

그런데 우리는 과일장수에게 다음과 같은 질문을 할 수도 있다.

 

"오늘 과일 좀 많이 파셨어요?"

 

그러면 과일장수는 다음과 같이 대답을 할 것이다.

 

"2,000원 벌었어, 남은 사과는 18개이고 말이야!"

 

그래서 이러한 '대화에 사용되는 함수'와 '변수를 초기화하는데 사용하는 함수'를 추가해서 과일장수의 틀을 다음과 같이 완성하겠다.

class FruitSeller
{
private:
	int APPLE_PRICE;	// 사과의 가격
	int numOfApples;	// 보유하고 있는 사과의 수
	int myMoney;	// 판매수익
public:
	void InitMembers(int price, int num, int money)
	{
		APPLE_PRICE = price;
		numOfApples = num;
		myMoney = money;
	}

	int SaleApples(int money)	// 사과 구매액이 함수의 인자로 전달
	{
		int num = money / 1000;	// 사과가 개당 1,000원이라고 가정
		numOfApples -= num;	// 사과의 수가 줄어들고,
		myMoney += money;	// 판매 수익이 발생한다.
		return num;	// 실제 구매가 발생한 사과의 수를 반환
	}

	void ShowSalesResult()
	{
		cout << "남은 사과: " << numOfApples << endl;
		cout << "판매수익: " << myMoney << endl;
	}
};

 

보통 하루를 기준으로 사과의 판매가가 변하는 일은 없으니 사과의 판매가가 변경이 일어나는 일을 막고싶다면

const int APPLE_PRICE=1000;

const int APPLE_PRICE=1000;

 

이렇게 const 선언을 하여 초기화를 해주고 싶지만 클래스의 멤버변수 선언문에서 초기화까지 하는 것을 허용하지 않는다. 그래서 객체를 생성한 다음에 InitMembers 함수의 호출을 통해서라도 상수 값을 초기화 했으면 좋겠다.

const int APPLE_PRICE;

 

그러나 이 역시 불가능하다. 상수는 선언과 동시에 초기화되어야 하기 때문이다. 현재로서는 APPLE_PRICE를 상수로 선언할 방법이 없다. 차차 알아가도록 하자.

 

이제 '나(me)'를 표현하기 위한 클래스를 정의할 차례인데, 이는 과일 구매자를 뜻하는 것이니, 클래스의 이름을 FruitBuyer라 하겠다. 데이터적인 측면에서 바라보면 구매자는 돈이 있어야 구매가 가능하다. 그리고 구매를 했다면 해당 물품을 소유하게 된다.

int myMoney;   // 소유하고 있는 현금의 액수
int numOfApples;   // 소유하고 있는 사과의 수

 

기능적 측면에서 바라보면 과일 구매자가 지녀야 할 기능은 '과일의 구매'이다. 이 함수의 이름을 BuyApple이라 하면, FruitBuyer 클래스는 다음과 같이 정의가 된다.

class FruitBuyer
{
	int myMoney;		// private:
	int numOfApples;	// private:
public:
	void InitMembers(int money)
	{
		myMoney = money;
		numOfApples = 0;	// 사과구매 이전이므로!
	}
	void BuyApples(FruitBuyer& seller, int money)
	{
		numOfApples += seller.SaleApples(money);
		myMoney -= money;
	}
	void ShowBuyResult()
	{
		cout << "현재 잔액: " << myMoney << endl;
		cout << "사과 개수: " << numOfApples << endl;
	}
};

 

우리는 지금 막 두 개의 클래스를 정의하였다. 그렇다면 객체를 생성하지 않고, 이 두 클래스 안에 존재하는 변수에 접근하고, 함수를 호출하는 것이 가능할까? 이들은 '실체(다시 말해서 객체)'가 아닌 '틀'이다. 따라서 접근도 호출도 불가능하다.

 

자 이제 우리가 해야 할 일은 앞서 정의한 클래스를 실체화시키는 것이다. 즉, 객체화시키는 것이다.

다음은 C++에서 정의하고 있는 두 가지 객체생성 방법이다. 이는 기본 자료형 변수 선언방식과 동일하다.

ClassName objName;   // 일반적인 변수의 선언방식
ClassName * ptrObj=new ClassName;   // 동적 할당방식(힙 할당방식)

 

즉, 우리가 정의한 FruitSeller 클래스와 FruitBuyer 클래스의 객체 생성방식은 다음과 같다.

FruitSeller seller;
FruitBuyer buyer;

 

그리고 이를 동적으로 할당하려면 다음과 같이 생성하면 된다.

FruitSeller * objPtr1=new FruitSeller;
FruitBuyer * objPtr2=new FruitBuyer;

 

이로써 기본적인 클래스의 정의방법과 객체의 생성방법, 그리고 클래스와 객체의 의미를 모두 설명하였다.

 

 

<사과장수 시뮬레이션 완료!>

- 이제 예제를 완성할 차례이다.

#include <iostream>
using namespace std;

class FruitSeller	// 과일장수의 틀
{
private:
	int APPLE_PRICE;	// 사과의 가격
	int numOfApples;	// 보유하고 있는 사과의 수
	int myMoney;	// 판매 수익

public:
	void InitMembers(int price, int num, int money)	// 변수를 초기화하는데 사용하는 함수
	{
		APPLE_PRICE = price;
		numOfApples = num;
		myMoney = money;
	}
	int SaleApples(int money)
	{
		int num = money / APPLE_PRICE;	// 사과가 개당 APPLE_PRICE원이라고 가정
		numOfApples -= num;	// 사과의 수가 줄어들고,
		myMoney += money;	// 판매 수익이 발생한다.
		return num;	// 판매한 과일의 수를 반환
	}
	void ShowSalesResult()
	{
		cout << "남은 사과: " << numOfApples << endl;
		cout << "판매 수익: " << myMoney << endl << endl;
	}
};

class FruitBuyer	// 구매자의 틀
{
	int myMoney;	// 소유하고 있는 현금의 액수
	int numOfApples;	// 소유하고 있는 사과의 수

public:
	void InitMembers(int money)	// 변수를 초기화하는데 사용하는 함수
	{
		myMoney = money;
		numOfApples = 0;	// 사과구매 이전이므로!
	}
	void BuyApples(FruitSeller& seller, int money)
	{
		numOfApples += seller.SaleApples(money);
		myMoney -= money;
	}
	void ShowBuyResult()
	{
		cout << "현재 잔액: " << myMoney << endl;
		cout << "사과 개수: " << numOfApples << endl << endl;
	}
};

int main(void)
{
	FruitSeller seller;	// FuritSeller 클래스의 객체 생성
	seller.InitMembers(1000, 20, 0);	// 변수 초기화 (가격, 개수, 판매수익)
	FruitBuyer buyer;	// FruitBuyer 클래스의 객체생성
	buyer.InitMembers(5000);	// 변수 초기화 (보유자금)
	buyer.BuyApples(seller, 2000);	// 과일의 구매

	cout << "과일 판매자의 현황" << endl;
	seller.ShowSalesResult();
	cout << "과일 구매자의 현황" << endl;
	buyer.ShowBuyResult();
	return 0;
}

 

코드 실행결과