728x90

황기태 저자의 명품 C++ Programming 개정판을 읽고 학습한 내용을 정리한 포스트입니다!

https://search.shopping.naver.com/book/catalog/32436115747

 

C++ Programming : 네이버 도서

네이버 도서 상세정보를 제공합니다.

search.shopping.naver.com

 

실행 오류의 종류와 원인

실행 오류의 원인은 크게 두 가지로 나뉜다.

  • 개발자의 논리가 잘못된 경우
  • 예외에 대한 대책을 준비하지 않는 경우

사용자가 입력을 잘못하든 항상 예측 못한 사용자 입력이나 예외 상황 발생을 걸러내도록 프로그래밍하는 것이 중요하다. 아래는 예외 상황에 대한 대처가 없는 평범한 코드에 오류가 발생하는 사례이다.

#include <iostream>
using namespace std;

int getExp(int base, int exp) { // base의 exp 지수승을 계산하여 리턴
	int value=1;
	for(int n=0; n<exp; n++) 
		value = value * base; // base를 exp번 곱하여 지수 값 계산
	return value;
}

int main() {
	int v= getExp(2, 3); // 2의 3승 = 8
	cout << "2의 3승은 " << v << "입니다." << endl;
	int e = getExp(2, -3); // 2의 -3승은 ?
	cout << "2의 -3승은 " << e << "입니다." << endl;
}


/*
2의 3승은 8입니다.
2의 -3승은 1입니다.
*/
  • -3을 입력하는 경우 오답(1)을 리턴한다.
  • 위의 경우 base나 exp에 음수가 넘어오는 상황에 대처하지 않고, 당연히 양수만 넘길 것이라는 개발자의 낙관적인 코딩의 결과이다.

 


조건문과 리턴 값을 이용하는 전형적인 오류 처리

  • if 조건문으로 오류 상황을 검사하여 오류 코드를 리턴하는 방법은 잘못된 입력 값을 걸러내어오류 발생을 막는 전형적인 방법이다.
  • 아래는 앞서 본 코드에서 매개 변수가 음수인 경우 -1을 리턴하도록 수정한 코드이다.
#include <iostream>
using namespace std;

int getExp(int base, int exp) { // base의 exp 지수승을 계산하여 ret에 저장
	if(base <= 0 || exp <= 0) {
		return -1; // 오류 리턴
	}
	int value=1;
	for(int n=0; n<exp; n++) 
		value = value * base; // base를 exp번 곱하여 지수 값 계산
	return value; // 정상 리턴. 계산된 값 리턴
}

int main() {
	int v=0;
	v = getExp(2, 3); // v = 2의 3승 = 8. getExp()는 8 리턴
	if(v != -1)
		cout << "2의 3승은 " << v << "입니다." << endl;
	else 
		cout << "오류. 2의 3승은 " << "계산할 수 없습니다." << endl;

	int e=0;
	e = getExp(2, -3); // 2의 -3 승 ?. getExp()는 -1 리턴
	if(e != -1)
		cout << "2의 -3승은 " << v << "입니다." << endl;
	else
		cout << "오류. 2의 -3승은 " << "계산할 수 없습니다." << endl;
}

리턴 값과 참조 매개 변수를 이용한 오류 처리

  • 참조 매개 변수를 이용하여 계산한 값을 리턴하고, 함수의 리턴 값은 true/false로 오류인지 아닌지 알리도록 수정하여
  • 앞서 본 코드를 개선한 코드이다.
#include <iostream>
using namespace std;

bool getExp(int base, int exp, int &ret) { // base의 exp 지수승을 계산하여 ret에 저장
	if(base <= 0 || exp <= 0) {
		return false; // 오류 리턴
	}
	int value=1;
	for(int n=0; n<exp; n++) 
		value = value * base; // base를 exp 만큼 곱하여 지수 값 계산
	ret = value; // 계산 값을 ret에 저장
	return true; // 정상 리턴. 계산된 값은 ret에 있음
}

int main() {
	int v=0;
	if(getExp(2, 3, v)) // v = 2의 3승 = 8. getExp()는 true 리턴
		cout << "2의 3승은 " << v << "입니다." << endl;
	else 
		cout << "오류. 2의 3승은 " << "계산할 수 없습니다." << endl;

	int e=0;
	if(getExp(2, -3, e)) // 2의 -3 승 ?. getExp()는 false 리턴
		cout << "2의 -3승은 " << v << "입니다." << endl;
	else
		cout << "오류. 2의 -3승은 " << "계산할 수 없습니다." << endl;
}

 

예외와 예외 처리

예외란?
  • 실행 중, 프로그램의 오동작이나 결과에 영향을 미치는 예상치 못한 상황 발생을 예외(exception)라고 부른다.
  • 위의 코드들에서 예상치 못한 음수가 넘어와서 2^-3을 1로 오답을 내게 한 것이 바로 예외이다.

 

C++ 예외 처리
  • 운영 체제는 하드웨어나 시스템 자원을 다루는 중에 발생하는 예외를 응용프로그램에게 알려주는 예외 처리 기능을 따로 두고 있다.
  • 하지만 이번에 살펴 볼 예외 처리는 오동작을 막는 구조적 방법으로 C++ 언어 차원에 국한된다.
  • 운영체제가 탐지한 예외를 C++ 프로그램에서 받거나 처리하고자 하면, 운영체제 API를 이용하고 운영체제 메뉴얼에 주어진 별도의 방법으로 코딩해야 한다.

 

예외 처리 기본 형식, try-thorw-catch

  • 전형적인 예외 처리 구조는 아래와 같다.
try { // 예외가 발생할 가능성이 있는 실행문
	.......
	예외 발견시{
    		throw XXX; // 예외 상황을 알림
	}
}
catch (처리할 예외 파라미터 선언) {
	예외 처리문
}

 

try {} 블록
  • 예외가 발생할 소지가 있는 문장들은 try {} 블록으로 묶고 예외를 처리할 catch() {} 블록을 바로 연결하여 선언한다.
  • 하나의 try {} 블록에 여러 개의 catch() {} 블록을 연달아 선언할 수 있다.
  • 예외가 탐지되면 throw 문을 실행하여 예외 발생을 알린다.

 

throw 문
  • try {} 안에서 실행되는 문으로, 현재 실행 중인 프로그램 내에 예외의 발생을 알린다.
  • 소위 '예외를 던진다'라고 표현하며 아래와 같이 '예외 값'을 던진다.
  • throw 문이 예외를 던지면, 던져진 예외 값의 타입과 일치하는 catch() {} 블록이 실행된다.
throw 3; // int 타입의 예외 값 3을 던짐
throw "empty stack"; // char* 타입의 문자열 예외를 던짐

 

catch(처리할 예외 파라미터 선언) { } 블록
  • catch()의 () 안에 '예외 값'을 받는 예외 파라미터(exception parameter)를 선언한다.
  • 예외 파라미터는 함수 파라미터와 같이 '예외 타입'과 '매개 변수'로 선언하며, throw 문이 던진 '예외 값'의 타입이 '예외 타입'과 일치하는 경우에 '예외 값'이 '매개 변수'에 전달되고 catch() {} 블록이 실행한다.
  • 예외 파라미터는 한 개만 선언 가능하다.

 

throw와 catch

  • throw 문으로 던지는 예외는 catch() { } 블록에 의해 처리된다.
  • throw는 정수(int), 문자(char), 문자열(char*), 실수(double), 객체 등 다양한 타입의 예외 값을 던질 수 있다.
  • 예외는 던져진 '예외 값'의 타입으로 구분하며, 하나의 catch() {} 블록은 하나의 예외 타입만 처리한다.


try - throw - catch의 예외 처리 과정

  • 아래의 그림을 보며 예외가 처리되는 구체적인 과정을 보자.
  • 0으로 나누는 예외를 처리하기 위한 예외 처리기를 작성하였고, 예외가 발생하는 경우와 그렇지 않는 경우에 따라 제어의 흐름이 어떻게 변하는지 보여준다.


하나의 try { } 블록에 다수의 catch() { } 블록 연결

  • 하나의 try { } 블록에 여러 개의 catch() { } 블록을 연결하여, try { } 블록에서 발생시키는 여러 타입의 예외를 처리할 수 있다.

  • try { } 블록에는 두 개의 catch() { } 블록이 연결되어 있다.
  • throw "음수 불가능"; 코드가 실행되면 catch(const char* s) { } 블록으로 점프하여 실행하고, throw 3;의 문장이 실행되면 catch(int x) { } 블록으로 점프하여 실행한다.

함수를 포함하는 try { } 블록

  • 가장 많은 프로그램 작성 사례로서, try { } 블록에서 호출한 함수가 예외를 던지는 경우이다.
  • 함수들이 여러 번 중첩되어(nested) 호출된다고 하더라도 try { } 블록 안에서 호출된 함수 throw 문을 실행하는 경우는 try { } 블록에 연결된 catch() { } 블록에서 예외가 처리된다.


예외를 발생시키는 함수의 선언

  • throw 문을 가지고 있는 함수는 함수 선언문에 예외 발생을 명시할 수 있다.
  • 그 형식은 함수에서 발생시키는 모든 예외 타입을 함수 원형 뒤에 throw()의 괄호 안에 나열한다.
double valueAt(double* p, int index) throw(int, char*) {
	if (index < 0)
		throw "index out of bounds exception"; // char* 타입 예외 발생
	else if (p == NULL)
		throw 0; // int 타입 예외 발생
	else
		return p[indx];
}

위와 같이 선언하는 것이 의무는 아니지만 아래와 같은 장점이 있다.

  • 프로그램의 작동을 명확히 한다.
  • 예외와 관련된 프로그램의 가독성을 높인다.

즉, 컴파일러가 그냥 넘어가기도 하는 선언되지 않은 예외 발생을 방지하고, 원형만 봐도 함수에서 발생시키는 예외를 알 수 있다.

 

어떤 예외라도 포착하는 catch() {} 블록
  • catch의 () 안에 예외 파라미터 대신 생략 부호( ... )를 주면, 어떤 예외라도 포착할 수 있다.
  • 그러나 catch( ... ) {} 블록은 반드시 마지막 catch()로만 사용해야 한다.

아래는 예외 처리를 가진 스택 클래스를 작성한 예시이다.

// MyStack.h

#ifndef MYSTACK_H
#define MTSTACK_H

class MyStack {
	int data[100];
	int tos;
public:
	MyStack() { tos = -1; }
	void push(int n) throw(char*);
	int pop() throw(char*);
};

#endif
// MyStack.cpp

#include "MyStack.h"

void MyStack::push(int n) {
	if(tos == 99) 
		throw "Stack Full";
	tos++;
	data[tos] = n;
}

int MyStack::pop() {
	if(tos == -1) 
		throw "Stack Empty";
	int rData = data[tos--];
	return rData;
}

 

// Main.cpp

#include <iostream>
using namespace std;

#include "MyStack.h"

int main() {
	MyStack intStack;
	try {
		intStack.push(100); // 100 푸시
		intStack.push(200); // 200 푸시
		cout << intStack.pop() << endl; // 팝 200
		cout << intStack.pop() << endl; // 팝 100
		cout << intStack.pop() << endl; // "Stack Empty" 예외 발생
	}
	catch(const char* s) {
		cout << "예외 발생 : " << s << endl;
	}
}

중첩 try { } 블록

  • try { } 블록 내에 다른 try { } 블록을 중첩(nested) 작성할 수 있다.
  • 이 때, 안쪽 try { } 블록의 throw에서 던진 예외를 처리할 catch() { } 블록이 없으면, 바깥 쪽 try { } 블록에 연결된 catch() { } 블록에 예외가 전달된다.


throw 사용 시주의 사항

throw 문의 위치
  • throw 문은 항상 try { } 블록 안에서 실행되어야 한다. 그렇지 않은 경우 시스템은 abort() 함수를호출하여프로그램을종료시킨다.
throw 3; // 실행되면 프로그램이 비정상 종료
...
try {
	...
}
catch(int n) {
	...
}

 

예외를 처리할 catch() { } 블록이 없으면 프로그램은 종료된다.
  • throw가 던지는 타입의 예외를 처리할 catch() { } 블록이 선언되어 있지 않은 경우, throw 문이 실행되면 시스템이 abort() 함수를 호출하여 종료 시킨다.
try {
	throw "aa"; // char* 타입의 예외를 처리할 catch() { } 블록이 없기 때문에 프로그램 종료
	...
}
catch(double p) {
	...
}

 

catch() { } 블록 내에도 try { } catch() { } 블록을 선언할 수 있다.


예외 클래스 만들기

  • '예외 값'으로 int, double 등의 기본 타입을 사용할 때보다 클래스를 사용하면 catch() { } 블록에 더 많은 정보를 전달 할 수 있다.
  • 아래는 두 양수를 입력받아 나누기한 결과를 출력하는 프로그램에 음수가 입력된 경우와 0으로 나누기가 발생하는 경우 서로 다른 예외로 처리하도록 예외 클래스를 작성한 사례이다.
#include <iostream>
#include <string>
using namespace std;

class MyException { // 사용자가 만드는 기본 예외 클래스 선언
	int lineNo;
	string func, msg;
public:
	MyException(int n, string f, string m) {
		lineNo = n; func = f; msg = m; 
	}
	void print() { cout << func << ":" << lineNo << ", " << msg << endl; }
};

class DivideByZeroException : public MyException { // 0으로 나누는 예외 클래스 선언
public:
	DivideByZeroException(int lineNo, string func, string msg)
		: MyException(lineNo, func, msg) { }
};

class InvalidInputException : public MyException { // 잘못된 입력 예외 클래스 선언
public:
	InvalidInputException(int lineNo, string func, string msg)
		: MyException(lineNo, func, msg) { }
};

int main() {
	int x, y;
	try {
		cout << "나눗셈을 합니다. 두 개의 양의 정수를 입력하세요>>";
		cin >> x >> y;
		if(x < 0 || y < 0)
			throw InvalidInputException(33, "main()", "음수 입력 예외 발생"); // 임시 객체 생성
		if(y == 0)
			throw DivideByZeroException(35, "main()", "0으로 나누는 예외 발생"); // 임시 객체 생성
		cout << (double)x / (double)y;
	}
	catch(DivideByZeroException &e) { 
		e.print();
	}
	catch(InvalidInputException &e) { 	
		e.print();
	}
}
728x90

'Programming Language > C++' 카테고리의 다른 글

[C++] C++ 코드와 C 코드의 링킹  (1) 2023.12.27
[C++] 사용자 조작자 만들기  (1) 2023.12.21
[C++] 삽입 연산자(<<)와 추출 연산자(>>)  (0) 2023.12.21
[C++] 포맷  (1) 2023.12.21
[C++] ostream과 istream  (1) 2023.12.20