C언어

C++ 프로그래밍 - 예외처리(Exception Handling)와 형변환 연산자(type casting operator)

머리큰개발자 2021. 5. 10. 20:47

예외처리(Exception Handling)

런타임에서 발생하는 에러를 다루기 위해서, C에서는 조건문을 이용하여 제한을 주었다.

C++에서는 드디어 일반적인 조건문과 예외처리문을 구별하여 쓸 수 있게 되었다.

 

try, catch, throw

예외가 발생할 수도 있는 문장과 그 덩어리를 try{} 블럭 안에 작성하고,

예상되는 에러를 처리하기 위해 catch(에러){}의 블럭 안에 예외 처리 코드를 작성한다.

따라서 try와 catch 는 항상 붙여서 작성해야 하므로 가운데 다른 문장이 들어오는 등 따로 작성할 수는 없다.

 

try 문 안에는 예외객체를 전달해주는 throw 키워드가 존재한다.

try 안에서 오류가 발생한다면 throw에 의해서 예외가 던져지고 try는 종료되고 catch 블록에서 예외를 처리하게 된다.

따라서 throw 가 던지는 타입과 catch가 받는 타입이 일치해야 하고, 만약 일치하지 않으면 catch문이 잡질 못한다.

#include <iostream>

using namespace std;

int main() {
	try {
		int num = 0;
		if (num == 0) {
			throw num;
		}

		cout << "정상적으로 코드가 진행될까요?\n";
	}
	catch(int a){
		cout << "num is zero..!!!!!!!\n";
	}
    cout << "정상종료";
}

위 코드의 실행 결과를 보면, num이 0일 때를 예외로 설정하고 throw num 한다.

throw를 만났기 때문에 try문은 종료되게 되고 int 타입의 예외를 잡는 catch 문에 의해 예외가 처리되고, 그 이후부터 코드가 다시 진행된다.

그 결과 정상적~~ 는 출력이 되지 않고 num is zero가 출력되는 것을 확인할 수 있다.

 

반대로, 만약 try 문 안에서 throw가 호출되지 않는다면 정상적으로 실행되고 catch문을 건너뛰게 된다.

 

만약 catch문이 throw가 던진 예외객체를 받지 못하고 멀리 멀리 예외가 던져진다면 어떻게 될까?

이러한 경우, 호출한 함수 쪽으로 예외와 처리할 책임이 넘어가게 되고, 해당 함수는 종료된다.

이것을 스택 풀기(?) (Stack Unwinding)이라고 한다. 

#include <iostream>

using namespace std;
void func2();
void func3();
void func1() {
	cout << "func1 called\n";
	func2();
	cout << "func1 finished\n";
}
void func2() {
	cout << "func2 called\n";
	func3();
	cout << "func2 finished\n";
}
void func3() {
	cout << "func3 called\n";
	throw 1;
	cout << "func3 finished\n";
}
int main() {

	try {
		func1();
	}
	catch (int ) {
		cout << "ERROR CATCHED\n";
	}
	cout << "정상종료\n";
}

메인에서는 func1()을 시도해보고 func2 -> func3 까지 호출된 후(스택에 쌓인 후) throw 1을 한다.

그 결과 func3는 예외를 던지면서 종료가 되기 때문에 func3 finished가 출력되지 않고 func2로 돌아가게 된다.

func2에서는 던져진 예외를 처리해야할 책임이 생기지만, catch문이 없으므로 책임을 지지 못한다. 

그래서 바로 종료되며 func1로 예외를 넘긴다.

물론 func1도 예외를 처리하지 않으므로 main 함수로 책임이 넘어오고 catch문에서 처리하면서 정상종료되는 것을 확인할 수 있다.

이렇게 함수 호출로 쌓인 스택이 다 풀려버리기 때문에 스택 풀기라고 불린다.

 

만약 main에서도 catch로 처리하지 않으면 어떻게 될까?

해보면 알겠지만, 프로그램을 종료시키는 함수가 호출되면서 프로그램이 종료된다.

그러므로 항상 잘 예외를 다뤄주도록 하자.

 

위에서, 만약 catch문의 타입과 throw 타입이 다를 경우 예외를 처리할 수 없다고 했다.

main에서 잡지 못하면 바로 프로그램 자체가 종료되어버리기 때문에 예외를 잘 처리해야 한다.

그래서 try-catch문은 catch 를 여러개 사용가능하고 순서대로 찾으므로, 타입별로 뒤에 붙여서 처리해주면 된다.

 

또한 함수가 예외를 던지는 경우 함수 호출 연산자 뒤에 어떤 타입을 던지는지 명시해주는 것이 좋다.

발생할 수 있는 예외의 종류도 함수의 특징으로 여겨지기 때문이다.

그리고 사용자가 이용하기도 쉬워진다 :)

 

따라서 위의 코드는 다음과 같이 사용하면 좋다.

#include <iostream>

using namespace std;
void func2();
void func3();
void func1() throw (int , char){
	cout << "func1 called\n";
	func2();
	cout << "func1 finished\n";
}
void func2() throw (int, char){
	cout << "func2 called\n";
	func3();
	cout << "func2 finished\n";
}
void func3() throw (int , char) {
	cout << "func3 called\n";
	throw 1;
	throw 'c';
	cout << "func3 finished\n";
}
int main() {

	try {
		func1();
	}
	catch (int ) {
		cout << "ERROR CATCHED\n";
	}
	catch (char) {
		cout << "char ERROR CATCHED\n";
	}
	cout << "정상종료\n";
}

int 혹은 char형의 예외를 던질 수 있다는 의미로 throw (int, char)를 함수 뒤에 붙인다.

또한 메인함수에서 처리하기 때문에 메인함수에 catch 문 두 개를 사용하여 int와 char형 예외를 처리한다.

만약 throw()로 예외 타입이 명시되지 않으면 예외가 전달될 경우 처리할 수 없으므로, 

해당 함수가 예외를 발생시킬 경우 프로그램이 즉시 종료된다.

 

이는 멤버 함수에도 똑같이 적용시킬 수 있기 때문에 클래스에서도 사용될 수 있는데,

만약 상속관계일 때 예외 처리를 그대로 사용하고 해당 클래스 타입의 예외를 던질 경우 

객체 다형성(부모 클래스로 자식 클래스를 가리키는 경우)에 의해 자식은 부모와 같은 타입처럼 여겨지므로

catch문 순서에 유의해야한다.

 

또한 catch 문은 모든 예외를 다 받을 수도 있다. 

try{} catch(...){} 처럼 사용한다면, catch는 모든 예외를 다 잡아서 처리해준다. 

하지만 어디서 어떻게 어떤 예외객체를 던지는지 전혀 알길이 없으므로 에러가 발생하는지만 확인할 수 있다.

 

메모리 할당을 받을 때 메모리가 부족해서 발생하는 예외도 존재한다.

bac_alloc &a 방식으로 예외가 전달된다.

형변환 연산자

C에서는 강제 형변환을 사용하여 형변환에 대한 안정성을 프로그래머가 확인하고 썼어야했다. 

하지만 C++에서는 컴파일러가 안정성을 판단하고, 컴파일 타임에 확인하여 런타임 중 에러를 방지하도록 할 수 있다.

(물론 컴파일 타임에 에러가 나면 안되겠지만)

 

형변환 연산자는 다음과 같은 4가지이다.

연산자명 사용법 기능
static_cast static_cast<T>(대상) 강제 형변환이되, 기본 자료형(const 제외)간 형변환+ 상속관계에서의 변환
const_cast const_cast<T>(대상) 상수형 타입->일반 타입으로 변환
reinterpret_cast reinterpret_cast<T>(대상) 상속 관계가 아니어도 모든 유형으로의 형 변환
dynamic_cast dynamic_cast<T>(대상) 자식 클래스 타입 (<- 제한적 사용) -> 부모 클래스 타입으로 변환,

앞선 설명과 다르게 static_cast와 reinterpret_cast 는 강제적으로 형을 변환해주므로 C와 다를바 없는 것처럼 느껴질 수 있다.

하지만, reinterpret를 제외한 다른 casting 은 각자 제한이 존재하고, 둘의 자료형이 변환 불가능한 경우를 컴파일러가 판단해 오류를 발생시켜주기 때문에 조금 더 안전하게 사용이 가능하다는 뜻이다.

물론 static_cast를 사용할 수 밖에 없을 때, type casting 이 실패한다면 런타임에서 null pointer 를 반환하므로 안정성을 위해 dynamic_Cast를 사용하거나 null pointer 처리를 따로 해줘야한다.

 

dynamic_cast는 부모-> 자식 클래스로의 변환도 가능하다.

이 때 부모 클래스가 하나 이상의 가상함수를 가지는 polymorphic 클래스여야 한다.

물론 dynamic_cast도 null pointer 를 반환해야되는 경우가 생긴다면 bad_cast exception이 발생하기 때문에

예외를 처리해주어야 한다.