read

아홉번째 열혈강의


ground

이어서 계속 진도를 나가보자. 약 640페이지 정도 되는 양에 이제 170페이지를 넘겼다. 갈 길이 멀다.


생성자의 활용


우리는 이전에 생성자가 무엇인지 알았다. 그럼 이를 응용해 보자.

#include <iostream>

using namespace std;

class FruitSeller {
private:
	int APPLE_PRICE;
	int numOfApples;
	int myMoney;
public:
	FruitSeller(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() const {
		cout << "남은 사과 : " << numOfApples << endl;
		cout << "판매 수익 : " << myMoney << endl << endl;
	}
};

class FruitBuyer {
private:
	int myMoney;
	int numOfApples;
public:
	FruitBuyer(int money) {
		myMoney = money;
		numOfApples = 0;
	}

	void BuyApples(FruitSeller &seller, int money) {
		numOfApples += seller.SaleApples(money);
		myMoney -= money;
	}

	void ShowBuyResult() const {
		cout << "현재 잔액 : " << myMoney << endl;
		cout << "사과 개수 : " << numOfApples << endl << endl;
	}
};

int main() {
	FruitSeller seller(1000, 20, 0);
	FruitBuyer buyer(5000);
	buyer.BuyApples(seller, 2000);

	cout << "과일 판매자의 현황" << endl;
	seller.ShowSalesResult();
	cout << "과일 구매자의 현황" << endl;
	buyer.ShowBuyResult();
	return 0;
}

그럼 생성자를 활용한 클래스가 완성되었다.


member Initializer


그럼 다음 내용을 한번 보도록 하자.

Point 클래스를 만들고, 거기에 따른 Rectangle 클래스를 만들어 보겠다.

class Point {
private:
	int x;
	int y;
public:
	Point(const int &xpos, const int &ypos);
	int GetX() const;
	int getY() const;
	bool SetX(int xpos);
	bool setY(int ypos);
};
Point::Point(const int &xpos, const int &ypos) {
	x = xpos;
	y = ypos;
}

다음과 같이 Point 클래스를 만들었다.

하지만 Rectangle 클래스의 생성자 정의는 조금 생각해 보아야 한다. Rectangle 클래스는 두 개의 Point 객체를 멤버로 지니고 있어서, Rectangle 객체가 생성되면 이 Point 객체가 함께 생성된다.

그럼 다음과 같이 생각할 수 있지 않을까?

Rectangle 객체를 생성하는 과정에서 Point 클래스의 생성자를 통해 Point 객체를 초기화 할 수 있지 않을까?

생성자는 멤버변수의 초기화를 목적으로 정의가 되니, 객체 생성과정에서의 생성자 호출은 객체의 초기화를 수월하게 해준다. 여기서는 멤버 이니셜라이져 (member initializer) 개념이 나온다.

class Rectangle {
private:
	Point upLeft;
	Point lowRight;
public:
	Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
	void ShowRecInfo() const;
};

Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2) :upLeft(x1,y1), lowRight(x2,y2) {
	//empty
}

여기서 :upLeft(x1,y1), lowRight(x2,y2) 이 부분이 멤버 이니셜라이져다.

이것의 의미는 다음과 같다.

객체 upLeft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라 객체 lowRight의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라

이렇게 완성될 수 있고 완성본을 보도록 하자.

Point.h

#ifndef __POINT_H_
#define __POINT_H_

class Point {
private:
	int x;
	int y;

public:
	Point(const int &xpos, const int &ypos);
	int GetX() const;
	int GetY() const;
	bool SetX(int xpos);
	bool SetY(int ypos);
};

#endif
Rectangle.h

#ifndef __RECTANGLE_H_
#define __RECTANGLE_H_

#include "Point.h"

class Rectangle {
private:
	Point upLeft;
	Point lowRight;

public:
	Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
	void ShowRecInfo() const;
};

#endif
Point.cpp

#include <iostream>
#include "Point.h"

using namespace std;

Point::Point(const int &xpos, const int &ypos) {
	x = xpos;
	y = ypos;
}

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;
}
Rectangle.cpp

#include <iostream>
#include "Rectangle.h"

using namespace std;

Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2) :upLeft(x1, y1), lowRight(x2, y2) {
//empty
}

void Rectangle::ShowRecInfo() const {
	cout << "좌 상단 : " << "[" << upLeft.GetX() << ", ";
	cout << upLeft.GetY() << "]" << endl;
	cout << "우 하단 : " << "[" << lowRight.GetX() << ", ";
	cout << lowRight.GetY() << "]" << endl;
}
RectangleConstructor.cpp

#include <iostream>
#include "Point.h"
#include "Rectangle.h"

using namespace std;

int main() {
	Rectangle rec(1, 1, 5, 5);
	rec.ShowRecInfo();
	return 0;

}

이제 제법 클래스 다운 모습이 되었다.

참고로 멤버 이니셜라이저를 사용하다 보면, 생성자 몸체에 비는 일이 빈번하니, 이상하게 여길 필요 없다.

마지막으로 우리는 객체의 생성과정을 다음과 같이 정리할 수 있다.

1단계 : 메모리 공간의 할당

2단계 : 이니셜라이저를 이용한 멤버변수(객체)의 초기화

3단계 : 생성자의 몸체부분 실행

C++의 모든 객체는 위 세가지 과정을 순서대로 거쳐서 생성된다. 물론 이니셜라이져가 선언되지 않았다면 메모리 공간의 할당과 생성자의 몸체부분의 실행으로 완성된다.

그럼 생성자도 정의되어 있지 않으면, 메모리 공간의 할당만으로 완료되는가?

그렇지 않다. 생성자를 선언하지 않더라도 반드시 호출되며, 이를 디폴트 생성자(default construnctor)라고 한다. 이 부분은 추후에 설명할 것이다.


member initializer를 이용한 const 상수(변수) 초기화


car

멤버 이니셜라이저는 객체가 아닌 멤버의 초기화에도 사용할 수 있다. 다음 소스를 보자.

class SoSimple {
private:
	int num1;
	int num2;
public:
	SoSimple(int n1, int n2) : num1(n1) {
		num2 = n2;
	}
};

위 클래스를 보면 객체가 아닌 멤버변수도 이니셜라이저를 통해 초기화가 가능하다.

num1(n1)

num1을 n1값으로 초기화하라는 뜻이다. 따라서 프로그래머는 생성자 몸체에서 초기화 하는 것과 이니셜라이저를 이용하는 것 중에 선택이 가능하다. 그러나 일반적으로 멤버변수로 초기화에 있어서는 이니셜라이저를 선호한다. 그 이유는

초기화의 대상을 명확히 인식 할 수 있다.

성능에 약간의 이점이 있다.

와 같다.

많은 C++ 프록래머는 이니셜라이져가 더 명확한 표현이라고 생각한다. 그리고 성능상의 이점은 추후 아주 나중에 설명이 될 것이다.

초기화를 비교해보면

int num1 = n1;

과 비교할 수 있는데 이니셜라이저를 통해 초기화 되는 멤버는 선언과 동시에 초기화가 이루어지는 것과 같은 유형의 바이너리 코드로 구성하기 때문이다.

반면 생성자 몸체에서의 초기화는

int num2;

num2 = n2;

즉, 이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성된다

반며, 생성자의 몸체부분에서 대입연산을 이용해 진행하면 각각 별도의 문장에서 진행되는 형태의 바이너리 코드가 생성된다.

그리고 여기서 const 변수는 선언과 동시에 초기화 해야 한다는 문제가 떠오른다.

const 멤버변수도 이니셜라이저를 이용하면 초기화가 가능하다

그럼 다음을 보자.

class FruitSeller {
private:
	const int APPLE_PRICE;
	int numOfApples;
	int myMoney;
public:
	FruitSeller(int price, int num, int money) : APPLE_PRICE(proce), numOfApples(num), myMoney(money) {};
		
	int SaleApples(int money) {
		int num = money / APPLE_PRICE;
		numOfApples -= num;
		myMoney += money;
		return num;
	}
	void ShowSalesResult() const {
		cout << "남은 사과 : " << numOfApples << endl;
		cout << "판매 수익 : " << myMoney << endl << endl;
	}
};

다음처럼 APPLE_PRICE를 초기화 해줄 수 있다.

참고로 const 변수와 const 상수는 같은 뜻이라고 해도 된다. const는 변수를 상수화 시키는 키워드 이기 때문이다.

이니셜라이저의 이러한 특징은 멤버변수로 참조자를 선언할 수 있게 한다.

const 변수와 마찬가지로 참조자도 선언과 동시에 초기화가 이루어 져야 한다. 그 예시를 보자.

class AAA {
public:
	AAA() {
		cout << "empty object" << endl;
	}
	void ShowYourName() {
		cout << "I am class AAA" << endl;
	}
};

class BBB {
private:
	AAA &ref;
	const int &num;

public:
	BBB(AAA &r, const int &n) : ref(r), num(n) {};
	void ShowYourName() {
		ref.ShowYourName();
		cout << " and " << endl;
		cout << "I ref num " << num << endl;
	}
};

int main() {
	AAA obj1;
	BBB obj2(obj1, 20);
	obj2.ShowYourName();

	return 0;
}

이렇게 참조자를 멤버변수로 선언하는 경우는 흔하지 않으나 유사코드를 봤을 때 이해할 수 있을 정도로 하면 될 듯 하다.


디폴트 생성자


우리는 이전에 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 한다 라는 것을 배웠다. 왜냐하면 메모리 공간의 할당 이후에 생성자 호출까지 완료되어야 객체라고 할 수 있기 때문이다.

따라서 생성자를 따로 생성하지 않는다면 C++ 컴파일러에 의해 디폴트 생성자가 자동으로 삽입된다.

디폴트 생성자는 어떤 파라미터도 없고, 내부적으로 아무일도 일어나지 않는다.

AAA(){} 와 같다.

이는 new 연산자를 이용한 객체의 생성에도 해당이 되는 얘기다.

AAA *ptr = new AAA;

이렇게 호출을 한다고 해도 맞는 말이다.

그러나 다음과 같다면 생성자는 호출되지 않는다.

AAA *ptr = (AAA*)malloc(sizeof(AAA));

malloc 함수 호출 시, 실제로는 AAA클래스의 크기정보만 바이트 단위로 전달되기 때문에 생성자가 호출 될 일 없다. 따라서 객체를 동적으로 할당할 때는 반드시 new 연산자를 이용해야 한다.


생성자 불일치


파라미터가 void형으로 선언되는 디폴트 생성자는, 생성자가 하나도 정의되어 있지 않을 때만 삽이이 됨을 알았다. 그럼 다음 클래스에는 생성자가 삽입이 되지 않을 것이다.

class SoSimple{
private:
	int num;
publick:
SoSimple(int n) : num(n){}
};

따라서

SoSimple simObj1(10);

SoSimple & simPtr1 = new SoSimple(2);

은 가능하지만

SoSimple simObj2;

SoSimple *simPtr2 = new SoSimple

은 불가능 하다. 요구되는 생성자가 정의되지도, 자동으로 생성되지도 않기 때문이다.

따라서 SoSimple() : num(0){} 과 같은 생성자가 선언이 되어야 한다.


private 생성자


앞서 보인 생성자들은 public으로만 선언했다. 객체의 생성이 클래스의 외부에서 진행되기 때문에 생성자는 public으로 선언되어야 한다.

그럼 클래스 내부에서 객체를 생성한다면? private로 선언되어도 가능한가?

맞는 얘기다. 예제를 보도록 하자.

class AAA {
private:
	int num;
public:
	AAA() : num(0) {}
	AAA& CreateInitObj(int n) const {
		AAA *ptr = new AAA(n);
		return *ptr;
	}
	void ShowNum() const { cout << num << endl; }
private:
	AAA(int n) : num(n) {}
};

int main() {
	AAA base;
	base.ShowNum();

	AAA &obj1 = base.CreateInitObj(3);
	obj1.ShowNum();

	AAA &obj2 = base.CreateInitObj(12);
	obj2.ShowNum();

	delete &obj1;
	delete &obj2;
	return 0;
}

위 예제는 힙 영역에 생성된 객체를 참조의 형태로 반환한다. 이는 힙에 할당된 메모리 공간은 변수로 간주하여, 참조자를 통한 참조가 가능하다 라는 사실을 다시 한 번 확인시켜 준다.

그리고 위 예제는 private 생성자의 예시가 된다.


소멸자의 이해와 활용


생성자와 반대로 객체 소멸시 반드시 호출되는 것은 소멸자 이다.

클래스의 이름앞에 ~ 가 붙는 형태의 이름을 갖는다.

반환형이 선언되어 있지 않고, 실제로 반환되지 않는다.

파라미터는 void 형으로 선언되어야 하므로 오버로딩도 디폴트 값 설정도 불가능 하다

~AAA(){...} 다음과 같은 형태다.

이 소멸자는 객체 소멸과정에서 자동으로 호출된다. 또한 직접 소멸자를 정의하지 않으면, 디폴트 생성자와 마찬가지로 자동 삽입된다.

만약 생성자 내에 new 연산자를 이용해 할당해 놓은 메모리가 있다면 delete 연산자를 이용해 소멸자에서 소멸해야 한다.

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

	void ShowPersonInfo() const {
		cout << "이름 : " << name << endl;
		cout << "나이 : " << age << endl;
	}

	~Person() {
		delete[] name;
		cout << "called destructor!" << endl;
	}
};

int main() {
	Person man1("나나",29);
	Person man2("뽀", 41);
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;

}

여기까지 마무리 하고 다음에는 클래스와 배열, this 포인터에 대해 알아 보겠다

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

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

Back to Overview