【CCG】源文件

作者:wallace-lai
发布:2024-02-25
更新:2024-03-05

一、接口与实现

规则1:如果项目没有其他约定,那么源文件使用cpp后缀,头文件使用h后缀

除非你的项目已经有了其它的后缀命名策略,否则一律按照规则1来处理。

规则2:头文件中不可包含对象定义或者非内联函数定义

如果你的头文件里含有对象或者非内联函数的定义,则可能导致链接步骤报错。实际上,在C++中,有所谓的单一定义规则ODR。ODR在函数定义方面有以下几点说法。

(1)一个函数在任何翻译单元中不能有一个以上的定义;

(2)一个函数在程序中不能有一个以上的定义;

(3)有外部链接属性的内联函数可以在一个以上的翻译单元中被定义,这些定义必须满足一个要求:它们全相同;

注意:将定义在头文件中,并不会引起链接器的不满,比如下面这个案例。

// header.h

class Base {
    Base() {}
    ~Base() {}

    void FormatBase(void) {
        cout << "FormatBase" << endl;
    }
};
// t1.cpp
#include "header.h"
void fun1(void)
{
    Base b;
    b.FormatBase();

    return;
}
// t2.cpp
#include "header.h"
void func2(void)
{
    Base b;
    b.FormatBase();
    return;
}

int main()
{
    Base b;
    b.FormatBase();

    return 0;
}

同理,内联函数和类一样,也是可以被定义在头文件中的。

规则5:.cpp源文件必须包含其接口定义的头文件.h

如果不这么做,则会在链接过程中出问题,比如下面的案例:

// impl.h

int func(int);
// impl.cpp
std::string func(int x)
{
    return to_string(x + 32);
}

此时源文件impl.cpp并没有包含对应的头文件impl.h,并且函数func的声明和定义中的返回值类型不一致。

// main.cpp

int main()
{
    int result = func(32);
    cout << result << endl;

    return 0;
}

在主函数中调用func,则会得到一个undefined reference的错误。

main.cpp:(.text+0x12): undefined reference to `func(int)'

如果遵循了规则5,那么类似这种浅显的错误就可以在编译过程中暴露出来,不用等到链接过程。

规则8:为所有的.h头文件使用宏防止出现多重包含

给所有的头文件都添加宏,用于防止出现多重包含的情况。

// footer.h
#ifndef FOOTER_H_
#define FOOTER_H_

// ...

#endif /* FOOTER_H_ */

有两点需要注意的:

(1)你应该保证不同的头文件的防护宏的名字应该是唯一的;

(2)使用#pragma once预处理指令来解决多重包含是不可移植的,因为#pragma指令并未标准化;

规则9:避免源文件之间循环依赖

比如下面这个案例就是典型的头文件之间循环依赖,无法通过编译。

// a.h
#pragma once

#include "b.h"
class A {
    B b;
};
// b.h
#pragma once

#include "a.h"
class B {
    A a;
};
// main.cpp

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

int main()
{
    A a;
    B b;

    return 0;
}

编译报错为:

b.h:6:5: error: ‘A’ does not name a type

解决办法是前置所依赖类的声明并使用指针,如下所示:

// a.h
#pragma once

class B;

class A {
    B *b;
};

规则10:避免对隐含#include进来的名字的依赖

比如下面这个案例在GCC 5.4和微软编译器19.00.23506版本中进行编译,前者没问题;后者无法通过编译。

#include <iostream>

int main()
{
    std::string s = "hello world";
    std::cout << s << std::endl;

    return 0;
}

原因在于GCC 5.4编译器的iostream头文件中包含了string;而微软编译器中的iostream没有。我们需要手动包含string以摆脱代码对隐含名字的依赖。

规则11:头文件应该是自包含的

应该说是在头文件能够做到自包含的情况下,尽量达成头文件的自包含。

二、命名空间

规则16:(仅)对代码迁移、基础程序库(比如std)或者在局部作用域中使用using namespace指令

下面的案例将无法通过编译,原因在于局部变量sqrt和标准库中的sqrt函数重名了。

#include <cmath>
using namespace std;

int g(int x)
{
    int sqrt = 7;
    // ...
    return sqrt(x);
}

不仅如此,using namespace指令隐藏了名称的来源,且破坏了代码的可读性。只允许在以下三种场景下使用using namespace指令:

(1)代码迁移(这是什么?);

(2)基础库std;

(3)局部作用域中;

规则17:不要在头文件的全局作用域中使用using namespace

头文件中全局作用域的using namespace会将名字注入到包括该头文件的每个源文件中。这种注入有一些不好的后果:

(1)当你使用这个头文件时,你无法再撤销其中的using指令;

(2)名字冲突的可能性急剧增加;

(3)对被包含的namespace的改变可能会破坏你的编译,比如因为其中引入了一个新的名字(导致冲突);

规则20:使用namespace表示逻辑结构

规则21:不要在头文件中使用匿名命名空间

规则22:为所有的内部(不导出的实体)使用匿名命名空间

匿名namespace使用的是内部链接。而内部链接意味着匿名namespace内的名称只能在当前翻译单元内引用,而不能导出。不能导出这一点同样适用于在匿名namespace中声明的名称。

而当你在头文件中使用匿名namespace时(规则21),每个翻译单元都定义了这个无名namespace的唯一实例,这会导致:

(1)产生的可执行文件大小会膨胀

(2)匿名namespace中的任何声明都将是不同翻译单元中的不同实体,这可能不是程序员期望的行为

匿名namespace的用法类似C语言里使用的static关键字。