-
头文件
- 一定要用
ifndef/define/endif
防止被重复引用 - 工程的头文件数目较多时(比如超过10个),可以考虑头文件和源文件保存在不同的目录,便于维护,如头文件位于include,源文件位于source
- 有些头文件时私有的,不会被用户直接调用,没有必要公开,可以与源文件放在一个目录
头文件搜索路径
#include “headfile.h”
搜索顺序- 搜索当前工作目录
- 搜索-I指定的目录
- 搜索
gcc
环境变量CPLUS_INCLUDE_PATH
(C程序使用的是C_INCLUDE_PATH
) - 搜索
gcc
内定目录:/usr/include
,/usr/local/include
,/usr/lib/gcc/x86_64-redhat-linux/4.1.1/include
,各目录存在相同文件时,先找到哪个使用哪个
#include <headfile.h>
搜索顺序- 搜索-I指定目录
- 搜索
gcc
环境变量CPLUS_INCLUDE_PATH
(C程序使用的是C_INCLUDE_PATH
) - 搜索
gcc
内定目录:/usr/include
,/usr/local/include
,/usr/lib/gcc/x86_64-redhat-linux/4.1.1/include
,各目录存在相同文件时,先找到哪个使用哪个
库文件搜索路径
- 编译时
- 搜索-L指定的路径
- 搜索
gcc
的环境变量LIBRARY_PATH
- 搜索内定目录
/lib
/usr/lib
/usr/local/lib
- 运行时
- 编译时指定的动态库搜索路径
- 搜索环境变量
LD_LIBRARY_PATH
- 搜索配置文件
/etc/ld.so.conf
指定的动态库路径 - 默认的动态库搜索路径
/lib
/usr/lib
- 一定要用
-
类的格式
- 以行为为中心:将private的数据放在前面,public的函数放在后面
- 以数据为中心:将public的函数放在前面,private的数据放在后面
- 建议以行为为中心,因为通常用户最关心的是接口
-
浮点数与数值比较
-
float和double类型都有精度限制,不要直接用==或!=与数值比较
-
// 变量x与数值a判断相等 if (x >= a - EPSINON || x <= a + EPSINON)
-
-
循环语句效率
- 多重循环中,将最长的循环放在内层,最短的循环放在外层,以减少CPU切换循环的次数
-
- 书中举的例子是不对的,实验证明其实长循环在外面时执行时间更短,因为数组是连续存储(行优先),按短循环在外的写法每次访问数据可能会缺页中断,与程序局部性原理冲突。(但该说法本身没有错误)
- 若循环体内存在逻辑判断,且循环次数很大,应将逻辑判断放到循环体外,这样编译器可以更好的进行优化处理,但是缺点是是程序看起来不够简洁
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HlBvF2aL-1608561492341)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20201110221427006.png)]
-
常量
- 使用const,不要使用#define,const可以进行类型安全检查
- 需要对外公开的常量放头文件中,不对外公开的放源文件头部(可把不同模块常量集中放在一个公共头文件中,便于管理)
- 类的const常量是在对象初始化完成的,每个对象的值可能不一样;如果要建立整个类中都恒定的常量,可以使用枚举,不会占用对象的存储空间,缺点是只能表示整数,且最大值有限
-
函数
-
输入参数如果是传值方式,最好使用
const &
方式,省去临时对象的构造和析构过程 -
有时函数不需要返回值,但为了增加灵活性如支持链式表达式,可以附加返回值,如strcpy:
-
char str[20]; int length = strlen(strcpy(str, “Hello World”));
-
函数返回一个对象时,最好使用“引用传递”提高效率,但有些场合只能“值传递”,否则出错(比如返回栈内对象)。
-
提高函数质量:
- 输入参数都要进行有效性检查;
- 检查其他途径进入函数体内的变量有效性(全局变量,文件句柄等)
- return语句的正确性,不要返回栈内存的指针和引用(函数结束后内存销毁,返回的指针/引用指向未知内存)
- return语句效率性进行检查,返回值对象时考虑效率:
return string(s1 + s2);
比string temp(s1 + s2); return temp
效率高(内置数据类型效率差不多),省去了temp的构造和析构的开销
-
-
内存管理
-
分配内存后立刻检查是否分配成功
-
释放内存后立即设为NULL,防止野指针
-
指针变量消亡了,不代表所指的内存会自动释放
-
内存被释放了,不代表指针变量会消亡或成了NULL指针(delete/free只是释放了内存,对指针变量没有影响,所以释放后最好手动把指针变量置为NULL)
-
野指针:
- 指针变量没有初始化,指针变量创建时不是NULL,缺省值是随机的,所以指针变量创建要初始化,或置为NULL
- delete或free后的指针不是NULL,指针变量的值没有变,最好手动设为NULL
-
函数参数是一个指针时,不要指望该指针区申请动态内存
-
编译器总是要为函数的每个参数制作临时副本,指针参数 p 的副本是
_p
,编译器使_p = p
。如果函数体内的程序修改了_p
的内容,就导致参数 p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p
申请了新的内存,只是把_p
所指的内存地址改变了,但是 p 丝毫未变。所以函数 GetMemory 并不能输出任何东西。事实上,每执行一次GetMemory 就会泄露一块内存,因为没有用 free 释放内存void GetMemory(char *p, int num) { p = (char *)malloc(sizeof(char) * num); } int main(void) { char *str = NULL; GetMemory(str, 100); // str 仍然为 NULL strcpy(str, "hello"); // 运行错误 }
-
-
为什么free函数不需要大小类型?
- 编译器知道要free的指针的大小(申请时时间上会多4个字节存放size)
-
new内置了sizeof,类型转换和类型安全检查,对于非内部数据类型。new创建对象时完了初始化,如果有多个构造函数,new的语句也可以有多种形式,但如果是创建对象数组,只能用无参构造函数。如:
Obj *objects = new Obj[10](5,3);
是错误写法
-
-
函数
-
相比C语言函数,C++增加了重载(overloaded)、inline、const、virtual四种新机制,其中const和virtual仅用于类成员函数,重载和inline可以用于全局函数或类成员函数
-
参数的缺省值只能出现在声明中,不能出现在定义体中;有多个参数时,只能从后向前缺省
-
编译器会根据参数为每个重载函数产生不同的符号,如果函数已经被C编译器编译了,则需要用
extern C
来包含调用的函数,让C++编译器知道函数生成符号是按C方式来的 -
重载、覆盖、隐藏区别
-
重载:相同范围(同一个类中),函数名相同,参数不同,virtual可有可无
-
覆盖:不同范围(子类覆盖父类),函数名相同,参数相同,父类函数必须有virtual关键字
-
隐藏:子类函数屏蔽(隐藏)了父类的同名函数
- 子类函数与父类函数同名,参数不同,无论父类函数有无virtual,都被隐藏
- 子类函数与父类函数同名,参数相同,父类函数无virtual被隐藏,否则是覆盖
隐藏不是将父类函数变没了,实际可以通过
父类名::函数名
方式访问,只是一个子类对象调用函数时是调用的子类的函数,调用不了父类的同名函数了
-
-
内联函数具备宏代码的效率,而且相比宏增加了安全性,可以操作类的数据成员,所以C++中用内联函数替代宏。编译器会为内联函数生成符号
inline是实现关键字,必须和函数体定义放一起才能使函数内联,若inline仅放在声明,不起作用
定义在类声明的成员函数自动成为内联,但良好的编程风格是类中声明,类外定义加上inline
-
-
类的构造、析构、赋值函数
- 如果不编写类的构造析构函数,编译器会自动为类生成默认的无参构造函数,拷贝构造函数,赋值函数,析构函数。默认的拷贝构造和赋值函数是浅拷贝,不是深拷贝
- 初始化列表在参数表之后,函数体之前。初始化列表的工作是在函数体代码之前执行的
- -如果类存在继承关系,子类必须在初始化列表中调用父类构造函数
- 类的const常量成员只能在初始化列表进行初始化
- 类的数据成员初始化有函数体内赋值和初始化列表两种方式,数据类型为内置类型时效率差不多,非内置类型时初始化列表方式效率更高(函数体赋值实际上会调用无参构造函数和赋值函数)
- 构造&析构次序
- 构造顺序首先调用基类的构造函数,然后是成员对象的构造函数,成员对象的构造顺序有类声明的次序决定,与初始化列表的顺序无关
- 析构的次序与构造的顺序相反
- 拷贝构造函数是对象创建时使用,赋值函数是对象已经存在了,不要混淆。
String a(“hello”); String c = a;
调用的是拷贝构造,因为对象c是创建,最好写成String c(a);
语义上更清晰 - 父类的构造、析构、赋值都不能被子类继承,如果存在继承关系要注意:
- 子类的构造函数要在初始化列表中调用父类构造函数
- 子类和父类的析构函数要加virtual关键字,否则可能内存泄漏
- 子类的赋值函数,不要忘记对父类的数据成员重新赋值
-
类的继承和组合
- 逻辑上A是B的一种(a kind of),且A的所有功能和属性对B有意义,则允许B继承A
- 逻辑上A是B的一部分(a part of),则不允许B继承A,是用A和其它东西组合出B
-
编程经验
- 尽量使用const,提高函数健壮性
- 修饰参数:输出参数不要加const,否则失去输出功能;如果是值传递参数,没必要加const(编译器会制作副本,加不加const都不会改变),对于非内部数据最好使用const &传递参数确保参数不被修改(效率高)
- 修饰返回值:返回是值传递方式时加不加const没有价值(会复制到外部临时存储单元中);函数返回引用的情况不多,一般只出现在类的赋值函数中(实现链式表达)
- const成员函数:任何不修改数据成员的函数都应声明为const类型,如果编写const函数时,不慎修改数据成员,或调用非const成员函数,编译器将报错。
- 先优化算法和数据结构,再优化执行代码
- 尽量使用const,提高函数健壮性
高质量C++编程
最新推荐文章于 2023-12-28 19:59:57 发布