-
读书笔记:从缺陷中学习C++
从缺陷中学习C++
1. 基础问题
- 注意运算符的优先级
- 不加括号的宏,可能会存在优先级的问题
- 污染环境的宏定义
- 多语句宏定义使用错误。 可以使用inline函数来规避,或者 while {…} do (0);
- char 转为 int 时高位符号扩展的问题。考虑符号的问题,可以先把char转为unsigned char,再转换为int
- int转为char时的数据丢失。系统的默认的char可能会是有符号,也有可能是无符号(如g++可以通过-funsiged-char来指定),导致转换后的值不同。最好就是避免从char到int的转换。对于变量的潜质转换,需要注意类型的截断和扩展
- 非法的数组下标
- 有符号int与无符号int比较的后果。再合理范围内,要将无符号的转换为有符号的再进行比较。
- 有符号的困惑(位域变量,可能无法设置你所需要的值)
- 整除的精度问题(1.赋值时一律时右边类型转换称左边类型,但是右边是表达式时,会先进行运算,然后才对运算的结果进行数据类型转换. 2.当不同类型的变量进行计算时,遵循由帝姬向高级转换的规则,例如,char会传还位整形,short转换位int, float会转换位double)
- 浮点数比较的精度问题。浮点数表示的精度有限,因此不能准确的表示一个小数。如果需要对浮点数类型进行比较,一般比较他们之间的差值再一个范围之内。
- 最小负整数取相反数溢出。如int在正数和负数的表示范围不对称。
- 临时变量溢出
- size_t导致的死循环(size_t时无符号的)
- 区分congtinue和return
- 指针常量和常量指针的区别
- 字符数组和指针不总是等价的
- 结构体成员变量初始化的隐患(对结构体进行赋值时,建议具体到结构体成员的变量名,避免修改成员顺序导致出错)
- 返回值非void的函数没有返回值
- cin>> 和 getline混用导致的奇怪问题
2. 编译问题
- 动态链接库加载错误版本。加载顺序:RPATH->LD_LIBRARY_PATH->/etc/ld.so.cache维护的so列表->/lib或者/usr/lib
- 相同名称静态库的链接顺序。 考虑-L的目录查找顺序,或者更改库名,比较冲突
- 使用命名空间来区分不同cpp中的同名类
- C++模版编译时依赖名称查找。编译时会分成两个阶段进行查找,第一阶段时扫描所有非依赖名称,第二阶段会扫描所有的依赖名称(依赖于模版参数,只有在实例化时才可以消除歧义的名称 )
- 违背ODR原则可能会带来意想不到的问题。ODR(One-Define-Rule)
- 变量公用内存时使用O2优化编译
3.库函数问题
- sprintf函数引起的缓存区溢出
- snprintf函数format参数的问题
- 错误使用snprintf函数返回值, snprintf 返回值是缓冲区大小没限制时写入的字符个数,并非成功写入的字符个数。
- 字符串复制不完整。 strcpy会忽略\0后面的字符
- string类的 c_str方法使用不当。 修改字符串后,不要通过之前保存的指针再访问该对象中的字符串
- string类的“[]”操作符使用不正确。 使用[]往string对象里面写数据,并不会触发修改size,调用string::empty()依然会返回true
- 不正确的字符串比较
- strncpy函数没有复制结束符。strcpy函数在复制结束时,不管目标地址空间是否足够(从而可能产生溢出),都会自动在末尾加‘\0’,但strncpy函数则不同。当source的字符串长度小于num时,strncpy函数在复制所有source的字符之后,会填充剩下的空间为‘\0’。当source的字符串长度大于或等于num时,strncpy函数就只是复制source的字符,这样destination并不是正常的以‘\0’结尾的字符串。
- 调用memcpy函数前未初始化缓冲区
- 误用sizeof操作符取字符串长度, sizeof返回的时占用的字节数,strlen才是获取字符串长度的函数.
- string类 find函数返回值判定。应该直接与string::npos进行对比。
- stringstream的清空。stringstream::clear()不会真正释放所占用的空间,需要调用streamstring::str(“”)来重制。
- 调用strptime函数前需初始化tm
- 使用feof函数的陷阱。feof函数的功能是检查一个stream在当前其文件结束标志是否被置位,如果已经置位,返回非零值,否则返回零。在正确获取最后一行代码时,文件结束标志位时不会被设置的。将在下一次读取时,才会设置。fgets函数的调用会修改文件结束标志,其他还会修改文件结束标志的类似的函数还有:fgetc、fgets、fread、fseek、getc、getchar、gets 等
- for循环中调用vector容器insert函数。注意insert返回的迭代器,指向的位置。
- multiset容器erase函数的误用。会删除所有与传入值相同的元素
- 慎用容器类erase函数的返回值
- for循环中调用vector容器erase函数。删除元素后,迭代器直接指向下一个元素。
- getopt函数参数问题
- 不用errno判断系统调用是否成功。只有在库函数调用失败时,errno 才会被赋值,当函数成功运行时,errno 的值不会被修改。
- strcat函数造成的段错误。得保证有足够的内存空间存放字符串。
- 危险的strdup函数。strdup会自动分配内存空间,并复制字符串。但strdup只malloc了内存,并没有释放。释放strdup内部动态分配的内存需要由调用者去做。
4.文件处理
- 程序异常退出时未关闭已打开文件
- 目录打开后未关闭
- 写文件没有调用fflush
- 读文件fread的返回值不能忽略。库函数 fread 返回已经成功读取的元素 items 的数量,代码中没有对返回值进行判断和处理,因为在 fread 执行的过程中有可能被信号中断,导致读取的元素的个数与预期的不一致,因此,在fread之后需要对返回值做判断,判断count是否等于size来判定读取的完整性。
- getline()使用不当导致的死循环
- 未重置流状态导致读文件错误。在第二次遍历文件时,重置文件流状态
5. 类和对象
- 类对象的浅复制。浅复制的意思就是C++只会对对象中的每个成员使用赋值运算符。当类很简单的时候(如没有动态分配内存的情况),浅复制不会出问题。但是如果其中一个类成员变量为指针变量,并且指向动态分配的空间,那么在赋值之后,被赋值的对象的指针变量将会指向原对象动态分配的空间,因此原对象被析构之后,被赋值对象的指针就变成悬挂指针。
- 构造函数中的操作符重载。在 C++中,如果一个函数返回值(非引用)时,会生成一个匿名的临时变量并将函数返回值赋值给匿名的临时变量;如果函数返回引用,则不会生成临时变量。
- 拷贝构造函数不能模板化。模板化的构造函数永远不会被编译器当做拷贝构造函数来使用,只会以转换构造函数的形式存在。
- 析构函数未捕获异常引发coredump。如果抛出的异常未被捕获,则会一直向上传递直到C++自动调用标准库中的terminate函数,默认情况下terminate会再次调用abort函数结束程序,同时生成coredump。如果抛出的异常未被捕获,则会一直向上传递直到C++自动调用标准库中的terminate函数,默认情况下terminate会再次调用abort函数结束程序,同时生成coredump。如果在栈展开的过程中,某个对象的析构函数又抛出了异常并且这个异常未被捕获,C++会调用terminate()函数,该函数默认调用abort()函数以非正常方式结束程序。此时程序被异常结束。析构函数中抛出异常往往是预示着这可能是一个Bad Design。如果析构函数非要抛出异常,或者调用了其他可能会抛出异常的函数方法,则析构函数应自己捕获这些异常。
- 构造函数中抛出异常引起内存泄露。在构造函数中抛出异常需要注意,因为在类的构造函数中抛出异常,系统是不会调用它的析构函数的,可能会造成资源泄露,所以,在构造函数中抛出异常前要记得释放已经申请的资源。
- 多态性未生效。实现多态的关键是使用虚函数,virtual 关键字修饰的成员函数,就是虚函数,其作用是“推迟联编”或者“动态联编”,即一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。
- 基类成员函数被隐藏。重载的特征:(1)相同的范围(在同一个类中);2)函数名字相同;(3)参数不同;4)virtual关键字可有可无。覆盖是指派生类函数覆盖基类函数,特征是:(1)不同的范围(分别位于派生类与基类);(2)函数名字相同;(3)参数相同;(4)基类函数必须有virtual 关键字。隐藏的特征如下:(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
- 匿名对象引起的内存泄露。借用“匿名对象”术语是指new了一个对象,但没有引用或对象指针指向它。
- 基类非虚析构函数引发内存泄露。C++标准规定,当一个派生类对象通过使用一个基类指针删除,如果这个基类的析构函数是非虚的,则删除结果是未知的。C++提供虚函数主要是为了实现多态性。如果一个基类被设计用于多态处理,则应将这样的基类的析构函数声明为虚函数。
- 删除
void*
指针引发内存泄露。当使用 delete 操作符进行释放对象时,delete 需要根据类型信息正确地释放指针所指向的内存块。即首先调用对象的析构函数,然后释放该对象指针。在调用对象的析构函数前,首先需要知道该对象的类型。如果不知道该对象的类型,则无法知道该调用谁的析构函数。另外,C++标准明确说明,针对void*
指针做 delete 操作会引发无定义行为。 - STL容器不会自动释放指针指向的对象
- 静态成员类内初始化。C++标准规定,在类的定义体内,只可以声明static成员变量,但不能初始化。static成员变量初始化必须在类定义体外进行。但这个规定有一个例外,允许整型 const static可以在类的定义体中定义并初始化。
- union作为类的成员时需要构造函数
- 成员函数尾部缺失const标注。一个类的const对象只能使用该类的const方法
- 使用memset初始化class。不建议使用 memset 对一个类的对象进行初始化。这是一种危险的操作,在某些情况下会导致程序crash。不管一个类中是否含有虚函数,都不要使用这种方式初始化对象,养成良好的编程习惯。对对象成员的初始化操作可以放在构造函数或init等特定函数中。
- dynamic_cast转换失败返回NULL
6. 内存使用
- 数组越界
- 数组定义和值初始化形式混淆
- 数组传参时的sizeof
- 临时对象的生存期
- 变量的作用域
- 指针变量的传值和传址
- 指针赋值和指针赋址的混淆。函数中传递指针或引用参数,要注意修改是指针本身还是指针的内容,若不希望改变指针本身,建议加 const声明
- 指针释放后再次使用。编码时遵循“谁分配,谁释放”的原则。
- 重复申请内存未释放
- delete与delete[]的区别。操作内存的时候,new[]一定要和delete[]对应
- 函数中途退出忘记释放内存
- 二维数组的内存泄露
- 临时变量内存不能返回
- 正确使用引用参数和引用返回值
- 试图产生的指针很可能不存在。使用指针前需要判断指针是否为NULL,避免空指针导致的程序异常
- 结构体成员内存对齐问题。内存字节对齐是指,为了保证CPU对内存的访问效率,各种类型数据需要按照一定的规则在内存存放,而不是完全字节挨字节的顺序存放。对于结构体数据类型,默认的字节对齐一般需满足3个准则。(1)结构体变量的首地址能够被其最宽数据类型成员的大小整除。(2)结构体每个成员相对结构体首地址的偏移量都是该成员本身大小的整数倍,如有需要会在成员之间填充字节。(3)结构体变量所占总空间的大小必定是最宽数据类型大小的整数倍。如有需要会在最后一个成员末尾填充若干字节,使得结构体所占空间大小是最宽数据类型大小的整数倍。
- String对象何时需delete
多线程问题
- 局部静态变量非线程安全。如果程序中存在静态变量,不管是静态全局变量还是静态局部变量,往往都存在线程安全问题,在多线程环境下需要特别注意。
- string类 append操作非线程安全
- 中途退出造成的线程阻塞。在多线程编程中,要特别注意临界区内中途返回的情况。每当互斥锁锁定的临界区内有return发生时,都要确认这样的操作是否会造成问题。
- 结构体位域成员线程安全问题。针对位域成员的读写操作并不保证是原子操作,这依赖于目标机器和编译器。所以,在多线程环境下,对于同一结构体内不同位域成员的访问是无法保证线程安全的。如果要保证线程安全,需要程序员自己做额外的措施
- 多线程写文件引发内容被覆盖
- 线程未join引起的内存泄露。在线程属性里,有一个很重要的属性是可分离状态(detachstate),用于表示新线程是否与进程中其他线程脱离同步。在线程属性里,有一个很重要的属性是可分离状态(detachstate),用于表示新线程是否与进程中其他线程脱离同步。
性能问题
- strlen用作循环条件影响性能
- STL容器 list使用时忌频繁调用 size()
- 误用clear回收vector内存。vector的clear()方法做的事情是把vector内的元素全部删除掉,并调用元素所存储对象的析构函数释放对象,但这并不会改变vector本身所占用内存的大小。解决这个问题的一个方法是,通过 swap()方法完全释放 vector 占用的内存。swap后,vector的capacity和size都变为0
- calloc在glibc高版本下性能劣于低版本下
-
相关阅读:
C++官网 Tutorials C++ Language Basics of C++:Constants
第一次线上 OOM 事故,竟和 where 1 = 1 有关
双向认证配置
彻底解决tomcat控制台提示至少有一个JAR被扫描用于TLD但尚未包含TLD的问题
前端UNIAPP端webview嵌入H5使用说明文档
一个网址包含有什么? URL-简介
【Mongoose应用和文件上传】一.Express框架访问MongDB数据库;二.node实现文件上传
CNN模型合集 | Resnet变种-WideResnet解读
VVC 帧内预测 predIntraAng()函数 (getCoLocatedLumaPU没写)
【Day_14 0510】▲幸运的袋子
-
原文地址:https://blog.csdn.net/roddick621/article/details/127876195