학습목표
- 프렌드 클래스
- 프렌드 클래스의 메소드
- 내포된 클래스
- 예외 처리,
try
블록,catch
블록 - 예외 클래스
- RTTI(실행 시간 데이터형 정보)
dynamic_cast
와typeid
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;
로 Remote
를 Tv
클래스에 대한 프렌드 클래스로 선언함으로써 Tv
클래스의 private
부분들을 직접적으로 접근 가능함
프렌드 멤버 함수
class Tv
{
friend void Remote::set_chan(Tv & t, int c);
...
}
Remote
클래스의 메소드들 중 Tv
클래스의 private
멤버에 직접 접근하는 메소드는 Remote::set_chan()
메소드 뿐임
따라서 클래스 전체를 프렌드로 만드는 대신 해당 메소드만 클래스에 대한 프렌드로 만드는 것이 가능함
단, 이 경우 컴파일러가 Remote
의 정의를 미리 알고있어야 하지만 Remote
는 Tv
객체를 사용하기 때문에 Tv
의 정의가 Remote
앞에 나타나야함
이러한 순환 종속을 피하기 위해 아래와 같이 사전 선언(forward declaration)을 사용함
class Tv;
class Remote { ... };
class Tv { ... };
Tv
와 Remote
의 순서를 반대로 할 경우 에러가 발생함
이는 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_error
와 runtime_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
를 사용한 메모리 대입시
- 메모리 요청을 충족시킬 수 없을 경우
new
가NULL
포인터를 리턴 new
가bad_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
클래스는 Sales
의 public
으로 선언되어있으므로 클라이언트 catch
블록들에서 해당 클래스를 하나의 데이터형으로 사용할 수 있음
단, 이 때 Sales::bad_index
형태로 사용되어야함 nbad_index
클래스도 마찬가지로 LabeledSales
의 public
으로 선언되어있으므로 클라이언트 코드에서 LabeledSales::nbad_index
로 사용할 수 있음
bad_index
와 nbad_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_handler
는 typedef
에 의해 매개변수를 사용하지 ㅇ낳고 리턴값도 없는 함수를 가리키는 포인터를 위한 데이터형 이름으로 정의됨 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);
와 같은 형태로 사용할 경우 포인터 pg
가 Superb *
형으로 안전하게 변환될 수 있는지 여부를 판단함
- 변환 가능하다면
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_cast
는 bad_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)
형태로 사용- 어떤 값을
const
나volatile
로 변환 또는 그 반대의 변환을 위해 사용 - 단, 이 때 대상들의 데이터형은 모두 동일해야함
- 대부분의 시간에는 상수로 존재하지만 이따금 값을 바꾸어 주어야 하는 경우에 유용하게 사용됨
// 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-name
이expression
이 가지는 데이터형으로 암시적으로 변환 가능할 때에만 유효함- 따라서 열거값은 데이터형 변환이 없어도 정수값으로 변환할 수 있기 때문에
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 |
댓글