C++ 예외(Exception)와 Abort함수

프로그래밍/C++ 2019.11.01 댓글 Plorence

프로그램을 실행하다 보면 가끔 정상적으로 실행을 계속할 수 없는 상황이 있습니다.

  • 사용 가능한 메모리보다 더 많은 양의 메모리를 요구함

  • 파일을 읽으려고 하는데 해당 파일이 없음

  • 인터넷 웹사이트에서 값을 가져오는데 인터넷 연결이 안 되어 있음

위와 같은 여러 상황이 생길 수 있습니다.

대부분의 프로그래머들은 이런 상황을 미리 예상하려고 노력합니다.

그래서 C++은 이러한 상황을 처리하기 위해서 예외를 추가했습니다.

 

abort() 호출

abort함수의 원형은 cstdlib헤더 파일에 들어있습니다.

일반적으로 abort()함수는 호출되었을 때 표준 에러 스트림에 "abnormal program temination"(비정상적인 프로그램 종료)과 같은 메시지를 보내고 프로그램을 종료시키도록 구현되어 있습니다.

#include <iostream>
#include <fstream>
#include <cstdlib>
int main(void) {
	std::ifstream is;
	is.open("list.txt");
	if (is.fail()) {
		std::cout << "파일이 없음.";
		std::abort();
	}
	return 0;
}

abort()함수 호출 시 나오는 문자열은 컴파일러마다 다릅니다.

그리고 abort()함수는 프로그램을 직접 종료하기 때문에 그리 올바른 방법은 아닙니다.

 

에러코드 리턴

에러코드 리턴은 abort함수를 호출하는 것보다 융통성 있는 방법입니다.

함수의 리턴값을 사용하여 문제가 뭔지 알립니다.

위에서 fail메서드 호출로 파일 읽기에 실패했는지 성공했는지에 대해 값을 반환합니다.

프로그램을 종료하지 않으려면 최소한 abort함수 말고 다른 방법을 택해야 합니다.

 

예외 매커니즘

예외는 앞서 말했듯이 계속 실행할 수 없는 상황에 대한 응답입니다.

프로그램의 어느 한 부분에서 다른 부분으로 제어를 넘기는 방법을 제공합니다. 예외처리는 세 단계로 이루어집니다.

  1. 예외를 발생시킨다.

  2. 핸들러를 사용하여 예외를 포착한다.

  3. try블록을 사용한다.

먼저 예외 발생 구문은 다른 위치에 있는 구문으로 프로그램 제어를 넘깁니다.

throw키워드는 예외의 발생을 나타냅니다. throw 키워드 뒤에는 문자열이나 객체와 같은 하나의 값이 따릅니다.

 

프로그램 내에서 그 문제의 해결을 원하는 장소에서 예외 핸들러(Exception handler)를 사용하여 예외를 포착합니다.

catch 키워드는 예외의 포착을 나타냅니다.

예외 핸들러는 catch 키워드로 시작합니다. 해당 키워드 뒤에는 데이터형 선언이 나타납니다.(예외 핸들러를 catch블록이라고 함)

try를 사용했다면 반드시 하나 이상의 예외 핸들러가 필요합니다.

#include <iostream>
#include <fstream>
int main(void) {
	std::ifstream is;
	is.open("list.txt");
	std::string s;
	try {
		if (is.fail()) {
			throw "파일 읽기 실패";
		}
		else {
			is >> s;
		}
	}
	catch (const char* Exceptr) {
		std::cout << Exceptr;
	}
	return 0;
}

예외 발생이 우려될 코드를 try블록으로 감싸고 예외 핸들러를 추가해주면 됩니다.

만약 발생한 예외의 데이터형과 예외 핸들러의 데이터형이 일치하는 게 하나도 없다면, 예외 핸들러는 해당 예외를 잡지 못합니다.

throw "파일 읽기 실패";는 문자열이기 때문에 예외 핸들러에서 const char *를 써야 캐치가 가능합니다.

일치하는 예외가 있다면 그 예외 핸들러는 블록의 코드를 실행합니다.

위 코드에서 예외가 발생했다면 예외 핸들러에서 잡고 해당 문자열을 출력합니다.

 

예외로 객체를 사용하기

아까 throw키워드 뒤에는 문자열이나 하나의 객체라고 했는데 문자열 대신에 객체를 사용해봅시다.

#include <iostream>
#include <fstream>
#include <cstring>
class FileReadException {
private:
	char* _FileName;
public:
	FileReadException(const char* Fptr) {
		_FileName = new char[strlen(Fptr) + 1];
		strcpy(_FileName, Fptr);
	}
	void Msg() {
		std::cout << "파일을 읽을 수 없습니다. 파일이름:" << _FileName;
	}
};
int main(void) {
	std::ifstream is;
	is.open("list.txt");
	std::string s;
	try {
		if (is.fail()) {
			throw FileReadException("list.txt");
		}
		else {
			is >> s;
		}
	}
	catch (const char* Exceptr) {
		std::cout << Exceptr;
	}
	catch (FileReadException  FrExce) {
		FrExce.Msg();
	}
	return 0;
}

파일 읽기 실패에 대한 클래스를 정의했습니다.

그리고 throw 할 때 문자열 대신 객체를 써주면 됩니다.

해당 예외가 발생했다면 첫 번째 예외 핸들러의 데이터형과 예외시 발생한 데이터형이 일치하지 않기 때문에 블록 안에 코드를 실행하지 않습니다.

두 번째 예외 핸들러는 서로 데이터형이 일치하기 때문에 해당 예외 핸들러의 블록이 실행됩니다.

(Msg멤버 함수를 호출함)

 

C++11에서의 예외 규격

C++98에서 추가된 구조로, C++11에서 무시된 게 있습니다.

즉 현재까지는 표준이지만 앞으로는 표준이 아닐 수 있다는 말입니다.

void func(int n=0) throw(std::exception); //exception이 발생할 수 있다.
void func(int n=0) throw(); //exception을 발생하지 않는다.

함수 원형과 정의에 모드 나타날 수 있습니다.

이러한 예외 처리를 만든 첫 번째 이유는 사용자에게 try 블록의 필요성을 알려 주기 위함입니다.

두 번째 이유는, 컴파일러로 하여금 런타임 체크용 코드를 추가하여 예외 사항이 위반되어 있는지 여부를 확인하기 위해서입니다.

예를 들어 위에 func함수가 직접적인 예외를 발생시키지는 않으나, func함수 내부에서 예외를 발생시킬 수 있는 함수를 호출할 수 있는 경우입니다.

표준에서 없어질 수 있으므로, 가급적 사용하지 않는 것이 좋습니다.

 

C++11은 한 가지 특별한 사항을 허용하고 있는데, 그것은 noexcept라는 새로운 키워드로서 예외를 발생하지 않는 함수를 지칭하는 데 사용됩니다.

#include <iostream>
#include <exception>
void func(int n=0) noexcept{
       if (n == 0) {
              throw std::exception();
       }
       return;
}
int main(void) {
       func(0);
}

컴파일 에러는 발생하지 않지만, 그 대신 경고가 발생합니다.

 

예외의 그 밖의 기능

만약 Func1함수의 return 구문을 만났다고 하면 return 구문은 Func1함수를 호출한 함수에 실행을 옮깁니다.

그러나 throw는 그 예외를 포착한 try-catch 조합이 있는 첫 번째 함수에 도달할 때까지 실행을 계속 옮깁니다.

이것을스택 풀기(unwinding the stack)이라고 하는데, 다음에 다루도록 하겠습니다.

(스택 풀기 게시글)

 

또 한 가지 차이는, 예외 지정자나 catch 블록이 참조를 지정할지라도 컴파일러는 언제나 예외가 발생할 때 임시 복사본을 만든다는 것입니다.

#include <iostream>
class Exce { //예외 클래스
private:
public:
};
void Func1() {
       Exce Error = Exce(); //객체 생성
       throw Error;
}
int main(void) {
       try {
              Func1();
       }
       catch (Exce & e) {
              std::cout << "Exception 발생!!";
       }
       return 0;
}

메인 함수에서 e는 Error 자체가 아니라 복사본을 참조합니다.

애초에 Func1함수가 종료된 후에 Error는 더 이상 존재하지 않기 때문입니다.

throw Exce();를 사용하는 게 더 깔끔합니다.

참조를 사용하는 일반적인 이유는 복사본을 만들 필요가 없는 효율성 때문입니다.

하지만 참조의 특성인 기초 클래스 참조는 파생 클래스 객체도 참조할 수 있기 때문에 사용합니다.

 

이러한 특성 때문에 예외들이 클래스 계층을 가지고 있고 서로 다른 데이터형들을 따로따로 처리하기를 원한다고 가정합시다.

#include <iostream>
class Exce { //예외 클래스
private:
public:
};
class Exce2 : public Exce {
};
class Exce3 : public Exce2 {
};
void Func1() {
       Exce Error = Exce(); //객체 생성
       throw Error;
}
int main(void) {
       try {
              Func1();
       }
       catch (Exce3 & e) {
              std::cout << "Exce3 발생!!";
       }
       catch (Exce2 & e) {
              std::cout << "Exce2 발생!!";
       }
       catch (Exce & e) {
              std::cout << "Exce 발생!!";
       }
       return 0;
}

참조의 특성 때문에 Exce를 첫 번째 순서로 배치한다면 Exce3, Exce2 예외들도 포착할 것입니다.

그래서 포착하려는 예외들이 클래스 계층을 가지고 있다면 이러한 catch 순서도 신경 써야 합니다.

만약 어떤 예외 데이터형을 기대하는지 모르는 경우 생략 부호(...)를 사용한다면 어떤 예외 데이터형도 포착합니다.

(switch의 default케이스와 비슷함)

#include <iostream>
class Exce { //예외 클래스
private:
public:
};
class Exce2 : public Exce {
};
class Exce3 : public Exce2 {
};
void Func1() {
       Exce Error = Exce(); //객체 생성
       throw Error;
}
int main(void) {
       try {
              Func1();
       }
       catch (Exce3 & e) {
              std::cout << "Exce3 발생!!";
       }
       catch (Exce2 & e) {
              std::cout << "Exce2 발생!!";
       }
       catch (...) {
              std::cout << "어떠한 예외가 발생했습니다!";
       }
       return 0;
}

이 경우에는 어떠한 예외가 발생했는지 알 수 없습니다.

댓글