C和指针 3.6 链接属性

缘起

《C和指针》 3.6节

分析

其实讲的就是extern关键字和static关键字. 我们考虑这样一个问题.

1
2
3
当组成一个程序的各个源文件被分别编译之后,所有的目标文件以及从一个或多个函数库中引用的函数链接在一起,形
成可执行文件. 然鹅,相同的标识符出现在几个不同的源文件中时,它们表示是相同的还是不同的实体(你可以理解为
内存地址不同)?

这个问题由标识符使用extern还是static修饰有关. 这两个关键字直接影响到标识符的链接属性. 链接属性有三种

  1. external(外部的)
  2. internal(内部的)
  3. none(无)

属于none的标识符,你声明几次,就存在几个单独的实体. 而internal链接属性的标识符在一份源文件内部多次声明都指向同一个实体. external链接属性则意味着即便在多份源文件中声明,也指向同一个实体.

上面说的比较抽象, 下面通过一个小例子就明白了,项目结构如下

panta.cpp 内容如下

1
2
3
4
5
6
7
8
9
10
11
12
#include "stdafx.h"
#include <algorithm>
using namespace std;

void sayHello();

int main()
{
sayHello();
printf("%d\n", a);
return 0;
}

而Hello.cpp的内容如下

1
2
3
4
5
6
7
#include "stdafx.h"
void sayHello()
{
puts("Hello");
}

int a = 10;

stdafx.h是vs自己的默认的(非c++标准)的头文件. 默认引入一些头文件(如stdio.h)以及预编译文件(.pch), 具体作用参见【1】.

显然上面的panta.cpp是无法编译通过的(确切讲是源码转换为obj过程是无法通过的,因为这个过程是每个源文件单独进行的,而编译的链接过程是诸多obj文件共同参与的). 会报错

1
error C2065: 'a' : undeclared identifier

所以我们为panta.cpp添加a的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
#include <algorithm>
using namespace std;

void sayHello();

int a; // 添加了a的声明

int main()
{
sayHello();
printf("%d\n", a);
return 0;
}

那么编译整个工程的时候,会报错

1
2
3
panta.obj : error LNK2005: "int a" (?a@@3HA) already defined in Hello.obj
1>D:\algorithm\acm\panta\Debug\panta.exe : fatal error LNK1169: one or more multiply
defined symbols found

啥意思? 就是在链接阶段(注意【2】中指出了所谓的编译过程包含源码->obj和obj->exe两个过程, 所以链接也属于编译过程)发生了错误——a重复定义了. 这说明什么? 说明默认源文件中定义的全局变量的链接属性是external的. 所以链接的时候出现了问题(单个源文件->obj过程不会出错的). 解决之道是要么将a在panta.cpp中不要定义为全局变量,而是定义为main函数中的局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
#include <algorithm>
using namespace std;

void sayHello();


int main()
{
sayHello();
int a = 1;
printf("%d\n", a);
return 0;
}

则编译运行结果就是

1
2
Hello
1

上面的运行结果说明了一个问题——函数默认情况下总是external的~ 当然,前提是你需要在panta.cpp中引入该函数的声明(即panta.cpp的第5行),不然源码->obj过程就会报错. 这也说明,其实开发的时候,都是引入头文件(接口,到include路径下寻找),然后头文件的实现由dll给出(到lib路径下寻找). 参见【3】

当然,其实这样的解决方案并不是我们想要的. 如果我们就想要全局的在panta.cpp中定义a呢? 就要让

panta.cpp中的a的链接属性改成internal的. 怎么改呢? 只需要将a的修饰符改成static就行了. 也就像下面这样修改 panta.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
#include <algorithm>
using namespace std;

void sayHello();

static int a = 1; // 将此a的链接属性限制在了当前源文件中.从而不会在链接阶段和Hello.cpp中声明的external 链接属性的a冲突.

int main()
{
sayHello();
printf("%d\n", a);
return 0;
}

则运行结果是

1
2
Hello
1

然后我们说了,函数的链接属性默认是external的(和全局变量默认链接属性一样). 那么我们将Hello.cpp中的sayHello方法改成static修饰(internal的链接属性)

1
2
3
4
5
6
7
#include "stdafx.h"
static void sayHello() // 通过static关键字将本函数的链接属性限制在了当前源文件中(其他源文件在链接阶段无法看到本方法)
{
puts("Hello");
}

int a = 10;

则此时两份cpp文件转换为obj文件的过程都不会报错的. 但是链接阶段就会报错

1
2
error LNK2019: unresolved external symbol "void __cdecl sayHello(void)" (?
sayHello@@YAXXZ) referenced in function _main

说的是panta.cpp中的函数原型声明sayHello找不到实现. 那么解决方案显然有两种

  1. panta.cpp自己写实现

    即panta.cpp改成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include "stdafx.h"
    #include <algorithm>
    using namespace std;

    void sayHello();

    static int a = 1;

    int main()
    {
    sayHello();
    printf("%d\n", a);
    return 0;
    }


    void sayHello() // panta.cpp自己实现上述函数原型sayHello的声明
    {
    puts("我爱一条船");
    }

    则编译运行整个项目(一个项目只有一个main入口),得到结果

    1
    2
    我爱一条船
    1

    即这里采用的实现是自己的, 而没有使用到Hello.cpp中的实现(因为看不见)

  2. 将Hello.cpp中的static关键字去掉. 注意,一旦Hello.cpp的sayHello函数的链接属性重新变成external的话, 则panta.cpp中自己写的实现必须要去掉,也就是下面的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # 下面是panta.cpp的
    #include "stdafx.h"
    #include <algorithm>
    using namespace std;

    void sayHello();

    int main()
    {
    sayHello();
    static int a = 1;
    printf("%d\n", a);
    return 0;
    }
    void sayHello()
    {
    puts("我爱一条船");
    }

    # 下面是Hello.cpp的
    #include "stdafx.h"
    void sayHello()
    {
    puts("Hello");
    }

    int a = 10;

    运行整个项目的时候,编译的链接阶段会报错

    1
    2
    3
    4
    panta.obj : error LNK2005: "void __cdecl sayHello(void)" (?sayHello@@YAXXZ) already 
    defined in Hello.obj
    1>D:\algorithm\acm\panta\Debug\panta.exe : fatal error LNK1169: one or more
    multiply defined symbols found

    报错的内容是显然的, 因为整个可执行文件中出现了2个一样的方法——重复了.

参考

【1】https://blog.csdn.net/follow_blast/article/details/81704460

【2】https://yfsyfs.github.io/2019/07/16/C%E5%92%8C%E6%8C%87%E9%92%88-2-1-%E7%8E%AF%E5%A2%83/

【3】https://yfsyfs.github.io/2019/07/16/C-C-%E6%A0%87%E5%87%86%E5%BA%93%E7%AE%80%E4%BB%8B/