read

열번째 열혈강의


sea

벌써 열번째 포스팅에 들어섰다. 내생각엔 한 40번째는 가야 끝날꺼같긴 한데… 열심히 공부해보자!


클래스와 배열 그리고 this 포인터


앞선 포스팅에서도 언급 했듯이 이번에는 배열과 this 포인터에 대한 얘기를 할 것이다.

아마 C언어를 할 때 구조체의 배열에 대해 다루어 본 적이 있을 것이다. 또한 구조체 포인터 배열 역시 다뤄보았을 것이다. 클래스도 이와 유사하다.

객체기반의 배열은 다음과 같이 선언한다.

SoSimple arr[10];

이를 동적으로 할당해 보면

SoSimple *ptrArr = SoSimple[10];

과 같은 모습이다. 이렇게 배열을 형성하면 10개의 SoSimple 객체가 모여서 배열이 된다. 구조체와 크게 다를바 없는 모습이다. 하지만 배열을 선언하는 경우에도 생성자는 호출이 된다. 단, 배열의 선언과정에서는 호출할 생성자를 별도로 명시하진 못한다.즉, 파라미터를 전달하지 못한다. 따라서 반드시 생성자의 형태가 다음과 같은 것이 정의가 되어 있어야 한다.

SoSimple(){...}

그리고 배열선언 이후에 각각의 요소를 원하는 값으로 초기화 하려면 일일이 초기화를 해주어야 한다. 그럼 다음을 보자.

class Person {
private:
	char* name;
	int age;
public:
	Person(char* myname, int myage) {
		int len = strlen(myname) + 1;
		name = new char[len];
		strcpy(name, myname);
		age = myage;
	}
	Person() {
		name = NULL;
		age = 0;
		cout << "call Person()" << endl;
	}
	void SetPersonInfo(char* myname, int myage) {
		name = myname;
		age = myage;
	}
	void ShowPersonInfo() const {
		cout << "이름 : " << name << ", ";
		cout << "나이 : " << age << endl;
	}
	~Person() {
		delete[] name;
		cout << "called destructor!" << endl;
	}
};

int main() {
	Person parr[3];
	char namestr[100];
	char* strptr;
	int age;
	int len;

	for (int i = 0; i < 3; i++) {
		cout << "이름 : ";
		cin >> namestr;
		cout << "나이 : ";
		cin >> age;
		len = strlen(namestr) + 1;
		strptr = new char[len];
		strcpy(strptr, namestr);
		parr[i].SetPersonInfo(strptr, age);
	}
	parr[0].ShowPersonInfo();
	parr[1].ShowPersonInfo();
	parr[2].ShowPersonInfo();
	return 0;
}

다음과 같이 객체 배열을 생성하면 void 형 생성자가 호출됨을 확인 할 수 있다. 또한 소멸자도 출력이 된다.

그럼 다음으로 객체 포인터 배열에 대해 알아보자.

객체 배열이 객체로 이루어진 배열이라면, 객체 포인터 배열은 객체의 주소 값 저장이 가능한 포인터 변수로 이루어진 배열이다. 그럼 다음 코드를 한번 보도록 하자.

class Person {
private:
	char* name;
	int age;
public:
	Person(char* myname, int myage) {
		int len = strlen(myname) + 1;
		name = new char[len];
		strcpy(name, myname);
		age = myage;
	}
	Person() {
		name = NULL;
		age = 0;
		cout << "call Person()" << endl;
	}
	void SetPersonInfo(char* myname, int myage) {
		name = myname;
		age = myage;
	}
	void ShowPersonInfo() const {
		cout << "이름 : " << name << ", ";
		cout << "나이 : " << age << endl;
	}
	~Person() {
		delete[] name;
		cout << "called destructor!" << endl;
	}
};

int main() {
	Person* parr[3];
	char namestr[100];
	char* strptr;
	int age;
	int len;

	for (int i = 0; i < 3; i++) {
		cout << "이름 : ";
		cin >> namestr;
		cout << "나이 : ";
		cin >> age;
		len = strlen(namestr) + 1;
		strptr = new char[len];
		strcpy(strptr, namestr);
		parr[i] = new Person(namestr, age);
	}
	parr[0]->ShowPersonInfo();
	parr[1]->ShowPersonInfo();
	parr[2]->ShowPersonInfo();
	delete parr[0];
	delete parr[1];
	delete parr[2];

	return 0;
}

위 객체배열과 출력은 같으나 main에서의 소스가 조금 바뀌었다. 즉, 저장의 대상을 객체로 하느냐, 객체의 주소 값으로 하느냐를 결정해야 한다. 따라서 차이점을 한번 잘 이해하길 바란다.


this 포인터의 이해


멤버함수 내에서는 this라는 이름의 포인터를 사용할 수 있는데, 이는 객체 자신을 가리키는 용도로 사용되는 포인터다.

간단한 예제로 알아보자.

class SoSimple {
private: 
	int num;
public:
	SoSimple(int n) : num(n) {
		cout << "num = " << num << ", ";
		cout << "address = " << this << endl;
	}
	void ShowSimpleData() {
		cout << num << endl;
	}
	SoSimple* GetThisPointer() {
		return this;
	}
};

int main() {
	SoSimple sim1(100);
	SoSimple* ptr1 = sim1.GetThisPointer();
	cout << ptr1 << endl;
	ptr1->ShowSimpleData();

	SoSimple sim2(200);
	SoSimple* ptr2 = sim2.GetThisPointer();
	cout << ptr2 << ", ";
	ptr2->ShowSimpleData();
	return 0;
}

소스코드와 실행결과를 통해서 this는 객체자신의 주소를 의미한다는 사실을 보았을 것이다. 이렇게 this 포인터는 그 주소값과 자료형이 정해져 있지 않은 포인터이다.

이런 this 포인터는 꾀나 유용하게 쓰이는데, 다음 클래스를 관찰해보자.

class ThisClass {
private:
	int num;
public:
	void ThisFunc(int num) {
		this->num = 207;
		num = 105;
	}
	...
};

위 클래스에서 ThisFunc 함수의 파라미터 값은 num이다. 그런데 이 이름은 멤버변수의 이름과 동일 하므로, 함수 내에서의 num은 파라미터 num을 의미한다. 따라서 변수의 이름만 참조하는 방법으로는 thisFunc함수 내에서 멤버변수 num에 접근이 불가능하다. 그러나 this를 활용하면 가능하다.

this->num = 207;

이것이 this 참조문이다. 위 문장에서 num은 파라미터 num이 아니라 멤버변수 num 이 된다.

이것을 활용한 예제를 보자.

class TwoNumber {
private:
	int num1;
	int num2;
public:
	TwoNumber(int num1, int num2) {
		this->num1 = num1;
		this->num2 = num2;
	}
	/*
	TwoNumber(int num1, int num2) : num1(num1), num2(num2){
	//empty
	}
	*/
	void ShowTwoNumber() {
		cout << this->num1 << endl;
		cout << this->num2 << endl;
	}
};

int main() {
	TwoNumber two(2, 4);
	two.ShowTwoNumber();
	return 0;
}

위 코드를 알겠는가? 여기서 멤버 이니셜라이져에서는 this 포인터를 쓸 수 없다. 그러나 위 해당 문장을 가능한 문장이다.

위처럼 변수를 따로 지정하지 않고 this를 이용하는 것도 한가지 방법이다.

그럼 여기서 한단계 더 나가보자.

Self-Reference 라는 것이 있다.

이것은 객체 자신을 참조할 수 있는 참조자를 의미한다.

this 포인터를 이용해서, 객체가 자신의 참조에 사용할 수 있는 참조자의 반환문을 구성할 수 있다. 그럼 다음 예제를 한번 보도록 하자.

class SelfRef {
private:
	int num;
public:
	SelfRef(int n) : num(n) {
		cout << "객체생성" << endl;
	}
	SelfRef& Adder(int n) {
		num += n;
		return *this;
	}
	SelfRef& ShowTwoNumber() {
		cout << num << endl;
		return *this;
	}
};

int main() {
	SelfRef obj(3);
	SelfRef &ref = obj.Adder(2);

	obj.ShowTwoNumber();
	ref.ShowTwoNumber();

	ref.Adder(1).ShowTwoNumber().Adder(2).ShowTwoNumber();
	return 0;
}

여기서 Adder 함수를 보자. 반환의 내용은 *this 인데 이는 이 문장을 실행하는 객체 자신의 포인터가 아닌 객체 자신을 반환하겠다는 의미다. 그런데 반환형이 참조형 SelfRef&로 선언되었다. 따라서 객체 자신을 참조할 수 있는 참조의 정보(참조값)가 반환된다.

아마 여기서 참조의 정보(참조값)이 이해가 안될 수 있다. 그럼 다음 예시를 보자.

int main(void){
	int num = 7;
    int &ref = num;
}

위 코드는 예전에 배웠듯이 변수 num을 참조자 ref가 참조한다. 그러면 대입연산 과정에서 참조자 ref에 무엇이 전달되는 것일까? 확실한 것은 num에 저장된 정수 값은 아니다. 그래서 대입연산에 어울리게 다음과 같이 표현하기도 한다.

변수 num을 참조할 수 있는 참조의 정보가 전달된다.

즉, 다음과 같이 이해하는 것이 자연스러울 수 있다.

변수 num을 참조 할 수 있는 참조 값이 참조자 ref에 전달되어, ref가 변수 num을 참조하게 된다.

이제 참조 값의 의미를 조금 쉽게 이해할 수 있을 것이다. 대입 연산자의 왼편에 참조자의 선언이 오거나 반환형으로 참조형이 선언된다면 그 때 전달되는 정보를 표현하기 위해 참조의 정보 또는 참조 값이라는 표현을 사용하는 것이다.


OOP 단계별 프로젝트 2단계


light

이제는 두번째 OOP 프로젝트를 시작해 볼 때가 되었다.

처음에는 프로젝트의 틀을 구성하는 것이 목적이였고 이제 진짜 시작인 셈이다.

이제 우리는 클래스의 기본적인 내용을 공부했다. 자잘한 문법적 요소에 신경쓰기 보다 무엇을 클래스로 만들것이고, 어떻게 클래스를 정의할 것인지에 더 신경써야 한다. C++ 문법의 80%만 이해해도 실무 프로젝트는 크게 무리없이 진행할 수 있다. 혼자서 프로젝트를 진행한다면 더욱 그렇다. 그러나 클래스를 제대로 못만들면 아무리 많이 안들 쓸모가 없다.

그럼 프로그램을 설명 하겠다.

Banking System 의 버전을 0.1에서 0.2로 올려보자. 아직은 클래스를 설계하는 게 익숙하지 않으니 버전 0.1에서 정의한 구조체 Account를 클래스 Account로 변경해 보겠다. 이는 struct를 class로 바꿔란 말이 아니라 다음과 같은 내용을 고민해 보라는 것이다.

어떻게 캡슐화를 시키고 정보를 은닉할까?

생성자와 소멸자는 어떻게 정의할까?

추가로 2가지만 제약을 걸겠다. 버전 0.1의 Account 구조체는 char형 배열을 멤버로 둬서 고객의 이름을 저장했는데, 버전 0.2의 Account 클래스는 이를 동적할당으로 구현하자. 즉 멤버변수로 char 포인터를 지니고 있어야 한다.

또 한가지는 객체를 저장하는 배열에 관한 것이다. 객체 배열을 선언하지 말고 객체 포인터 배열을 선언해서 저장하도록 하자.

꼭 직접해보고 다음 코드를 보길 바란다.

무엇보다 본인이 짠 코드와 다음 코드를 비교해보자. 다소 차이가 있을 수 있는 데 특히 캡슐화를 어떻게 했느냐에 따라 큰 차이를 보인다.

/*
 * Banking System Ver 0.2
 * 작성자: 윤성우
 * 내  용: Account 클래스 정의, 객체 포인터 배열 적용
 */

#include <iostream>
#include <cstring>

using namespace std;
const int NAME_LEN=20;

void ShowMenu(void);       // 메뉴출력
void MakeAccount(void);    // 계좌개설을 위한 함수
void DepositMoney(void);       //     
void WithdrawMoney(void);      //     
void ShowAllAccInfo(void);     // 잔액조회

enum {MAKE=1, DEPOSIT, WITHDRAW, INQUIRE, EXIT};

class Account
{
private:
	int accID;      // 계좌번호
	int balance;    //     
	char * cusName;   // 고객이름

public:
	Account(int ID, int money, char * name)
		: accID(ID), balance(money)
	{
		cusName=new char[strlen(name)+1];
		strcpy(cusName, name);
	}

	int GetAccID() { return accID; }

	void Deposit(int money)
	{
		balance+=money;
	}

	int Withdraw(int money)    // 출금액 반환, 부족 시 0
	{
		if(balance<money)
			return 0;
		
		balance-=money;
		return money;
	}

	void ShowAccInfo()
	{
		cout<<"계좌ID: "<<accID<<endl;
		cout<<"이  름: "<<cusName<<endl;
		cout<<"잔  액: "<<balance<<endl;
	}

	~Account()
	{
		delete []cusName;
	}	
};

Account * accArr[100];   // Account 저장을 위한 배열
int accNum=0;        // 저장된 Account 

int main(void)
{
	int choice;
	
	while(1)
	{
		ShowMenu();
		cout<<"선택: "; 
		cin>>choice;
		cout<<endl;
		
		switch(choice)
		{
		case MAKE:
			MakeAccount(); 
			break;
		case DEPOSIT:
			DepositMoney(); 
			break;
		case WITHDRAW: 
			WithdrawMoney(); 
			break;
		case INQUIRE:
			ShowAllAccInfo(); 
			break;
		case EXIT:
			return 0;
		default:
			cout<<"Illegal selection.."<<endl;
		}
	}

	for(int i=0; i<accNum; i++)
		delete accArr[i];
	return 0;
}

void ShowMenu(void)
{
	cout<<"-----Menu------"<<endl;
	cout<<"1. 계좌개설"<<endl;
	cout<<"2. 입    금"<<endl;
	cout<<"3. 출    금"<<endl;
	cout<<"4. 계좌정보 전체 출력"<<endl;
	cout<<"5. 프로그램 종료"<<endl;
}

void MakeAccount(void) 
{
	int id;
	char name[NAME_LEN];
	int balance;
	
	cout<<"[계좌개설]"<<endl;
	cout<<"계좌ID: ";	cin>>id;
	cout<<"이  름: ";	cin>>name;
	cout<<"입금액: ";	cin>>balance;
	cout<<endl;

	accArr[accNum++]=new Account(id, balance, name);
}

void DepositMoney(void)
{
	int money;
	int id;
	cout<<"[입    금]"<<endl;
	cout<<"계좌ID: ";	cin>>id;
	cout<<"입금액: ";	cin>>money;
	
	for(int i=0; i<accNum; i++)
	{
		if(accArr[i]->GetAccID()==id)
		{
			accArr[i]->Deposit(money);
			cout<<"입금완료"<<endl<<endl;
			return;
		}
	}
	cout<<"유효하지 않은 ID 입니다."<<endl<<endl;
}

void WithdrawMoney(void)
{
	int money;
	int id;
	cout<<"[출    금]"<<endl;
	cout<<"계좌ID: ";	cin>>id;
	cout<<"출금액: ";	cin>>money;
	
	for(int i=0; i<accNum; i++)
	{
		if(accArr[i]->GetAccID()==id)
		{
			if(accArr[i]->Withdraw(money)==0)
			{
				cout<<"잔액부족"<<endl<<endl;
				return;
			}

			cout<<"출금완료"<<endl<<endl;
			return;
		}
	}
	cout<<"유효하지 않은 ID 입니다."<<endl<<endl;
}

void ShowAllAccInfo(void)	
{
	for(int i=0; i<accNum; i++)
	{
		accArr[i]->ShowAccInfo();
		cout<<endl;
	}
}

이 다음 부터는 다음 포스팅에 하겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

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

Back to Overview