블로그 이미지
대갈장군

calendar

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

Notice

2014. 8. 7. 22:57 프로그래밍

출처: http://www.cplusplus.com/articles/Gw6AC542/


 1> 왜 우리는 헤더 파일이 필요한가? 


만약 당신이 C++ 처음 접하는 사람이라면 프로그램이 왜 #include 를 하는지와 여러개의 .cpp 파일이 필요한 지 궁금할 것이다. 이유는 다음과 같다.


  1. Compile 속도를 높인다. 당신의 프로그램이 커지면 커질수록 코드의 양도 많아진다. 만약 모든 것이 하나의 파일에 다 들어간다면 작은 변경에도 큰 파일 전체가 컴파일 되어야 하므로 시간이 오래 걸린다. 물론 작은 프로그램의 경우에는 이거 별거 아니지만 적당한 규모 이상일 경우에는 컴파일 타임을 무시 할 수 없다.
  2. 코드가 보다 잘 정렬된다. (Organized) 만약 내가 코드의 각 부분을 컨셉에 따라 잘 나누어 놓으면 어떤 파일을 변경해야 하는지 손 쉽게 알 수 있으므로 잘 정리된 프로그램은 수정이 용이 하다.
  3. interface를 implementation으로 부터 분리 할 수 있다. 즉, 헤더에 프로그램의 interface를 정의해 주고 cpp 코드에 implementation을 하면 아주 깔끔하다.
C++ 프로그램들은 두 단계를 거쳐 생성된다. 첫번째 단계는 각각의 소스 파일들이 각각 컴파일 되는 것이다. 컴파일러는 각각의 컴파일된 소스파일에 대한 intermediate file들을 생성해 낸다. 이 중간 단계의 파일들을 우리는 object file이라고 부른다. 그리고 일단 모든 파일들이 각각 컴파일 된 후에는 모든 object 파일들을 링크하게 된다. 바로 이때 Binary 파일 (exe 파일)이 생성된다.


자, 좀 더 자세히 설명하자면, 각각의 소스 파일이 다른 소스 파일과는 완전히 별개로 컴파일 된다. 이말인 즉슨, a.cpp라는 소스 파일은 b.cpp라는 소스 파일 안에 무슨 일이 벌어지고 있는지 전~혀 모르고 있다는 말이다. 예를 들자면,


// in myclass.cpp

class MyClass
{
public:
  void foo();
  int bar;
};

void MyClass::foo()
{
  // do stuff
}


// in main.cpp

int main()
{
  MyClass a; // Compiler error: 'MyClass' is unidentified
  return 0;
}



위 두 파일의 경우 분명히 MyClass가 myclass.cpp에 선언되었지만 main.cpp에서는 이 클래스에 대해서 알수 있는 길이 없다는 것. 즉, 에러가 난다. 너무나도 당연한 에러다.


여기서 헤더파일의 역활이 드러난다. 헤더 파일은 다른 .cpp 파일들에게 interface를 공개하는 역활을 한다. 물론 implementation은 cpp파일에 남겨두고 오직 interface만 공개하는 것이 헤더 파일의 임무이다. 위의 경우를 예를 들자면 헤더 파일은 다음과 같아야 한다.


// in myclass.h

class MyClass
{
public:
  void foo();
  int bar;
};


// in myclass.cpp
#include "myclass.h"

void MyClass::foo()
{
}


//in main.cpp
#include "myclass.h"  // defines MyClass

int main()
{
  MyClass a; // no longer produces an error, because MyClass is defined
  return 0;
}


자, 이제 main.cpp에서 MyClass 클래스에 대해서 구조를 알 수 있으므로 컴파일에 에러가 나지 않는다.



 2> 확장자에 따른 차이점은?  


그렇다면 여기서 궁금한 것 한가지, .h / .cpp / .hpp / .cc 등등과 같은 서로 다른 확장자의 파일들은 무슨 차이가 있는가?


생각보다 룰이 간단한데, 모든 .h__ 파일들은 걍 헤더 파일이다.

모든 .c__ 파일들은 모두 C++ 소스 코드들이다. 무슨 확장자던 관계없다.

C 코드는 반드시 .c 파일이어야 한다. (요거만 예외적이네)


  3> Include Guards


C++ 컴파일러는 뇌가 없다. 우리가 시키는 대로만 하는 것이 컴파일러이다. 고로 헤더를 두번 include 시키게 되면 이미 정의된 놈이라는 어이없는 에러가 마구마구 터질 것이다. 아마도 어떤 바보가 이렇게 하겠느냐고 질문하겠지만 이런 경우는 의외로 빈번하게 발생한다. 예를 들자면 다음과 같은 경우이다.


1
2
// x.h
class X { };


1
2
3
4
// a.h
#include "x.h"

class A { X x; };


1
2
3
4
// b.h
#include "x.h"

class B { X x; };


1
2
3
4
// main.cpp

#include "a.h"  // also includes "x.h"
#include "b.h"  // includes x.h again!  ERROR 


a.h와 b.h가 둘다 x.h를 include 하는 상황에서 main.cpp에서 a.h와 b.h를 두 번 include 하게 되면 x.h가 두 번 불러지는 셈이된다. 고로 에러...


종종 어떤 사람들은 헤더 파일에서는 include를 하지 말라는 터무니 없는 말을 하는데 이것은 잘못된 지식이다. 헤더에 무슨 파일을 포함하던지 상관이 없다. 단, 두 가지 조건을 만족해야 한다.


1. 반드시 필요한 헤더만 include하라

2. include guards를 통해서 multiple include를 미연에 방지하라


여기서 처음 보는 단어의 등장. Include Guards라는 것인데, 이것은 다음과 같은 것이다.


1
2
3
4
5
6
7
8
//x.h

#ifndef __X_H_INCLUDED__   // if x.h hasn't been included yet...
#define __X_H_INCLUDED__   //   #define this so the compiler knows it has been included

class X { };

#endif 


자, 이렇게 헤더를 둘러쌓으면 x.h가 두번째 include 될때 __X_H_INCLUDED__가 이미 정의되었으므로 헤더가 통째로 스킵되어 버린다. 고로 에러가 없이 지나간다. 고로, 무조건 include guard를 해라, 모든 헤더파일에 해라. 해도 무해하다... 해라 해라 또 해라.



  4> 올바른 include 방법


내가 만드는 클래스는 종종 다른 클래스들에게 의존적인 경우가 많이 있을 것이다. 예를 들자면 파생 클래스의 경우 부모 클래스를 알고 있어야 한다. 부모가 있어야 자식이 나오기 때문이지.


일단 두 가지 종류의 Dependency 가 있는데 다음과 같다.

1. Forward declare가 가능한 것

2. #include가 되어야만 하는 것


자, 예를 들어 Class A가 Class B를 사용하고 있다면 Class B는 Class A의 Dependency 중 한 개다. 그렇다면 Class A 내부에서 B를 어떻게 사용하는 가에 따라 forward declared 될 것인지 아니면 included 되어야 할지 분류가 가능하다. 다음과 같은 룰을 따르면 된다.


1. 만약 A가 B에게로 어떤 reference 도 만들지 않았을 경우: 아무것도 할 필요 없음

2. 만약 B에게 향하는 유일한 reference 가 friend declaration인 경우: 아무것도 할 필요 없음

3. 만약 A가 B 포인터 혹은 레퍼런스를 포함한 경우: Forward Declare

4. 만약 하나 혹은 하나 이상의 함수가 B의 object/pointer/reference를 입력 변수로 가진 경우 (또는 리턴 타입으로 가지는 경우): Forward Declare

5. 만약 B가 A의 부모 클래스라면: #include "b.h"

6. 만약 A가 B의 객체를 포함한다면: #include "b.h"


자, 말로 설명하니 좀 이해가 안된다. 코드로 보면, 

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
//=================================
// include guard
#ifndef __MYCLASS_H_INCLUDED__
#define __MYCLASS_H_INCLUDED__

//=================================
// forward declared dependencies
class Foo;
class Bar;

//=================================
// included dependencies
#include <vector>
#include "parent.h"

//=================================
// the actual class
class MyClass : public Parent  // Parent object, so #include "parent.h"
{
public:
  std::vector<int> avector;    // vector object, so #include <vector>
  Foo* foo;                    // Foo pointer, so forward declare Foo
  void Func(Bar& bar);         // Bar reference, so forward declare Bar

  friend class MyFriend;       // friend declaration is not a dependency
                               //   don't do anything about MyFriend
};

#endif // __MYCLASS_H_INCLUDED__ 


위에서 설명한 모든 경우에 대해서 잘 보여주고 있다. Forward Declare는 헤더 시작 부분에 먼저 정의 하라는 의미네. 여기서 명심해야 할 점은, Foo class의 경우 MyClass 본체에서 포인터로만 사용되고 있기 때문에 include를 할 필요가 없고 다만 forward declare만 해주면 된다는 점. 최대한 include를 최소화 하는 방향으로 가는 것이 가장 올바른 코딩이다. 무분별한 include는 문제를 야기 할 수 있다는 점 명심하란다.



  5> 왜 위의 방법이 '올바른' 방법인가?


그렇다면 왜 필자가 말하는 방법이 이른바 '올바른' include 방법인가? 일단 해답은 Object Oriented에 있다. 필자가 이루고자 하는 기본적인 것은 myclass.h 파일이 그 자체로 self-contained, 즉, 완전체가 되기 위해서는 위와 같은 법칙을 따라야 한다는 것이다. 만약 다른 소스 파일에서 myclass를 써야 한다면 #include "myclass.h" 단 한줄로 모든것을 끝내버릴 수 있다. 


반면 헤더 파일안에 헤더 파일을 include 하지 않는 원칙을 따르게 되면 다음과 같은 문제가 생긴다. myclass를 사용하려면 myclass가 필요로 하는 모든 헤더 파일을 먼저 추가해야 하는 문제가 발생한다. 다음과 같은 코드를 보자.

1
2
3
4
5
6
7
//example.cpp

//  I want to use MyClass
#include "myclass.h"   // will always work, no matter what MyClass looks like.
                       // You're done
               //  (provided myclass.h follows my outline above and does
               //   not make unnecessary #includes) 


위의 코드를 보면 myclass.h를 포함시켰는데 만약 필자가 제시한 방법대로 했다면 다른 어떤 파일도 추가로 필요하지 않지만 만약 그렇지 않은 경우라면 다음과 같은 이유로 에러가 빠빡 터진다.


Here is an example of why so-and-so's method is bad:

1
2
3
4
5
//example.cpp

//  I want to use MyClass
#include "myclass.h"
   // ERROR 'Parent' undefined 



so-and-so: "Hrm... okay...."

1
2
3
#include "parent.h"
#include "myclass.h"
   // ERROR 'std::vector' undefined 



1
2
3
4
#include "parent.h"
#include <vector>
#include "myclass.h"
   // ERROR 'Support' undefined 



so-and-so: "WTF? MyClass doesn't even use Support! But alright..."

1
2
3
4
5
#include "parent.h"
#include <vector>
#include "support.h"
#include "myclass.h"
   // ERROR 'Support' undefined 


제일 마지막 줄에 Support가 없어서 에러가 터지는 이유는 parent.h에서 support 클래스를 사용하고 있기 때문이란다. 고로 support.h가 parent.h 보다 앞에 include되어야 에러가 안난다. 자, 고로 문제는 헤더 파일에 다른 헤더 파일을 포함하지 않는 경우, 하나의 클래스를 사용하기 위해 해당 클래스가 사용하는 모든 헤더파일을 먼저 불러와야 한다는 단점과 더불어 헤더 파일의 순서 또한 중요하다는 것을 알 수 있다. 이런 문제는 사실 비일비재 하게 발생하고 있다.



  6> Circular Dependencies


1
2
3
4
// a.h -- assume it's guarded
#include "b.h"

class A { B* b; };


1
2
3
4
// b.h -- assume it's guarded
#include "a.h"

class B { A* a };


상호 의존이라 함은 위의 예제 코드 처럼 서로가 서로의 클래스르 사용하는 경우를 말한다. 그냥 보기에는 문제가 없어 보이지만 실제로 컴파일러의 흐름을 따라가보면 아래와 같은 문제가 발생한다.


1
2
// a.cpp
#include "a.h" 


The compiler will do the following:

1
2
3
4
5
6
7
8
9
10
11
12
#include "a.h"

   // start compiling a.h
   #include "b.h"

      // start compiling b.h
      #include "a.h"

         // compilation of a.h skipped because it's guarded

      // resume compiling b.h
      class B { A* a };        // <--- ERROR, A is undeclared 


자 위 코드를 차례대로 따라가 보면, a.cpp를 컴파일 시작하면 우선 a.h 헤더 파일이 선두이므로 a.h를 컴파일 하기 시작하는데 a.h의 선두에는 b.h가 선언되므로 b.h를 컴파일 하려고 하는데 다시 b.h의 선두에 a.h가 선언 되므로 a.h를 컴파일 하려고 하자, include guard가 되어버린 경우, a.h를 스킵하고 지나가버리게 된다. 고로 A라는 클래스를 알 수 없다는 에러가 터진다. 이것이 circular include의 문제점이다. 


이것을 방지하려면 앞서 살펴본 원칙대로 forward declare를 하면 된다. 왜냐면 reference 타입만 사용된 경우에는 forward declare를 통해 이런 circular include의 연결 고리를 끊어줄 수 있기 때문이다.


고로 만약 두 헤더 파일이 서로의 클래스를 직접적인 객체로 사용하는 경우에는 이 circular include 문제를 해결할 수 가 없다. 


1
2
3
4
5
6
7
8
// a.h (guarded)

#include "b.h"

class A
{
  B b;   // B is an object, can't be forward declared
};


1
2
3
4
5
6
7
8
// b.h (guarded)

#include "a.h"

class B
{
  A a;   // A is an object, can't be forward declared
};


헌데 잘 보면 이러한 경우는 구조 자체가 설계가 잘못 된 경우다. 이런것은 infinite recursion을 발생시키게 된다. 고로 올바른 형태는 다른 클래스의 포인터를 사용하는 것이 올바르다. Forward declare가 바로 이런 이유로 사용되는 것이다.



 7> 인라인 함수 


1
2
3
4
5
6
7
8
9
10
class B
{
public:
  void Func(const A& a)   // parameter, so forward declare is okay
  {
    a.DoSomething();      // but now that we've dereferenced it, it
                          //  becomes an #include dependency
               // = we now have a potential circular inclusion
  }
};


자, 위의 코드를 보면 인라인 함수가 circular inclusion 문제를 야기할 수 있음을 알 수 있다. 입력 변수로 선언된 시점 까지는 forward declare가 가능하지만 DoSomething()이라는 함수를 호출하는 순간 객체가 구현되므로 반드시 #include를 해야하는 문제가 발생한다.


고로 이것을 고치려면 아래와 같이 바꾸면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// b.h  (assume its guarded)

//------------------
class A;  // forward declared dependency

//------------------
class B
{
public:
  void Func(const A& a);  // okay, A is forward declared
};

//------------------
#include "a.h"        // A is now an include dependency

inline void B::Func(const A& a)
{
  a.DoSomething();    // okay!  a.h has been included
}


단순히 봐서 이게 뭐가 다르지 라는 생각이 들것이다. 헤더 파일을 인라인 함수 앞에 가져다 놓고 인라인 함수를 클래스 선언에서 분리했다는 점 외에는 차이점이 없다. 그런데... 이것은 완벽하게 안전하다. 왜냐면 Class B가 완전히 정의된 후에 a.h가 include 되었기 떄문이란다. 흠.


그런데 헤더 파일 안에 함수가 구현되어 있는게 보기 싫은 분들은 다음과 같이 해도 된다. 


1
2
3
4
5
6
7
// b.h

    // blah blah

class B { /* blah blah */ };

#include "b_inline.h"  // or I sometimes use "b.hpp" 


1
2
3
4
5
6
7
8
9
10
11
// b_inline.h (or b.hpp -- whatever)

#include "a.h"
#include "b.h"  // not necessary, but harmless
                //  you can do this to make this "feel" like a source
                //  file, even though it isn't

inline void B::Func(const A& a)
{
  a.DoSomething();
}


인라인 함수 부분은 .hpp 파일로 분리해 놨다. 이제 보니 hpp가 왜 hpp인지 알겠다. 헤더 파일이지만 코드를 구현하고 있기 때문에 pp를 붙여 놓은 것이다.



 8> Forward Declaring Templates 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.h

// included dependencies
#include "b.h"

// the class template
template <typename T>
class Tem
{
 /*...*/
  B b;
};

// class most commonly used with 'int'
typedef Tem<int> A;  // typedef'd as 'A' 


1
2
3
4
5
6
7
8
9
10
11
// b.h

// forward declared dependencies
class A;  // error!

// the class
class B
{
 /* ... */
  A* ptr;
};


사실 forward declare은 굉장히 간단한 것이다. 헌데 만약 그것이 template class에 대한 것이라면 문제가 좀 복잡해 진다. 위의 예를 보면 알수 있듯이 template 클래스 Tem이라는 것이 B 클래스를 사용하고 있다. 고로 b.h를 반드시 include 해야 하고, typedef을 통해 A라는 클래스를 Tem<int> 클래스로 정의했다.


그리고 b.h에서 A 클래스를 forward declare 하려고 하면 에러가 빡 터진다.. 왜?


왜냐면 A는 클래스가 이니라 사실 typedef이기 때문이다. 문제는 a.h를 include 할 수 없는데 왜냐면 circular inclusion 문제가 발생하기 때문이다. 그러면 어떻게 해결하느냐?


a.h에서 해준 것 처럼 똑같이 어떤 템플릿 클래스를 typedef 했는지 고대로 알려주면 A라는 클래스를 forward declare 가능하다. 즉, 아래와 같이 친절하게 알려주어야 한다.


1
2
template <typename T> class Tem;  // forward declare our template
typedef Tem<int> A;               // then typedef 'A' 


근데 이건 단순히 class A라는 한 줄에 비해 참 보기 싫다. 그래서 실용적인 해결 방법은 이러한 템플릿 클래스의 typedef를 따로 헤도로 선언하여 포함시키는 것이다. 아래와 같이...


1
2
3
4
5
6
7
8
9
10
//a.h

#include "b.h"

template <typename T>
class Tem
{
 /*...*/
  B b;
};


1
2
3
4
//a_fwd.h

template <typename T> class Tem;
typedef Tem<int> A;


1
2
3
4
5
6
7
8
9
//b.h

#include "a_fwd.h"

class B
{
 /*...*/
  A* ptr;
};


이런식으로 템플릿 클래스의 typedef 또한 forward declare가 가능하다는 점~






posted by 대갈장군