http://www.thisisgame.com/webzine/series/nboard/212/?n=90642

 

객체 생성 및 소멸 과정 중에는 가상함수를 호출하면 절대로 안 됩니다!!

우선 코드부터 봅시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>
 
class Transaction
{
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction :: Transaction()
{
      /**
* 생성자에서 순수가상함수를 호출했음
*/
logTransaction();
}
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
void BuyTransaction::logTransaction() const
{
}
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
void SellTransaction::logTransaction() const
{
}
int main()
{
BuyTransaction b;
return 0;
}






위의 코드는 다행스럽게도(?) LINK 에러를 내준다.

언뜻 봐서는 부모클래스의 순수가상함수를 자식들이 재정의 했고, BuyTransaction 선언으로

부모생성자가 호출되면서, 자식클래스의 정의함수로 넘어가게끔 되어 문제가 없어 보인다.

 

하지만,

이 코드가 LINK 에러를 내뱉는 이유는?

 

BuyTransaction 클래스는 Transaction 클래스를 상속받았기 때문에

상속받은 클래스의 객체가 생성될 때에는 부모 클래스의 생성자가 먼저 호출된다.

부모의 생성자가 먼저 호출된 상태에서 가상 함수를 호출하면, 현재 정의가 되지 않았기 때문에 오류가 생기게 된다.

 

즉!
부모의 생성자가 먼저 호출된다는 것은 그 동안에는 부모의 클래스 타입, 즉 Transaction 클
래스 타입으로 인식을 하게 된다는 것입니다.
그러므로, Transaction()함수의 정의는 찾을 수 없으니까 LINK에러가 나는 것입니다.

 

소멸자의 경우도 비슷하게 생각하면 된다. 

파생 클래스에서 정의된 소멸자가 수행되고, 부모 클래스의 소멸자가 수행이된다. 여기서 부모 클래스 소멸자가 수행될 때는 그 객체가 부모 클래스 타입이라고 인식하게 된다는 것이다.

 
위의코드가 에러가 나지 않는다면, 끔찍한 일(?)이 발생 합니다.
 
가장중요하게 우리가 알아야 할 부분은?
 
1. 생성자 및 소멸자에서는 가상함수를 호출 하면 안된다.
2. 파생 클래스의 생성자 및 소멸자 수행시 부모 클래스의 생성자/소멸자를 수행할 때, 컴파일러는 부모 클래스의 타입으로 인식하게 된다
 
 
그러면, 위의코드처럼 우리가 원하는건 파생클래스 객체 선언과 동시에 로그정보를 호출되기를 원한다고 가정 해봅시다. 방법은 많이 있겠지만, 책의 나와 있는 방법으로 간단하게 해결하도록 하겠습니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
 
class Transaction 
  public
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std::string& logInfo) const;   
};
Transaction::Transaction(const std::string& logInfo)
    logTransaction(logInfo); 
}
void Transaction::logTransaction(const std::string& logInfo) const
{
    std::cout<<logInfo.c_str()<<std::endl;
}
 
class BuyTransaction : public Transaction 
public:
    BuyTransaction(const char * log) : Transaction(createLogString(log)){ }
private:
    static std::string createLogString(const char * log) { return log; }
}; 
 
int main() 
    BuyTransaction b("TEST");
    return 0;
}
 
 
위의 코드는 순수가상함수였던 (logTransaction) 함수를 비가상함수로 바꾸고, 자식클래스 생성자들로 하여금 필요한 로그정보를 부모생성자로 넘기고 있습니다.  
 
 

이것 만은 잊지 말자!

◆ 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당된느 클래스의 파생클래스 쪽으로는 내려가지 않으니까요

 


 

class Widget {}

 

vector<Widget> v;

위의 클래스가 선언되어 있고, vector<Widget> v의 사이즈가 10이라고 가정합니다.

 

v가 사이즈가 10인데, 첫번째 것을 소멸시키는 도중에 예외가 발생되었다고 가정하면, 나머지 아홉 개는 소멸자가 호출되지 않아 메모리 누수가 생깁니다.

 

위의 문제를 방지하기 위해서는 소멸자에게 예외가 나오는 것을 방지해야 합니다.

예를 들어봅시다.

 

ex ) DB를 접속하는 클래스

class DBConnection {

  public:

  /**

   * 생성자 소멸자가 아닌 따로 함수를 만들어서 생성 소멸을 할 수 있게 작업

   */

  static DBConnection create();

  void close();

};

 

ex) DBConnetion에 대한 자원 관리 클래스

class DBConn {

public :

 ~DBConn()

  {

      db.close();

   }

private:
  DBConnection db;
};
 
int main()
{
  /**
  * DBConnection 객체를 생성하고 이것을 DBConn 객체로 넘겨서 관리를 맡깁니다.
  * DBConn 인터페이스를 통해 DBConnection 객체를 사용합니다.
  * main함수 종료시 DBConn 객체가 소멸됩니다. 따라서 DBConnection 객체에 대한 close
  * 함수의 호출이 자동으로 이루어 집니다.
  */
  DBConn dbc ( DBConnection::create() );
  return 0;
}
 
위의 코드처럼
사용자의 망각을 사전에 차단하는 DBConnection에 대한 자원 관리 클래스를 만들어서 그 클래스의 소멸자에서 close를 호출하게 만드는 것입니다.
 
위와 같은 방법은 close() 함수가 제대로 호출만 된다면 문제 없다.
하지만 close() 함수를 호출했는데, 여기서 예외가 발생한다면, 또 문제가 생기게 된다.
 
이것을 피하기 위한 방법은 두가지가 있다.
 
첫째. close에서 예외가 발생하면 프로그램을 바로 끝냅니다. 대개 abort를 호출합니다.
 
DBConn::~DBConn()
{
    try { db.close(); }
    catch(...)
   {
       // close 호출이 실패했다는 로그를 작성합니다.
       std::abort();
   }
 
try ~ catch 문을 이용하여 일단 close() 함수를 실행한 뒤, 예외가 발생시 abort() 함수를 이용하여
프로그램을 끝내버린다.
 

객체 소멸이 진행되다가 에러가 발생한 후 더이상 프로그램을 실행할 수 없는 상황일 경우에 사용한다.

둘째. close를 홀출한 곳에서 일어난 예외를 삼켜버립니다. 

DBConn::~DBConn()
{
    try { db.close(); }
    catch(...)
    {
       // close 호출이 실패했다는 로그를 작성합니다.     
     }
}
 
예외 발생시 catch 블럭으로 들어가긴 하지만 아무것도 수행하지 않는다. 즉, 예외를 삼킨 것이다.
 

예외 삼키기를 선택한 것이 제대로 빛을 보려면, 발생한 예외를 그냥 무시한 뒤라도 프로그램이 신뢰성 있게 실행을 지속할 수 있어야 합니다.

 

이 두가지 방법 또한 문제점들을 가지고 있습니다.

 

이유는?

이 두 방법은 예외가 발생한 후의 처리를 하는 것인데, 실제로 close()가 예외를 던지게 된 요인에 대

한 조치를 취하는 대책이 전무한 상태입니다.

 

더 나은 방법은 아래와 같습니다. 

DBConn 클래스에서 close()를 직접 제공하는 방법이 있습니다.

 

이렇게 만들면, close() 함수가 실행 중에 발생하는 예외를 사용자가 처리할 수 있습니다.

 

class DBConn {

public:

   ...

   void close()              // 사용자에게 호출을 위해 정의한 close() 함수

  {

db.close();

closed = true; 

   }

   ~DBConn()

  {

if( !closed )

{

    try{

      db.close();

    }

    catch( ... )

   {

       // close 호출 실패 로그 출력

       // ...

   }

}

   }

 

private:

DBConnection db;

bool closed;

};

 

close() 호출 책임을 소멸자에서 사용자에게로 떠넘긴다.

예외를 처리할 수 있는 기회를 사용자에게 주는 것입니다. 이것 마저 없다면 사용자는 예외에 대처할 기회를 못잡게 됩니다. ( 2차 검증으로 DBConn 소멸자에서 마무리 )

 

 

즉!

예외가 생길 수 있는 코드는 소멸자보다는 다른 함수에서 비롯되어야 한다.

 

이것 만은 잊지 말자!

◆ 소멸자에서는 예외가 빠져나가면 안됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야합니다.

◆ 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.


다형성이란?

 

사전적 의미로는 여러 형태를 갖는 것 으로 정의된다. C#, C++, Java 와 같은 객체지향 언어에서는 여러 개의 클래스가 같은 메시지에 대해서 각자 다른 방법으 작동할 수 있는 능력 이라 할 수 있겠 

 

쉽게 말해서,

부모의 추상함수를 자식 클래스들이 오버라이딩 하여, 부모의 인터페이스로 각각의 자식 클래스에 정의된 함수를 호출 할 수 있는 능력이라고 말할 수 있겠습니다.  

 

ex ) 다형성을 통한 ptk 제어 /관리 가능. ( 소스에서 일단 오버라이딩 제외 )

class TimeKeeper  

{     
   public
       TimeKeeper();

      ~TimeKeeper(); // 현재 가상소멸자가 아니다.

  };
  class AtomicClock : public TimeKeeper  {  };   

  class WaterClock : public TimeKeeper  {  }; 

  class WristWatch : public TimeKeeper  {  }; 

 

class CTimeFactory
{

  public:

       static TimeKeeper  * CreateAtomic(){  // 팩토리 함수

return new AtomicClock; 

       }

       static TimeKeeper  * CreateWater(){    // 팩토리 함수

          return new WaterClock;

       }

       static TimeKeeper  * CreateWrist(){     // 팩토리 함수

return new WristWatch;

       }

}
  
void main() 
{      
      TimeKeeper* ptk = CTimeFactory::CreateAtomic();

      delete ptk;  // 메모리 누수 방지 

}

 

팩토리 함수란?

새로 생성된 파생 클래스 객체에 대한 기본 클래스 포인터를 반환하는 함수를 의미한다.

 

위의 코드는 다음과 같은 의미를 갖는다.

1) CTimeFactory::CreateAtomic() 가 반환하는 포인터가 파생 클래스에 대한 포인터라는 점 ( AtomicClock* )

2) 이 포인터가 가리키는 객체가 삭제될 때는 기본 클래스 포인터(TimeKeeper* 포인터)를 통해 삭제되다는 점

3) 기본 클래스에 들어있는 소멸자가 비가상 소멸자라는 점

 

 

C++ 규칙에서, 기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될 때, 그 기본 클래스에 비가상 소멸자가 들어있으면 프로그램 동작은 어떻게 될지 모릅니다.

 

보통 그 객체의 파생 클래스 부분이 삭제가 되지 않습니다.

즉, 완전히 제거가 되지 않는다는 것이다.

 

그럼 이에 대한 해결책,

바로 기본 클래스에 가상 소멸자를 선언해주면 된다.   virtual ~TimeKeeper();

 

보통

그 클래스에 가상함수를 하나라도 가지고 있다면, 가상 소멸자를 가지는 것이 맞다고 봅니다.

 

가상 소멸자를 가지고 있지 않다면,

이는 어떤 클래스의 기본 클래스로 쓰이는 경우가 없다고 이해하면된다.

 

그렇다고, 가상소멸자, 가상함수를 난무 해서는 안됩니다.

가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조가 하나 들어 갑니다.

이 자료구조는 프로그램 실행 중에 주어진 객체에 대해 어떤 가상 함수를 호출해야 하는지를 결정하는데 쓰이는 정보입니다.

 

즉! 가상함수를 가지는 클래스에는 별도로 vptr(가상 함수 테이블 포인터)가 추가됩니다. 이는 가상 함수 테이블을 가리키는 포인터인데, 가상함수 테이블은 vtbl이라고 하며 이는 가상함수 포인터들을 담고 있는 배열입니다.

 

가상함수가 추가되면,

객체의 크기가 커질뿐만아니라, 다른언어로의 이식성은 기대를 접는것이 좋습니다.

 

1. 객체의 크기는 vptr( 가상 함수 테이블 포인터) 크기가 더해지면서, 커지고,

2. 이식성은 vptr을 구현환경에 따라 세부사항이 달라지는 문제가 있으므로 힘듭니다.

 

따라서

가상 소멸자를 선언하는 것은 그 클래스에 가상 함수가 하나라도 들어있는 경우에만 한정

 

ex ) 가상함수가 전혀 없는데도 비가상 소멸자 때문에 오류인 예 STL string을 상속받은 예 

class SpecialString : public std::string  

 

    ...


   
void main() 
 { 
      SpecialString *pss = new SpecialString("ABC");
      string* ps;   
      ps = pss;   
      delete ps;
  } 

 

위의 코드는 string 객체 ps(부모클래스 객체)를 소거 함으로써,

SpecialString 클래스의 소멸자가 호출되지 않는다. 이코드는 정상 수행을 할 수가 없다.

 

가상소멸자가 없는 클래스는 STL 컨테이너 타입 전부가 바로 여기에 속한다.

표준 컨테이너를 상속받아서 클래스를 만들지 않아야 한다.

 

 

경우에 따라서는 순수 가상 소멸자를 두면 편리하게 쓸 수도 있습니다.

 

추상클래스는

본래 기본 클래스로 쓰일 목적으로 만들어진 것이고, 기본 클래스로 쓰이려는 클래스는 가상 소멸자를 가져야 합니다. 그리고 순수가상함수가 있으면 바로 추상 클래스가 됩니다.

 

 class AWOV 

 {

    public

       virtual ~AWOV() = 0// 순수 가상 소멸자 

 };     
 

일단 위의 클래스는 순수가상함수를 가지므로, 추상 클래스 입니다.

동시에 순수 가상함수가 가상 소멸자이므로, 소멸자 호출 문제로 고민 할 필요도 없습니다.

 

여기서 중요한게,

이 순수 가상 소멸자의 정의를 두지 않으면 안된다는 것입니다.

 

 AWOV :: ~AWOV() {} // 정의 

 

컴파일러는 ~AWOV의 호출코드를 만들기 위해 파생 클래스 소멸자를 사용할 것이므로, 잊지 말고 꼭 정의 해야한다. 

 

 

정리하자면,

 

기본 클래스에 가상 소멸자를 선언하는 것은 다형성(polymorphic)을 가진 기본클래스, 그러니까 기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록 설계된 기본 클래스에만 적용됩니다.( 참고. 팩토리 패턴 )

 

 

 

이것 만은 잊지 말자! 

◆ 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 합니다.

◆ 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 합니다.




http://www.thisisgame.com/webzine/news/nboard/4/?n=91120



http://www.thisisgame.com/webzine/news/nboard/266/?n=91092




Work smarter not harder with Project Search & Replace! Make changes to your entire project with the click of a button. Open the PSR Editor Window, search for something, then replace it. No code required!

Now with Full Source Code!

Update artwork, fix animations, swap out prefabs, meshes, colors, references, and more!

Search and replace data inside Prefabs, Scenes, Components, MonoBehaviours, Materials, and more.

Search across the entire project, a specific folder, scene, your current selection, prefab, even a specific MonoBehaviour if you like.

Additional Features:

* Save Searches = Save a search and run it again later!
* Subsearches = Chain searches together to filter your results. Search for all materials with a certain color, and a certain texture.
* Conditional Searches - Match when a value exists, when it doesn't, or any value.
* Dependency Search - Search objects and all other objects that object depends on.
* Search While Playing - Looking for a needle in a haystack during playback? Let PS&R find that elusive data!
* Go To Item - Select and display a prefab in the Inspector by clicking a button in the results.
* Vertical and Horizontal layouts - The Vertical Layout displays less info in a more compact layout, while the Horizontal Layout provides extra data.
* Asset Type Search - Search inside just prefabs, scenes, materials and more.
* Copy To Clipboard - Export your search results to plain text.
* Advanced String Search - Search inside strings with case insensitive search, and even regular expressions.
* Granular Searches - Search specific subproperties of Vector3 and more. For example: search for all transform positions with an 'x' of 42 and change the 'y' to 43.
* Lots To Search? - Have a big project? It can handle it. The paginated UI can display thousands of results without bogging down your machine.
* And More!

Asset version: 1.7.0



'0x0010 > Unity Asset' 카테고리의 다른 글

7 Painted Cartoon Skyboxes v1.0  (0) 2019.02.13
Love/Hate v1.9.5  (0) 2019.02.13
Memory Game Starter Kit v1.1  (0) 2019.02.10
Ultimate Mobile Pro v2019.2.8  (0) 2019.02.10
6 Games Match-3 Puzzle Action Game Pack v2.0  (0) 2019.02.09



Classic Memory Game Starter Kit

Features:
-Flexible Board
-Basically unlimited amount of Pairs
-Supports even odd numbers of Pairs (e.g. 5, 7)
-Card cover/uncover animation
-Easy to customize, just add your own sprites and play
-C# code

Asset version: 1.1



'0x0010 > Unity Asset' 카테고리의 다른 글

Love/Hate v1.9.5  (0) 2019.02.13
Project Search & Replace v1.7.0  (0) 2019.02.10
Ultimate Mobile Pro v2019.2.8  (0) 2019.02.10
6 Games Match-3 Puzzle Action Game Pack v2.0  (0) 2019.02.09
Card Game Starter Kit v1.1  (0) 2019.02.09

+ Recent posts