본문 바로가기

고급C,C++

[C++] 프렌드, 예외, 기타 사항

프렌드 클래스의 모든 메서드는 오리지널 클래스의 private 멤버와 protected 멤버에 접근할 수 있다. 또한 더욱 제한을 가하여 어떤 클래스의 특정 멤버 함수들만 다른 클래스의 프렌드가 되도록 지정할 수 있다. 클래스는 함수, 멤버함수, 클래스 중 어떤 것이 프렌드인지 정의한다. 프렌드 관계는 바깥 세계에서는 설정할 수 없다. 그러므로 프렌드들이 클래스의 private 부분에 대한 외부 접근을 허용하더라고도 그들이 객체 지향 프로그래밍 철학을 실제로 위반하는 것은 아니다. 오히려 public 인터페이스에 더 많은 융통성을 제공한다. 

1. 프랜드클래스
Tv 클래스와 리모콘 클래스 사이의 관계
friend class Remote;
프렌드 선언은 public, private, protected 부분 어디에나 둘 수 잇다.


2. 프렌드 멤버 함수
class Tv
{
public:
friend void Remote::set_chan(Tv & t, int c);
...
}
//Remote::set_chan()만이 Tv의 private 멤버들을 변경할 수 있다.


















예외 메커니즘

 while(cin>>x>>y)
{
try
{
z = hmean(x,y);
}
catch (const char * s)
{
cout<<s<<endl;
cout<<"두수를 새로 입력하십시오: ";
continue;
}
}

double hmean(double a, double b)
{
if(a == -b)
{
throw "잘못된..."
}
return 2.0*a*b/(a+b);
}





예외로 객체 사용하기


#include <iostream>
#include <cmath>

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

class bad_gmean
{
private:
double v1;
double v2;
public:
bad_gmean(double a = 0, double b = 0) : v1(a),v2(b){}
const char * mesg();
};
inline void bad_gmean::mesg()
{
return "gmean() 전달인자들은 >= 0 이어야 합니다."<<endl;
}

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

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

double x, y, z;

cout<<"두 수를 입력하십시오:";
while(cin>>x>>y)
{
try{
z = hmean(x,y);
gmean(x,y);

}
catch(bad_hmean & bg)
{
bg.mesg();
continue;
}
catch(bad_gmean & hg)
{
cout<<hg.mesg();
break;
}
}
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);
}











stdexcept 예외 클래스

사용자가 what() 메서드에 의해 리턴되는 문자열을 제공하는 (생성자가 string을 받는다) 클래스

logic_error
일반적인 논리에러
domain_error
전달인자 범위를 벗어나면(아크상인에서 정의역 -1~1인데 전달인자가 정의역인 범위를 벗어날때)
invalid_argument
기대하지 않은 값이 함수에 전달되었을 경우: 0,1을 기대하지만 문자열을 받아올 경우
length_error
최대 허용 길이를 벗어 날때(문자열 길이)
out_of_bounds
일반적으로 인덱싱 에러 예를 들면 사용된 인덱스가 그 배열에 대해 적절하지 않으면 operator()[]가 out_of_bounds 예외를 발생시키는, 배열 비슷한 클래스를 정의할 수 있다.


runtime_error 패밀리 : 예측이 어렵고 예방도 어려운, 실행하는 동안에 나타날 수 있는 에러들.
range_error
overflow_error
underflow_error


try
{
..
}
catch(out_of_bounds & oe) //out_of_bounds 에러를 포착한다.
{...}
catch(logic_error & oe) //나머지 logic_error 패밀리를 포착한다.
{...}
catch(exception & oe) // runtime_error, exception 객체들을 포착한다.
{...}



bad_alloc 예외와 new

C++ 시스템들은 new를 사용할 때 일어나는 메모리 할당 문제를 해결하는 두 가지 방법을 제공한다. 첫 번째 방법은, 메모리 요청을 충적시킬 수 없을 경우에, new가 널 포인터를 리턴하는 것이다. 두 번째 방법은, new 가 bad_allocation예외를 발생 시키는 것이다. 


//두가지 모두 한 예제로 보자.
#include <iostream>
#include <new>
#include <cstdlib>

using namespace std;

struct Big
{
double stuff[2000];
};

int main()
{

Big * pb;

try
{
cout<<" 덩치 큰 메모리 블록을 요청하고 있습니다.\n";
pb = new Big[10000]; //1,600,000,000 바이트
cout<<" 새로운 요청이 거부되었습니다.\n";
}
catch(bad_alloc & ba)
{
cout<<"예외가 포착되었습니다.\n";
cout<<ba.what()<<endl;
exit(EXIT_FAILURE);
}
if(pb!=0)
{
pb[0].stuff[0] = 4;
cout<<pb[0].stuff[0]<<endl;
}
else
cout<<"pd는 널 포인터입니다.\n";
delete[] pb;
return 0;
}






잘못된 예외
예외가 발생한 후에도, 문제를 일으킬 수 있는 두 가지 가능성이 있다. 첫째, 예외가 예외 지정을 사용하는 함수 안에서 발생하면, 그 예외는 예외 지정자 리스트에 있는 데이터형들 중의 어느 하나와 일치해야 한다. 만약 예외가 예외 지정과 일치하지 않으면, 그 예외는 기대하지 않는 예외(unexpected exception)라는 낙인이 찍히고, 기본적으로 프로그램 실행이 중지된다. 예외가 이 첫 번째 장애물을 뛰어넘었다고 하더라도, 예외는 반드시 포착되어야 한다. 예외가 포착되지 않으면, 포착되지 않ㄴㄴ 예외라는 낙인이 찍히고, 기본적으로 프로그램 실행이 중지된다. 그러나 기대하지 않은 예외나 포착되지 않은 예외에 대한 프로그램의 응답을 사용자가 바꿀 수 있다.

<포착되지 않는 예외> : 이것은 try 블록이 없거나, 예외와 일치하는 catch 블록이 없을 때 발생한다.
포착되지 않는 예외는 프로그램을 즉각 중지시키지 않는다. 먼저 프로그램은 terminate()라는 함수를 호출한다. 기본적으로 terminate()가 abort() 함수를 호출한다. terminate()가 호출하는 abort() 함수 대신에 다른 함수를 호출하도록 등록함으로써, terminate()의 행동을 바꿀 수 있다. 이를 위해 set_terminate() 함수를 호출하라. set_terminate() 와 terminate()는 exeption 헤더 파일에 등록 되어 있다.

typedef void (*terminate_handler)();
terminate_hander set_terminate(terminate_handler f) throw();
void terminiate();

void myQuit()
{
cout<<"포착되지 않는 예외가 발생하여 프로그램을 중지시킵니다.\n";
exit(5);
}
set_terminate(myQuit);
예외가 발생했으나 포착되지 않으면, 프로그램은 terminate()를 호출하고 terminate()가 myQuit()를 호출한다.






<기대하지 않는 예외> 
어떤 함수에 예외 지정을 사용함으로써, 그 함수의 사용자들은 어떤 예외들이 포착될 것인지 알 수 있다. 예를 들어, 다음과 같은 함수 원형이 있다고 가정하자.
double Argh(double, double) throw(out_of_bounds);
그러면 이제, 그 함수를 다음과 같은 방법으로 사용할 수 있다.
try{
x = Argh(a,b);
{
catch(out_of_bounds & ex)
{
...
}

어떤 예외들이 포착될지를 안다는 것은 바람직한 일이다. 기본적으로 포착되지 않는 예외는 프로그램을 중지시킨다. 그러나 여기에서 약간 생각할 것이 딨다. 원칙적으로 예외 지정은, 그 예외 지정을 사용하는 함수가 호출하는 함수들이 발생시키는 예외들을 포함해야 한다. 예를 들어, Argh()가 retort 객체 예외를 발생시키는 Duh() 함수를 호출한다면, Duh() 예외 지정뿐만 아니라 Argh() 예외 지정에도 retort가 들어있어야 한다. 모든 함수들을 혼자서 신중하게 작성한다면 가능할지 모르겠지만, 그렇지 않은 경우에는 이 일이 제대로 이루어지리라고 보장 할 수 없다. 예를 들면, 예외 지정을 사용하지 않는 함수들이 들어 있는, 오래된 상업용 라이브러리를 구입하여 사용하는 경우도 있다. 그러므로 어떤 함수가 예외 지정에 없는 예외를 발생시킨다면, 무슨 일이 벌어지는지 자세히 살펴보아야 한다. 기대하지 않는 에외의 행동은 포착되지 않는 예외의 행동과 많이 비스하다. 기대하지 않는 예외가 발생했다면, 프로그램은 unexpected() 함수를 호출한다. 이어서 unexpected() 함수는 terminate() 함수를 호출하고, terminate()는 기본적으로 abort()를 호출한다. terminate()의 행동을 바꾸는 set_terminate() 함수가 준비되어 있듯이, unexpected()의 행동을 바꾸는 set_unexpected() 함수가 준비되어 있다. 이 새로운 함수들도 exception 헤더 파일에 선언되어 있다.

typedef void (*unexpected_hander)();
unexpected_handler set_unexpected(unexpected_hander f) throw();
void unexpected();



사용자가 set_unexpected() 를 위해 제공하는 함수의 행동은, 사용자가 set_terminate()에 제공하는 함수의 행동보다 더 세밀히 제어할 수 있다. 특별히 unexpected_handler 함수는 다음과 같은 선택을 가지고 있다.
.terminate()(디폴트 행동), abort() 또는 exit()를 호출하여 프로그램을 종료시킬 수 있다.
.예외를 발생시킬 수 있다.

두번째 예외를 발생키는 역할을 할때
 .새로 발생된 예외가 오리지널 예외 지정과 일치하면, 프로그램은 그 곳으로부터 정상적으로 진행된다. 즉, 새로 발생된 예외와 일치하는 catch 블록을 찾는다.
 .새로 발생된 예외가 오리지널 예외 지정과 일치하지 않고, 그 예외 지정에 std:bad_exception 형이 들어있지 않으면, 프로그램은 terminate()를 호출한다. bad_exception형은 exception형으로부터 파생횐다. 그리고 exception 헤더 파일에 선언되어 있다.
 .새로 발생된 예외가 오리지널 예외 지정과 일치하지 않고, 오리지널 예외 지정에 std::bad_exception 형에 들어 있으면, 그 일치하지 않는 예외는 std::bad_exception형의 예외로 대체된다.

=>>>>요약하면
기대되는 것이든 기대되지 않는 것이든 모든 예외를 포착하고 싶으면 다음과 같이 해볼 수 있다. 먼저 exception 헤더 파일의 선언들을 사용할 수 있게
 #include <exception> using namespace std;
그 다음에, 기대하지 않는 예외들을 bad_exception형으로 변환하고, 적절한 원형을 가지고 있는 대체 함수를 설계한다.
void myUnexpected()
{
throw std::bad_exception(); 또는 그냥 throw;
}
예외 없이 그냥 throw만 사용하면 오리지널 예외가 다시 발생횐다. 그러나 예외 지정에 그 데이터형이 들어 있으면, 그 예외는 bad_exception 객체로 대체된다 
그 다음에, 프로그램의 시작 위치에 이 함수를 프로그래머가 선택한 기대하지 않는 예외 행동으로 등록한다.
set_unexpected(myUnexpected);
마지막으로, 예외 지정과 catch 블록들의 연쇄에 bad_exception 형을 포함시킨다.
double Argh(double, double) throw(out_of_bounds, bad_exception);
...
try{
x = Argh(a,b);
}
catch(out_of_bounds & ex)
{
...
}
catch(bad_exception & ex)
{
...
}






예외 주의사항
void test1(int n)
{
string mesg("나는 무한 루프에 빠졌습니다!");
...
if(oh_no)
throw exception();
...
return;
}
string  클래슨느 동적 메모리 할당을 사용한다. 일반적으로, mesg를 위한  string 파괴자는, test1() 함수가 return 명령문에 도달하여 종료될 때 호출 된다. throw 명령문은 그 함수를 보다 일찍 종료시키지만, 스택 풀기 기능의 덕택으로 파괴자가 호출되는 것을 허용한다. 따라서 이 경우에 메모리가 바르게 관리된다.

void test2(int n)
{
double * ar = new double[n];
...
if(oh_no)
throw exception();
...
delete[] ar;
return;
}
여기에는 문제가 있따. 스택 풀기는 스택으로부터 변수 ar를 삭제한다. 그러나 함수가 일찍 종료하므로 함수 끝에 있는 delete[] 명령문이 실행되지 않는다. 포인터는 삭제되었지만 그 포인터가 지시하는 메모리 블록은 여전히 살아있다. 그런데거기에 접근할 수가 없다. 다시 말해 메모리 누설이 발생한다.

이와 같은 메모리 누설은 해결할 수 있다. 예를 들어, 예외를 발생시킨 함수에서 그 예외를 포착하고, catch 블록 안에 메모리 해제 코드를 넣고, 그리고 예외를 다시 발생시킨다.

void test3(int n)
{
double * ar = new double[n];
...
try{
if(oh_no)
throw exception();
}
catch(exception & ex)
{
delete [] ar;
throw;
}
...
delete [] ar;
return;
}
그러나 이 방법은 실수로 무언가를 빠뜨리거나 다른 에러를 저지를 가능성을 높인다. 또 다른 해결책이 있다!! 바로 auto_ptr 템플릿 이용!!!!