C++ 中的外部和内部链接

Posted by 武龙飞 on June 24, 2020

C/C++ 编译流程

首先看一下C/C++编译流程图:

整个流程如下:

  1. 预处理器(Preprocess)将头文件内容在cpp文件中展开,展开后的文件这里称作TU(Translation Unit)
  2. 编译器(Compile)将TU编译为Object文件
  3. 链接器(Link)将Object文件,以及库文件链接生成可执行文件

本文讨论步骤一中展开和步骤三中链接问题。

定义和申明

申明(declaration)的作用告知编译器符号(symbol) 存在,不需要知道变量长度或函数内容。

定义(definition) 告知编译器需要为变量分配对应尺寸的内存,以及函数内容。

函数的申明和定义如下:

int foo();                      // 申明函数
int foo() {return 5;}           // 定义函数

变量的申明和定义如下:

extern int a;                   // 申明变量
int a = 10;                     // 定义变量

变量需要注意的两种情况:

extern int a = 10;             // 此处是变量定义,extern失去效果
int a;                         // 此处也为变量定义,同时变量a的内容未知

前向申明(ForwardDeclaring)

C++中有前向申明符号的概念。允许C++先申明变量类型或符号,不需要包含具体的实现。好处是当此类型的头文件修改时,使用了此类型的文件不需要编译。

// foo.h
class Player;               // 这里申明类型Player即可
int foo(Player p);

这样当player.h文件修改后,引用Player的文件不需要重新编译。

一次定义规则

在C/C++中同一变量或函数申明不限次数,但定义只能为一次,这称作 一次定义规则(one difinition rule)。比如在C++中:

int foo();
int foo();
int foo();
int foo();
int foo(){return 5;}

但是定义两次不可以:

int foo(){return 5;}
int foo(){return 9;}

预处理器

预处理器执行CPP文件中的宏指令,通过执行指令后得到TS。看一个例子:

// player.h
#ifdef PLAYER_H_
#define PLAYER_H_
#define DEBUG 1
#ifdef DEBUG
void log_debug();
#else
void log_info(); 
#endif
void log();
#endif
// player.cpp
#include "player.h"
void log_debug(){

}

void log_info(){

}
void log(){
#ifdef DEBUG
    log_debug();
#else
    log_info(); 
#endif
}

预处理器处理后的内容为:

void log_debug();
void log();

void log_debug(){

}

void log_info(){

}
void log(){
    log_debug();
}

内部链接

符号(symbol)的内部链接指的是符号只在当前TU里可见。这就意味着在头文件定义了内部链接符号,在所有包含此头文件的TU里,都会是独立的符号。缺点是每个TU里都需要为此变量分配内存。

C/C++中使用static申明内部链接:

static int a = 42;

这里添加一个例子来说明内部链接:

// config.h
static int s_max_year = 9999;
// earth.h
void set_max_earth_year();
// mar.h
void set_max_mar_year();
// earth.cpp
#include "config.h"
#include "earth.h"
void set_max_earth_year()
{
    s_max_year = 2999;
}
// mar.cpp
#include "config.h"
#include "mar.h"
void set_max_mar_year()
{
    s_max_year = 1999;
}
// main.cpp
#include <iostream>
#include "config.h"
#include "earth.h"
#include "mar.h"

int main()
{
    set_max_earth_year();
    set_max_mar_year();
    std::cout << "max year:" << s_max_year <<"\n";      // 9999
}

在C++中提供了另外一种方式,匿名命名空间(Anonymous Namespaces) 来实现内部链接:

namespace {int variable = 0; }          // static int variable = 0 效果一致

因为config.h包含在mar.cppearth.cpp后,s_max_year在这两个TU里是独立的。虽说内部链接会占用内存,并且让代码变的难以理解,但是通过内部链接TU之间可以隐藏自己的值。

外部链接

符号如果是外部链接,那意外这此符号在其他TU可见。此符号在所有包含它的TU里共享,但只允许在一个TU里定义。在C/C++中使用关键字extern来申明外部链接,函数默认是extern

extern int g_cur_year;
extern void set_cur_year();

外部链接的符号用在全局变量设置上。C语言可以直接使用宏来定义常量:

#define CLK 10000

作为C++程序员,建议使用命名空间:

// global.h
namespace Global
{
    extern unsigned int g_max_year;
}

// global.cpp
namespace Global
{
    unsigned int g_max_year = 9999;
}

参考

internal_and_external_linkage_in_c++