read

다섯번째 열혈강의


computer

이제 오늘부터 아마 C++의 느낌을 제대로 느낄 듯하다.

객체지향의 꽃인 class를 시작해 보겠다.


C++에서의 구조체


먼저 우리는 C를 공부했기에 구조체를 알고 있다. 만일 모른다면 C를 조금 공부하고 오기를 바란다.

C언어로 프로그램을 만들면 구조체의 정의는 항상 따라온다. 그럼 이 구조체라는 것의 이점이 무엇일까? 아마 다양한 답변이 있겠지만 공통으로 나오는 말은

연관 있는 데이터를 하나로 묶으면, 프로그램의 구현 및 관리가 용이하다.

라는 말일 것이다. 소프트웨어를 단순히 표현하자면

소프트웨어 = 데이터의 표현 + 데이터의 처리

인데, 표현해야할 데이터는 항상 부류를 형성한다. 그리고 이 데이터들은 함께 생성, 이동소멸이 되는 특성이 있다. 그래서 구조체는 연관 있는 데이터를 묶을 수 있는 문법적 장치로 매우 큰 도움을 준다.

그럼 예시를 들어 보도록 하자.

레이싱게임의 자동차를 한번 표현해 보자.

여러가지 정보들이 필요한데 예를 들면

소유주, 연료량, 현재속도, 취득점수, 취득아이템

또한 게임을 종료하거나 로그아웃하면 위의 정보는 DB에 저장되어야 하고 게임을 시작할 때 복원이 되어야 한다.

이런 정보를 구조체를 이용하면 매우 쉬워진다.

struct Car{
	char gamerID[ID_LEN]; //소유자 id
    int fuelGauge;//연료량
    int curSpeed;//현재속도
};

전부는 아니지만 몇몇정보들을 제외하고 구조체로 만들어 보았다. 그럼 이것을 기반으로 간단한 예제를 만들건데, 그전에 C++에서의 구조체를 좀더 생각해보자.

먼저, C에서의 선언은 다음과 같다.

struct Car basicCar;

struct Car simpleCar;

앞에 키워드 struct는 이어서 선언되는 자료형이 구조체를 기반으로 정의된 자료형임을 나타낸다. 만약 struct를 생략하려면 typedef 선언을 추가해주어야 한다.

하지만 C++에서는 기본 자료형 변수의 선언방식이나 구조체를 기반으로 정의된 자료형의 변수 선언방식에 차이가 없다.

이 의미는 typedef가 필요가 없다는 것이다.

Car basicCar; Car simpleCar;

그럼 예제를 보자.

#include<iostream>

using namespace std;

#define ID_LEN 20
#define MAX_SPD 200
#define FUEL_STEP 2
#define ACC_STEP 10
#define BRK_STEP 10

struct Car{
	char gamerID[ID_LEN];
	int fuelGauge;
	int curSpeed;
};

void ShowCarState(const Car &car) {
	cout << "소유자ID : " << car.gamerID << endl;
	cout << "연료량 : " << car.fuelGauge << "%" << endl;
	cout << "현재속도 : " << car.curSpeed << "km/s" << endl << endl;
}

void Accel(Car &car) {
	if (car.fuelGauge <= 0) {
		return;
	}
	else
		car.fuelGauge -= FUEL_STEP;

	if (car.curSpeed + ACC_STEP >= MAX_SPD) {
		car.curSpeed = MAX_SPD;
		return;
	}

	car.curSpeed += ACC_STEP;
}

void Break(Car &car) {
	if (car.curSpeed < BRK_STEP) {
		car.curSpeed = 0;
		return;
	}
	car.curSpeed -= BRK_STEP;
}

int main() {

	Car run99 = { "run99",100,0 };
	Accel(run99);
	Accel(run99);
	ShowCarState(run99);
	Break(run99);
	ShowCarState(run99);

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

	return 0;
}

여기서 함수는 결국 데이터를 처리하는 도구이니 데이터와 함께 부류를 이룬다고 해도 무방하다.

그래서 이 책의 필자는 위의 세개의 함수에 대해 다음 같이 말한다.

구조체 Car와 함께 부류를 형성하여, Car와 관련된 데이터의 처리를 담당하는 함수들

즉, 위 함수들은 구조체 Car에 종속적인 함수들이다. 그런데 전역함수의 형태이므로 이 함수들이 구조체 Car에 종속적임을 나타내지 못하고 있다. 그래서 다른영역에서 이 함수를 호출하는 실수도 생길 수가 있다.


구조체 안의 함수


그럼 구조체 내에 함수를 묶어버리면 데이터와 함수를 모두 나타낼 수 있어 확실한 구분이 가능해진다. 그럼 한번 묶어보자. C++은 구조체 내에 함수선언이 가능하다.

#include<iostream>

using namespace std;

#define ID_LEN 20
#define MAX_SPD 200
#define FUEL_STEP 2
#define ACC_STEP 10
#define BRK_STEP 10

struct Car{
	char gamerID[ID_LEN];
	int fuelGauge;
	int curSpeed;

	void ShowCarState() {
		cout << "소유자ID : " << gamerID << endl;
		cout << "연료량 : " << fuelGauge << "%" << endl;
		cout << "현재속도 : " << curSpeed << "km/s" << endl << endl;
	}

	void Accel() {
		if (fuelGauge <= 0) {
			return;
		}
		else
			fuelGauge -= FUEL_STEP;

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

		curSpeed += ACC_STEP;
	}

	void Break() {
		if (curSpeed < BRK_STEP) {
			curSpeed = 0;
			return;
		}
		curSpeed -= BRK_STEP;
	}
};

그럼 변화를 한번 보자.

삽입되기 이전에 함수내부는 car를 파라미터로 받아서 해당 정보를 받아 연산한다. 그러나 삽입된 이후에는 정보가 존재하지 않는다. 그 이유는 구조체 내에 삽입되면서 구조체 내의 변수에 직접접근이 가능해졌기 때문이다.

예제처럼 run99와 sped77이 선언되면, 각각의 구조체 변수들이 생성되고 함수가 생성된다.

사실 함수는 모든 구조체가 공유를 하지만, 논리적으로 각각의 변수가 자신의 함수를 가지는 것과 같은 효과 및 결과를 보이기에 그림을 그린다면 각자 가지고 있는 식으로 그려넣는게 좋다.

그럼 예제를 완성해보자.

#include<iostream>

using namespace std;

#define ID_LEN 20
#define MAX_SPD 200
#define FUEL_STEP 2
#define ACC_STEP 10
#define BRK_STEP 10

struct Car{
	char gamerID[ID_LEN];
	int fuelGauge;
	int curSpeed;

	void ShowCarState() {
		cout << "소유자ID : " << gamerID << endl;
		cout << "연료량 : " << fuelGauge << "%" << endl;
		cout << "현재속도 : " << curSpeed << "km/s" << endl << endl;
	}

	void Accel() {
		if (fuelGauge <= 0) {
			return;
		}
		else
			fuelGauge -= FUEL_STEP;

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

		curSpeed += ACC_STEP;
	}

	void Break() {
		if (curSpeed < BRK_STEP) {
			curSpeed = 0;
			return;
		}
		curSpeed -= BRK_STEP;
	}
};

int main() {

	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;
}

별거 안한거 같지만 이미 C언어에서 C++로 많이 넘어왔다.

그럼 이제 메크로로 선언해둔 상수들이 구조체 Car에만 의미 있는 상수들이므로 이것들도 struct안에 선언을 해보겠다.

열거형 enum을 사용할 것인데, enum을 struct 내에 그냥 선언해도 되지만, namespace를 이용해서 밖에 선언해두고 쓰면 그리 부담없이 쓸 수 있다. 그러면 다른 구조체들 중에 해당상수를 사용할 수도 있다.

#include<iostream>

using namespace std;

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

struct Car{
	char gamerID[CAR_CONST::ID_LEN];
	int fuelGauge;
	int curSpeed;

	void ShowCarState() {
		cout << "소유자ID : " << gamerID << endl;
		cout << "연료량 : " << fuelGauge << "%" << endl;
		cout << "현재속도 : " << curSpeed << "km/s" << endl << endl;
	}

	void 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 Break() {
		if (curSpeed < CAR_CONST::BRK_STEP) {
			curSpeed = 0;
			return;
		}
		curSpeed -= CAR_CONST::BRK_STEP;
	}
};

int main() {

	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;
}

그러면 구조체지만 매우 느낌이 달라졌을 것이다. 근데 구조체가 함수까지 전부 선언하다보니 너무 커졌다.


함수는 외부로 뺄 수 있다.


함수가 포함된 구조체를 보면, 쉽게 눈에 들어와야 하는 정보들이 있다.

선언되어있는 변수 정보

선언되어있는 함수 정보

보통 프로그램을 분석할 때, 흐름이나 골격 위주로 분석하는 경우가 많다. 이럴 때 보통 함수의 기능만 파악하지 내부를 다 보진 않는다. 따라서 구조체를 쓸 때, 함수의 종류와 기능이 한눈에 들어오는 게 좋다. 따라서 함수를 따로 빼서 사용해 보겠다.

간단히 할 수 있으므로 예제를 보겠다.

#include<iostream>

using namespace std;

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

struct Car{
	char gamerID[CAR_CONST::ID_LEN];
	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() {

	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;
}

사실, 앞서 언급은 안했지만 구조체 안에 함수가 정의되어 있으면 다음과 같은 의미가 들어간다.

함수를 인라인으로 처리하시오

반면, 위 예제처럼 밖으로 뺀다면 이런 의미가 사라지므로 혹시나 inline으로 사용하고 싶다면 명시적으로 넣어주면 된다.

inline void Car::ShowCarState(){...}

여기까지가 구조체에 대한 얘기의 전부이다. 후에 C++에서의 구조체는 class의 일종으로 간주된다. 즉, 위에서 정의한 구조체를 그냥 class라고 해도 틀리지 않는다.


Class와 Object(객체)


computer

앞서 설명한 내용을 보면, 구조체는 class의 일종이다.

그렇다면 구조체와 class의 차이는 무엇일까?

구조체와 클래스는 유일한 차이점이 있다.

키워드 struct를 대신해서 class를 사용하면, 구조체가 아닌 클래스가 된다. 아래 코드를 보도록 하자.

class Car{
	char gamerID[CAR_CONST::ID_LEN];
	int fuelGauge;
	int curSpeed;
	void ShowCarState();
	void Accel();
	void Break();
};

바꾼건 struct에서 class밖에 없다. 이렇게 해도 class로 변환이 된다.

그런데 메인에서가 문제다. 앞서 예제에서 구조체를 선언할 때는

Car run99 = {"run99",100,0};

으로 선언했는데, 클래스는 선언하지 못한다.

이유는 클래스 내에서 선언된 함수가 아니라 다른 영역에서 초기화하려 했기 때문이다. 클래스는 기본적으로 별도의 선언을 하지않으면 클래스 내에 선언된 변수는 클래스 내에 선언된 함수에서만 접근이 가능하다.

즉, 다음과 같이 초기화를 한다

Car run99;

그럼 접근법은 무엇일까?

외부적으로 접근 하는 건 사실상 불가능하다.

그래서 접근과 관련해서 별도의 선언을 하지 않으면, 클래스 내에서 선언된 변수나 함수에 대한 접근을 허용하지 않으므로, 접근과 관련된 지시를 별도로 내려주어야 한다.

이렇게 클래스는 정의를 하는 과정에서 각각의 변수나 함수의 접근 허용범위를 별도로 선언해야 한다.


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


C++에서 접근제어 지시자는 총 3가지이다.

public : 어디서든 접근허용

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

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

먼저 protected는 상속과 관계있으므로 나중에 보도록 하고, 여기서는 public과 private를 먼저 보자.

#include<iostream>

using namespace std;

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();
};

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;
}

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

이렇게 되면 몇가지 사실을 알 수 있다.

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

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

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

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

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

즉, struct와 class의 선언차이가 구조체와 클래스의 유일한 차이점이다.

구조체도 클래스도 접근제어 지시자의 선언이 가능하고 그 의미도 동일하다. 다만 접근제어 지시자를 선언하지 않았을 때, 클래스는 private, 구조체는 public으로 될 뿐 이다.

왜 private로 하는 것일까? 그것은 당연히 정보 은닉(Information Hiding) 때문이다.

이는 다음 챕터에서 얘기하도록 하자.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

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

Back to Overview