C/C++相关
C++编译链接模型
C++的三大约束:
- 与C兼容
- 零开销(zero overhead)
- 你不需要为你不使用的特性付出(时间或空间)开销;
- 你使用的任何特性都应该尽可能地高效,至少要和你自己手写代码实现该特性的性能一致。
- 不符合的:运行时类型识别(RTTI)和异常
- 值语义
- 现代更多语言是引用语义
C++因为使用include机制,导致编译效率非常低,会将库代码重新parse一遍
- 现代更多语言是引用语义
c++20之后开始有模块化引用头,即import 包名
C语言是单遍编译的,只扫描一次,所以必须先定义才能访问,否则无法立即生成目标代码。
C语言采用了隐式函数声明,代码使用了前文未定义的函数时,编译器认为该函数是int xxx(int, ...)
型(C语言一开始没有函数原型的说法,后续从C++借用,也不区分int和指针)
以上两点影响了C++的函数重载决议:当编译器读到一个函数调用时,只能从目前已看到的同名函数中选出最佳函数,而不是全局最佳函数例如:
void fun(int x){
printf("fun int: %d",x);
}
int main(void){
fun('a');
return 0;
}
void fun(char c){
printf("fun char:%c",c);
}
最终输出fun int: 97
前向声明:
函数的前向声明:
大多数情况下,我们把函数原型声明在.h文件中,而把定义写在.cpp文件中,头文件中的函数原型声明就是函数的前向声明。
类的前向声明:
如果只看到指针或者引用就行,可以使用前向声明,表示有这么个类。
如果仅声明一个返回值或者参数带有这个类的函数,可以使用前向声明。
永远不要重载&&、||、,(逗号)、operator&这几个操作符!!
链接
各个目标文件各自编译完之后,可能会有各种引用,比如一个模块引用了某个库的函数,编译时并不知道这个库的地址,所以只能先空着。链接的目的就在于将这些空填上。
链接思路:
- 扫描两遍,第一遍记录每个符号地址,第二遍查符号表填补空白
- 如果要求只在后面引用前面:那么只需扫描一遍,那么当遇到空白时,其所使用的符号都已经在前面出现,直接填即可
- (one-pass)如果要求只在前面引用后面:那么也只需扫描一遍,不再记录符号地址,而是存储空白(待重填写项),那么后面一定到某个位置可以找到地址,将空白填上,就可以不再处理这个符号对应的空白了。
一般使用one-pass方式,这样相比于第二种方式内存消耗少(不需要存储库中所有符号,只需要存储需要填补的空白即可,而且一旦填上就可以忘掉)
C++增加了函数重载链接规则(name mangling)和弱定义vague linkage规则(同一个符号有多份互不冲突的定义)
vague linkage要求代码满足一次定义原则ODR
函数重载决议
返回值类型不参与函数重载决议
所以当声明和定义返回值类型并不一致的时候,编译器并不会报错。
头文件使用
尽量降低编译依赖
将定义式之间的依赖关系降至最小,避免依赖循环
总是写#include guard
…
库文件组织
- 动态库
- 静态库
- 源码编译(推荐)
libstdc++是C++标准库,跟C++编译器直接相关
glibc是c标准库,跟Linux操作系统的版本直接相关
一般来说不会随意更改版本
面向对象的反思
朴实为贵,如果不用继承就可以做好事情,就不要通过继承来增加复杂度。只有使用继承(其他技术也一样)可以简化设计的时候,才去用它。
必要时可以动大手脚,避免积重难返。让代码保持清晰是王道!
值语义与对象(引用)语义
C++设计之初,就有一个重要的特性:抽象数据类型ADT,或者说数据抽象。数据抽象听起来似乎和基于对象的编程很像,但他们有本质不同。
数据抽象的目的是将一系列数据作为一个自定义数据类型,并可以提供操作该类型的函数(包括全局函数以及类型成员函数),通常是可拷贝的(拷贝是有意义的,比如将一个复数赋值给另一个复数)。也因此需要考虑拷贝控制(深拷贝)。STL中的大多容器都是值语义的。
基于对象的编程里的对象通常是不可拷贝的(拷贝无意义),这个对象通常代表了一个实体,比如一个线程对象,拷贝并没有什么意义,比如一个连接,拷贝过去之后socket是同一个吗?由谁释放?二者这样做也违背了设计连接对象的初衷。
数据抽象是值语义的,而基于对象的编程/面向对象编程中的对象是引用语义的。
C++没有垃圾回收,直接使用裸指针在面向对象编程中资源回收是个痛点,最好使用智能指针(虽然看起来代码更难看了,但没办法,这是为了避免更大的代价)
C++的数据抽象是0成本的,性能损失小
C++经验谈
代码逻辑应当直截了当,尽量减少依赖关系。
- 交换变量老老实实用tmp,不要用异或,异或并不能提高效率和减少内存占用
- 别重载new和delete
- 整数除法当操作数为负时,结果在各语言中不一定相同
- 版本控制友好的代码:
作者是站在命令行diff的角度来说的,当前来说,图形化的diff当然效率更高,那么有一些规则其实也没必要太在意,下面是我认为需要注意的:
- 多行注释也用//,这样在提交记录中能显示所有注释内容(嗯,现在大家好像都这样做,/**/都快没人用了,哈哈)
- 使用static_cast之类的类型转换,原因是便于查找
- string的实现
- 直接拷贝:eager copy
- COW:copy on write
- 短字符串优化SSO:有一段短缓冲区,超过限制则转为指针模式
问题
- Child和Parent class相互指涉(依赖)?前向声明用法
- 编译器如何处理inline函数中的static变量?
- 什么是一次定义原则ODR?
- P418如何利用g++找到某个头文件是如何引入的,比如只包含了iostream却可以使用std::string
参考书籍
《C++编程规范》陈硕-侯捷
《代码整洁之道》