#include <iostream>
class Base {
public:
int baseValue;
Base(int value) : baseValue(value) {}
virtual void print() const {
std::cout << "Base value: " << baseValue << std::endl;
}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int base, int derived) : Base(base), derivedValue(derived) {}
void print() const override {
std::cout << "Derived value: " << derivedValue << std::endl;
}
};
int main() {
Derived derivedObj(10, 20);
// Rule 1: p_ancestor = p_descendant
Base* p_ancestor = &derivedObj;
// Rule 2: No data members will be lost
p_ancestor->print(); // Calls the overridden print function in Derived class
return 0;
}
main 함수에서 파생 클래스 객체를 생성하고, 기본 클래스 포인터로 가리키는 것을 확인할 수 있습니다. 이렇게 하면 데이터 멤버가 손실되지 않고, 가상 함수를 통해 파생 클래스의 버전이 호출됩니다.
1. p_ancestor가 p_descendant를 가리키는 것은 허용됩니다.
p_ancestor가 기본 클래스(Base class)의 포인터이고, p_descendant가 이를 상속한 파생 클래스(Derived class)의 포인터일 경우, p_ancestor에 p_descendant를 할당할 수 있습니다. 이를 통해 기본 클래스 포인터로 파생 클래스 객체를 가리킬 수 있습니다.
2. 데이터 멤버는 손실되지 않습니다.
기본 클래스 포인터 p_ancestor로 파생 클래스 객체를 가리키는 경우, 실제로 파생 클래스 객체의 모든 데이터 멤버가 그대로 유지됩니다. 데이터 멤버에 대한 정보 손실이 발생하지 않습니다.
에러 핸들링은 말 그대로 예외처리를 하는 방법에 관한 것이다. if - else 문이나 swith문에서도 이와 같은 방식으로 처리가 가능하지만, 가독성의 측면과 기능을 구분하는 차이에서 error handling과 관련된 exception의 기능을 직접 사용하는 것이 훨씬 더 좋을 것이다.
기본적으로 throw
와 catch
를 이용해서 에러를 처리한다.
아래는 예제 코드이다.
#include <iostream>
#include <stdexcept>
class Resource{
public:
Resource (int id) : id_(id){}
~Resource(){std::cout << "리소스 해제 " << id_ << std::endl;}
private:
int id_;
};
int func3() {
Resource r(3);
return 0;
}
int func2() {
Resource r(2);
func3();
std::cout << "실행!" << std::endl;
return 0;
}
int func1() {
Resource r(1);
func2();
std::cout << "실행!" << std::endl;
return 0;
}
int main (){
try{
func1();
} catch(std::exception& e){
std::cout << "Exception" << e.what();
}
return 0;
}
리소스 해제 3
실행!
리소스 해제 2
실행!
리소스 해제 1
예외가 발생하지 않을 경우에는, 해당 코드를 실행했을 때의 결과값처럼 함수안에 있는 실행을 출력하는 문구가 잘 작동한다. 하지만 에러가 발생하게 될 경우, 객체에 대해서만 소멸자만 진행되고, 출력과 관련된 내용은 실행시키지 않는다.
#include <iostream>
#include <stdexcept>
class Resource {
public:
Resource(int id) : id_(id) {}
~Resource() { std::cout << "리소스 해제 : " << id_ << std::endl; }
private:
int id_;
};
int func3() {
Resource r(3);
throw std::runtime_error("Exception from 3!\n");
}
int func2() {
Resource r(2);
func3();
std::cout << "실행 안됨!" << std::endl;
return 0;
}
int func1() {
Resource r(1);
func2();
std::cout << "실행 안됨!" << std::endl;
return 0;
}
int main() {
try {
func1();
} catch (std::exception& e) {
std::cout << "Exception : " << e.what();
}
}
리소스 해제 : 3
리소스 해제 : 2
리소스 해제 : 1
Exception : Exception from 3!
보이는 것과 같이, 리소스 해제인 소멸자 함수에 대한 작동은 제대로 이루어지지만 그 이외의 기능은 실행하지 않는다. 예외가 발생하지 않았을 경우에는 정상적으로 작동이 되므로 이 차이를 알고 있어야 한다.
그리고 이러한 과정을 스택 풀기라고 한다.
기본적으로 catch
는 어떤 종류의 throw든 잘 받을 수 있습니다. 물론 번거롭게 다 설정을 해야하겠지만요. 그렇기에 이 부분에 대해서는 문제가 없지만, 클래스 객체를 리턴하게 될 때 특이한 현상이 발생합니다.
#include <exception>
#include <iostream>
class Parent : public std::exception {
public:
virtual const char* what() const noexcept override { return "Parent!\n"; }
};
class Child : public Parent {
public:
const char* what() const noexcept override { return "Child!\n"; }
};
int func(int c) {
if (c == 1) {
throw Parent();
} else if (c == 2) {
throw Child();
}
return 0;
}
int main() {
int c;
std::cin >> c;
try {
func(c);
} catch (Parent& p) {
std::cout << "Parent Catch!" << std::endl;
std::cout << p.what();
} catch (Child& c) {
std::cout << "Child Catch!" << std::endl;
std::cout << c.what();
}
}
1
Parent Catch!
Parent!
2
Parent Catch!
Child!
위와 같이 출력값이 나오게 된다. 2번을 보자.
int func
에서 throw
를 통해 Child
클래스 객체를 던졌고, 받아서 Child Catch!
와 함께 c.what()
를 실행해야하지만, Parent Catch
가 나온다. 왜 그럴까??
심플하게 생각하면 된다. 자식 클래스의 잘못은 부모 클래스가 처리한다.
catch 문
의 경우 가장 먼저 대입될 수 있는 객체를 받는다.
Parent& p = Child();
는 가능하다. 상속과 포인터의 개념을 잘 따라왔다면 이것의 역은 성립하지 않는다는 것을 알 수 있을 것이다.
void someFunction() throw(); // 빈 예외 명세; 모든 예외를 처리하지 않으면 프로그램이 종료됩니다.
void someFunction() throw(DivideByZero, OtherException);
// DivideByZero 및 OtherException을 제외한 다른 모든 예외는 프로그램을 종료시킵니다.
void someFunction();
// 모든 종류의 예외를 정상적으로 처리하며, catch 블록에 잡히지 않으면 프로그램이 종료됩니다.
예외 명세란, 해당 함수에서 발생하는 예외에 대해서 명시해놓은 곳으로, 함수 뒤에 작성한다.
void someFunction() throw()
는 이 함수내에서는 예외가 발생하지 않는다는 것을 말하고, 만약 발생한다면 프로그램이 종료된다.
void someFunction() throw(DivideByZero, OhterException)
는 발생할 수 있는 예외를 명시하여, 이 함수 내에서는 해당 예외가 발생할 수 있으며, 이외에 예외에 대해서는 프로그램을 종료시킵니다.
void someFunction()
는 함수는 어떤 예외도 던질 수 있으며, 예외가 발생하면 해당 예외가 catch 블록에 잡히지 않으면 std::terminate()가 호출되어 프로그램이 종료된다.
일반적으로 예외처리 블록은 가장 구체적인 것 부터 작성한다. 이러한 예외 처리 구조를 통해 예외가 어떤 종류인지에 따라 적절한 조치를 취할 수 있습니다. 또한, 가장 구체적인 예외부터 처리하므로 예외에 대한 정확하고 명확한 대응이 가능하다.
상위 클래스보다 하위 클래스의 예외를 먼저 처리하는 것이 일반적이다.
catch(...)
로 Default Catch가 가능하다. 모든 예외를 받는 것이 가능하다는 말이다..
noexcept
를 통해 예외가 발생하지 않는다는 것을 명시할 수도 있다
#include
int foo() noexcept {}
int bar(int x) noexcept { throw 1; }
int main() { foo(); }
noexcept
를 붙였다고 해서, throw로 객체를 던질 수 없는 것은 아니다. 오류는 발생하나 컴파일은 된다. noexcept
가 붙은 함수가 예외를 발생시키지 않는구나~~~ 하고 컴파일을 실행하게 되는 것입니다.
본 내용은 씹어먹는c++을 통해 알게된 내용을 개인적인 공부를 위해 정리한 포스팅입니다. 저작권을 해칠 의도가 없으며, 모든 것은 해당 블로그 저자의 지식재산입니다.
https://modoocode.com/210