-
[04/04 ~] C++a 2022. 4. 4. 09:22반응형
03-1. C++에서의 구조체
1. 구조체의 등장배경은 무엇인가>
연관있는 데이터를 하나로 묶으면, 프로그램의 구현 및 관리가 용이하다.
소프트웨어를 단순히 표현하면 다음과 같다.
" 소프트웨어 = 데이터의 표현 + 데이터의 처리 "
그런데 '표현해야 하는 데이터'는 항상 부류를 형성하기 마련이다. 그리고 이렇게 부류를 형성하는 데이터들은 함께 생성, 이동 및 소멸된다는 특성이 있다. 그래서 구조체는 연관 있는 데이터를 묶을 수 있는 문법적 장치로 데이터의 표현에 매우 큰 도움을 준다. 예를 들어서 레이싱게임의 캐릭터로 등장하는 '자동차'를 표현한다고 가정해 보자. 다음과 같은 정보가 모여서 게임 상의 자동차가 표현돼야 한다.
- 소유주
- 연료량
- 현재 속도
- 취득 점수
- 취득 아이템
게임 사용자가 게임을 종료하면, 위의 정보는 데이터베이스(또는 파일)에 함께 저장돼야 하며, 다시 게임을 시작하면, 저장된 위의 정보는 모두 함께 복원돼야 한다. 따라서 이들 정보를 이용해서 다음과 같이 구조체를 정의하면 프로그래밍이 한결 수월해진다.
struct Car{
char gamerID[ID_LEN]; //소유자id,
int fuelGauge; //연료량
int curSpeed; //현재 속도
}취득 점수와 취득 아이템을 제외한 나머지를 모두 포함하여 구조체로 정의하였다. 그럼 이어서 이 구조체를 기반으로 간단한 예제를 작성해 보겠다. 그런데 이에 앞서 C++에서의 구조체 변수 선언에 대한 이야기를 할 필요가 있다.
2. C++에서의 구조체 변수의 선언
C언어에서 구조체 변수를 선언하는 방법은 다음과 같다.
struct Car basicCar; struct Car simpleCar;
앞에 삽입된 키워드 struct는 이어서 선언되는 자료형이 구조체를 기반으로 정의된 자료형임을 나타낸다. 그리고 키워드 struct를 생략하려면 별도의 typedef를 선언해야 한다. 하지만 c++에서는 기본 자료형 변수의 선언방식이나 구조체를 기반으로 정의된 자료형의 변수 선언 방식에 차이가 없다. 즉, 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]; //소유자ID int fuelGauge; //연료량 int curSpeed; //현재 속도 }; void showCarState(const Car &car){ cout<<"소유자ID: "<<car.gamerID<<endl; cout<<"연료량 : "<<car.fuelGauge<<"%"<<endl; cout<<"현재 속도 : "<<car.curSpeed<<"km/s"<<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(void){ Car run99 = {"run99", 100, 0}; Accel(run99); Accel(run99); ShowCarState(run99); Break(run99); Car sped77 = {"sped77", 100, 0}; Accel(sped77); Break(sped77); ShowCarState(sped77); return 0; }
- ShowCarState 함수에서는 단순히 정보를 출력하기만 하므로 const 참조자를 매개변수로 선언하였다.
- 액셀을 밟을 때마다 연료가 줄어들고 스피드가 올라간다.
- 구조체 변수의 초기화는 {} 안에 해당 변수를 초기화 하는 것이다.
함수는 결국 데이터의 처리를 담당하는 도구이니, 데이터와 함께 부류를 형성하는 것은 매우 당연하다. 따라서 필자는 위에 정의된 세 개의 함수에 대해 다음과 같이 이야기하고자 한다.
" 구조체 Car와 함께 부류를 형성하여, Car와 관련된 데이터의 처리를 담당하는 함수들이다. "
따라서 위의 함수들은 구조체 Car에 종속된 함수라고 말할 수 있다. 그런데 전역함수의 형태로 표현되어서, 이 함수들이 Car 구조체의 종속임을 나타내지 못하고 있다. 따라서 엉뚱한 데에서 이 함수를 호출하는 실수를 범할 수 있다.
3. 구조체 안에 함수 삽입하기
구조체 car에 종속적인 함수들은 구조체 안에 함께 묶어버리면 어떻겠는가? 그렇게 되면 자동차와 관련된 데이터와 함수를 모두 묶는 셈이 되기 때문에 보다 확실한 구분이 가능하다. 그럼 앞서 보인 예제를 이용해서 함수를 묶어보자.
#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]; //소유자ID int fuelGauge; //연료량 int curSpeed; //현재 속도 void showCarState(){ cout<<"소유자ID: "<<car.gamerID<<endl; cout<<"연료량 : "<<car.fuelGauge<<"%"<<endl; cout<<"현재 속도 : "<<car.curSpeed<<"km/s"<<endl; } void Accel(){ 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(){ if(car.curSpeed < BRK_STEP){ car.curSpeed = 0; return; } car.curSpeed -= BRK_STEP; } }; int main(void){ Car run99 = {"run99", 100, 0}; Accel(run99); Accel(run99); ShowCarState(run99); Break(run99); Car sped77 = {"sped77", 100, 0}; Accel(sped77); Break(sped77); ShowCarState(sped77); return 0; }
구조체 안에 삽입된 함수의 정의에는 매개변수에 대한 정보가 없다. 괄호가 텅 비어있음. 함수가 구조체 안에 있으면 구조체 내에 선언된 변수에 직접 접근이 가능해졌기 때문이다.
5. 구조체 안에 enum 상수의 선언
예제를 보면, 다음의 매크로 상수들이 존재한다.
#define ID_LEN 20 #define MAX_SPD 200 #define FUEL_STEP 2 #define ACC_STEP 10 #define BRK_STEP 10
그런데 이들 상수 역시 구조체 Car에게만 의미가 있는 상수들이다. 즉, 다른 영역에 사용하도록 정의된 상수가 아니니, 이들도 구조체 내에 포함시키는 것이 좋을 수 있다. 이럴 때에는 열거형 enum을 사용하여 다음과 같이 구조체 내에서만 유효한 상수를 정의하면 된다.
struct Car{ enum{ #define ID_LEN 20 #define MAX_SPD 200 #define FUEL_STEP 2 #define ACC_STEP 10 #define BRK_STEP 10 }; ... };
enum의 선언을 구조체 내에 넣는 게 부담스럽다면, 이름공간을 사용해서 상수가 사용되는 영역을 명시하는 것도 방법이 될 수 있다. 그리고 이렇게 이름 공간을 이용하면, 몇몇 구조체들 사이에서만 사용하는 상수들을 선언할 때 특히 도움이 되며, 위에서 보인 방법보다 가독성도 좋아지는 경향이 있다.
#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<<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(void){ Car run99 = {"run99", 100, 0}; Accel(run99); Accel(run99); ShowCarState(run99); Break(run99); Car sped77 = {"sped77", 100, 0}; Accel(sped77); Break(sped77); ShowCarState(sped77); return 0; }
CAR_CONST 이름공간 안에 구조체 Car에서 사용하는 상수들을 모아 놓았다.
ID_LEN과 같은 상수를 이용하기 위해 CAR_CONST::ID_LEN으로 표현한다. 이 문장만 봐도 이 상수가 어느 영역에서 선언되고 있는지 알 수 있다.
6. 함수는 외부로 뺄 수 있다.
하뭇가 포함돼 있는 C++의 구조체를 보는 순간, 다음의 정보들이 쉽게 눈에 들어와야 코드의 분석이 용이하다.
- 선언돼 있는 변수 정보
- 정의돼 있는 함수 정보
보통 프로그램을 분석할 때 골격 위주로 분석하는 경우가 많다. 그리고 이러한 경우에는 함수의 기능만 파악을 하지, 함수의 세부 구현까지 신경을 쓰지는 않는다. 구조체를 보는 순간, 정의돼 있는 함수의 종류와 기능이 한눈에 들어오게끔 코드를 작성하는 것이 좋다. 따라서 구조체 내에 정의된 함수의 수가 많거나 그 길이가 길다면, 다음과 같이 구조체 밖으로 함수를 빼낼 필요가 있다. 이럴 때엔 함수의 프로토타입을 구조체 안에 두고, 함수의 정의를 구조체 밖으로 빼내는 것이다. 다만, 빼낸 다음에 해당 함수가 어디에 정의돼 있는지에 대한 정보만 추가해주면 된다.
#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<<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}; Accel(run99); Accel(run99); ShowCarState(run99); Break(run99); Car sped77 = {"sped77", 100, 0}; Accel(sped77); Break(sped77); ShowCarState(sped77); return 0; }
03-2. class와 object
지금까지 설명한 c++의 구조체는 클래스의 일종이다. 그렇다면 클래스와 구조체에는 어떤 차이점이 있을까?
1. 클래스와 구조체의 유일한 차이점
키워드 struct를 대신해서 class를 사용하면, 구조체가 아닌 클래스가 된다. 즉, 아래의 코드는 클래스의 정의이다.
class Car{ char gamerID[CART_CONST::ID_LEN]; int fuelGauge; int curSpeed; void ShowCarState(){...} void Accel(){...} void Break(){...} };
그런데 이렇게 키워드를 바꿔놓으면 앞서 예제에서 보였던 다음의 방식으로 변수(구조체 변수)를 선언하지 못한다.
Car run99 = {"run99", 100, 0};
그렇다면 어떻게 클래스를 초기화해야할까? 클래스 내에 선언된 변수는 기본적으로 클래스 내에 선언된 함수에서만 접근이 가능하다. 그러나 클래스는 멤버의 접근과 관련해서 다음과 같이 이야기한다.
" 접근과 관련해서 별도의 선언을 하지 않으면, 클래스 내에 선언된 변수 및 함수에 대한 접근은 허용하지 않을 테니, 접근과 관련되 지시를 별도로 내려줘 "
이렇듯 클래스는 정의를 하는 과정에서 각각의 변수 및 함수의 접근 허용범위를 별도로 선언해야 한다.
public , protected , private
접근제어 지시자들 각각의 의미는 다음과 같다.
- public : 어디서든 접근 허용
- protected : 상속관계에 놓여있을 때, 유도 클래스에서의 접근 허용
- private : 클래스 내(클래스 내에 정의된 함수)에서만 접근 허용
이 중에서 protected는 '상속'과 관련이 있으므로 나중에 살펴보기로 하고, 여기서는 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{ 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::InitMember(char *ID, int fuel){ strcpy(gamerID, ID); fuelGauge=fuel; curSpeed = 0; } void Car::ShowCarState(){ cout<<"소유자 ID :"<<gamerID<<endl; cout<<"fuel : "<<fuelGauge<<endl; cout<<"current Speed "<<curSpeed<<endl; } void Car::Accel(){ if(fuelGauge <= 0){ return; } else{ fuelGauge -=CAR_CONST::FUEL_STEP; } if(curSpeed+CAT_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.InitMembers("run99",100); run99.Accel(); run99.Break(); return 0; }
예제를 통해 알 수 있는 사실은 다음과 같다.
- 접근제어 지시자 A가 선언되면, 그 이후에 등장하는 변수나 함수는 A에 해당하는 범위 내에서 접근이 가능하다.
- 그러나 새로운 접근제어 지시자 B가 선언되면, 그 이후로 등장하는 변수나 함수는 B에 해당하는 범위 내에서 접근이 가능하다.
- 함수의 정의를 클래스 밖으로 빼도, 이는 클래스의 일부이기 때문에, 함수 내에서는 private으로 선언된 변수에 접근이 가능하다.
- 키워드 struct를 이용해서 정의한 구조체(클래스)에 선언된 변수와 함수에 별도의 접근제어 지시자를 선언하지 않으면, 모든 변수와 함수는 public으로 선언된다.
- 키워드 class를 사용해서 정의한 클래스에 선언된 변수와 함수에 별도의 접근제어 지시자를 선언하지 않으면, 모든 변수와 함수는 private으로 선언된다.
그리고 알아 둘 점은, 구조체도 클래스도 둘 다 접근제어 지시자의 선언이 가능한 것이다. 다만 접근제어 지시자를 선언하지 않았을 때 클래스는 private로, 구조체는 public으로 선언할 뿐이다.
2. 용어정리: 객체(Object), 멤버 변수, 멤버 함수
구조체 변수, 클래스 변수라는 표현은 이제 어울리지 않는다. 구조체와 클래스는 변수의 성격만 지니는 것이 아니기 때문이다. 그래서 객체(object)라고 부른다.
그리고 클래스를 구성하는 (클래스 내부에서 선언된) 변수를 가리켜 '멤버 변수'라고 하고, 클래스를 구성하는 (클래스 내부에서 선언된) 함수를 가리켜 '멤버 함수'라고 한다.
3. c++에서의 파일 분할
어떤 프로그램이건 하나의 파일에 모든 것을 담지 않느낟. 특히 c++은 클래스 별로 헤더파일과 소스 파일을 생성해서 클래스의 선언과 정의를 분리한다. 그럼 이어서 클래스를 대상으로, 파일을 나누는 기준을 설명한다.
클래스 Car를 대상으로 파일을 나눌 때에는 보통 다음과 같이 파일을 구분한다.
- 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와 관련된 문장의 옳고 그름을 판단하는 데에 사용된다. 예를 들면 아래의 코드를 컴파일하는데 있어서 위의 정보는 반드시 필요하다.
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.h]
#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<<"gamerID: "<<gamerID<<endl; cout<<"fuel : "<<felGauge<<endl; cout<<"curSpeed: "<<curSpeed<<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; }
CAR_CONST에 접근하기 위해 맨 위에 Car.h 헤더파일을 포함하였다.
[RacingMain.cpp]
#include "Car.h" int main(void){ Car run99; run99.InitMembers("run99", 100); run99.Accel(); run99.ShowCarState(); retun 0; }
4. 인라인 함수는 헤더파일에 함께 넣어야 해요.
파일 분할 예제에서 Car.cpp에 정의된 함수 ShowCarState와 Break를 다음과 같이 인라인화 한 다음에, 그대로 Car.cpp에 넣으면 컴파일 에러가 발생한다.
inline void Car::ShowCarState(){ cout<<"ID"<<gamerID<<endl; cout<<"연료량: "<<fuelGauge<<"%"<<endl; cout<<"현재 속도: "<<curSpeed<<"km/s"<<endl; } inline void Car::Break(){ if(curSpeed<<CAR_CONST::BRK_STEP){ curSpeed=0; return; } curSpeed -= CAR_CONST::BRK_STEP; }
혹시 컴파일 에러가 발생하는 이유를 알겠는가? 인라인 함수의 특징을 잘 생각해보면 그 이유를 알 수 있는데, 이유는 다음과 같다.
" 컴파일 과정에서 함수의 호출 문이 있는 곳에 함수의 몸체 부분이 삽입돼야 하므로"
예를 들어서 다음의 main함수를 컴파일 한다고 가정해 보자.
int main(void){ Car run99; run99.InitMembers("run99",100); run99.Accel(); run99.Break(); }
이 때 Break 함수가 인라인 함수가 아니라면, Break 함수가 Car클래스의 멤버함수인지만 확인을 하고 컴파일은 완료가 된다. 그러나 Break 함수가 인라인 함수이기 때문에, Break 함수의 호출문장은 컴파일러에 의해서 Break 함수의 몸체로 대체돼야 한다. 때문에 인라인 함수는 클래스의 선언과 동일한 파일에 저장돼서 컴파일러가 동시에 참조할 수 있게 해야 한다. 아래와 같이 말이다.
[CarInline.h]
#ifndef __CARINLINE_H__ #define __CARINLINE_H__ #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(); }; inline void Car::ShowCarState(){ cout<<"소유자ID "<<gamerID<<endl; cout<<"연료량 "<<fuelGauge<<"%"<<endl; cout<<"현재 속도"<<curSpeed<"km/s"<<endl; } inline void Car::Break(){ if(curSpeed< CAR_CONST::BRK_STEP){ curSpeed = 0; return; } curSpeed -= CAR_CONST::BRK_STEP; } #endif
[CarInline.cpp]
#include <cstring> #include "CarInline.h" using namespace std; void Car::InitMembers(char * ID, int fuel){ strcpy(gamerID, ID); fuelGauge=fuel; curSpeed=0; } 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; }
컴파일러는 파일 단위로 컴파일을 한다. 즉, A.cpp와 B.cpp를 동시에 컴파일해서 하나의 실행파일을 만든다 해도, A.cpp의 컴파일 과정에서 B.cpp를 참조하지 않으며, B.cpp의 컴파일 과정에서 A.cpp를 참조하지 않는다. 그래서 클래스의 선언과 인라인 함수의 정의를 함께 묶어둔 것이다.
03-3 객체지향 프로그래밍의 이해
지금까지는 c언어에서 c++로 자연스럽게 이동할 수 있도록, 구조체를 시작으로 클래스를 설명하였다. 그런데 이번에는 객체지향의 관점에서 클래스를 전혀 다른 방법으로, 다시 한번 설명하고자 한다. 구조체를 확장한 것이 클래스라고 인식하는 것 자체는 문제가 되지 않으나, 그것이 전부라고 인식하는 것은 문제가 있기 때문이다.
1. 객체지향 프로그래밍의 이해
c++은 객체지향 언어이다. 따라서 객체지향에 대한 이해가 필요한데, 이를 위해서 필자는 객체지향의 우월성을 강조할 것이다. 그러나 이것이 c언어와 같은 절차지향적 언어보다 모든 면에서 우월함을 뜻하는 것은 아니다. 물론 절차지향도 그 나름의 장점이 있다.
객체는 영어로 Object이다. 그리고 이것의 사전적 의미는 다음과 같다.
"사물, 또는 대상"
즉 Object는 우리 주변의 존재하는 물건이나 대상 전부를 의미한다. 그렇다면 객체를 지향하는 프로그래밍이라는 것은 무엇일까?
예를 들어 다음 상황을 프로그래밍한다고 가정해보자.
"나는 과일 장수에게 사과 두 개를 구매했다."
이 문장에 삽입돼 있는 객체의 종류는 다음과 같다. : 나, 과일장수, 사과
그렇다면 나라는 객체는 과일장수라는 객체로부터 과일 객체를 구매하는 액션을 취할 수 있어야 한다. 객체지향 프로그래밍은 현실에 존재하는 사물과 대상, 그에 따른 액션을 그대로 실체화 시키는 형태의 프로그래밍이다.
2. 객체를 이루는 것은 데이터와 기능입니다.
프로그램 상에 과일장수 객체가 존재한다고 가정해 보자. 이 객체는 무엇으로 이뤄져야 하겠는가? 물론 과일장수는 한 가정의 아버지이며, 토요일마다 축구를 하는 사람일 수도 있다. 그러나 프로그램 상에서 바라보는 과일장수의 관점은 '과일의 판매'에 있다. 따라서 프로그램 상에서 바라보는 과일 장수는 다음과 같은 형태이다.
- 과일장수는 과일을 팝니다.
- 과일장수는 사과 20개, 오렌지 10개를 보유하고 있습니다.
- 과일장수의 과일 판매 수익은 현재까지 50,000원입니다.이 중에서 첫 번째는 과일장수의 행동을 의미한다. 그리고 두 번째와 세 번째는 과일장수의 상태를 의미한다. 이처럼 객체는 하나 이상의 상태와 행동으로 구성이 되며, 상태는 변수, 행동은 함수로 표현된다.
- int numOfApples
- int myMoneyint SaleApples(int money){
int num = money/1000; //사과 한 개 당 1000원이라고 할 때
numOfApples -= num; //사과의 수가 줄어들고
myMoney += money; //판매 수익이 생긴다
return num;
}3. 클래스 기반의 두 가지 객체 생성 방법
이제 우리가 해야 할 일은 앞서 정의한 클래스를 실체화 시키는 것이다. 즉, 객체화시키는 것이다. 다음은 c++에서 정의하고 있는 두 가지 객체 생성 방법이다.
ClassName onjName; //일반적인 변수의 선언 방식 ClassName * ptrObj = new ClassName; // 동적 할당 방식(힙 할당)
즉, FruitSeller와 FruitBuyer 클래스의 객체 생성 방식은 다음과 같다.
FruitSeller seller; FruitBuyer buyer; FruitSeller * objPtr1 = new FruitSeller; FruitBuyer * objPtr2 = new FruitBuyer;
4. 사과장수 시뮬레이션
이제 예제를 완성할 차례이다. 이 예제가 특히 의미를 갖는 이유는 두 객체가 서로 대화를 하기 때문이다. 그럼 객체는 어떻게 대화를 주고 받는지 예제를 통해서 확인해 보자.
[FruitSaleSim.cpp]
#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; } }; class FruitBuyer{ int myMoney; //클래스 안에서 생성된 변수는 기본적으로 private int numOfApples; public : void InitMembers(int money){ myMoney = money; numOfApples=0; } void BuyApples(FruitSeller &seller, int moeny){ numOfApples+=seller.SaleApples(money); myMoney -= money; } void ShowBuyResult(){ cout<<"현재 잔액 :"<<myMoney<<endl; cout<<"사과 개수 :"<<numOfApples<<endl; } }; int main(void){ FruitSeller seller; seller.InitMembers(1000,20,0); FruitBuyer buyer; buyer.InitMembers(5000); buyer.BuyApples(seller, 2000); }
04-1 정보 은닉 (Information Hiding)
우리는 객체의 생성을 목적으로 클래스를 디자인한다. 그렇다면 좋은 클래스가 되기 위한 조건으로 어떤 것들이 있을까? 여기에는 '정보은닉'과 '캡슐화'가 있다. 이는 좋은 클래스가 되기 위한 최소한의 조건이다.
1. 정보은닉의 이해
제한된 방법으로의 접근만 허용을 해서 잘못된 값이 저장되지 않도록 도와야 하고, 또 실수를 했을 때 실수가 쉽게 발견되도록 해야 한다.
[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<<"Out of the range"<<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(xpos > 100 || xpos <0){ cout<<"Out of the x range"<<endl; return false; } x = xpos; return true; } bool Point::SetY(int ypos){ if(ypos < 0 || ypos > 100){ cout<<"Out of the Y range"<<endl; return false; } y = ypos; return true; }
먼저, 멤버변수에 값을 저장하는 함수 InitMembers, SetX, SetY는 0 이상 100 이하의 값이 전달되지 않으면, 에러 메시지를 출력하면서 값의 저장을 허용하지 않는 형태로 정의되었다. 따라서 잘못된 값이 저장되지 않을 뿐더러, 값이 잘못 전달되는 경우 출력된 메시지를 통해서 문제가 있음을 확인할 수 있다.
" 멤버변수를 private으로 선언하고, 해당 변수에 접근하는 함수를 별도로 정의해서, 안전한 형태로 멤버 변수에 접근을 유도하는 것이 '정보은닉'이다. 이는 좋은 클래스가 되기 위한 기본 조건이다."
Get함수는 '엑세스 함수(access function)'라 하는데, 멤버변수를 private으로 선언하면서 클래스 외부에서의 멤버변수 접근을 할 수 있게 하는 함수들이다.
또, const로 함수를 선언하면 멤버변수에 저장된 값을 변경하지 않겠다는 선언이다. 또한, const 함수 안에서는 const가 아닌 함수의 호출이 제한된다. const로 선언되지 않은 함수는 아무리 멤버변수에 저장된 값을 변경하지 않더라도, 변경할 수 있는 가능성이 있다. 따라서 이러한 함수의 호출을 아예 허용하지 않는 것이다.
반응형'a' 카테고리의 다른 글
AES on GPGPU-Sim (0) 2022.04.04 [JS] Node.js 란 (0) 2022.04.04 AES 암호화 과정 정리 (0) 2022.04.03 [c++] (0) 2022.04.01 [알고리즘] AES 암호화 알고리즘 (0) 2022.04.01