手把手写C++服务器:C/C++编译链接模型函数重载隐患头文件使用规范
Posted 沉迷单车的追风少年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手写C++服务器:C/C++编译链接模型函数重载隐患头文件使用规范相关的知识,希望对你有一定的参考价值。
前言:C++兼容C,在编译上有明显的体验。有一个流传很久的段子,C/C++程序员逃避工作的正当借口就是:“我的程序正在编译”。对于服务端编程,不管是时间资源还是硬件资源,都非常宝贵,力图做到极致优化。因此了解C/C++编译的前世今生、背后的原理、常见的优化手段,对之后的服务端编程来说非常重要。
目录
为什么C/C++编译比Java、Python、golang慢很多?
为什么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语言编译器为了尽量减少内存使用情况下实现分离编译。
怎样解决?
这种情况下编译器不会报错!链接器会生成警告或报错。因此:
- 在c语言里面开来还是要学习c++的编程习惯,使用函数之前一定要声明。不然,即使编译能通过,运行时也可能会出一些莫名其妙的问题。
- 重视编译器的警告,少给自己挖坑。
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了。
前向声明的好处
- 不必要的#include 会增加编译时间.
- 混乱随意的#include可能导致循环#include,可能出现编译错误.
前向声明的限制
- 不能定义foo类的对象;
- 可以用于定义指向这个类型的指针或引用。(很有价值的东西);
- 用于声明(不是定义)使用该类型作为形参或者返回类型的函数。
头文件使用规范
- 将文件间的编译依赖降至最小。
- 将定义式之间的依赖关系降至最小,避免循环依赖。用#ifdef、#define、#endif等控制编译依赖。
- 让class名字、头文件名字、源文件名字直接相关,方面源码定位。
- 在头文件内写内部#include guard,不要在源文件写外部护套。
参考:
以上是关于手把手写C++服务器:C/C++编译链接模型函数重载隐患头文件使用规范的主要内容,如果未能解决你的问题,请参考以下文章