▣ 예외의 개념
● 예외, Exception
▷ 예외: 프로그램 실행 중 비정상적 event가 발생하는 것
- 예를 들면, 자원의 부족이나 비정상적 데이터로 인한 비정상적 사건
▷ 예외 상황에 대비를 하지 않으면 프로그램이 실행되지 않는다.
- 프로그램이 문제 없이 동작하도록 에외 발생에 대비한 처리를 미리 정의해놔야 한다.
● 예외 상황 예시
▷ 비정상적 데이터 처리의 예
double func(double a, double b)
{
return a*b/(a-b);
}
// a == b 이면 오류가 발생한다.
위와 같은 경우를 대비해서 아래와 같은 처리를 해준다.
double func(double a, double b)
{
if ( a == b) {
cout << "분모가 0이 되어 연산을 할 수 없습니다." << endl;
exit(EXIT_FAILURE);
return a*b/(a-b);
}
- a == b가 되면 이에 대한 메시지를 띄우고 EXIT_FAILURE로 문제가 발생하여 프로그램이 종료됨을 알려주고 프로그램을 강제로 종료한다.
▷ 프로그램이 요청한 자원을 할당할 수 없는 경우
void f() {
int *p = new(nothrow) int[1000000000];
... // 핟당된 메모리 활용 명령
}
- 시스템이 요청한 메모리 크게를 할당하지 못했을 때 badalloc이라는 예외 객체를 던지는데, 그것을 처리하지 않으면 프로그램이 비정상적으로 동작하게 된다. 그래서 이를 해결하기 위해 new(nothrow)를 이용해서 예외를 발생시키지 않는 new 연산자 형태로 동작하게 된다.
void f() {
int *p = new(nothrow) int[1000000000];
if (!p) {
cerr << "Memory allocation error" << endl;
exit(EXIT_FAILURE);
}
}
- 요청한 메모리 크기를 할당하지 못하면 실행 중 오류가 발생해서 프로그램이 비정상적으로 종료된다. 이런 경우에는 new(nothrow)가 nullptr을 return한다.
- 그래서 p가 nullptr인 경우에 메시지를 띄우며 EXIT_FAILURE로 프로그램을 강제 종료하고, 아닌 경우에는 메모리가 제대로 할당된 것이니 프로그램이 정상동작한다.
▣ C++ 언어의 예외처리 체계
● C++ 언어의 에외처리 구문
▷ try { }, catch { }, throw 문장으로 구성된다. (JAVA와 동일)
RetType1 someFunction()
{
try {
// 예외 발생 가능성 있는 부분
someDangerousFunction();
}
catch (eClass e) {
// 발생한 예외 처리
exceptionProcRtn();
}
}
RetType2 someDangerousFunction()
{
if (예외검출조건)
throw eObject; // 예외 발생 알림
else
... // 정상처리
}
- 에러 발생을 알리기 위해 throw 명령으로 예외 객체를 전달하고 catch { }으로 전달되고 이 때, catch의 인수 eClass e에 eObject 객체가 들어가서 catch 블록 내의 exceptionProcRtn으로 적절한 처리가 이루어진다.
- 에러가 발생하지 않은 경우에는 else의 정상처리 구문을 수행하고 someDangerousFunction()이 종료되고 그 다음 동작을 정상적으로 수행한다.
● 예외 처리 예시
▷ HMean.cpp
- 예외가 발생하면 "We cannot calculate ~ " 문자열이 catch의 인수인 const char* s로 매개변수 전달되어 예외처리가 수행된다.
- catch { }에서 예외처리 후 continue 명령에 의해 다시 while (cin >> x >> y) { } 구문으로 올라가서 다시 수행된다.
#include <iostream>
using namespace std;
double hmean(double a, double b)
{
if (a == -b)
throw "We cannot calculate harmony mean!";
return 2.0*a*b / (a+b);
}
int main()
{
double x, y, z;
cout << "Enter two numbers (x, y) : ";
while (cin >> x >> y) {
try { // 예외 발생할 수 있는 부분
z = hmean(x, y);
}
catch (const char* s) { // 예외처리
cout << s << endl;
cout << "Enter another numbers (x, y) : ";
continue;
}
cout << "harmony mean = " << z << endl;
cout << "Enter next two numbers (x, y), if you want to quit press Q : ";
}
return 0;
}
▷ 출력결과
● 예외 유형에 따른 처리
▷ 하나의 try { } 블록에 여러 catch { } 블록을 사용한다.
- throw된 예외 객체 자료형에 맞게 매개변수가 선언된 catch { }에서 예외를 처리하게 된다.
- 여러 예외처리 블록을 만들고 마지막에 catch (...) { }를 통해 나머지 모든 예외처리가 가능하게 해준다.
try {
.... // 에외 발생할 수 있는 함수 호출
}
catch (eClass1 e) {
.... // 예외처리 블록 1
}
catch (eClass2 e) {
.... // 예외처리 블록 2
}
catch (eClass3 e) {
.... // 예외처리 블록 3
}
catch (...) { // 점 3개만 써야 함. 다른 때와 달리 의미가 있는 점들임...
.... // 그 외 나머지 모든 예외처리
}
● 제어 전달
f1( ), f2( ), f3( )함수의 수행 과정을 살펴보고 정상적인 처리 과정과 예외 처리 과정을 살펴본다.
void f1()
{
....
try {
f2( );
}
catch (const char* s) {
....
}
...
}
void f2()
{
....
f3( );
...
return;
}
void f3()
{
....
if (ex_condition)
throw "help";
...
return;
}
▷ 정상적인 제어 흐름
- f1( )함수 시작 후 f1( )안에 f2( )호출
- f2( )안에 f3( ) 호출
- f3( )안에 예외 조건 만족하지 않으면 정상적으로 return
- 다시 f2( )로 돌아와서 f3( ); 다음 구문부터 정상 수행해서 return
- 다시 f1( )으로 돌아와서 f2( ) 구문 다음에 있는 catch { } 건너뛰고 그 다음 구문부터 실행 ~~
▷ 예외 발생 시 제어 흐름
- f1( )함수 시작 후 f1( )안에 f2( )호출
- f2( )안에 f3( ) 호출
- f3( )안에 예외 상황 발견되면 throw 명령 수행 후 f1( )의 catch { }의 인수로 전달된다.
- f1( )의 catch { }에서 예외처리를 수행하고 그 다음 구문부터 수행된다.
- 이 경우에는 f2( )와 f3( )에서 throw 명령 뒤의 구문들이 수행되지 않고 또 제대로 return되지 않아 소멸절차를 거치지 않아서, 때에 따라서 각 함수에 필요없는 자원 소실 문제를 야기한다.
● 예외처리에 따른 자원 관리 문제 가능성
▷ 자원 소실이 가능한 상황
void f()
{
int *p = new int[1000];
for (int i=0 ; i<1000 ; i++)
p[i] = i;
.....
if (ex_condition)
throw "exception";
.....
delete[] p;
}
- 예외 처리를 위해 catch 블록으로 돌아올 때 f( )가 호출되기까지 그 중간 과정을 거친 함수들의 지역변수는 정상적으로 소멸된다. p라는 변수 자체는 없어졌다.
- 하지만 throw 명령이 실행되면서 나머지 문장들이 실행되지 않아서 p가 가리키고 있던 동적 할당 메모리는 소멸되지 않는다.
- 이런 경우에는 smart pointer를 이용해서 자원 소실 문제를 해결할 수 있다.
▷ smart pointer 이용
- unique_ptr : 할당된 메모리를 한 개의 포인터만 가리킬 수 있다.
☞ 다른 unique_ptr에 대입할 수 없고, 이동 대입만 가능하다.
☞ unique_ptr이 제거되거나 nullptr을 대입하면 가리키고 있던 메모리를 반납한다.
- shared_ptr : 할당된 메모리를 여러 개 포인터로 가리킬 수 있다.
☞ 다른 shared_ptr에 대입, 이동 대입 둘 다 가능하다.
☞ 포인터가 제거되거나 nullptr을 대입하는 등을 통해 그 메모리가 가리키는 shared_ptr이 없는 상태가 되면 메모리를 반납한다.
▷ smart pointer 이용 예시
- unique_ptr과 shared_ptr을 사용하기 위해 <memory>를 헤더파일에 include해줘야 한다.
- unique_ptr로 선언한 포인터가 가리키는 메모리를 다른 포인터가 가리키게 할 순 없고 포인터 이동은 가능하다.
- move(p1)으로 p2로 포인터 이동을 시키고 p2=nullptr;을 통해 빈 포인터로 만들어주면 가리키는 메모리가 없기 때문에 저절로 소멸되게 된다. delete[ ] 절차를 굳이 하지 않아도 된다.
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1{ new int };
unique_ptr<int> p2;
*p1 = 10;
cout << *p1 << endl;
p2 = move(p1); // p2 = p1;은 불가
cout << *p2 << endl;
p2 = nullptr; // 가리키고 있던 메모리는 해제됨
return 0;
}
▷ smart pointer 이용 예시 (예외처리상황)
- unique_ptr을 활용해서 예외 처리로 인한 자원 소실을 방지한다.
- throw "exception" 아래 구문은 실행되지 않지만 지역변수인 포인터 p는 소멸이 되고 동시에 unique_ptr이므로 p가 가리키고 있던 메모리도 같이 delete 소멸시켜준다.
#include <iostream>
#include <memory>
void f()
{
std::unique_ptr<int[]> p { new int[1000] };
for (int i=0; i<1000; i++)
p[i] = i;
.....
if (ex_condition)
throw "exception";
.....
}
▷vector를 활용해서 예외처리로 인한 자원 소실 방지
- STL의 컨테이너인 vector는 변수가 없어지면 자동적으로 그 안에 가지고 있던 메모리를 시스템에 반납하기 때문에이런 자원 소실 문제를 신경쓰지 않아도 된다.
#include <iostream>
#include <memory>
void f()
{
std::vector<int> p(1000);
for (int i=0; i<1000; i++)
p[i] = i;
.....
if (ex_condition)
throw "exception";
.....
}
● noexcept 지정자
▷ noexcept 함수 지정
- 함수가 예외를 일으키지 않음을 미리 지정한다.
- 어떤 함수가 예외를 발생시킬 가능성이 없다는 걸 확신할 때 써서 예외처리를 신경쓸 필요 없다는 걸 명시한다.
- 프로그램에서 예외가 발생하면 그 프로그램 내의 모든 함수들을 검토할텐데 이런 일을 건너뛰게 해줘서 컴파일러의 예외 처리에 대한 부담이 덜어져 코드 최적화에 도움이 된다.
- noexcept라고 썻는데 함수에 오류 발생하면 프로그램에 문제가 생기겠죠 당연히..
- 예시
template <typename T>
T max(const vector<T>& v) noexcept {
auto p = v.begin();
T m = *p++;
for (; p != v.end(); p++)
if (m < *p) m = *p;
return m;
}
▣ 예외처리 클래스
● 클래스에서의 예외처리 활용
▷ 예외처리 클래스 활용
- 클래스 설계할 때 예외처리 기능을 포함하면 그 클래스 객체 내에서 예외가 발생했을 때 그 원인과 위치를 파악하기 쉽다.
- 클래스 선언문 안에 예외처리 담담 클래스를 선언하면 된다.
● 클래스 예외처리 예시
▷ IntArray1.h
- IntArray 클래스 내부에 BadIndex라는 클래스를 선언해줘서 예외의 유형을 알리는 역할을 하게끔 한다.
- 이 경우에는 [ ] 첨자를 잘못 사용했을 때 예외가 발생했음을 알려주기 위해 BadIndex 클래스를 선언했다.
.......
const int DefaultSize = 10;
class Array {
int *buf;
int size;
public:
Array(int s = DefaultSize);
virtual ~Array() {delete[] buf;}
int& operator[](int offset);
const int& operator[](int offset) const;
int getsize() const { return size; }
friend ostream& operator<<(ostream&, Array&)
class BadIndex {}; // exception class
};
▷ IntArray1.cpp
- offset이 0보다 작거나 배열 size보다 클 때 BadIndex 클래스를 예외객체로 전달한다.
#include "IntArray1.h"
using namespace std;
Array::Array(int s) : size(s) // 생성자
{
buf = new int[s];
memset(buf, 0, sizeof(int)*s);
}
int& Array::operator[](int offset)
{
if (offset < 0 || offset >= size) // 예외조건 검사
throw BadIndex(); // 예외객체 생성, 전달
return buf[offset];
}
▷ IA1Main.cpp
- i = 10일 때 배열의 인덱스 범위를 벗어나기 때문에 오류가 발생하기 때문에 try{ } 블록에 넣어준다.
- cerr은 오류 메시지를 콘솔에 출력하게 해주는 기능을 한다.
#include <iostream>
#include "IntArray1.h"
using namespace std;
int main()
{
Array arr(10);
try {
for (int i=0; i<=10; i++)
arr[i] = i;
}
catch (Array::BadIndex e) {
cerr << "인덱스 범위 오류 발생" << endl;
}
cout << arr << endl;
return 0;
}
● 클래스 예외처리 예시 - 위의 예에서 예외 정보 전달 기능을 추가한 버전
▷ IntArray1.h
- 잘못 사용된 인덱스인 n을 wrongIndex 멤버에 넣어줘서 예외객체를 만들어준다.
- 이로 인해, 어떤 인덱스 때문에 예외가 발생했는지 알 수 있게 해준다.
.......
const int DefaultSize = 10;
class Array {
int *buf;
int size;
public:
Array(int s = DefaultSize);
virtual ~Array() {delete[] buf;}
int& operator[](int offset);
const int& operator[](int offset) const;
int getsize() const { return size; }
friend ostream& operator<<(ostream&, Array&)
class BadIndex { // exception class
/* 이 부분 수정 */
public:
int wrongIndex;
BadIndex(int n)
: wrongIndex(n) {}
};
};
▷ IntArray1.cpp
- offset 값을 실매개변수로 받아서 IntArray1.h에서 wrongIndex로 객체가 전달된다.
#include "IntArray1.h"
using namespace std;
Array::Array(int s) : size(s) // 생성자
{
buf = new int[s];
memset(buf, 0, sizeof(int)*s);
}
int& Array::operator[](int offset)
{
if (offset < 0 || offset >= size)
/* 이 부분 수정 */
throw BadIndex(offset);
return buf[offset];
}
▷ IA1Main.cpp
- 어떤 인덱스 때문에 오류가 발생했는지 알려준다.
#include <iostream>
#include "IntArray1.h"
using namespace std;
int main()
{
Array arr(10);
try {
for (int i=0; i<=10; i++)
arr[i] = i;
}
catch (Array::BadIndex e) {
/* 이 부분 수정 */
cerr << "인덱스 범위 오류 발생 --> "
<< e.wrongIndex << endl;
}
cout << arr << endl;
return 0;
}
● exception 클래스
▷ exception 클래스가 뭔가?
- C++ 언어에서 예외 처리를 위해 예외처리 담당 클래스의 기초 클래스로서 제공되는 클래스다.
- 헤더파일 <exception>을 소스파일에 포함시켜서 이용한다.
- 가상함수 what( )을 멤버함수로 가지고 있음
☞ 예외의 종류를 char* 형태로 반환한다.
☞ exception의 파생 클래스에서 재정의하여 사용한다.
▷ 예시 - IntArray1.h 수정
#include <exception> // 이 부분 수정
const int DefaultSize = 10;
class Array {
int *buf;
int size;
public:
Array(int s = DefaultSize);
virtual ~Array() {delete[] buf;}
int& operator[](int offset);
const int& operator[](int offset) const;
int getsize() const { return size; }
friend ostream& operator<<(ostream&, Array&)
/* 이 부분 수정 */
class BadIndex : public exception { // exception class
public:
int wrongIndex;
BadIndex(int n) : wrongIndex(n), exception() {}
const char* what() const
{ return "Array Exception::";}
};
};
● 예외 객체를 다시 throw하기
▷ catch 블록에서 처리를 완결할 수 없는 예외를 전달하는 경우
- 현재 단계의 catch 블록에서 처리를 완결할 수 없는 예외에 대한 후속 처리가 가능하게끔 예외 객체를 다시 throw할 수 있다.
- 예시
class ExceptionClass : public exception {
.....
};
int f(int a) {
if (a < 0)
throw ExceptionClass();
....
};
int g(int b) {
try {
f(b);
.....
}
catch (ExceptionClass e)
..... // 현재 단게의 예외처리
throw;
}
}
int h(int c) {
try {
g(c);
.....
}
catch (ExceptionClass e) {
.... //후속 예외처리
}
}
'Programming > C++' 카테고리의 다른 글
[Modern C++ 공부 - Day1] 지역 변수 local variable (0) | 2023.06.15 |
---|---|
[Modern C++ 공부 - Day0] C++11, 14, 17는 뭐가 다를까? (1) | 2023.06.15 |
C++ 언어 기초 (15) - STL, vector, 알고리즘, map (0) | 2020.09.07 |
C++ 언어 기초 (14) - 템플릿 template (Feat. 버블정렬) (0) | 2020.09.05 |
C++ 언어 기초 (13) - 추상클래스, 다중상속 (0) | 2020.09.04 |