源文件未编译怎么解决_C++编译链接过程中的一些缺陷

前言

C语言是一门非常古老的语言,创立于1972年,距今已经有48年的历史,和很多更现代的语言(python、C#、golang)相比,C语言的编译过程中存在一些缺陷。这些缺陷不仅会加重开发人员的负担,也会隐藏一些难以发现的bug。而C++为了保持与C的兼容,也继承其中的很多缺陷。下面是一些常见的C++编译缺陷。

缺陷1:编译出来的目标文件中,函数的符号没有返回值信息,全局变量的符号没有类型信息。

C++编译过程大致可以分为两个步骤:编译和链接。编译时会为代码中的每个函数和全局变量创建符号。链接时会通过符号信息,将.o文件之间的函数调用、全局变量引用关系关联起来。

但是为函数和全局变量生成的符号信息存在缺陷,函数的符号不包含返回值信息,全局变量的符号不包含类型信息。

假设我们有一个 foo.cpp 文件,其中定义了一个全局变量 double g_Pi =3.14 ,一个函数 int foo(int width,int height),如下所示:

// foo.cpp

编译foo.cpp生成的foo.o,其中的符号文件如下:

# 仅进行编译,生成foo.o文件
$ nm foo.o

可以看出,函数 foo 的符号是 _Z3fooii,其中只有两个参数的信息,没有返回值的信息。而全局变量 g_Pi 的符号则没有包含任何类型信息。

因此,在另外一个文件 main.cpp 中,可以像这样错误的使用 foo 和 g_Pi,在程序编译和链接的过程中,不会出现任何错误提示:

// main.cpp

编译的时候,没有任何报错,但运行的时候,就出错了。

1374389535
-nan

我们知道,优秀的语言,应该是在编译过程中能发现尽量多的bug。但C++为了保持兼容,在设计上继承了C的符号系统的缺陷,这就导致这类问题无法在编译层面解决。

缺陷2:头文件和源文件中的变量和函数原型,需要程序员手工保持其一致

C/C++代码的组织方式是,可以将代码放到多个源文件中,各源文件如果想调用对方的函数,只需要 include 相应的头文件即可。头文件中有源文件中函数和全局变量的定义。

由此带来的一个问题是,一个函数或者全局变量的定义,会出现在两个文件中,并且必须保持一致。假设源文件中的函数原型发生变化,还需要修改头文件中的原型,否则肯能导致编译或者链接的错误。而且由于C++支持重载,在大多数情况下,是出现令人恼火的链接错误。

此外,如果程序员在修改头文件时因疏忽犯错,导致头文件和源文件中函数原型不一致,在某些情况下,编译器是识别不出来的,可能要等到运行的时候才会出错,而这个运行错误很可能要等程序运行很长时间才发现。上一部分缺陷1中,已经举例出一些这样的场景。这里再举另外一个场景。

假设有一个函数 cylindrical_volume,计算圆柱形的体积,其函数的原型和实现如下

// cylindrical_volume.h

假设程序员在重构代码时,将 cylindrical_volume.cpp 中函数的两个参数互换了位置,但忘记修改头文件了。由于这个修改不会改变函数的符号信息,因此这个bug在编译和链接都不会暴露出来,直到程序运行时才会出现。

// cylindrical_volume.h

这个代码中,main函数以为自己计算的是半径为1,高度为2的圆柱形体积(等于6.28),但其实计算的是半径为2,高度为1的圆柱形体积(等于12.56),程序最终得到一个错误的输出。

缺陷3:链接库在编译参数中的先后顺序,需要程序员人工维护

这可能是对C/C++初学者最不友好的缺陷了。笔者记得自己在工作后第一次碰到这个问题时,向旁边的同事狠狠地吐槽:谁TM再说C++是一门高级语言,劳资就跟谁急!

C++对链接库的先后顺序是有要求的,假设程序用到了两个静态库 libx.a 和 liby.a,其中 liby.a 会用到 libx.a 中的函数,也就是说 liby.a 依赖 libx.a,那么在链接参数需要这样写,也就是说被依赖的库,应该要写到依赖库的后面。

$ g++ -o main main.cpp -l liby.a libx.a

那奇葩的事情来了,假如 libx.a 和 liby.a 相互调用了对方的函数,那怎么办呢,到底将谁放在前面呢? 答案是需要写三个链接参数:

# or
$ g++ -o main main.cpp -l libx.a liby.a libx.a

那为什么C++要求一定将被依赖的库放到后面呢?因为C++从C哪里继承了一个编译特性——单遍编译

所谓单遍编译,是指编译的过程中,编译器只扫描一次源代码,链接器也只扫描一次链接对象,在任何时候,编译器和链接器都不会回头看前面的源代码或者链接对象。

C++由于语法更复杂,目前编译器已经没有办法做到单遍编译,但链接器目前仍然保持了单遍编译的特性。

链接器由于要在一轮的扫描中,解析所有对象文件中所有未决的符号,因此需要以特定的顺序来扫描这些有相互依赖关系的对象文件。C和C++选择的方式是被依赖的文件放在后面,这样链接器在扫描的过程中,只需要记住当前所有未决的符号,在后面的对象文件中找到相应的符号后,再对其进行解析就可以了。

C++的这个特性,能让链接工具工作效率更高,并且更容易开发。但代价却是增加了程序员的工作负担。

总结

C++虽然号称是一门高级语言,是一门现代的语言,但因为要兼容C语言的特性,存在很多设计上的缺陷。如果一个人只学习C/C++,可能对这些缺陷没有感觉,认为一切都是理所当然的,甚至将这些缺陷当作是语言的特点。但当你接触更多语言后,对比之下,这种设计上的缺陷就会变得很明显了。

兼容C语言,是C++能广泛流行的原因之一,但也因为这个原因,导致C++相比其他语言,对开发者不那么友好,这终将导致其他语言逐步蚕食C++的领域。所谓成也萧何,败也萧何。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值