头文

头文件 属于 C++

2 个练习

关于 头文件

在 C++ 中,声明通常与定义分开。 声明会被归入所谓的头文件,相应的实现则放在源文件里。 你可以把头文件看作一种 API。 头文件会告诉你代码库能提供_什么_,而不会深入_如何_实现的细节。

头文件与源文件

头文件最常见的扩展名是.h。 有些项目使用.hpp,或者干脆不写扩展名。

定义位于单独的.cpp文件中。 为了让这些部分重新结合,源文件会在开头_包含_对应的头文件。

如果你想写一个名为“quick_math”的库,它提供一个你想经常使用的函数“super_root”,那么文件会像下面这样:

// A file named quick_math.h
#pragma once

namespace quick_math {

double super_root(double x, int n);

}
// A file named quick_math.cpp
#include <cmath>

#include "quick_math.h"

double quick_math::super_root(double x, int n) {
    while (n) {
        x = std::sqrt(x), --n;
    }
    return x;
}

如果你需要包含一个只有实现才需要的头文件,那么相应的#include行只需要写在源文件里。 头文件中包含的所有内容在.cpp文件中同样可用,比如下面例子里的string库。 注意:头文件里声明后面需要;,但源文件里定义后面不需要。

Note

Exercism 上很多 C++ 练习一开始都有两个几乎空白的文件:头文件和源文件。 你需要查看*_test.cpp文件,了解预期函数的名称和命名空间,才能解出这道练习。

类与头文件

类可能会变得非常复杂,它们与头文件/源文件划分之间的关系也容易让人困惑。 一种可能的组织方式是把所有实现细节都留在源文件里,把所有声明和成员变量都放在头文件里:

// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0);
    void give_water();
    std::string get_name();
    int get_size();
    void start_next_day();
};
}  // namespace robots
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"

robots::Flower::Flower(std::string name, int size) {
    this->name = "Robotica " + name;
    this->size = size;
}

void robots::Flower::start_next_day() {
    if (!needs_water) ++size;
    needs_water = true;
}

std::string robots::Flower::get_name() { return name; }

int robots::Flower::get_size() { return size; }

当头文件被当作 API 概览使用时,人们就会到这里查找默认值之类的信息。 因此,构造函数中size形参的默认值是在头文件里处理的,而不是在实现里。 源文件中的定义会加上命名空间robots和类Flower作为前缀。

另一种组织方式是_仅头文件_库,它完全没有.cpp文件:

// A file named robot_flower.h
#pragma once

#include <string>

namespace robots {

class Flower {
   private:
    bool needs_water{};
    int size{};
    std::string name{};

   public:
    Flower(std::string name, int size = 0) {
        this->name = "Robotica " + name;
        this->size = size;
    }
    void give_water() { needs_water = false; }
    std::string get_name() { return name; }
    int get_size() { return size; }
    void start_next_day() {
        if (!needs_water) ++size;
        needs_water = true;
    }
};
}  // namespace robots

项目可能会组合使用这些组织方式,至于每种使用场景最适合哪一种,则有很多讨论。

包含守卫

你可能已经注意到上面示例头文件中的#pragma once行。 这叫做包含守卫,它确保文件内容在编译期间只被包含一次,以避免出错。 还有一种更复杂的包含守卫写法,以#ifndef开始、以#endif结束,详见下文。

Advanced

前向声明

C++ 代码是按顺序求值的。 如果你想使用一个函数,编译器在使用它的那一刻就必须知道它。 有时无法找到一种线性的顺序来排列源代码中的定义。 看看下面的例子:

int myFunction(int n) {
    if (n < 10) {
        return n;
    } else {
        return myOtherFunction(n / 10);
    }
}

int myOtherFunction(int m) { return myFunction(m / 2); }

定义myFunction时,编译器还不知道myOtherFunction。 遗憾的是,这种循环引用的问题无法通过调换顺序来解决。

C++ 提供了前向声明,让编译器在myFunction和myOtherFunction被定义之前就知道它们。 编译器会假定定义会在声明之后的某个时刻出现。 下一个例子展示了如何为函数使用前向声明。

int myFunction(int n);       // Forward declaration of myFunction
int myOtherFunction(int m);  // Forward declaration of myOtherFunction

// Definition of myFunction
int myFunction(int n) {
    if (n < 0) {
        return 0;
    } else {
        return myOtherFunction(n - 2);
    }
}

// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }

通过 ifndef 实现包含守卫

同一个文件在项目中被多次包含是没问题的。 头文件不应该包含定义。 整个项目中同一个定义不能出现多次。 这被称为“单一定义规则”。 编译器会强制执行这一点。

借助包含守卫,可以轻松避免同一个定义被意外地多次包含。 它们通过编译阶段的一种特殊机制形成。 其目标是:只有当某个变量尚未被设置时才包含文件,然后在文件被包含后把它设置好。 通常这个变量的名字会取文件名的一种变体。 另一种方法是生成一个 UUID,以降低不小心重复使用同一名字的风险。 下面的语法中,变量名为MY_HEADER_FILE_H。

#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H

// file content

#endif

#pragma once的问题在于,pragma 并不是 C++ 语言的官方组成部分,各家编译器的实现也不尽相同。 许多大型项目已经改用更简单的 pragma 方式,但仍有少数项目保持谨慎。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 头文件