链接常见错误

一.概要

对于语言初学者,开始的时候总是把所有的代码写在一个源文件中。当代码行非常庞大时候,这样的做法往往很难维护和修改。而真正的工程项目都是由多人共同完成的,因此划分模块,组织良好清晰的文件结构显得非常重要。本文主要针对C/C++语言初学者在组织工程文件结构时所遇到的众多问题和概念给予总结。

二.认识编译单元

开始时,自己写的小工程只有一个源文件时,通过编译链接就可生成可执行文件。这个过程往往忽视了链接的存在。链接的主要工作就是将多个编译单元各自生成的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。而编译单元就是一个源文件,当我们只有一个源文件时,链接的作用往往体现不出来。                                                        

编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。一般情况一个源文件.c/.cpp就是一个编译单元,如下例:两个编译单元分别独立编译,编译所做的一件重要事情就是看是否有未定义的东西。在main.cpp我们开始声明的f()就是告诉编译器:有f()这个函数。编译器就按照声明的格式去编译后续调用f()的代码,然后首先会在本文件末寻找f()的具体定义,如果找不到就等到链接的时候看看其他编译单元是否有,如果还找不到就会报链接错误了!

因此我们可以再一个编译单元A中定义一个函数f,而只要在其他编译单元声明f,就可以使用它了。通常以头文件的形式展现这个过程:


而在编译器的眼里,没有头文件。因为在预编译阶段,所有”#include”,都只是一段代码的插入。那么所有的.h头文件全部都被插入到众多.c/.cpp源文件中。此时.h文件的作用就终结了。编译器只看到了两个编译单元:


编译f.cpp单元时一切OK,编译main.cpp时main函数外面声明的f()格式去调用f()函数,编译器也未在自己的编译单元中找到f()的定义,那么之后将在链接的时候去其他编译单元查找f()的定义。

三.链接那些事儿

3.1 error: undefined reference


此例,两个编译单元各自通过编译,main.cpp在编译时没有在自己内部找到声明的f()函数的定义。在链接过程中就会去其他编译单元找,也没找到就会报undefined reference错误了。那对于每个编译单元中未找到的定义,在链接的时候都要去其他哪些编译单元中找呢?这就看makefile的写法了。如本例:gcc -o object f.o main.o 显示的将main.o和f.o链接。那么链接时候main.o和f.o就会互相查找是否有自己需要的定义。

3.2 error: mutiple definition


3.1中的例子我们说了未定义的错误,现在讨论多重定义的错误,如上面这三个编译单元,各自编译OK。链接的时候main.o就得去其他编译单元找f()的定义,可以发现有两个编译单元提供了f()的定义,此时就会报mutiple definition了。

3.3不得不说的声明和定义

如前所述,我们发现对于函数有定义和声明。在一个编译单元里面定义,在别的编译单元只要声明就可以使用。这种特性叫做外连接。

函数是外连接的,那么变量呢?局部变量有它限制的作用域,不会牵扯到链接问题。只有全局变量才会牵扯到链接。


链接这两个编译单元gcc a1.o main.o 就会报mutiple definition错误,说明全局变量也是外连接的。也就是你在自己模块定义的全局变量将会影响到所有链接单元。因此使用全局变量要小心。有的初学者甚至把全局变量定义在头文件中,那样若多个编译单元include此头文件,那么这些编译单元都有一个同名全局变量,链接必然出错。如果想使用其他编译单元的全局变量怎么办呢?使用extern关键字声明。正确的使用如下:


用法和函数几乎一样,在a1.cpp定义,而在main.cpp中声明一下就可以使用了,就是告诉编译器,你先用着,看看咱自己编译单元有没有定义,没有的话等链接的时候再看看其他编译单元吧。

因此只有函数和全局变量属于外连接,而链接时,主要是链接函数和全局变量。

四:全局变量的声明和定义

从上面的学习已经知道,一个函数一般有两部分组成:声明部分和定义部分。函数的声明是函数的原型,而函数的定义是函数的本身。想在任何编译单元使用某个函数,只需声明一个这个函数就可以。编译器会帮你在其他编译单元找到对应函数定义的。

而对于变量而言,对变量而言,声明和定义的关系稍微复杂,在声明部分出现的变量有两种情况:一种是需要建立存储空间的(如int a),另一种是不需要建立存储空间的(如:extern a).前者为“定义性声明”,或简称“定义”。后者称为“引用性声明”。广义的说他们都是声明。但我们通常为了叙述方便,把建立存储空间的声明称定义,而把不需要建立存储空间的声明称为声明。总结一下:只有全局变量的声明和定义不是一回事。

对全局变量用static声明,则该变量就变成了内连接。即在某个编译单元定义一个全局变量后,在其他编译单元用extern声明此全局变量,依然访问不到此全局变量。

五.让头文件扮演模块向外公开的接口

很多情况下头文件这个角色往往把初学者绕的很晕。需掌握以下几点:

(1)#include一个头文件就是插入代码。

(2)头文件A.h里面经常放对A.c编译单元里面的函数声明。因此别的编译单元#include A.h,其实就插入这个A.c编译单元的函数声明。那么可以理解为A.h通常就是A.c编译单元往外提供的使用接口。

(3)使用内部包含卫哨#ifndef...#define..#endif来防止重复声明或定义。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值