程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

手把手写C++服务器(2):C/C++编译链接模型、函数重载隐患、头文件使用规范

发布于2021-05-29 18:55     阅读(977)     评论(0)     点赞(24)     收藏(4)


前言:C++兼容C,在编译上有明显的体验。有一个流传很久的段子,C/C++程序员逃避工作的正当借口就是:“我的程序正在编译”。对于服务端编程,不管是时间资源还是硬件资源,都非常宝贵,力图做到极致优化。因此了解C/C++编译的前世今生、背后的原理、常见的优化手段,对之后的服务端编程来说非常重要。

目录

为什么C/C++编译比Java、Python、golang慢很多?

万恶之源:C语言隐式函数声明

什么是隐式函数声明?

隐式函数声明的原因

怎样解决?

C语言单遍编译模型

函数重载带来的编译歧义

函数重载还有其他隐患吗?

C++前向声明

什么是前向声明?

前向声明的好处

前向声明的限制

头文件使用规范

参考:


为什么C/C++编译比Java、Python、golang慢很多?

这个问题想完全解释清楚还是挺复杂的,主要原因是Java、Python和golang这些现代编程语言,模块化和编译优化做得很好,而C/C++无法摆脱头文件、预处理和元我呢间的束缚:

  • Python、Golang这种解释型语言,import的时候直接把对应模块的源文件解析了一遍,不再是简单的把源文件包含进来。
  • Java这种编译型语言,编译出来的目标文件直接包含了足够多的元数据,import的时候只需要读目标文件的内容,不需要读源文件。

    Java的编译只会生成字节码文件,而不会生成汇编(更不会到机器语言)。Java程序运行时,字节码文件会装载入java虚拟机,虚拟机实时将字节码“翻译”成机器指令来运行。java在不同平台上实现虚拟机,针对虚拟机编译就可以实现代码可移植性。C语言代码编译成的是机器码,通常不能在不同指令系统的机器上运行。c代码的编译一般是直接针对硬件的

万恶之源:C语言隐式函数声明

什么是隐式函数声明?

在C语言中,函数在调用前不一定非要声明。如果没有声明,那么编译器会自动按照一种隐式声明的规则,为调用函数的C代码产生汇编代码。

代码在使用前文未定义函数的时候,编译器不检查函数原型:既不检查参数个数,也不检查参数类型与返回值类型。所有未声明的函数都返回int,并且能接受任意个数的int类型参数。

这里如果是未声明的函数正好是int返回类型,不会有问题。但是,如果未声明函数是double、char等其他类型,函数入参多个的时候,就会出问题了,具体可以参见:万恶之源:C语言中的隐式函数声明

隐式函数声明的原因

C语言编译器为了尽量减少内存使用情况下实现分离编译。

怎样解决?

这种情况下编译器不会报错!链接器会生成警告或报错。因此:

  1. 在c语言里面开来还是要学习c++的编程习惯,使用函数之前一定要声明。不然,即使编译能通过,运行时也可能会出一些莫名其妙的问题。
  2. 重视编译器的警告,少给自己挖坑。

C语言单遍编译模型

编译程序其实可以分为两种,一种是单遍(one pass)编译程序,一种是多遍编译程序。顾名思义,单遍编译程序就是只对源代码读取一遍,便可得到目标代码;但是编译器只能看到目前(当前语句/符号之前)已解析过的代码,看不到之后的代码,过眼即忘。而多遍编译程序则需要读取多遍,才能完成转化过程。C语言是按照单遍编译来设计的

  • C语言要求结构体必须先编译,才能访问成员,否则编译器不知道结构体成员的类型和偏移量,就无法立刻生成目标代码。
  • 局部成员变量必须先定义再使用。如果把定义放大后面,编译器在第一看到一个局部变量时不知道他的类型和在stack中的位置,也就无法立刻生成代码。
  • 为了方便编译器分配stack空间,C语言要求局部变量只能在语句块开始处定义。
  • 对于外部变量,编译器只需要记住类型和名字,不需要知道地址,因此需要先声明再使用。在生成的目标代码中,外部变量是空白,留给链接器填上。
  • 当编译器看到一个函数调用时候,按隐式函数声明规则,编译器可以立刻生成调用函数的参数入参、返回值、调用,唯一不能确定的是函数的实际地址,编译器留下一个空白给链接器填充。

函数重载带来的编译歧义

为了实现函数重载,C++编译器普遍采用名字改变的方法,为每个重载函数生成独一无二的名字,在链接阶段找到正确的重载版本。

在下面的例子中,会调用fun(int):

  1. void fun(int) {
  2. cout << "int";
  3. }
  4. void handle() {
  5. fun('a');
  6. }
  7. void fun(char) {
  8. cout << "char";
  9. }
  10. int main() {
  11. fun();
  12. }

 如果互换一下fun(char)和handle()的位置,会输出什么呢?调用fun(char)

  1. void fun(int) {
  2. cout << "int";
  3. }
  4. void fun(char) {
  5. cout << "char";
  6. }
  7. void handle() {
  8. fun('a');
  9. }
  10. int main() {
  11. fun();
  12. }

这是由于C++继承了C语言的单遍编译,但是又由于C++语言有前向声明的特性,所以又不是严格的单遍编译。因此对于函数重载来说,带了一些歧义,并且这些歧义一般不会告警或报错,需要特别注意!

函数重载还有其他隐患吗?

还记得文章前面说的吗,C语言有隐式函数声明吗?当在一段C++程序里面,如果源文件用到了重载函数,但是函数运行声明的返回类型是错误的,链接器无法捕捉到这样的错误!

举个例子:

  1. int fun(bool) { //如果将返回类型错误写成void,无法报错,会当做函数重载处理
  2. std::cout << "int";
  3. }
  4. int main() {
  5. fun(true);
  6. return 0;
  7. }

C++前向声明

什么是前向声明?

可以声明一个类而不定义它。这个声明,有时候被称为前向声明(forward declaration)。比如class Screen; 在声明之后,定义之前,类Screen是一个不完全类型(incompete type),即已知Screen是一个类型,但不知道包含哪些成员,具有哪些操作。不完全类型只能以有限方式使用,不能定义该类型的对象,不完全类型只能用于定义指向该类型的指针及引用或者用于声明(而不是定义)使用该类型作为形参类型或返回类型的函数
类的前向声明只适用于指针和引用的定义,如果是普通类类型就得使用include了。

前向声明的好处

  1. 不必要的#include   会增加编译时间. 
  2. 混乱随意的#include可能导致循环#include,可能出现编译错误.

前向声明的限制

  1. 不能定义foo类的对象;
  2. 可以用于定义指向这个类型的指针或引用。(很有价值的东西);
  3. 用于声明(不是定义)使用该类型作为形参或者返回类型的函数。

头文件使用规范

  1. 将文件间的编译依赖降至最小。
  2. 将定义式之间的依赖关系降至最小,避免循环依赖。用#ifdef、#define、#endif等控制编译依赖。
  3. 让class名字、头文件名字、源文件名字直接相关,方面源码定位。
  4. 在头文件内写内部#include guard,不要在源文件写外部护套。

参考:

原文链接:https://blog.csdn.net/qq_41895747/article/details/117291506



所属网站分类: 技术文章 > 博客

作者:我爱编程

链接:http://www.javaheidong.com/blog/article/207070/1d0c1bb12d062cc1257e/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

24 0
收藏该文
已收藏

评论内容:(最多支持255个字符)