read

여섯번째 열혈강의


computer

이전에 이어서 Class에서의 용어정리 부터 시작하겠다.

private와 관련된 내용은 다음 chapter에서 다룰 예정이다.


용어정리


먼저 용어를 알면 편의가 따라온다. 구조체 변수, 클래스 변수라는 표현은 이제 더이상 어울리지 않는다. 구조체와 클래스는 변수의 셩격만 지니는 것이 아니기 때문이다. 그래서 변수라는 표현 대신해서 객체(object)라는 표현을 할 것이다.

객체는 무엇을 의미하고, 객체라고 부르는 이유는 무엇인가? 궁금할지도 모르겠는데 이것은 잠시 후 객체지향과 관련한 이론적인 이야기부터 할 것이다. 어쨋든 이전 포스팅에서 보였던 runn99는 변수가 아닌 객체다.

그리고 클래스를 구성하는(클래스 내에 선언된) 변수를 가리켜 멤버변수라 하고, 클래스를 구성하는(클래스 내에 정의된) 함수를 가리켜 멤버함수라고 한다.

즉, Car 클래스를 구성하는 멤버변수는 다음과 같다.

- char gamerID(CAR_CONST::ID_LEN);
- int fuelGauge;
- int curSpeed;

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

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

멤버는 구성원이란 의미가 있다. 따라서 저런 네이밍이 됬다고 보면 된다.


C++에서의 파일분할


모든 프로그램을 하나의 파일안에 담아 둔다면 그만큼 바보같은 짓도 없다. 특히 C++은 클래스 별로 헤더파일과 소스 파일을 생성해서 클래스의 선언과 정의를 분리하는 경우가 많아 파일의 수가 굉장히 많이 만들어진다. 그럼 이어서 클래스를 대상으로, 파일을 나누는 기준을 설명할 텐데, 기본적으로 C의 내용은 알고 있다고 가정한다.

- 헤더파일의 역할을 알고 있다.
- C언어를 대상으로 헤더파일에 들어가야  내용을 구분할  있다.
- 헤더파일의 중복포함을 막기 위해서 사용하는 매크로 #infdef ~ #endif을 알고 있다.
-  이상의 파일을 컴파일해서 하나의 실행파일을 만드는 법을 알고 있다.
- 링커(Linker) 하는 일을 알고 있다.

위 내용 중 일부를 조금 덜 알고 있더라도 다음 설명을 이해하는데 크게 무리는 없다. 사실 앞으로의 내용중에 이해가 가지 않는 부분이 있을 때, 위 내용을 복습 및 참고 하라는 차원에서 적어 두었다고 한다.

잘 모르는 부분이 있다면 서핑을 해서 알아보고 시작하는 것이 좋을 듯 하다.

보통 파일을 구분할 때는 다음과 같다.

- Car.h 클래스의 선언을 담는다.
- Car.cpp 클래스의 정의(멤버함수의 정의) 담는다.

여기서 말하는 클래스의 선언은 다음과 같다.

class Car{
	private:
    	char gamerID[CAR_CONST::ID_LEN];
        int fuelGauge;
        int curSpeed;
    public:
    	void InitMembers(char* ID, int fuel);
        void ShowCarState();
        void Accel();
        void Break();
};

이는 컴파일러가 Car 클래스와 관련된 문장의 오류를 잡아내는데 필요한 최소한의 정보로써, 클래스를 구성하는 외형적인 틀을 보여준다. 따라서 이를 가리켜 클래스의 선언(declaration)이라 한다. 즉, 위의 정보는 클래스 Car와 관련된 문장의 옳고 그름을 판단하는데 사용된다.

예를 들어 아래와 같은 코드들을 컴파일할 때, 반드시 필요한 정보다.

int main(){
	Car run99;
    run99.fuelGauge = 100; //fuelGauge private임을 확인하고 에러를 발생한다.
    run99.Accel(20); //Accel 함수의 매개변수가 void 형임을 알고 에러를 발생시킴
}

클래스의 정의(definition)에 해당하는 다음 함수의 정의는 다른 문장의 컴파일에 필요한 정보를 가지고 있지 않다. 따라서 함수의 정의는 컴파일 된 이후에, 링커에 의해 하나의 실행파일로 묶이기만 한다.

void Car::InitMembers(char* ID,int fuel){...}
void Car::ShowCarState(){...}
void Car::Accel(){...}
void Car::Break(){...}

그럼 결론을 보자. Car 클래스와 관련된 문장의 컴파일 정보로 사용되는 클래스의 선언은 헤더파일에 저장을 해서, 필요한 위치에 쉽게 포함될 수 있도록 해야 하며, 클래스의 정의는 소스파일에 저장해서, 컴파일이 되도록 하면 된다. 그럼 지금까지 내용을 실습해보자.

헤더파일


#ifndef __CAR_H_
#define __CAR_H_

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

class Car {
private :
	char gamerID[CAR_CONST::ID_LEN];
	int fuelGauge;
	int curSpeed;
public :
	void InitMembers(char* ID, int fuel);
	void ShowCarState();
	void Accel();
	void Break();
};

#endif
Car.cpp

#include <iostream>
#include <cstring>
#include "Car.h"
using namespace std;

void Car::InitMembers(char* ID, int fuel) {
	strcpy(gamerID, ID);
	fuelGauge = fuel;
	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::ACC_STEP;
}

void Car::Break() {
	if (curSpeed < CAR_CONST::BRK_STEP) {
		curSpeed = 0;
		return;
	}
	curSpeed -= CAR_CONST::BRK_STEP;
}
RacingMain.cpp

#include "Car.h"

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

이렇게 분할해보면, 클래스 Car의 구성하는 멤버 파악이 한결 수월하다. 뭔가 훨씬 정리가 된 기분이다. 처음에는 많이 부담스럽고 익숙지 않을 수 있다. 그러나 익숙해지면 이게 더 편하고 좋은 코드라는 것을 알 수 있을 것이다.


인라인 함수는 헤더파일에 함께..


앞서 보인 예제에서 Car.cpp에 ShowCarState와 Break함수를 inline함수로 바꾸면 컴파일 에러가 나온다.

그것은 인라인 함수에 대한 특징을 안다면 쉽게 접근할 수 있는데,

컴파일 과정에서 함수의 호출 문이 있는 곳에 함수의 몸체 부분이 삽입되어야 한다

를 알고 있다면 이해가 갈 것이다.

참고로, 컴파일러는 파일단위로 컴파일을 하기 때문에, 하나의 실행파일을 만든다 해도 한 파일의 컴파일 과정에서 다른 파일을 참조하지 않는다. 따라서 클래스의 선언과 인라인 함수의 정의를 함께 묶어둔다.


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


computer

이제까지는 C언어에서 C++로 자연스럽게 이동할 수 있도록, 구조체를 시작으로 클래스를 설명하였다. 이번에는 객체지향의 관점에서 클래스를 전혀 다른 방법으로 다시 한번 설명 하고자 한다. 구조체를 확장한 것이 클래스라고 인식하는 것은 문제가 되지 않는데, 그게 전부라고 알고 있으면 안된다.

C++은 객체지향 언어이다. 따라서 객체지향에 대한 이해가 필요한데, 먼저 객체지향의 우월성을 강조한다고 한다. 그러나 절차지향 언어에도 그만의 강점이 있다는 것도 알아두도록 하자.

객체는 영어로 Object이다. 특히 C++에서 말하는 Object의 의미는 사물, 또는 대상이다. 즉, Object는 우리 주변에 존재하는 물건(연필, 나무, 지갑, 돈 등등)이나 대상(철수, 친구, 선생님 등등) 전부를 의미한다. 그럼 문장을 이용해 한번 알아보자.

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

이 문장에서 객체는 나, 과일장수, 사과 이다.

그렇다면 라는 객체는 과일장수라는 객체로 부터 과일객체의 구매라는 액션을 취할 수 있어야 한다. 그런데 객체지향 프로그래밍에서는 그리고 과일장수와 같은 객체를 등장시킬 수 있을 뿐만 아니라, 라는 객체가 과일장수라는 객체로부터 과일객체를 구매하는 행위도 그대로 표현 할 수 있다.

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

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

다음과 같은 문장을 실체화 시킬 것인데, 사과는 일단 코드를 간결하게 하기 위해 과일장수만 시키도록 하겠다.


객체를 이루는 것은 데이터와 기능


프로그램상에 과일장수 객체가 존재한다고 가정해 보자. 그럼 이 객체는 무엇으로 이뤄져야 하겠는가? 물로 과일장수의 개인적인 사정이 있을 수 있겠지만, 무엇보다 여기서 중요한 관점은 과일의 판매에 있다. 따라서 프로그램상에서 바라보는 과일장수는 다음과 같다.

1. 과일 장수는 과일을 판다

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

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

이중 첫번째는 과일장수의 행동(behavior)을 의미한다. 그리고 두번째와 세번째는 과일장수의 상태(state)를 의미한다.

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

그럼 변수부터 표현 해보자.

보유하고 있는 사과의 수 -> int numOfApples;

판매 수익 -> int myMoney;

이번에는 행위에 대한 함수를 표현해 보자.

int SaleApples(int money){ //사과 구매액이 함수의 파라미터로 전달.
	int num = money / 1000; // 사과를 개당 1000원으로 가정
    numOfApples -= num; //사과의 수가 줄어듦
    myMoney += num; //판매 수익이 발생
    return num; //실제 구매가 발생한 사과의 수를 반환
}

그럼 변수와 함수는 만들어 졌고, 객체로 실체화를 시키도록 하자.


과일장수의 정의와 멤버변수의 상수화에 대한 논의


객체를 생성하기 전에 객체 생성하기 위한 틀(mold)를 먼저 만들어야 한다.

이는 현실 세계에서 물건을 만들기 위해 틀을 짜는 행위에 비유할 수 있다. 아마 C++의 설명에 있어서 붕어빵을 가장 많이 예시로 들 것이다.

따라서 위에 말했던 변수와 함수들로 틀을 만들어 보자.

class fruitSeller{
private:
	int APPLE_PRICE;
    int numOfApples;
    int myMoney;
public:
	int SaleApples(int money){
        int num = money / APPLE_PRICE;
        numOfApples -= num;
        myMoney += num;
        return num;
    }
};

모습은 매우 친근한 class의 정의다. 즉, FruitSeller라는 이름의 클래스가 과일장수의 틀이 되는 것이다. 그런데 다음과 같은 대화를 보자.

오늘 과일 많이 파셨어요?

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 / APPLE_PRICE;
        numOfApples -= num;
        myMoney += num;
        return num;
    }
    
    void ShowSalesRresult(){
    	cout << "남은 사과 : " << numOfApples << endl;
        cout << "판매 수익 : " <<myMoney << endl;
    }
};

이렇게 class의 정의에서 사과가격을 의미하는 APPLE_PRICE의 이름을 대문자로 한 것은 사과의 가격은 일정하다라는 가정의 결과이다.

즉, 가격을 상수라고 가정한 것이다. 보통 실제론 변동이 있기는 하지만 상수라고 가정하는 것도 일리는 있고, 따라서 다음과 같이 const선언으로 변경이 일어나지 않도록 하자.

const int APLLE_PRICE = 1000;

그러나 불가능하다. 클래스의 멤버변수 선언문에서 초기화까지 하는 것을 허용하지 않기 때문이다. 그래서 일단 const를 선언하고, 객체를 생성한 다음에 initMembers함수의 호출을 통해 상수 값을 초기화 하기로 하자.

conset int APPLE_PRICE;

그러나 이 역시 불가능 하다. 상수는 선언과 동시에 초기화 되어야 하기 때문이다. 즉, 현재로써 APPLE_PRICE를 상수로 선언할 방법이 없다.

일단 여기서는 이렇게 방법이 없다는 것을 알아 보는 것으로 마치고 추 후에 생성자라는 것을 배우면 그 때 제대로 처리를 해보자.

이번 포스팅은 여기서 일단 마치고 다음 포스팅에서 계속 이어서 적도록 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview