preface

Recently, I’ve been tortured by the redefine error of C++ header files. And I found this post pretty good. Thus, I decide to translate it for better understanding. For the origin document, please go to cpp forum for details.

为什么需要头文件

  1. 加速编译。
    • 随着代码量的增多,如果所有代码都放一个文件里,会使得每次修改都需要编译整个文件,即使只是改了一个小符号,也会使整个文件重新编译。在代码量少的情况下这不是问题,但对于大项目,一次编译通常需要好几分钟。
  2. 让代码更整洁,更有组织。
    • 将不同功能或概念拆分到不同的特定文件中,会使得修改和查找更方便。
  3. 便于将接口和实现分离开

头文件会带来的缺点是,如果不能很好的理解其中的工作原理,会使得编写过程更复杂。

C++程序的编译通常有两个部分。首先每个源文件各自编译,编译器为每一个编译后的源文件生成目标文件(Object Files)。所有源文件都各自编译完成吼,编译器会将这些目标文件联系起来(link),然后生成二进制可运行代码。由于各自独立编译,所以源文件之间互相不会知道其他文件中的操作,因此我们需要一个头文件(Header Files)。头文件会使得接口对其他文件.cpp源文件可见,同时能让其具体实现保存在其他.cpp源文件中。

.h/.cpp/.hpp/.cc 等文件格式的区别

  • 头文件使用.h__扩展名(具体哪个并不要紧)
  • C++源文件使用.c__扩展名
  • C源文件使用.c作为扩展名

通常来讲,头文件以#include的方式被使用且不会被编译,而源文件会被编译且不能#include.

include保护

有些时候一个头文件可能会被多次#include,这会导致重定义的编译错误,因此可以采用
include guard的机制来避免重复链接。

#ifndef __X_H_INCLUDED__
#define __X_H_INCLUDED__
class X {};
#endif

include的正确姿势

类于类之间通常会有一些依赖关系,可能是派生关系,或者是一个类中包含另一个类的对象等。通常我们需要知道两种基本的依赖关系:

  1. 需要被前向定义(forward declare)的
  2. 需要被包含的#include

这篇文章总结的一些include规则如下:

  • 什么也不做:A没有对B的任何引用
  • 什么也不做:对B的引用都只有友元声明(friend declaration)*
  • 前向声明:如果A中包含一个对B类型的指针或引用 如 `B* myb;`
  • 前向声明:如果A类的某个或多个函数以B对象或B的指针或引用为参数或返回值 如 `B function(B myb);`
  • 加#include: 如果B是A的父类
  • 加#include:如果A中包含一个B对象的实例对象 如 `B myb;`

*注:友元声明是用来授予函数或另一个类对本类中私有或保护变量的访问权限的

// include guard
#ifndef __X_H_INCLUDED__
#define __X_H_INCLUDED__
// forward declare
class Foo;
class Bar;
#include <vector>
#include "parent.h"
class X: public Parent{
public:
    std::vector<int> a;
    Foo* foo;  // pointer of foo
    void Func(Bar& bar); // reference of Bar
    friend class MyFriend; // do nothing
};
#endif

循环引用(circular dependencies)

互相引用

函数内联(function inlining)

内联函数的规矩是他们的函数内容需要出现在调用了他们的每一个cpp源文件中,否则就会出现链接错误(linker errors), 但内联函数可以打破循环引用。一个例子:

class B{
public:
void Func(A& a){     // forward declaration
    a.doSomething(); // here we referenced a, so we need to include A, potentially a circular inclusion
}
}

虽然内联函数需要在头文件里出现,但它们可以不出现在类的定义中。如下:

// b.h guarded
class A; // forward declaration
class B{
public:
    void Func(A& a);  // forward declaration
};
#include "a.h" // include dependencies
inline void B::Func(A& a){
    a.doSomething();  // a is included
}

通过把include放在B的类定义之后,这样即使A也include了b.h,也不会出现循环依赖的关系(因为A再引用B时,B已经是有定义的了)。但把include放在类定义之后有点奇怪,可以把该include放在另一个头文件中。

// b.h
class A; // forward declaration
class B{
public:
    void Func(A& a);
};
#include "b_inline.h" // 或者用hpp扩展名 "b.hpp"
// b.hpp
#include "a.h"
#include "b.h"
inline void B::Func(A& a){ a.doSomething(); }

与此同时,对类B依然可以有一个b.cpp来实现该类的其他方法。
*注:还有一小节是关于模板中的引用错误的,之后再看吧,现在还没用过以及不懂C++里的模板(逃

Categories: Uncategorized

Leave a Reply

Your email address will not be published. Required fields are marked *