일곱번째 열혈강의
이전에 템플릿에서 클래스로의 확장과 객체의 개념, 객체에서 틀의 개념과 함께 클래스로 이어지는 부분을 포스팅 했다.
지금까지 과일장수로서의 객체를 설정했고, 상수초기화에 대한 어려움을 야기시키며 생성자라는 것이 처음 등장하게 되면서 거기에 대한 궁금증만 제시해 둔 상태다.
앞으로 private로 설정된 멤버변수의 의미와 생성자등을 자세히 다룰 것이긴 한데 아마 다음 chapter에서의 내용이 될 것이다.
지금은 나에 대한 객체를 표현해 볼까 한다.
“나”를 표현하는 클래스의 정의
이제 나를 표현하기 위한 클래스를 정의할 차례인데, 이는 과일 구매자를 뜻하는 것이다. 따라서 클래스의 이름을 FruitBuyer라고 정하자. 그럼 이 클래스는 어떤 변수와 함수들로 채워야 할까? 이는 이전 포스팅처럼 먼저 데이터적인 측면부터 보도록 하자. 구매자에게 있어서 가장 중요한 것은 돈이다. 돈이 있어야 구매가 가능하기 때문이다. 그리고 구매를 했다면 물품 보유가 된다. 따라서 다음 변수들을 생각할 수 있다.
소유하고 있는 현금의 액수 -> int myMoney;
소유하고 있는 사과의 수 -> int numOfApples;
그럼 이제 기능적인 측면을 보자. 구매자의 입장에서는 과일의 구매가 기능일 것이고, 이 기능을 담당할 함수가 추가되어야 한다.
class FruitBuyer{
int myMoney; //private
int numOfApples; //private
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;
}
};위에서 변수에는 private를 따로 선언하지 않았는데, class를 설명할 때 아무 조건이 없으면 private라고 했으므로 자동으로 private가 된다. (구조체 였다면 public이였을 것이다.)
클래스 기반의 두가지 객체생성 방법
우리는 여태 2가지 클래스를 정의했다. 그러면 객체를 생성하지 않고 이 두 클래스 안에 존재하는 변수에 접근하고 함수를 호출하는 것이 가능 할까? 언뜻 가능할 것 처럼 보이기도 한다. 그러나 이들은 실체(다시 말해서 객체)가 아는 틀이다. 따라서 접근도 호출도 불가능하다. 이는 자동차의 엔진과 자동차의 외형을 생산 할 수 있는 틀이 있다고 해서, 이들을 타고 달릴 수 없는 것과 같은 이치이다. 그럼 이제 앞서 정의한 클래스를 실체화 해보자. 여기서 이제 실체화는 객체화 시킨다는 말로 바뀐다는 걸 알 수 있다. 객체 생성에는 2가지 방법이 있는데, 이는 기본 자료형 변수의 선언방식과 같으므로 쉽게 접근 할 수 있다.
ClassName objName; // 일반적인 변수의 선언방식
ClassName * ptrObj = new ClassName; // 동적 할당방식(힙 할당방식)
즉, 우리가 정의한 클래스의 객체 생성은 다음과 같다.
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;
numOfApples -= num;
myMoney += money;
return num;
}
void ShowSalesResult() {
cout << "남은 사과 : " << numOfApples << endl;
cout << "판매 수익 : " << myMoney << endl << endl;
}
};
class FruitBuyer {
int myMoney;
int numOfApples;
public:
void InitMember(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() {
FruitSeller seller;
seller.InitMembers(1000, 20, 0);
FruitBuyer buyer;
buyer.InitMember(5000);
buyer.BuyApples(seller, 2000);
cout << "과일 판매자의 현황" << endl;
seller.ShowSalesResult();
cout << "과일 구매자의 현황" << endl;
buyer.ShowBuyResult();
return 0;
}여기서 주목해야 할 부분은
numOfApples += seller.SaleApples(money);
이곳과
buyer.BuyApples(seller, 2000);
이곳이다. 처음으로 두개의 class를 서로 대화시킨 예제이다.
객체간의 대화방법(message Passing 방법)
위 예제의 두 부분을 보면 FruitBuyer객체에 존재하는 함수 내에서 FruitSeller 객체의 함수 SaleApples를 호출하고 있다. 이 것은 다음과 같다.
seller, 사과 2000원치 주세요
자 이것은 매우 중요한데, 앞서
**나라는 객체가 과일장수라는 객체로부터 과일객체를 구매하는 행위도 그대로 표현할 수 있다. **
라는 말을 했었다. 즉 다음과 같은 메시지를 전달하고 있는 것이다.
이처럼 하나의 객체가 다른 하나의 객체에게 메시지를 전달하는 방법(어떠한 행위의 요구를 위한 메시지 전달)은 함수호출을 기반으로 한다.
그래서 객체지향에서는 이러한 형태의 함수호출을 메시지 전달(Message Passing)이라고 한다.
관계를 형성하는 둘 이상의 클래스는 의외로 만들기가 쉽지않다. 하나의 독립된 클래스를 정의하는 건 쉬울지 몰라도 관계를 형성해서 정의하는 것은 의외로 까다롭고 단순히 함수 호출로 이해하면 별 것 아니지만 메시지 전달로 보면 매우 중요하다.
정보은닉(information Hiding)
드디어 chapter 4로 들어왔다. chapter 4부터는 클래스의 완성이라는 제목으로 시작한다.
먼저 정보은닉이라는 말에 대해 알아보도록 하자. 우리는 객체의 생성을 목적으로 클래스를 디자인한다. 그렇다면 좋은 클래스가 되기위한 조건은 무엇일까?
여기에는 정보은닉과 캡슐화가 있다. 이는 좋은 클래스가 되기 위한 최소한의 조건이다.
자, 그림판에 보면 여러가지 도형들을 그릴 수 있다. 삼각형, 사각형, 원 등등.. 이 것을 C++로 구현한다고 해보자. 다양한 종류의 클래스를 정의해야 할 것이고, 점의 위치좌표를 표현하는 목적의 클래스는 기본적으로 필요하다.
class Point{
public:
int x; //0이상 100이하
int y; //0이상 100이하
}이 클래스를 가지고 정보은닉에 대한 이야기를 진행해 볼 것이다. 일단 이 클래스에서 멤버 변수 x와 y는 0이상 100이하이고, 좌상단 좌표가 [0,0], 우 하단 좌표가[100,100]이라고 가정하도록 하겠다.
먼저 문제점이 드러나는 코드를 한번 볼 것이다.
#include<iostream>
using namespace std;
class Point {
public:
int x;
int y;
};
class Rectangle {
public:
Point upLeft;
Point lowRight;
public:
void ShowRecInfo() {
cout << "좌 상단 : " << "[" << upLeft.x << ", ";
cout << upLeft.y << "]" << endl;
cout << "우 하단 : " << "[" << lowRight.x << ", ";
cout << lowRight.y << "]" << endl << endl;
}
};
int main() {
Point pos1 = { -1, 4 };
Point pos2 = { 5, 9 };
Rectangle rec = { pos2,pos1 };
rec.ShowRecInfo();
return 0;
}여기서 한 문장이 설명이 필요할 것이다.
Rectangle rec;
이 문장에 의해 객체가 생성되면 메모리상에는 두 개의 Point객체가 포함되어 있게 된다. 이렇게 객체가 멤버로 등장한다고 해서 크게 문제되는 것은 없고, 그럼 그 다음 문장도 실행결과를 알 수 있을 것이다.
Rectangle rec = {pos2, pos1};
이 문장은 객체를 생성하고 초기화를 한다. 즉, 미리 생성된 두개의 Point 객체에 저장값이 Rectangle 객체 멤버에 대입이 된다.
그럼 이제 이해가 됬을 것이고, 어떠한 문제가 있었는지 알아보자.
점의 좌표는 0이상 100이하가 되어야 하는데, 그렇지 못한 Point 객체가 있다.
직사각형을 의미하는 Rectangle 객체의 좌 상단 좌표 값이 우 하단 좌표 값보다 크다.
자 위와 같이 유효한 범위가 아닌 것들이 생긴다. 이러한 것은 프로그래머의 실수에서 발생하는 경우가 대부분이다. 또한 좌 상단의 좌표 값이 우 하단 좌표 값보다 큰 걸로 봐서, 두 좌표의 값이 서로 바뀐 듯 하다. 이 역시 실수라고 생각할 수 있다. 중요한 것은 다음이다.
프로그래머의 실수에 대한 대책이 하나도 준비되어 있지 않다.
사실 위와 같은 것들은 컴파일러에도 나오지 않고, 쉽게 발견이 되지 않는다. 때문에 제한된 방법으로의 접근만 허용을 해서 잘못된 값이 저장되지 않도록 도와야 하고, 또 실수를 했을 때, 실수가 쉽게 발견되도록 해야 한다.
다음 예제를 보자.
Point.h
#ifndef __POINT_H_
#define __POINT_H_
class Point {
private:
int x;
int y;
public:
bool InitMembers(int xpos, int ypos);
int GetX() const;
int GetY() const;
bool SetX(int xpos);
bool SetY(int ypos);
};
#endif위는 헤더 파일이다.
먼저 멤버변수 x와 y를 private으로 선언해서 임의로 값이 저장되는 것을 막아놓았다. 즉 x와 y라는 정보를 은닉한 상황이다. 대신 값의 저장 및 참조를 위한 함수를 추가로 정의했다. 따라서 이 함수 내에서 멤버변수에 저장되는 값을 제한할 수 있게 되었다.
Point.cpp
#include <iostream>
#include "Point.h"
using namespace std;
bool Point::InitMembers(int xpos, int ypos) {
if (xpos < 0 || ypos < 0) {
cout << "벗어난 범위의 값 전달" << endl;
return false;
}
x = xpos;
y = ypos;
return true;
}
int Point::GetX() const{
return x;
}
int Point::GetY() const{
return y;
}
bool Point::SetX(int xpos) {
if (0 > xpos || xpos > 100) {
cout << "벗어난 범위의 값 전달" << endl;
return false;
}
x = xpos;
return true;
}
bool Point::SetY(int ypos) {
if (0 > ypos || ypos > 100) {
cout << "벗어난 범위의 값 전달" << endl;
return false;
}
y = ypos;
return true;
}먼저 멤버변수에 값을 저장하는 함수 InitMembers, SetX, SetY로 에러 메시지 출력을 정의했다. 그러면 한번 결론을 내보자.
멤버변수를 private로 선언하고, 해당 변수에 접근하는 함수를 별도로 정의해서, 안전한 형태로 멤버변수의 접근을 유도하는 것이 바로 정보은닉이며, 이는 좋은 클래스가 되기 위한 기본조건이 된다.
그리고 흔히 get? set?과 같은 함수명을 자주쓰는 것들이 있는데, 이들을 가리켜 엑세스 함수(access function)이라고 한다. 이들은 멤버변수를 private로 선언하면서 클래스 외부에서의 멤버변수 접근을 목적으로 정의되는 함수들이다. (보통 getter, setter라고들 많이 말한다.) 또한 이 함수들은 선언이 되더라도 쓰이지 않을 때가 많다.
그럼 왜 선언을 해놓는 것일까?
물론 호출될 함수 위주로 멤버함수를 구성하는 것이 맞지만, 나중에 필요할 수 있다고 생각되는 것들도 더불어 포함시키는 경우도 많다.
그럼 이어서 Rectangle 클래스를 한번 보겠다.
Rectangle.h
#ifndef __RECTANGLE_H_
#define __RECTANGLE_H_
#include "Point.h"
class Rectangle {
private:
Point upLeft;
Point lowRight;
public:
bool InitMembers(const Point &ul, const Point &lr);
void ShowRecInfo() const;
};
#endif헤더 파일이다. private로 선언을 했다는 것이 보이고, 초기화 함수도 별도로 선언해 주었음이 보인다. 이 함수들을 한번 보도록 하자.
#include <iostream>
#include "Rectangle.h"
using namespace std;
bool Rectangle::InitMembers(const Point &ul, const Point &lr) {
if (ul.GetX() > lr.GetX() || ul.GetY() > lr.GetY()) {
cout << "잘못된 위치정보 전달" << endl;
return false;
}
upLeft = ul;
lowRight = lr;
return true;
}
void Rectangle::ShowRecInfo() const {
cout << "좌 상단 : " << "[" << upLeft.GetX() << ", ";
cout << upLeft.GetY() << "]" << endl;
cout << "우 하단 : " << "[" << lowRight.GetX() << ", ";
cout << lowRight.GetY() << "]" << endl;
}아까부터 계속해서 const라는 것이 함수뒤에 붙고 있는데, 이는 잠시 후 설명할 예정이다.
오류 상황에 대한 처리의 방법은 프로그램의 성격 및 내용에 따라 달라질 수 있으니 멤버변수의 접근에 제한을 두었다는 것에 주목하길 바란다.
그럼 마지막 main을 보자.
#include <iostream>
#include "Point.h"
#include "Rectangle.h"
using namespace std;
int main(void) {
Point pos1;
if (!pos1.InitMembers(-2, 4))
cout << "초기화 실패" << endl;
if (!pos1.InitMembers(2, 4))
cout << "초기화 실패" << endl;
Point pos2;
if (!pos2.InitMembers(5, 9))
cout << "초기화 실패" << endl;
Rectangle rec;
if (!rec.InitMembers(pos2, pos1))
cout << "직사각형 초기화 실패" << endl;
if (!rec.InitMembers(pos1, pos2))
cout << "직사각형 초기화 실패" << endl;
rec.ShowRecInfo();
return 0;
}함수의 return값으로 bool형을 선언했기에 위와 같이 설정할 수 있다. 그리고 성공여부에 따라 조치를 다르게 취할 수 있다. 지금은 단순히 오류가 났다고 출력하는 정도지만 프로그램이였다면 아마 다시 입력받는 형태가 되었을 것이다.
const 함수
앞서 선언한 함수들을 보자.
int GetX() const;
int GetY() const;
void ShowRecInfo() const
여기서 const의 의미는 무엇일까?
이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다
라는 의미가 된다.
파라미터도 아니고, 지역변수도 아닌 멤버변수에 저장된 값을 변경하지 않겠다는 선언이다. 따라서 const 선언이 추가된 멤버함수 내에서 멤버변수의 값을 변경한다면 컴파일 에러가 발생한다. 이렇게 함수를 const로 선언하면 혹여나 실수했을 때 알 수 있게 된다. 그럼 실수를 최소화 할 수 있을 것이다.
이런 const 함수에는 또다른 특징이 있는데, 다음 코드를 보자.
class SimpleClass{
private:
int num;
public:
void InitNum(int n){
num=n;
}
intGetNum(){
return num;
}
void ShowNum() const{
cout << GetNum() << endl;
}
}위에 클래스 정의에서 ShowNum은 const함수로 선언되었다. 그리고 실제로 함수 내에서는 num 값이 변경되지 않는다. 그래도 불구하고 GetNum을 호출할 때 컴파일 에러가 나온다. 그 이유는 다음과 같다.
const 함수 내에서는 const가 아닌 함수의 호출이 제한된다.
const로 선언되지 않은 함수는 아무리 멤버변수에 저장된 값을 변경하지 않더라도, 변경할 수 있는 능력을 지니게 된다. 따라서 가능한 호출을 허용하지 않는 것이다. 그럼 유사한 특성을 하나 더 보도록 하자.
class EasyClass{
private:
int num;
public:
void InitNum(int n){
num = n;
}
int GetNum(){
return num;
}
};
class LiveClass{
private:
int num;
public:
void InitNum(const EasyClass &easy){
num = easy.GetNum();
}
};여기서 InitNum의 파라미터 easy는 const 참조자 이다. 그런데 이를 대상으로 GetNum을 호출하면 컴파일 에러가 발생한다. 이는 GetNum이 const가 아니라서 그런데, C++에서는 const 참조자를 대상으로 값의 변경 능력을 가진 함수의 호출을 허용하지 않는다.(변경여부는 상관없다) 따라서 const 참조자를 이용해서는 const 함수만 호출이 가능하다.
이제 내가 작성할 코드의 안전성은 한층 높아졌다고 볼 수 있다.
오늘 포스팅이 길이가 좀 길어졌다. 중요한 내용이고 끊어지기를 원치 않아서 길게 적을 수 밖에 없었다.
다음 포스팅에서 캡슐화(encapsulation)에 대해 알아보도록 하겠다.