头文件

参考:

Header files (C++)

Headers and Includes: Why and How

学习头文件的使用规范

为什么要使用头文件

通常将变量、函数、类、结构体的声明放置在头文件(header file)中,在源文件中使用#include指令插入头文件

有两个作用:

  1. 保证所有源文件使用同一个声明
  2. 加快编译时间
  3. 结构化程序,比如接口和实现分离

include guard

通常,头文件具有include guard#pragma once指令,以确保它们不会多次插入到单个.cpp文件中

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

namespace N
{
    class my_class
    {
    public:
        void do_something();
    };
}

#endif /* MY_CLASS_H */

头文件包含内容

头文件和源文件一样,也可以包含定义,但是不推荐使用,因为这会造成同一个名称的多次定义

以下内容是不允许或被认为是非常糟糕的做法:

  • 在全局作用域或命名空间进行内置类型定义
  • 非内联函数定义
  • const变量定义
  • 聚合定义(aggregate definitions
  • 未命名的名称空间
  • using指令

使用using指令不一定会导致错误,但可能会导致问题,因为它将命名空间引入直接或间接包含该头文件的每个.cpp文件的作用域

.h/.hpp/.c/.cpp/.cc的区别

不同的文件扩展名可表示不同的作用

  • 头文件:使用.h__扩展名,比如.h、.hpp、.hxx
  • c++源文件:使用.c__扩展名,比如.cpp、.cxx、.cc
  • c源文件:使用.c扩展名

循环依赖问题

有两个头文件a.hb.h,各自头文件定义了类,同时相互调用,这会产生循环依赖Circular Dependencies)问题

错误示例

主程序使用类a

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

class A {
public:
    B *b;
};

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

class B {
public:
    A *a;
    int h;
};

// main.cpp
#include "a.h"

int main() {
    A aa;
    aa.b = new B();
    aa.b->h = 3;

    cout << aa.b->h << endl;

    return 0;
}

会出现编译问题:

[ 40%] Building CXX object CMakeFiles/first.dir/a.cpp.o
In file included from /home/zj/CLionProjects/first/a.h:8:0,
                 from /home/zj/CLionProjects/first/a.cpp:5:
/home/zj/CLionProjects/first/b.h:14:5: error: ‘A’ does not name a type
     A *a;
     ^

编译器首先编译源文件a.cpp,根据#include指令编译头文件a.h,再根据#include指令编译b.h,最后根据#include指令编译a.h。然而因为include guard保护,所以跳过了a.h的编译,造成b.h中的类A未进行声明

解决方案:前向声明

在使用之前进行前向声明forward declaring),所以需要在a.h中声明类B,在b.h中声明类A。完整代码如下:

# a.h
#ifndef FIRST_A_H
#define FIRST_A_H

#include "b.h"

class B;

class A {
public:
    B *b;
};

#endif //FIRST_A_H

# b.h
#ifndef FIRST_B_H
#define FIRST_B_H

#include "a.h"

class A;

class B {
public:
    A *a;
    int h;
};

#endif //FIRST_B_H

# main.cpp
#include "a.h"

int main() {
    A aa;
    aa.b = new B();
    aa.b->h = 3;

    cout << aa.b->h << endl;

    return 0;
}

执行结果

3

编写规范

参考:在开发大C++工程的时候如何判断和避免循环include?

  1. 使用include guard
  2. A.cpp文件中放置A.h在第一位
  3. 出现循环依赖,重新思考文件布局,拆分文件
  4. .h使用前向声明,在.cpp引入对应.h文件

示例代码

#pragma once
#include <vector> // #include directive
#include <string>

namespace N  // namespace declaration
{
    inline namespace P
    {
        //...
    }

    enum class colors : short { red, blue, purple, azure };

    const double PI = 3.14;  // const and constexpr definitions
    constexpr int MeaningOfLife{ 42 };
    constexpr int get_meaning()
    {
        static_assert(MeaningOfLife == 42, "unexpected!"); // static_assert
        return MeaningOfLife;
    }
    using vstr = std::vector<int>;  // type alias
    extern double d; // extern variable

#define LOG   // macro definition

#ifdef LOG   // conditional compilation directive
    void print_to_log();
#endif

    class my_class   // regular class definition,
    {                // but no non-inline function definitions

        friend class other_class;
    public:
        void do_something();   // definition in my_class.cpp
        inline void put_value(int i) { vals.push_back(i); } // inline OK

    private:
        vstr vals;
        int i;
    };

    struct RGB
    {
        short r{ 0 };  // member initialization
        short g{ 0 };
        short b{ 0 };
    };

    template <typename T>  // template definition
    class value_store
    {
    public:
        value_store<T>() = default;
        void write_value(T val)
        {
            //... function definition OK in template
        }
    private:
        std::vector<T> vals;
    };

    template <typename T>  // template declaration
    class value_widget;
}