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

Posted 沉迷单车的追风少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器:C/C++编译链接模型函数重载隐患头文件使用规范相关的知识,希望对你有一定的参考价值。

前言: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):

void fun(int) {
    cout << "int";
}

void handle() {
    fun('a');
}

void fun(char) {
    cout << "char";
}

int main() {
    fun();
}

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

void fun(int) {
    cout << "int";
}

void fun(char) {
    cout << "char";
}

void handle() {
    fun('a');
}

int main() {
    fun();
}

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

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

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

举个例子:

int fun(bool) {    //如果将返回类型错误写成void,无法报错,会当做函数重载处理
    std::cout << "int";
}

int main() {
    fun(true);
    return 0;
}

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,不要在源文件写外部护套。

参考:

以上是关于手把手写C++服务器:C/C++编译链接模型函数重载隐患头文件使用规范的主要内容,如果未能解决你的问题,请参考以下文章

手把手写C++服务器:C++编译常见问题编译优化方法C++库发布方式

手把手写C++服务器:专栏文章-汇总导航更新中

手把手写C++服务器:编译实操——打开gcc/g++世界

手把手写C++服务器(26):常用I/O操作创建文件描述符

手把手写C++服务器(29):手撕echo回射服务器代码

手把手写C++服务器:永远滴神vim(源码安装插件管理颜色主题代码高亮快捷键设置搜索替换环境保护)