cpp module 05

이호용·2021년 9월 13일
0

cpp

목록 보기
6/16

C++ - Module 05

Repetition and Exceptions

반복문 그리고 예외처리.

try : 예외를 던지는 부분
catch : 예외 받는 부분이 있다.

예외처리에 효율적인 try chach 를 배워보자.

먼저 기존의 방법인 if문 에러처리.

int fact(int n) {
	if (n == 0) return 1;
    return n * fact(n - 1);
}

int main(){
	int n;
    cin >> n;
    if (n<0){
    	std::cout << ": 음수입니다." << std::endl;
    }
    else {
		std::cout << n << " ! = " << fact(n) << endl; 
	}
}

이를 try chach문으로 바꾼다면

int fact(int n) {
	if (n == 0) return 1;
    return n * fact(n - 1);
}

int main(){
	int n;
    cin >> n;
    try {
    	if (n < 0){
        	throw n;}
        std::cout << n << " ! = " << fact(n) << endl;
    }
    catch (int e){
    	std::cout << e <<  ": 음수입니다." << std::endl;
    }
}

try 블록 내에서 예외가 발생하면, catch 블록이 실행되고 나서, 예외가 발생한 지점 이후에 실행하는 것이 아니라, catch 블록의 이후가 실행된다.

왜 if문 안쓰고 try chach를 사용할까?

test_class tmp("메개인자~~입니다!");
if(tmp.fail())
{
	error_fun(tmp, 2);
}

만약 if문으로 에러를 처리하면, if문에서 에러 발생해도 if문안에서 에러가 난 tmp객체를 계속 사용할수 있다. 그래서 더 불안정하다.

test_class tmp("메개인자~~입니다!");
if(tmp.fail())
	throw error_exception;

try catch문은 이런식으로 throw가 발생하면, 바로 catch문으로 이동하므로 자동으로 에러가 발생한 지역객체tmp가 소멸된다.

try catch에서 throw로 다양한 값들을 던질수 있는데, chach에서 받을떄 일치하는 매게변수들이 없다면 런타임 에러가 발생함.
그래서 catch(...)으로 일치하는 매게변수가 없을떄 다 받아서 처리하는 방법이 있다.

throw 예제..

// 아무 타입도 지정하지 않았으므로, 예외를 던지지 않는다.
void no_except() throw();
 
// 모든 타입에 대해 예외를 던진다
void bar() throw(...) {};    // C++11부터 추가된 parameter pack, 즉 C++11 이전엔 이 형태가 불가능
void baz() {};
 
class X {};
class Y {};
class Z : public X {};
class W {};
 
// 함수 foo는 X와 Y, 그리고 그들의 자식을 예외로 던질 수 있다.
void foo() throw(X, Y)
{
    int n = 0;
 
    if (n)
        throw Y();    // OK
 
    if (n)
        throw Z();    // Z는 X의 자식이므로, OK
 
    // 예외로 던질 수 없는 걸 던졌으므로, std::unexpected 호출
    throw W();
}

int main(void)
{
	try{
    	foo();
    }
    chach(X &tmp){}
    chach(Y &tmp){}
    chach(Z &tmp){}
}

throw를 통해 어디로 예외를 던질지 정할수 있다.
험슈 foo는 X와 Y에 대해 예외를 던질수 있다. 그리고 그 자식들에게도.

try 블록을 묶는 기준은 예외가 발생할만한 영역을 묶는 것이 아니다!

예외가 발생할만한 영역만 묶는 것이 아니라, 그와 관련된 모든 문장을 함께 묶어서 이를 하나의 '일(work)'의 단위로 구성해야 한다.

예제 사이트

std::exception

자 이제 에러처리를 표준적인 방법으로 하는 걸 배워보겠다.
c++를 사용하다보면 많은 라이브러리를 활용한다.
여러 라이브러리를 활용하다보면 잘못된 방법으로 사용하는 경우가 있는데, 많은 라이브러리들이 이러한 경우 try chach문을 통해 에러처리 해주도록 설계되어있다.

우리가 만든 에러처리 뿐아니라 다른 이들이 만든 에러까지 처리해줄수 있도록 표준으로 지정된 std::exception으로 에러처리를 해보자.

exeption 을 이용한 try catch문 예제

  3 int main( void )
  4 {
  5     try
  6     {
  7         Bureaucrat tmp("hohoo", 152) ;
  8         //Bureaucrat tmp2("hihi", 170);
  9         //1 ~ 150 아니면 에러처리.
 10         std::cout << tmp << std::endl;
 11         tmp.decrement();
 12         tmp.decrement();
 13     }
 14     catch (std::exception & e)
 15     {
 16         std::cerr << e.what() << std::endl;
 17     }
 18 }

위 예제는 약간 심화과정인데, 이해하면 기본과정도 이해된다. 배워보도록 하자.

위의 코드에서 throw가 안보인다. throw는 Bureaucrat 클래스 안에 있다.

  7 class Bureaucrat{
  8     public:
  9         Bureaucrat ( void );
 10         Bureaucrat (Bureaucrat &rhd);
 11         Bureaucrat ( std::string const &Name, int const &grade );
 12
 13         class GradeTooHighException : public std::exception{
 14             const char *what() const throw();
 15         };
 16         class GradeTooLowException : public std::exception{
 17             const char *what() const throw();
 18         };

어려워 보이지만 생각보다 간단하다.
Bureaucrat안에서 발생하는 에러를 처리하기 위해 Bureaucrat안에서 GradeTooHighException,GradeTooLowException 이라는 exception의 자식 클래스를 생성했다.

이제 GradeTooHighException안에 what 함수를 넣어주기위해 what 함수를 오버라이딩 한다.

const char *what() const throw();

  1. const
    반환하는 문자열은 출력만해주면 될거같다. const로 정의했다.

  2. what
    exeption클래스에서 what함수는 에러난 이유를 반환해주기로 약속되어있다. 에러발생한 이유를 명시해주도록 함수이름은 what을 오버라이딩한다.

  3. const
    this의 내용을 못 바꾸도록 const시켜주었다.

  4. throw()
    exception 클래스를 상속받을 때, what() 함수를 override 할때는, throw() 를 사용한다. 이유는 정확히 모르겠다.

void func_1(int n) throw(bad_thing);
// 함수 func_1에서는 bad_thing 타입의 예외가 발생할 가능성이 있다.
void func_2(int n) throw();
// 함수 func_2에서는 예외를 발생시키지 않으나, 예외를 발생시키는 함수를 호출할 가능성이 있다.
void func_3(int n) noexcept;
// 함수 func_3는 예외를 발생시키지 않는다.

operator

operator를 사용하는 이유?
연산자 또는 비교문을 따로 지정을 하지 않으면 객체에서는 사용할 수 없다.(int a + int b이런건 가능하지만, tmp_class a + tmp_class b 이런건 불가능)
객체 또한 operator를 사용해 편리한 기능들을 추가 하기 위해서는 연산자 오버로딩 과정을 거쳐야한다.

객체를 연산자 오버로딩을 하는 방법은 크게 두가지가 있다. 하나는 전역에서 정의 하는 방법이고, 다른 하나는 객체내에서 연산자 오버로딩을 하는 방법이다.

operator는 전역으로도 사용할 수 있고,

1. 객체내에서 연산자 오버로딩.

  7 class Bureaucrat{
  8     public:
  9         Bureaucrat ( void );
 10         Bureaucrat (Bureaucrat &rhd);
 11         Bureaucrat ( std::string const &Name, int const &grade );
 
  25         Bureaucrat &operator=(Bureaucrat const &rhd);
 68 Bureaucrat &Bureaucrat::operator=(Bureaucrat const &rhd)
 69 {
 70     this->grade = rhd.grade;
 71     return (*this);
 72 }

객체내에서 선언하는 operator의 매게변수는 하나다.

ex) Bureaucrat1 + Bureaucrat2
Bureaucrat1 : 은 자기자신
Bureaucrat2 : 매게변수로 들어온 Bureaucrat이다.
return 은 Bureaucrat을 다시 리턴해줬다.
return 이 다시 자기자신인 이유는 Bureaucrat1 + Bureaucrat2 + Bureaucrat3처럼 다음 연산자가 왔을 때 다시한번 연산하기 위해서는 앞서 연산했던 결과가 필요하기 떄문이다.

2. operator가 전역변수 일때

  7 class Bureaucrat{
  8     public:
  9         Bureaucrat ( void );
 10         Bureaucrat (Bureaucrat &rhd);
 11         Bureaucrat ( std::string const &Name, int const &grade );
 
  25         Bureaucrat &operator=(Bureaucrat const &rhd);
}
  34 std::ostream& operator<<(std::ostream& os, const Bureaucrat& B); // 전역변수

자 34번 코드를 보면, operator가 class내에서 선언된게 아니라 전역에서 선언되었다.

객체 + 객체 처럼 연산자를 하기 위해서 피연산자 2개가 필요한데, 전역에서 선언하면 앞서 설명한 클래스 자기자신이 이라는 존재가 없기 때문에 전역에서 선언한 operator는 매게변수로 두개의 객체를 넣어주어야한다.

os << Bureaucrat 이런 형태다.
cout << Bureaucrat2 의 예제이다.

 74 std::ostream &operator<<(std::ostream &os, Bureaucrat const &B)
 75 {
 76     os << "<" << B.getName() << ">, bureaucrat grade <" << B.getGrade() << ">.";
 77     return (os);
 78 }

전역변수니까 어디서든 원하는 곳에서 함수초기화를 시켜주면 된다.

3. 전역변수 private 접근(friend)

전역변수로 operator를 선언했기 때문에, Bureaucrat클래스의 private한 변수나 매게함수는 접근할수가 없다.

이를 해결하기 위해 만들어 진게 friend다 .

friend를 쓰면 전역으로 선언한 함수더라도 원하는 클래스의 private한 자료에 접근할 수 있다.

  5 class Bureaucrat{
  6 	private:
  7			int test;
  8     public:
  9         Bureaucrat ( void );
 10         Bureaucrat (Bureaucrat &rhd);
 11         Bureaucrat ( std::string const &Name, int const &grade );
 
 25         Bureaucrat &operator=(Bureaucrat const &rhd);

  
 26		friend std::ostream& operator<<(std::ostream& os, const Bureaucrat& B); // Cents의 친구로 지정.

}
  34 std::ostream& operator<<(std::ostream& os, const Bureaucrat& B); // 전역변수

26 번 코드처럼 private를 접근하고 싶은 클래스안에 friend할 전역변수명을 적어 해당 전역변수를 private까지 접근하게 해주도록 지정할 수 있다.

객체 매게인자.

클래스도 자료형이기 때문에 당연히 매게변수로 넘겨줄수 있다.

Form라는 클래스의 매게함수(beSigned) 중 메게인자로 Bureaucrat객체를 넘겨주는 예제를 한번 살펴보자.

  8 class Bureaucrat;
  9
 10 class Form{
 11     public:
 12         Form ( void );
 13         Form ( std::string const &Name, int const &SignGrade,int const ExecuteGrade );
 14         Form (Form &rhd);

 28         void                beSigned(const Bureaucrat &rhd);

지금까지 만들어왔던 매게함수랑 별 다를건 없다. 아니 똑같다.

하지만 이걸 따로 적는 이유는, Bureaucrat 클래스가 Form 클래스와 같은 파일 내에 없다면

class Bureaucrat; 로 Bureaucrat가 있다는걸 선언해줘야 Form클래스를 만들때 컴파일 에러가 안난다.

class Bureaucrat 의 의미는 지금은 정의는 하지않고 앞으로 정의될거라고 암시해준다.

클래스간 교차참조(Bureaucrat에도 Form을 사용하고, Form도 Bureaucrat을 사용)

자 이제 문제다. 만약, Bureaucrat에도 Form 이 있고 Form에도 Bureaucrat이 있다면?

Bureaucrat.hpp
 10 class Bureaucrat{
 11     public:
 27         void        signForm(Form &F) const;
 28         void        executeForm(Form const & form) const;
 36 };

Form.hpp
 10 class Form{
 11     public:
 38         virtual void        execute(Bureaucrat const &rhd) const = 0;
 38}

코드를 보면 Bureaucrat클래스 정의할려고 하면 Form이 필요하다. Form을 호출할려고 하면 Bureaucrat이 필요해서 정의 를 불려오려고 하면 컴파일 에러가 난다.

이러한 경우 우리는
Bureaucrat은 나중에 정의할거니 나중에 링크걸어서 가져와라고 암시해주어야하고
Form도 나중에 정의할테니 나중에 가져와라고 암시해주어야한다.

Bureaucrat.hpp
  8 class Form; // 나중에 정의할걸 암시
 10 class Bureaucrat{
 11     public:
 27         void        signForm(Form &F) const;
 28         void        executeForm(Form const & form) const;
 36 };

Form.hpp
  8 class Bureaucrat; // 나중에 정의할걸 암시
 10 class Form{
 11     public:
 38         virtual void        execute(Bureaucrat const &rhd) const = 0;
 38}

이러한 방법 말고도 상속으로 해결하는 방법이 있다.
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=icysword&logNo=140190552673
가서 보고 참고 할것!

부모(생성자, 소멸자) 자식(생성자,소멸자) 순서

자식 생성자가 실행될때, 부모 생성자중 매게변수가 없는 기본 생성자가 먼저 호출된다.

하지만 자식이 생성될때 부모의 기본생성자가 아닌 다른 생성자를 사용하고 싶다면 초기화리스트를 사용하면 된다.

	//A.hpp
  4 class A{
  5     public :
  6     int number;
  7     A();
  8     A(int number);
  9 };
  
  //B.hpp
  6 class B: public A{
  7     public :
  8         B();
  9         B(int number);
 10         void print_number();
 11 //  ~B();
 12 };
 
 //B.cpp
   4 B::B()
  5 {
  6     number = 0;
  7     std::cout << "B()" << std::endl;
  8 }
  9
 10 B::B(int number) : A(number)
 11 {
 12     std::cout << "B(int number)" << std::endl;
 13 }
 14
 15 void B::print_number()
 16 {
 17     std::cout << number << std::endl;
 18 }
 
 //main.cpp
  4 int main (void)
  5 {
  6     B aaa;
  7     aaa.print_number();
  8     return 0;
  9 }

B.hpp 의 10번째 줄을 보면 A(number)를 통해 부모의 생성자를 어떤것을 할건지 선택하는걸 볼수 있다.

B::B(int number) : A(number)

소멸자는???

  4 int main (void)
  5 {
  6     B aaa;
  7     aaa.print_number();
  8     return 0;
  9 }

 14 A::~A()
 15 {
 16     std::cout << "A__destructor" << std::endl;
 17 }
 
  20 B::~B()
 21 {
 22     std::cout << "B__destructor" << std::endl;
 23 }

코드를 실행해보면 아래와 같이 나온다.

생성자는 부모 먼저 생성되고 자식인 B()생성자가 생성되고,
수멸자는 B()먼저 소멸되고 부모가 소멸된다.

그런데 자식 객체인 B()만 인스턴스화 했는데 왜 부모의 생성자와 소멸자도 함께 불러오는가?! 이는 상속의 특성인데, 자식은 부모의 모든것을 물려받는다.

업케스팅시 생성자 소멸자는?!

B aaa;

방금전까지 자식객체를 인스턴스화 하는데는 아무런 문제도 없었다.

그러나, 우리는 cpp를 하다보면 업케스팅 다운케스팅도 한다.

A *aa = new B();
delete aa;

업케스팅의 예로 가지고 왔다. 이건?? 생성자 소멸자가 정상적으로 동작할까?

이상하다. 생성자 까지는 일반적인 인스턴스화 처럼 같게 나오는데 소멸자가 자식은 소멸되지 않는다.

그 이유는 '='이 있을때 코드는 오른쪽에서 왼쪽으로 실행된다.

new B()할때 일반적으로 자식 인스턴스화 할때 처럼 실행된다.

고로 부모의 생성자와 자식의 생성자가 실행이 되었고 그 후 형변환 되어 aa라는 곳에 넣어주었다. 이때 new B()에서 할당한 주소는 부모와 자식 모두가 사용가능한 공간이 할당된다.

그러나 형변환 과정을 거치면서 *aa는 부모의 공간만 접근 가능해진다.

고로 aa를 delete시킬때 부모의 공간은 소멸시키지만 자식의 공간은 접근 못해 소멸시키지 못한다.

int main (void)
{
	printf("A클래스의 크기 = %lu, B클래스의 크기= %lu\n", sizeof(A), sizeof(B));
	return 0;
}

위의 그림들과 같이 메모리가 생성되는지 확인해볼려고 A와 B 클래스의 크기를 찍어 보았다.

예를 들어 A객체에 int a가 있고 B객체에 int b 가 있으면 B객체의 크기는 8바이트다. A객체의 int a까지 상속받아 메모리에 포함되어 있기 때문이다.

변수는 객체의 메모리에 올라가지만, 함수는 객체에 포함되어 저장되지 않습니다. 함수는 다른 공간에 저장되어 호출할때마다 불러오는 형태로 사용됩니다. 그래서 객체가 여러개 더라도 함수는 하나이기때문에 메모리를 아낄수 있어요!

virtual 소멸자

업케스팅 시 소멸자 호출 해결책(virtual)
virtual 키워드를 함수에 붙여주면 해당 클래스에는 v-table이란 것이 생성되게 됩니다.

 //A.hpp
 4 class A{
 5     public :
 6     ~A();
 7     int number;
 8     int number2;
 9 }
 
 //B.hpp
 6 class B: public A{
 7     public :
 8         int BB;
 9         int BB2;
10 }

int main(void)
{
	printf("A클래스의 크기 = %lu, B클래스의 크기= %lu\n", sizeof(A), sizeof(B));

	return 0;
}

A 클래스와 B클래스의 사이즈를 찍어보면 선언해둔 int의 크기 만큼 사이즈가 잡힌다.

class A의 소멸자 ~A();만 virtual ~A()로 바꿔 보았다.

virtual ~A();

클래스의 크기가 바꼈다. 이상하다. 앞에서 이야기 했을땐, 매게함수는 클래스와 따로 저장된다고 햇는데?

여기서 virtual을 사용하게 되면, v-table을 가르키는 virtual table pointer가 추가가 되서 그렇습니다.

부모 클래스에 virtual이 선언되면 자식 클래스는 자동으로 B클래스에 대한

virtual pointer를 만들게 됩니다.


그로인해 new B()를 형변환 해서 A를 하더라도 *aa에는 ~B에 대한 정보가 저장됩니다.

int main (void)
{
	A *aa = new B();
	delete aa;

	return 0;
}

고로 업케스팅이 있어도 *aa에는 ~B()에 대한 정보가 있으므로 B소멸자를 실행하고 A소멸자를 실행하고 종료됩니다.

0개의 댓글