본문 바로가기
개인공부/C++ 기초플러스

C++ Primer 15

by 하고싶은건많은놈 2023. 2. 21.

학습목표

  • 프렌드 클래스
  • 프렌드 클래스의 메소드
  • 내포된 클래스
  • 예외 처리, try 블록, catch 블록
  • 예외 클래스
  • RTTI(실행 시간 데이터형 정보)
  • dynamic_casttypeid
  • static_cast, const_cast, reinterpret_cast

15.1 프렌드

클래스도 프렌드가 될 수 있음

  • 프렌드 클래스의 모든 메소드는 오리지널 클래스의 private 멤버 및 protected 멤버에 접근할 수 있음
  • 어떤 클래스의 특정 멤버 함수들만 다른 클래스의 프렌드가 되도록 지정할 수 있음

프렌드 클래스

TV 클래스와 리모콘 클래스가 존재할때, 이 둘은 is-a도, has-a 관계도 아님
그러나 리모콘 클래스는 TV 클래스의 상태를 변경할 수 있으며, 따라서 리모콘 클래스를 TV 클래스의 프렌드로 만들어야함

// tv.h

#ifndef TV_H_
#define TV_H_

class Tv
{
	public:
		friend class Remote;
		enum {Off, On};
		enum {MinVal, MaxVal = 20};
		enum {Antenna, Cable};
		enum {TV, DVD};

		Tv(int s = Off, int mc = 125) : state(s), volume(5),
			maxchannel(mc), channel(2), mode(Cable), input(TV) {}
		void onoff() { state = (state == On) ? Off : On; }
		bool ison() const { return state == On; }
		bool volup();
		bool voldown();
		void chanup();
		void chandown();
		void set_mode() { mode = (mode == Antenna) ? Cable : Antenna; }
		void set_input() { input = (input == TV ) ? DVD : TV; }
		void settings() const;

	private:
		int state;
		int volume;
		int maxchannel;
		int channel;
		int mode;
		int input;
};

class Remote
{
	private:
		int mode;
	public:
		Remote(int m = Tv::TV) : mode(m) {}
		bool volup(Tv & t) { return t.volup(); }
		bool voldown(Tv & t) { return t.voldown(); }
		void onfoff(Tv & t) { t.onoff(); }
		void chanup(Tv & t) { t.chanup(); }
		void chandown(Tv & t) { t.chandown(); }
		void set_chan(Tv & t, int c) { t.channel = c; }
		void set_mode(Tv & t) { t.set_mode(); }
		void set_input(Tv & t) { t.set_input(); }
};

#endif
// tv.cpp

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

bool Tv::volup()
{
	if (volume < MaxVal)
	{
		volume++;
		return true;
	}
	else
		return false;
}

bool Tv::voldown()
{
	if (volume > MinVal)
	{
		volume--;
		return true;
	}
	else
		return false;
}

void Tv::chanup()
{
	if (channel < maxchannel)
		channel++;
	else
		channel = 1;
}

void Tv::chandown()
{
	if (channel > 1)
		channel--;
	else
		channel = maxchannel;
}

void Tv::settings() const
{
	using std::cout;
	using std::endl;
	cout << "TV = " << (state == Off ? "OFF" : "ON") << endl;
	if (state == On)
	{
		cout << "볼륨 = " << volume << endl;
		cout << "채널 = " << channel << endl;
		cout << "모드 = " << (mode == Antenna ? "지상파 방송" : "케이블 방송") << endl;
		cout << "입력 = " << (input == TV ? "TV" : "DVD") << endl;
	}
}
// use_tv.cpp

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

int main()
{
	using std::cout;
	Tv s42;
	cout << "42\" TV의 초기 설정값:\n";
	s42.settings();
	s42.onoff();
	s42.chanup();
	cout << "\n42\" TV의 변경된 설정값 : \n";
	s42.settings();
	
	Remote grey;

	grey.set_chan(s42, 10);
	grey.volup(s42);
	grey.volup(s42);
	cout << "\n리모콘 사용 후 42\" TV의 설정값 : \n";
	s42.settings();

	Tv s58(Tv::On);
	s58.set_mode();
	grey.set_chan(s58, 28);
	cout << "\n58\" TV의 설정값 : \n";
	s58.settings();
	return 0;
}

friend class Remote;RemoteTv 클래스에 대한 프렌드 클래스로 선언함으로써 Tv 클래스의 private 부분들을 직접적으로 접근 가능함


프렌드 멤버 함수

class Tv
{
	friend void Remote::set_chan(Tv & t, int c);
	...
}

Remote 클래스의 메소드들 중 Tv 클래스의 private 멤버에 직접 접근하는 메소드는 Remote::set_chan() 메소드 뿐임
따라서 클래스 전체를 프렌드로 만드는 대신 해당 메소드만 클래스에 대한 프렌드로 만드는 것이 가능함
단, 이 경우 컴파일러가 Remote의 정의를 미리 알고있어야 하지만 RemoteTv 객체를 사용하기 때문에 Tv의 정의가 Remote 앞에 나타나야함
이러한 순환 종속을 피하기 위해 아래와 같이 사전 선언(forward declaration)을 사용함

class Tv;
class Remote { ... };
class Tv { ... };

TvRemote의 순서를 반대로 할 경우 에러가 발생함
이는 Tv 클래스 선언시 Remote 메소드가 프렌드로 선언되어있기 때문에 반드시 Remote 클래스의 set_Chan() 메소드에 대해 알고있어야 하기 때문

// tvfm.h

#ifndef XXX_H_
#define XXX_H_

class Tv;

class Remote
{
	public:
		enum State{Off, On};
		enum {MinVal, MaxVal = 20};
		enum {Antenna, Cable};
		enum {TV, DVD};
	private:
		int mode;
	public:
		Remote(int m = TV) : mode(m) {}
		bool volup(Tv & t);
		bool voldown(Tv & t);
		void onfoff(Tv & t);
		void chanup(Tv & t);
		void chandown(Tv & t);
		void set_chan(Tv & t, int c);
		void set_mode(Tv & t);
		void set_input(Tv & t);
};

class Tv
{
	public:
		friend void Remote::set_chan(Tv & t, int c);
		enum State{Off, On};
		enum {MinVal, MaxVal = 20};
		enum {Antenna, Cable};
		enum {TV, DVD};

		Tv(int s = Off, int mc = 125) : state(s), volume(5),
			maxchannel(mc), channel(2), mode(Cable), input(TV) {}
		void onoff() { state = (state == On) ? Off : On; }
		bool ison() const { return state == On; }
		bool volup();
		bool voldown();
		void chanup();
		void chandown();
		void set_mode() { mode = (mode == Antenna) ? Cable : Antenna; }
		void set_input() { input = (input == TV ) ? DVD : TV; }
		void settings() const;

	private:
		int state;
		int volume;
		int maxchannel;
		int channel;
		int mode;
		int input;
};

inline bool Remote::volup(Tv & t) { return t.volup(); }
inline bool Remote::voldown(Tv & t) { return t.voldown(); }
inline void Remote::onfoff(Tv & t) { t.onoff(); }
inline void Remote::chanup(Tv & t) { t.chanup(); }
inline void Remote::chandown(Tv & t) { t.chandown(); }
inline void Remote::set_chan(Tv & t, int c) { t.channel = c; }
inline void Remote::set_mode(Tv & t) { t.set_mode(); }
inline void Remote::set_input(Tv & t) { t.set_input(); }
#endif

인라인 함수들은 내부 링크를 가지기 때문에 함수를 사용하는 파일 안에 함수 정의가 들어있어야함
inline 키워드를 제거하여 외부 링크를 갖게할 경우 함수 정의를 구현 파일에 넣을 수도 있음


그 밖의 프렌드 관계

class Tv
{
	friend class Remote;
	public:
		void buzz(Remote & r);
		...
};

class Remote
{
	friend class Tv;
	public:
		void bool volup(Tv & t) { t.volup(); }
		...
};

inline void Tv:buzz(Remote & r) { ... }

클래스들을 상호 프렌드(mutual friend) 관계로 설정할 수 있음
Remote 객체를 사용하는 Tv의 메소드인 Tv::buzz()의 경우 원형은 Remote 클래스 선언 앞에 둘 수 있으나 정의는 뒤에 두어야함
Tv 객체를 사용하는 Remote::volup() 메소드의 경우 Tv 클래스 선언 뒤에 등장하기 때문에 클래스 선언 안에서 곧바로 정의 가능함


공유 프렌드

class Analyzer;
class Probe
{
	friend void sync(Analyzer & a, const Probe & p);
	friend void sync(Probe & p, const Analyzer & a);
	...
};

class Analyzer
{
	friend void sync(Analyzer & a, const Probe & p);
	friend void sync(Probe & p, const Analyzer & a);
	...
};

inline void sync(Analyzer & a, const Probe & p) { ... }
inline void sync(Probe & p, const Analyzer & a) { ... }

하나의 함수가 서로 다른 두 클래스에 들어있는 private 데이터에 접근해야 할 경우, 해당 함수는 각 클래스의 멤버 함수여야하지만 이는 불가능함
따라서 한 클래스의 멤버이자 다른 클래스에 대해서 프렌드로 만들 수 있으나 두 클래스 모두에 대해 프렌드로 만들 수도 있음



15.2 내포 클래스

클래스 안에 선언된 클래스를 내포 클래스(nested class)라고함
클래스 선언을 내포하고있는 클래스의 멤버 함수들은 내포 클래스의 객체들을 생성하여 사용할 수 있음
클래스 외부에서는 내포 클래스 선언이 public 부분에 들어있고, 사용 범위 결정 연산자를 사용했을 경우에만 내포 클래스를 사용할 수 있음

다른 클래스의 객체를 클래스의 멤버로 가지는 컨테인먼트와는 다름
클래스를 내포시킬시엔 클래스 멤버가 생성되지 않고, 대신 클래스 선언을 내포하고있는 클래스에만 지역적으로 알려지는 하나의 데이터형을 정의함

class Queue
{
	class Node
	{
		public:
			Item item;
			Node * next;
			Node(const Item & i) : item(i), next(0) {}
	};
	...
};

boll Queue::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node(item);
	...
}

Node 객체는 Queue::enqueue() 메소드에서 유일하게 생성함
Node의 생성자를 클래스 선언이 아닌 메소드 정의 파일에 정의하고싶다면 Node 클래스가 Queue 클래스 내부에 정의된다는 사실을 반영해야함
즉, Queue::Node::Node(const Item & i) : item(i), next(0) {}의 형태로 사용해야함


내포 클래스와 접근

내포 클래스가 선언된 장소가 내포 클래스의 사용 범위 즉, 프로그램의 어느 부분에서 내포 클래스의 객체를 생성할 수 있는지를 결정함
내포 클래스의 public, protected, private 부분이 해당 클래스 멤버에 대한 접근을 제한함

내포 클래스가 제2 클래스의 private 부분에 선언될 경우

  • 해당 클래스 외부에서는 내포 클래스가 존재한다는 사실조차 알지 못함
  • 파생 클래스에서도 기초 클래스의 private 부분에 직접 접근할 수 없기 때문에 내포 클래스가 보이지 않음

내포 클래스가 제2 클래스의 protected 부분에 선언될 경우

  • 해당 클래스 외부에서는 보이지 않음
  • 파생 클래스에서는 내포 클래스가 보이며, 내포 클래스형의 객체를 직접 생성할 수 있음

내포 클래스가 제2 클래스의 public 부분에 선언될 경우

  • public이므로 해당 클래스 외부에서 사용할 수 있음
  • 단, 클래스 사용 범위가 있으므로 외부에서 사용시엔 클래스 제한자를 사용해야함

클래스가 선언된 장소가 클래스의 사용 범위 또는 가시 범위를 결정함
어떤 특정 클래스가 사용 범위에 들어올 경우 일반적인 접근 제어 규칙(public, protected, private, friend)이 내포 클래스의 멤버들에 대한 접근 가능 여부를 결정함


템플릿에서의 내포

// queuetp.h

#ifndef QUEUETP_H_
#define QUEUETP_H_

template<class Item>
class QueueTP
{
	private:
		enum {Q_SIZE = 10};
		class Node
		{
			public:
				Item item;
				Node * next;
				Node(const Item & i) : item(i), next(0) {}
		};
		Node * front;
		Node * rear;
		int items;
		const int qsize;
		QueueTP(const QueueTP & q) : qsize(0) {}
		QueueTP & operator=(const QueueTP & q) { return *this; }
	public:
		QueueTP(int qs = Q_SIZE);
		~QueueTP();
		bool isempty() const { return items == 0; }
		bool isfull() const { return items == qsize; }
		int queuecount() const { return items; }
		bool enqueue(const Item & item);
		bool dequeue(Item & item);
};

template <class Item>
QueueTP<Item>::QueueTP(int qs) : qsize(qs)
{
	front = rear = 0;
	items = 0;
}

template <class Item>
QueueTP<Item>::~QueueTP()
{
	Node * temp;
	while (front != 0)
	{
		temp = front;
		front = front->next;
		delete temp;
	}
}

template <class Item>
bool QueueTP<Item>::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node(item);
	items++;
	if (front == 0)
		front = add;
	else
		rear->next = add;
	rear = add;
	return true;
}

template <class Item>
bool QueueTP<Item>::dequeue(Item & item)
{
	if (front == 0)
		return false;
	item = front->item;
	items--;
	Node * temp = front;
	front = front->next;
	delete temp;
	if (items == 0)
		rear = 0;
	return true;
}

#endif
// nested.cpp

#include "queuetp.h"
#include <iostream>
#include <string>

int main()
{
	using std::string;
	using std::cin;
	using std::cout;

	QueueTP<string> cs(5);
	string temp;

	while (!cs.isfull())
	{
		cout << "이름을 입력하십시오. 도착하신 순서대로 사은품을 드립니다.\n"
			 << "이름 : ";
		getline(cin, temp);
		cs.enqueue(temp);
	}
	cout << "큐가 가득 찼으므로, 지금부터 사은품을 나누어 드리겠습니다.\n";
	while (!cs.isempty())
	{
		cs.dequeue(temp);
		cout << temp << " 님! 감사합니다. 안녕히 가십시오.\n";
	}
	return 0;
}

클래스 템플릿을 사용하는 경우에도 내포 클래스의 사용은 아무런 문제를 발생시키지 않음



15.3 예외

사용할 수 없는 파일을 열려고 시도하거나, 사용가능한 메모리보다 더 많은 양의 메모리를 요구하거나, 처리할 수 없는 값들을 만날 경우 정상적으로 실행을 계속할 수 없는 상황에 직면함
C++의 예외(exception)은 이러한 상황을 처리하기 위하여 도구를 제공함

abort() 호출

// error1.cpp

#include <iostream>
#include <cstdlib>

double hmean(double a, double b);

int main()
{
	double x, y, z;

	std::cout << "두 수를 입력하십시오 : ";
	while (std::cin >> x >> y)
	{
		z = hmean(x, y);
		std::cout << x << ", " << y << "의 조화평균은 "
				  << z << "입니다.\n";
		std::cout << "다른 두 수를 입력하십시오(끝내려면 q) : ";
	}
	std::cout << "프로그램을 종료합니다.\n";
	return 0;
}

double hmean(double a, double b)
{
	if (a == -b)
	{
		std::cout << "매개변수들을 hmean()에 전달할 수 없습니다.\n";
		std::abort();
	}
	return 2.0 * a * b / (a + b);
}

0으로 나누기가 발생한 경우 abort() 함수를 호출하여 문제를 해결할 수 있음

  • abort() 함수의 원형은 cstdlib 헤더 파일에 포함
  • 호출될시 표준 에러 스트림에 "abnormal program termination(비정상적인 프로그램 종료)" 등의 메시지를 보내고 프로그램을 종료시킴
  • 프로그램을 기동시킨 부모 프로세스나 운영체제의 컴파일러에 종속적인 어떤 값을 리턴함
  • 파일 버퍼를 비우는지의 여부는 시스템마다 다를 수 있으며, 메세지 출력 없이 파일 버퍼만 비우고싶은 경우 exit()를 사용

에러 코드 리턴

// error2.cpp

#include <iostream>
#include <cfloat>

bool hmean(double a, double b, double * ans);

int main()
{
	double x, y, z;

	std::cout << "두 수를 입력하십시오 : ";
	while (std::cin >> x >> y)
	{
		if (hmean(x, y, &z))
			std::cout << x << ", " << y << "의 조화평균은 "
				  << z << "입니다.\n";
		else
			std::cout << "서로 부정인 두 수의 조화평균은 구할 수 없습니다.\n";
		std::cout << "다른 두 수를 입력하십시오(끝내려면 q) : ";
	}
	std::cout << "프로그램을 종료합니다.\n";
	return 0;
}

bool hmean(double a, double b, double * ans)
{
	if (a == -b)
	{
		*ans = DBL_MAX;
		return false;
	}
	else
	{
		*ans = 2.0 * a * b / (a + b);
		return true;
	}
}

프로그램의 비정상 종료보다 함수의 리턴값을 사용하여 어떤 문제가 발생했는지를 알리는것이 유용함
즉, 위와 같이 에러 상황이 발생한경우 프로그램을 바로 종료하는 대신 bool형 함수의 리턴값으로 해당 상황을 알릴 수 있음
전통적인 C에서는 리턴 조건을 전역변수 errno를 사용하여 활용함


예외 메커니즘

C++의 예외 처리는 다음과 같은 단계로 이루어짐

  • 예외를 발생시킴
  • 핸들러를 사용하여 예외를 포착
  • try 블록을 사용

예외 발생시 다른 위치에있는 구문으로 프로그램의 제어를 넘김
throw 키워드가 예외의 발생을 나타내며, 그 뒤에는 예외의 특징을 나타내는 하나의 값이 뒤따름

프로그램은 문제 해결을 원하는 장소에서 예외 핸들러(exception handler)를 사용하여 예외를 포착함
catch 키워드가 예외의 포착을 나타매녀, 그 뒤에는 예외 핸들러가 취할 조치들을 나타내는 코드 블록이 중괄호로 묶여 나타남

try 블록은 특별한 예외들이 발생할 수 있는 하나의 코드 블록으로, 그 뒤에는 하나 이상의 catch 블록이 등장함

// error3.cpp

#include <iostream>

double hmean(double a, double b);

int main()
{
	double x, y, z;

	std::cout << "두 수를 입력하십시오 : ";
	while (std::cin >> x >> y)
	{
		try {
				z = hmean(x, y);
		}
		catch (const char * s)
		{
			std::cout << s << std::endl;
			std::cout << "두 수를 새로 입력하십시오 : ";
			continue;
		}
		std::cout << x << ", " << y << "의 조화평균은 "
				  << z << "입니다.\n";
		std::cout << "다른 두 수를 입력하십시오(끝내려면 q) : ";
	}
	std::cout << "프로그램을 종료합니다.\n";
	return 0;
}

double hmean(double a, double b)
{
	if (a == -b)
		throw "잘못된 hmean() 매개변수 : a = -b 는 허용되지 않습니다.";
	return 2.0 * a * b / (a + b);
}

try 블록 내의 구문이 예외를 발생시킬경우 catch 블록에서 해당 예외를 처리함
위에서는 예외 발생이 문자열로 이루어져있으나, 클래스형이 일반적인 선택임
try 블록에서 예외 발생을 감지했을시 발생된 예외의 데이터형(여기서는 문자열)과 일치하는 예외 핸들러(catch 블록)를 찾아 실행함
예외가 발생하지 않고 try 블록 내의 구문들의 실행이 완수된다면 catch 블록을 건너뛰어 그 다음 구문으로 넘어감

함수에서 예외 발생시 try 블록이 없거나 데이터형이 일치하는 예외 핸들러가 없을 경우 프로그램은 기본적으로 abort() 함수를 호출함
이 행동은 사용자가 수정할 수 있음


예외로 객체 사용하기

일반적으로 예외를 발생시키는 함수들은 객체를 발생시킴

  • 서로 다른 예외 데이터형을 사용함으로써 서로 다른 함수들과 예외를 발생시키는 상황을 구별할 수 있음
  • 객체 자체에 정보를 전달할 수 있으며, 이를 사용하여 예외가 발생하는 여러 조건들을 식별할 수 있음
// exc_mean.h

#ifndef EXC_MEAN_H_
#define EXC_MEAN_H_
#include <iostream>

class bad_hmean
{
	private:
		double v1;
		double v2;
	public:
		bad_hmean(double a = 0, double b = 0) : v1(a), v2(b) {}
		void mesg();
};

inline void bad_hmean::mesg()
{
	std::cout << "hmean(" << v1 << ", " << v2 << ") : "
			  << "잘못된 매개변수 : a = -b\n";
}

class bad_gmean
{
	public:
		double v1;
		double v2;
		bad_gmean(double a = 0, double b = 0) : v1(a), v2(b) {}
		const char * mesg();
};

inline const char * bad_gmean::mesg()
{
	return "gmean() 매개변수들은 >= 0 이어야 합니다.\n";
}

#endif
// error4.cpp

#include "exc_mean.h"
#include <iostream>
#include <cmath>

double hmean(double a, double b) throw(bad_hmean);
double gmean(double a, double b) throw(bad_gmean);

int main()
{
	using std::cout;
	using std::cin;
	using std::endl;

	double x, y, z;

	cout << "두 수를 입력하십시오 : ";
	while (std::cin >> x >> y)
	{
		try {
				z = hmean(x, y);
				cout << x << ", " << y << "의 조화평균은 "
					 << z << "입니다.\n";
				cout << x << ", " << y << "의 기하평균은 "
					 << gmean(x,y) << "입니다.\n";
				cout << "다른 두 수를 입력하십시오 (끝내려면 q) : ";
		}
		catch (bad_hmean & bg)
		{
			bg.mesg();
			cout << "다시 하십시오.\n";
			continue;
		}
		catch (bad_gmean & hg)
		{
			cout << hg.mesg();
			cout << "사용된 값 : " << hg.v1 << ", "
				 << hg.v2 << endl;
			cout << "죄송합니다. 더 이상 진행할 수 없습니다.\n";
			break;
		}
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

double hmean(double a, double b) throw(bad_hmean)
{
	if (a == -b)
		throw bad_hmean(a, b);
	return 2.0 * a * b / (a + b);
}

double gmean(double a, double b) throw(bad_gmean)
{
	if (a < 0 || b < 0)
		throw bad_gmean(a, b);
	return std::sqrt(a * b);
}

C++11에서의 예외 규격

double harm(double a) throw(bad_thing);
double marm(double a) throw();

throw() 부분은 타입 리스트의 존재 유무에 상관없이 예외 사항을 나타내며, 함수 원형 및 함수 정의에서 모두 나타날 수 있음
사용자에게 try블록의 필요성을 알려주고 컴파일러로 하여금 런타임 체크용 코드를 추가하여 예외 사항이 위반되어있는지의 여부를 확인함

그러나 이러한 기능은 C++11 부터는 deprecated되어 더이상 권장되지 않음

  • C++의 예외처리는 컴파일시가 아닌 실행중에 체크됨
  • 따라서 프로그래머에게 모든 예외가 핸들링되고있다는 보증이 불가능함
  • throw로 명시한 예외들을 구분하기 위해 컴파일시 추가적인 코드가 들어가 코드 최적화를 방해하고 실행시 오버헤드를 야기함

그러나 예외를 발생시키지 않는 함수의 경우 C++11에서는 throw()를 대체하는 noexcept 키워드를 사용할 수 있음
이는 double mean() noexcept; 형식으로 사용됨


스택 풀기

C++11은 스택에 정보를 올려두면서 함수 호출을 처리함

  • 프로그램은 호출한 함수 구문의 주소를 스택에 올려놓고, 호출된 함수의 실행이 끝나면 스택에 올려놓은 주소를 사용하여 프로그램 실행을 계속할 장소를 결정
  • 함수 호출은 자동변수로 취급되는 함수 매개변수들을 스택에 올려놓음

따라서 어떤 함수가 리턴 호출이 아닌 예외를 통해 종료되더라도 스택에서 메모리가 해제되어야함

  • 따라서 예외를 만날시 try블록에 들어있는 리턴주소에 도달할 때까지 계속해서 스택을 해제함
  • 이 때 함수 리턴과 마찬가지로 스택에 올라와있는 모든 자동 클래스 객체들에 대해 클래스 파괴자들이 호출됨
// error5.cpp

#include "exc_mean.h"
#include <iostream>
#include <cmath>
#include <string>

class demo
{
	private:
		std::string word;
	public:
		demo(const std::string & str)
		{
			word = str;
			std::cout << "demo " << word << " 생성 \n";
		}
		~demo()
		{
			std::cout << "demo " << word << " 파괴\n";
		}
		void show() const
		{
			std::cout << "demo " << word << " 생존\n";
		}
};

double hmean(double a, double b);
double gmean(double a, double b);
double means(double a, double b);

int main()
{
	using std::cout;
	using std::cin;
	using std::endl;

	double x, y, z;
	{
		demo d1("main()에서 블럭을 찾았습니다");
		cout << "두 수를 입력하십시오 : ";
		while (std::cin >> x >> y)
		{
			try {
					z = means(x, y);
					cout << x << "와 " << y
						<< "의 평균의 평균은 " << z << endl;
					cout << "다음 두 수를 입력하십시오 : ";
			}
			catch (bad_hmean & bg)
			{
				bg.mesg();
				cout << "다시 하십시오.\n";
				continue;
			}
			catch (bad_gmean & hg)
			{
				cout << hg.mesg();
				cout << "사용된 값 : " << hg.v1 << ", "
					<< hg.v2 << endl;
				cout << "죄송합니다. 더 이상 진행할 수 없습니다.\n";
				break;
			}
		}
		d1.show();
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

double hmean(double a, double b)
{
	if (a == -b)
		throw bad_hmean(a, b);
	return 2.0 * a * b / (a + b);
}

double gmean(double a, double b)
{
	if (a < 0 || b < 0)
		throw bad_gmean(a, b);
	return std::sqrt(a * b);
}

double means(double a, double b)
{
	double am, hm, gm;
	demo d2("found in means()");
	am = (a + b) / 2.0;
	try
	{
		hm = hmean(a, b);
		gm = gmean(a, b);
	}
	catch(bad_hmean & bg)
	{
		bg.mesg();
		std::cout << "means()에서 잡힘\n";
		throw;
	}
	d2.show();
	return (am + hm + gm) / 3.0;
}

프로그램이 예외가 잡히는 곳까지 도달하기 위해 스택을 풀 때, 스택에 있는 자동 기억공간 변수들이 해제되며 변수가 클래스 객체인 경우에는 해당 객체의 파괴자가 호출됨


예외의 그 밖의 기능

함수의 return 구문은 해당 함수를 호출한 함수로 실행을 옮김
반면 throw는 예외를 포착한 try-catch 조합이 있는 첫번째 함수에 도달할 때까지 실행을 계속해서 옮김

예외 지정자나 catch블록이 참조를 지정하더라도 컴파일러는 언제나 예외가 발생할 때 임시 복사본을 만듬
그럼에도 불구하고 참조를 사용하는 이유는 기초 클래스 참조가 파생 클래스 객체도 참조할 수 있기 때문임
즉, 기초 데이터형에 대한 예외를 지정할 경우 해당 데이터형으로부터 파생된 어떠한 파생 객체에도 부합함

예외 데이터형에 생략 부호를 사용하여 catch (...)형태로 사용할 경우 모든 데이터형의 예외를 포착할 수 있음


exception 클래스

exception 클래스 : C++ 언어를 지원하기 위해 사용하는 다른 예외 클래스들의 기초 클래스

  • 사용자가 작성하는 코드는 exception 객체르 발생시키거나 exception 클래스를 기초 클래스로 사용할 수 있음
  • what()이라는 가상 멤버가 주어지며, 이는 시스템 특성에 따라 하나의 문자열을 리턴함
  • 단, 이 메소드는 가상이기 때문에 exception 클래스로부터 파생된 클래스 안에서 재정의할 수 있음

stdexcept 예외 클래스

stdexcept 헤더 파일은 logic_errorruntime_error 클래스를 정의함
두 클래스는 exception으로부터 public으로 파생된 클래스임

class logic_error : public exception
{
	public:
		explicit logic_error(const string& what_arg);
		...
};

class domain_error : public logic_error
{
	public:
		explicit domain_error(const string& what_arg);
		...
};

생성자의 매개변수로는 string 객체가 사용되어 what() 메소드에 의해 `C스타일 문자열로 리턴된 문자 데이터를 제공함

logic_error 패밀리의 경우 일반적인 논리 에러들을 서술함

  • domain_error, invalid_argument, length_error, out_of_bounds 등의 클래스가 포함됨
  • 각각의 클래스는 logic_error의 생성자와 같이 사용자가 what()메소드에 의해 리턴되는 문자열을 제공하는 생성자를 가지고있음

runtime_error 패밀리의 경우 실행하는 동안에 나타날 수 있는, 예측과 예방이 어려운 에러들을 서술함

  • range_error, overflow_error, underflow_error 등의 클래스가 포함됨
  • 각각의 클래스는 runtime_error 생성자와 같이 사용자가 what()메소드에 의해 리턴되는 문자열을 제공하는 생성자를 가지고있음
try
{
	...
}
catch(out_of_bounds & oe)
{...}
catch(logic_error & oe)
{...}
catch(exception & oe)
{...}

상속 관계에 있기 때문에 예외를 일괄적으로 함께 다룰 수 있음
위의 경우 out_of_bounds 예외를 개별적으로 포착하고, 나머지 logic_error 예외 패밀리를 하나의 그룹으로, exception 객체 및 runtime_error 패밀리의 객체들을 집합적으로 처리함

bad_alloc 예외와 new

new를 사용한 메모리 대입시

  • 메모리 요청을 충족시킬 수 없을 경우 newNULL 포인터를 리턴
  • newbad_alloc 예외를 발생시킴
// newexcp.cpp

#include <iostream>
#include <new>
#include <cstdlib>
using namespace std;

struct Big
{
	double stuff[20000];
};

int main()
{
	Big * pb;
	try {
		cout << "큰 메모리 블록 대입을 요청하고있습니다.\n";
		pb = new Big[100000];
		cout << "메모리 블록 대입 요청이 거부되었습니다.\n";
	}
	catch (bad_alloc & ba)
	{
		cout << "예외가 감지되었습니다!\n";
		cout << ba.what() << endl;
		exit(EXIT_FAILURE);
	}
	cout << "메모리 블록이 성공적으로 대입되었습니다.\n";
	pb[0].stuff[0] = 4;
	cout << pb[0].stuff[0] << endl;
	delete [] pb;
	return 0;
}

NULL 포인터와 new

대부분의 소스코드는 new가 실패할 경우 null 포인터를 리턴하도록 작성되어있음
현재의 표준은 아래와 같이 null을 리턴하는 과거의 new를 대체하는 형태를 제공함
이를 통해 사용자가 원하는 행동을 플래그(flag)나 스위치(switch)로 세팅하여 이행할 수 있음

int * pi = new (std::nothrow) int;
int * pa = new (std::nothrow) int[500];

예외, 클래스, 상속

예외, 클래스, 상속은 서로 상호작용함

  • 하나의 예외 클래스를 다른 것으로부터 파생시킬 수 있음
  • 클래스 정의 안에 예외 클래스 선언을 내포시킴으로써 예외들을 클래스 안에 병합시킬 수 있음
  • 내포된 선언들은 상속될 수 있으며, 그들 자신이 기초 클래스의 역할을 할 수 있음
// sales.h

#ifndef SALES_H_
#define SALES_H_
#include <stdexcept>
#include <string>

class Sales
{
	public:
		enum {MONTHS = 12};
		class bad_index : public std::logic_error
		{
			private:
				int bi;
			public:
				explicit bad_index(int ix, const std::string & s = "Sales 객체에서 인덱스 에러\n");
				int bi_val() const { return bi; }
				virtual ~bad_index() throw() {}
		};
		explicit Sales(int yy = 0);
		Sales(int yy, const double * gr, int n);
		virtual ~Sales() {}
		int Year() const { return year; }
		virtual double operator[](int i) const;
		virtual double & operator[](int i);
	private:
		double gross[MONTHS];
		int year;
};

class LabeledSales : public Sales
{
	public:
		class nbad_index : public Sales::bad_index
		{
			private:
				std::string lbl;
			public:
				nbad_index(const std::string & lb, int ix,
			 			   const std::string & s = "LabeledSales 객체에서 인덱스 에러\n");
				const std::string & label_val() const { return lbl; }
				virtual ~nbad_index() noexcept {}
		};
		explicit LabeledSales(const std::string & lb = "없음", int yy = 0);
		LabeledSales(const std::string & lb, int yy, const double *gr, int n);
		virtual ~LabeledSales() {}
		const std::string & Label() const { return label; }
		virtual double operator[](int i) const;
		virtual double & operator[](int i);
	private:
		std::string label;
};

#endif

bad_index 클래스는 Salespublic으로 선언되어있으므로 클라이언트 catch 블록들에서 해당 클래스를 하나의 데이터형으로 사용할 수 있음
단, 이 때 Sales::bad_index 형태로 사용되어야함
nbad_index 클래스도 마찬가지로 LabeledSalespublic으로 선언되어있으므로 클라이언트 코드에서 LabeledSales::nbad_index로 사용할 수 있음

bad_indexnbad_index 클래스들은 예외 클래스를 상속받기 때문에 throw()예외를 사용할 수 있으나, C+11에서 파괴자는 예외를 사용할 수 없음

// sales.cpp

#include "sales.h"
using std::string;

Sales::bad_index::bad_index(int ix, const string &s)
	: std::logic_error(s), bi(ix)
{
}

Sales::Sales(int yy)
{
	year = yy;
	for (int i = 0; i < MONTHS; ++i)
		gross[i] = 0;
}

Sales::Sales(int yy, const double * gr, int n)
{
	year = yy;
	int lim = (n < MONTHS) ? n : MONTHS;
	int i;
	for (i = 0; i < lim; ++i)
		gross[i] = gr[i];
	for ( ; i < MONTHS; ++i)
		gross[i] = 0;
}

double Sales::operator[](int i) const
{
	if (i < 0 || i >= MONTHS)
		throw bad_index(i);
	return gross[i];
}

double & Sales::operator[](int i)
{
	if (i < 0 || i >= MONTHS)
		throw bad_index(i);
	return gross[i];
}

LabeledSales::nbad_index::nbad_index(const string & lb, int ix, const string & s)
	: Sales::bad_index(ix, s)
{
	lbl = lb;
}

LabeledSales::LabeledSales(const string & lb, int yy)
	: Sales(yy)
{
	label = lb;
}

LabeledSales::LabeledSales(const string &lb, int yy, const double * gr, int n)
	: Sales(yy, gr, n)
{
	label = lb;
}

double LabeledSales::operator[](int i) const
{
	if (i < 0 || i >= MONTHS)
		throw nbad_index(Label(), i);
	return Sales::operator[](i);
}

double & LabeledSales::operator[](int i)
{
	if (i < 0 || i >= MONTHS)
		throw nbad_index(Label(), i);
	return Sales::operator[](i);
}
// use_sales.cpp

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

int main()
{
	using std::cout;
	using std::cin;
	using std::endl;

	double vals1[12] =
	{
		1220, 1100, 1122, 2212, 1232, 2334,
		2884, 2393, 3302, 2922, 3002, 3544
	};

	double vals2[12] =
	{
		12, 11, 22, 21, 32, 34,
		28, 29, 33, 29, 32, 35
	};

	Sales sales1(2011, vals1, 12);
	LabeledSales sales2("Blogstar", 2012, vals2, 12);

	cout << "첫 번째 try 블록 : \n";
	try
	{
		int i;
		cout << "Year = " << sales1.Year() << endl;
		for (i = 0; i < 12; ++i)
		{
			cout << sales1[i] << ' ';
			if (i % 6 == 5)
				cout << endl;
		}
		cout << "Year = " << sales2.Year() << endl;
		cout << "Label = " << sales2.Label() << endl;
		for (i = 0; i <= 12; ++i)
		{
			cout << sales2[i] << ' ';
			if (i % 6 == 5)
				cout << endl;
		}
	}
	catch(LabeledSales::nbad_index & bad)
	{
		cout << bad.what();
		cout << "Company : " << bad.label_val() << endl;
		cout << "잘못된 인덱스 : " << bad.bi_val() << endl;
	}
	catch(Sales::bad_index & bad)
	{
		cout << bad.what();
		cout << "잘못된 인덱스 : " << bad.bi_val() << endl;
	}

	cout << "\n다음 try 블록 : \n";
	try
	{
		sales2[2] = 37.5;
		sales1[20] = 23345;
		cout << "try 블록 2의 끝\n";
	}
	catch(LabeledSales::nbad_index & bad)
	{
		cout << bad.what();
		cout << "Company : " << bad.label_val() << endl;
		cout << "잘못된 인덱스 : " << bad.bi_val() << endl;
	}
	catch(Sales::bad_index & bad)
	{
		cout << bad.what();
		cout << "잘못된 인덱스 : " << bad.bi_val() << endl;
	}
	cout << "프로그램을 종료합니다.\n";

	return 0;
}

잘못된 예외

예외가 예외 지정을 사용하는 함수 안에서 발생할시 해당 예외는 예외 지정자 리스트에 있는 데이터형들 중의 어느 하나와 일치해야함
그렇지 않을 경우 해당 예외는 기대하지 않는 예외(unexpected exception)으로 지정되며 기본적으로 프로그램 실행이 중지됨

예외가 포착되지 않을경우, 즉 try 블록이 없거나 예외와 일치하는 catch 블록이 없을 경우 포착되지 않는 예외(uncaought exception)으로 지정되며 기본적으로 프로그램 실행이 중지됨
이러한 예외들에 대한 프로그램의 응답을 사용자가 변경할 수 있음

포착되지 않는 예외의 경우

  • 프로그램이 terminate() 함수를 호출함
  • terminate() 함수는 기본적으로 abort()를 호출함
  • 이 때 terminate() 함수가 호출하는 함수를 변경하여 행동을 바꿀 수 있으며, 이를 위해 exception 헤더 파일에 선언되어있는 set_terminate() 함수를 호출할 수 있음
typedef void (*terminate_handler)();
terminate_handler set_terminate(terminate_handler f) throw(); // c++98
terminate_handler set_terminate(terminate_handler f) noexcept; /c++11
void terminate(); // c++98
void terminate() noexcept; // c++11

terminate_handlertypedef에 의해 매개변수를 사용하지 ㅇ낳고 리턴값도 없는 함수를 가리키는 포인터를 위한 데이터형 이름으로 정의됨
set_terminate() 함수는 매개변수를 사용하지 않고 리턴형이 void인 함수의 이름을 매개변수로 사용하며, 이전에 등록된 함수의 주소를 리턴함
set_terminate() 함수를 한번 이상 호출시 terminate()는 가장 최근의 set_terminate() 호출에 의해 설정된 함수를 호출함

#include <exception>
using namespace std;

void myQuit()
{
	cout << "포착되지 않는 예외가 발생하여 프로그램을 중지시킵니다.\n";
	exit(5);
}

set_terminate(myQuit);

예외 발생시 포착되지 않으면 프로그램은 terminate()를 호출하고, terminate()MyQuit()를 호출함

typedef void (*unexpected_handler)();
unexpected_handler set_unexpected(unexpected_handler f) throw(); // c++98
unexpected_handler set_unexpected(unexpected_handler f) noexcept; // c++11
void unexpected(); // c++98
void unexpected() noexcept; // c++11

기대하지 않는 예외의 경우 프로그램은 unexpected() 함수를 호출하고, unexpected() 함수는 terminate() 함수를 호출하며 terminate() 함수는 abort() 함수를 호출함
이 때 마찬가지로 exception 헤더 파일에 선언된 set_unexpected() 함수를 사용하여 unexpected 함수의 행동을 변경할 수 있음

  • terminate()(디폴트), abort(), exit()를 호출하여 프로그램을 종료
  • 예외를 발생시킴
  • 예외를 발생시켰을 때의 결과는 함수의 오리지널 예외 지정에 달림
    • 새로 발생된 예외가 오리지널 예외 지정과 일치하면 프로그램은 그곳으로부터 정상적으로 진행하여 새로 발생된 예외와 일치하는 catch 블록을 찾음
    • 새로 발생된 예외가 오리지널 예외 지정과 일치하지 않고, 예외 지정에 bad_exception 형이 들어있지 않으면 terminate()가 호출됨
    • 새로 발생된 예외가 오리지널 예외 지정과 일치하지 않고, 오리지널 예외 지정에 bad_exception형이 들어있으면 해당 예외는 bad_exception형의 예외로 대체됨
#include <exception>
using namespace std;

void myUnexpected()
{
	throw std::bad_exception();
}

set_unexpected(myUnexpected);

예외 주의사항

예외를 사용하면 프로그램의 크기가 커지고 실행 속도가 떨어짐
템플릿 함수들은 사용하는 특별한 특수화에 따라 서로 다른 종류의 얘외를 발생시킬 수 있기 때문에 예외 지정과 어울리지 않음

void test1(int n)
{
	string mesg("무한루프!");
	...
	if (oh_no)
		throw exception();
	...
	return ;
}

string 클래스는 동적 메모리 대입을 사용
throw 구문이 실행되면 함수가 return 구문에 도달하지 않고 일찍 종료되지만 스택 풀기 기능에 의해 파괴자가 호출되어 메모리가 올바르게 관리됨

void test2(int n)
{
	double * ar = new double[n];
	...
	if (oh_no)
		throw exception();
	...
	delete [] ar;
	return;
}

throw 구문이 실행될 경우 함수가 일찍 종료되므로 delete [] 구문이 실행되지 않아 메모리 누수가 발생함

void test3(int n)
{
	double * ar = new double[n];
	...
	try {
		if (oh_no)
			throw exception();
	}
	catch (exception & ex)
	{
		delete [] ar;
		throw;
	}
	...
	delete [] ar;
	return;
}

위와같이 예외를 발생시킨 함수에서 예외를 포착하고 catch 블록 안에서 메모리를 해제한 후 다시 예외를 발생시킴으로써 문제를 해결할 수 있음
그러나 실수가 발생하거나 다른 에러를 저지를 가능성이 높아지므로 auto_ptr 템플릿을 사용하는 방법이 권장됨



15.4 RTTI

RTTI : runtime type identification, 실행 시간 데이터형 정보
프로그램이 실행 도중에 객체의 데이터형을 결정하는 표준 방법을 제공

RTTI의 목적

하나의 공통 기초 클래스로부터 상속된 클래스 계층이 있을 때, 클래스 계층에 속해있는 클래스의 어떤 객체를 기초 클래스 포인터가 지시하도록 설정할 수 있음
클래스들 중 하나의 객체를 생성하고, 이를 기초 클래스 포인터에 대입하고 싶은 경우 해당 포인터가 지시하는 객체의 종류를 알아야하는 경우가 존재함

  • 파생 객체가 상속되지 않은 메소드를 가지고 있을 경우
  • 디버깅 목적으로 어떤 종류의 객체들이 생성되는지 추적하고 싶을 경우

RTTI의 동작 방식

dynamic_cast 연산자가 기초 클래스형을 지시하는 포인터로부터 파생 클래스형을 지시하는 포인터를 생성, 불가능하다면 널포인터를 리턴
typeid 연산자는 어떤 객체의 정확한 데이터형을 식별하는 하나의 값을 리턴
type_info 구조체는 어떤 특별한 데이터형에 대한 정보를 저장

RTTI는 가상 함수들을 가지고있는 클래스 계층에 대해서만 사용 가능
파생 객체들의 주소를 기초 클래스 포인터들에 대입해야하는 유일한 클래스 계층이기 때문

dynamic_cast 연산자

포인터가 지시하는 객체형이 무엇인지 알려주는 대신, 객체의 주소를 특정형의 포인터에 안전하게 대입할 수 있는지를 판별

class Grand { ... };
class Superb : public Grand { ... };
class Magnificent : public Superb { ... };

Grand * pg = new Grand;
Grand * ps = new Superb;
Grand * pm = new Magnificent;

Magnificent * p1 = (Magnificent *) pm; // #1
Magnificent * p2 = (Magnificent *) pg; // #2
Superb * p3 = (Magnificent *) pm; // #3

#1은 포인터와 객체가 동일한 데이터형이기 때문에 안전함
#2는 기초 클래스 객체를 파생 클래스에 대입하므로 불안전함

  • 이 때 프로그램은 기초 클래스 객체가 파생 클래스의 특성을 가지고있다고 예상함
  • 그러나 파생 클래스는 기초 클래스에 없는 데이터 멤버들을 가질 수 있으므로 문제가 발생할 수 있음
    #3는 파생 클래스 객체의 주소를 기초 클래스 포인터에 대입하므로 안전함

어떤 메소드를 호출하는데 반드시 정확한 데이터형 일치가 필요한 것이 아니므로, 데이터형 변환이 안전한지 물어보는 것이 어떤 종류의 객체를 지시하는지 물어보는 것보다 더 일반적이고 유용함
Superb * pm = dynamic_cast<Superb *>(pg);와 같은 형태로 사용할 경우 포인터 pgSuperb *형으로 안전하게 변환될 수 있는지 여부를 판단함

  • 변환 가능하다면 dynamic_cast 연산자는 해당 객체의 주소를 리턴
  • 변환 불가능하다면 널포인터인 0을 리턴
// rtti1.cpp

#include <iostream>
#include <cstdlib>
#include <ctime>
using std::cout;

class Grand
{
	private:
		int hold;
	public:
		Grand(int h = 0) : hold(h) {}
		virtual void Speak() const { cout << "나는 Grand 클래스이다!\n"; }
		virtual int Value() const { return hold; }
};

class Superb : public Grand
{
	public:
		Superb(int h = 0) : Grand(h) {}
		void Speak() const { cout << "나는 Superb 클래스이다!!\n"; }
		virtual void Say() const { cout << "내가 가지고 있는 Superb 값은 " << Value() << "이다.\n"; }
};

class Magnificent : public Superb
{
	private:
		char ch;
	public:
		Magnificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}
		void Speak() const { cout << "나는 Magnificent 클래스이다!!\n"; }
		void Say() const { cout << "네가 가지고 있는 문자는 " << ch
						   << "이고, 정수는 " << Value() << "이다.\n"; }
};

Grand * GetOne();

int main()
{
	std::srand(std::time(0));
	Grand * pg;
	Superb * ps;
	for (int i = 0; i < 5; i++)
	{
		pg = GetOne();
		pg->Speak();
		if ((ps = dynamic_cast<Superb *>(pg)))
			ps->Say();
	}
	return 0;
}

Grand * GetOne()
{
	Grand * p;
	switch (std::rand() % 3)
	{
		case 0: p = new Grand(std::rand() % 100);
			break;
		case 1: p = new Superb(std::rand() % 100);
			break;
		case 2: p = new Magnificent(std::rand() % 100,
				'A' + std::rand() % 26);
			break;
	}
	return p;
}

Say() 함수는 Grand 클래스에는 정의되어있지 않기 때문에 구분이 필요
따라서 if (ps = dynamic_cast<Superb *>(pg)) 구문을 사용함

  • pg 객체가 Superb 또는 Magnificient 클래스라면 ps에는 정상적으로 객체의 주소가 대입되어 조건문이 참이 됨
  • 객체가 Grand 클래스였을 경우 ps에는 널포인터가 대입되어 조건문이 거짓이 됨
#include <typeinfo>
...
try
{
	Superb & rs = dynamic_cast<Superb &>(rg);
	...
}
catch (bad_cast &)
{
	...
};

dynamic_cast를 참조와 같이 사용할 경우, 널포인터에 해당하는 참조값이 존재하지 않기 때문에 dynamic_castbad_cast형의 예외를 발생시킴
이 예외는 exception 클래스로부터 파생되었으며 typeinfo 헤더 파일에 정의되어있음

typeid 연산자와 type_info 클래스

typeid의 연산자를 사용하여 두 객체의 데이터형이 같은지 결정할 수 있음

  • 매개변수로 클래스의 이름 또는 객체로 평가되는 식을 사용
  • typeinfo 헤더 파일에 정의되어있는 type_info 클래스형의 객체에 대한 참조를 리턴
  • type_info 클래스는 ==!= 연산자를 오버로딩하여 데이터형을 비교하는데 사용
  • 두 객체의 데이터형이 같다면 true, 아니라면 false를 반환
  • 널 포인터에 대한 값과 비교할시 exception 클래스로부터 파생된 bad_typeid 예외를 발생시킴
  • type_info 클래스에 대한 구현은 각각 다를 수 있으며, 일반적으로는 클래스의 이름인 시스템에 따른 문자열을 리턴하는 name() 멤버를 포함함
// rtti2.cpp

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <typeinfo>
using std::cout;

class Grand
{
	private:
		int hold;
	public:
		Grand(int h = 0) : hold(h) {}
		virtual void Speak() const { cout << "나는 Grand 클래스이다!\n"; }
		virtual int Value() const { return hold; }
};

class Superb : public Grand
{
	public:
		Superb(int h = 0) : Grand(h) {}
		void Speak() const { cout << "나는 Superb 클래스이다!!\n"; }
		virtual void Say() const { cout << "내가 가지고 있는 Superb 값은 " << Value() << "이다.\n"; }
};

class Magnificent : public Superb
{
	private:
		char ch;
	public:
		Magnificent(int h = 0, char c = 'A') : Superb(h), ch(c) {}
		void Speak() const { cout << "나는 Magnificent 클래스이다!!\n"; }
		void Say() const { cout << "네가 가지고 있는 문자는 " << ch
						   << "이고, 정수는 " << Value() << "이다.\n"; }
};

Grand * GetOne();

int main()
{
	std::srand(std::time(0));
	Grand * pg;
	Superb * ps;
	for (int i = 0; i < 5; i++)
	{
		pg = GetOne();
		cout << "지금 " << typeid(*pg).name() << "형을 처리하고있습니다.\n";
		pg->Speak();
		if ((ps = dynamic_cast<Superb *>(pg)))
			ps->Say();
		if (typeid(Magnificent) == typeid(*pg))
			cout << "그래, 너야말로 진짜 Magnificent 클래스이다.\n";
	}
	return 0;
}

Grand * GetOne()
{
	Grand * p;
	switch (std::rand() % 3)
	{
		case 0: p = new Grand(std::rand() % 100);
			break;
		case 1: p = new Superb(std::rand() % 100);
			break;
		case 2: p = new Magnificent(std::rand() % 100,
				'A' + std::rand() % 26);
			break;
	}
	return p;
}

RTTI의 오용

Grand * pg;
Superb * ps;
for (int i = 0; i < 5; i++)
{
	pg = GetOne();
	pg->Speak();
	if ((ps = dynamic_cast<Superb *>(pg)))
		ps->Say();
}

위 코드를 아래와같이 typeid를 사용하여 dynamic_cast와 가상 함수들을 무시한 형태로 다시 작성할 수 있음

Grand * pg;
Superb * ps;
Magnificent * pm;
for (int i = 0; i < 5; i++)
{
	pg = GetOne();
	if (typeid(Magnificent) == typeid(*pg))
	{
		pm = (Magnificent *) pg;
		pm->Speak();
		pm->Say();
	}
	else if (typeid(Superb) == typeid(*pg))
	{
		ps = (Superb *) pg;
		ps->Speak();
		ps->Say();
	}
	else
		pg->Speak();
}

그러나 코드가 길어질 뿐만 아니라 각 클래스의 이름을 명시적으로 지정해야하는 심각한 문제가 존재하기 때문에 사용이 권장되지 않음



15.5 데이터형 변환 연산자

struct Data
{
	double data[200];
};

struct Junk
{
	int junk[100];
};

Data d = {2.5e33, 3.5e-19, 20.2e32};
char * pch = (char *) (&d); // #1 
char ch = char (&d); // #2
Junk * pj = (Junk *) (&d); // #3

위 세가지 변환은 모두 이치에 맞지 않지만 C에서는 세가지가 모두 허용됨
따라서 데이터형 변환 과정을 더욱 엄격하게 규정하는 데이터형 변환 연산자가 추가됨
dynamic_cast

  • dynamic_cast < type-name > (expression) 형태로 사용
  • 어떤 클래스 계층 내에서 업캐스트를 허용하고 다른 데이터형 변환은 허용하지 않기 위해 사용

const_cast

  • const_cast < type-mame > (expression) 형태로 사용
  • 어떤 값을 constvolatile로 변환 또는 그 반대의 변환을 위해 사용
  • 단, 이 때 대상들의 데이터형은 모두 동일해야함
  • 대부분의 시간에는 상수로 존재하지만 이따금 값을 바꾸어 주어야 하는 경우에 유용하게 사용됨
// constcast.cpp

#include <iostream>
using std::cout;
using std::endl;

void change(const int * pt, int n);

int main()
{
	int pop1 = 38383;
	const int pop2 = 2000;

	cout << "pop1, pop2: " << pop1 << ", " << pop2 << endl;
	change(&pop1, -103);
	change(&pop2, -103);
	cout << "pop1, pop2 : " << pop1 << ", " << pop2 << endl;

	return 0;
}

void change(const int * pt, int n)
{
	int * pc;

	pc = const_cast<int *>(pt);
	*pc += n;
}

const_cast는 포인터의 접근을 변경할 수는 있으나, const로 선언된 값을 변경하려는 시도는 그 결과가 정의되지 않음
즉, pc 포인터는 const_cast를 지니고있기 때문에 값을 변화시키는데 사용될 수 있으나 그 원래 값이 const가 아니어야함

static_cast

  • static_cast < type-name > (expression)의 형태로 사용
  • type-nameexpression이 가지는 데이터형으로 암시적으로 변환 가능할 때에만 유효함
  • 따라서 열거값은 데이터형 변환이 없어도 정수값으로 변환할 수 있기 때문에 static_cast의 사용이 가능함

reinterpret_cast

  • reinterpret_cast < type-name > (expression)의 형태로 사용
  • 위험한 데이터형 변환을 하기 위해 사용
  • 포인터형을 보다 작은 정수형 또는 부동소수점형으로는 변환할 수 없음
  • 함수 포인터를 데이터 포인터로 또는 그 반대도 불가능함


연습문제

1.
a)

class snap {
	friend class clasp;
	...
};	
class clasp { ... };

b)

class muff;

class cuff {
	public:
		void snip(muff &) { ... }
		...
};
class muff {
	friend void cuff::snip(muff &);
	...
}

c)

class muff;

class cuff {
	public:
		void snip(muff &) { ... }
};
class muff {
	friend void cuff::snip(muff &);
	...
};

2. 사전 선언만으로는 충분하지 않기 때문에 불가능함

A가 B의 멤버 함수를 프렌드로 가지려면 B의 선언이 A 선언보다 앞에 있어야하지만, 이 때 사전 선언을 하더라도 B의 멤버 이름은 알려주지 않기 때문

3.

class Ribs
{
	private:
		class Sauce
		{
				int soy;
				int sugar;
			public:
				Sauce(int s1, int s2) : soy(s1), sugar(s2) {}
		};
		...
};

위의 경우 `Sauce` 클래스에 대한 접근을 할 수 있는 `public` 인터페이스에 `Sauce` 객체의 생성자 뿐임    

4. `return`의 경우 호출한 함수로 되돌아가 실행을 계속하지만, `throw`의 경우 `try` 블록을 발견할 때까지 함수 호출들의 연쇄를 거슬러 올라가 `catch` 블록으로 넘어감    

5. 가장 마지막 계층부터 순서대로 배치하여 가장 빠른 파생 클래스가 마지막으로 와야함     

6.  #1의 경우 `pg`에 `Grand` 클래스가 오면 널포인트를 반환되어 조건문이 거짓이 되고, 나머지 클래스들의 경우 변환이 가능하기 때문에 `ps`에 객체의 주소가 리턴되어 조건문이 참이 됨    
#2의 경우 `pg`가 `Superb` 클래스인 경우에만 조건문이 참이 됨  

7. `dynamic_cast`는 한 클래스 계층에서 업캐스트만 허용함    
`static_cast`는 업캐스트와 다운캐스트를 모두 허용함    

'개인공부 > C++ 기초플러스' 카테고리의 다른 글

C++ Primer 16  (0) 2023.02.21
C++ Primer 15 Exercise  (0) 2023.02.21
C++ Primer 14 Exercise  (0) 2023.02.21
C++ Primer 14  (0) 2023.02.21
C++ Primer 13 Exercise  (0) 2023.02.19

댓글