auto 关键字
auto 在使用时非常方便,但是编译器自动判断的类型可能预期不符,例如 Eigen 在进行矩阵运算时,就应该尽可能避免使用 auto,否则会产生难以预期的结果。详见本博文。
函数返回值为智能指针
函数返回值为智能指针时,如果对返回值直接进行寻址并将寻址结果用于初始化引用,则会导致悬空引用的问题,例如:
std::shared_ptr<Typre> func();
const Type& tmp = *func();
这是因为对返回值寻址后,返回值智能指针的引用计数为0,空间被释放,从而导致悬空引用。
size_t 作为循环变量
size_t 通常用作存储容器尺寸的变量,但是该类型为无符号类型,因此在使用 size_t 作为循环变量时,需要额外注意避免出现负数情况,否则 size_t 会发生溢出。
inline 关键字的误导性
inline
关键字常被理解为将函数声明为内联函数。
但实际上,编译器并不会确保 inline
函数会被内联。
inline
的实际作用为向链接器声明该函数能够被重复定义,因此使得该函数能够被直接定义在头文件中,从而提高该函数被内联的可能性。
目前编译器会自动判断函数是否需要内联,通常不再需要手动设置 inline
(模版自带inline性质,无需重复声明)。【参考】
类成员的初始化顺序
c++标准的12.6.2节的13.3指出:
Then, non-static data members are initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).
因此,在非委派构造函数中(delegating constructor),类中变量的初始化顺序为声明顺序,而与初始化列表的顺序无关。
模板超出最大递归限制
如果模板中定义了递归操作,需要额外注意模板递归的停止条件,例如下述代码:
template<int N>
int foo() {
if (N == 1) return 1;
return N + foo<N-1>();
}
上述代码想要利用模板求解1到N之和,但是编译器会给出模板实例化时超出限制的报错(template instantiation depth exceeds maximum of 900
)。这是由于在实例化 foo<N-1>
时,没有给定编译器的停止条件。
在 C++17
之前,上述问题可以通过特化模板来解决,如下述代码所示:
template<int N>
int foo() {
return N + foo<N-1>();
}
template<>
int foo<1>() {
return 1;
}
上述代码在 N = 1
时特化了模板,且该特化中不包含迭代,从而使得编译器可以在 N = 1
时停下。
在 C++17
之后,上述问题则可以使用 if constexpr
来解决,该指令用于在编译时判断分支是否应该被抛弃,代码如下:
template<int N>
int foo() {
if constexpr (N == 1) return 1;
else return N + foo<N-1>();
}
需要注意,else
在这里不能省略,因为 if constexpr
只能对于 if
和 else
两个分支进行判断,外部代码无法判断是否被抛弃。
单个参数包用作构造函数唯一参数时可能导致的问题
在使用参数包(parameter pack)作为构造函数的唯一参数时,默认拷贝构造函数在输入非const时会被其覆盖,可能导致一些难以排查的问题。
智能指针与普通指针的使用
智能指针的作用在于管理所有权,因此不涉及所有权的操作不应使用只能指针。
这里的所有权指的是能够决定指针指向的对象的生命周期的代码。
std::set / std::map 的插入
之前错误的使用set
和map
,先使用find判断元素是否存在,不存在则进行插入否则进行其他操作,实际上,insert函数会在内部进行相关判断,且其返回值为一个pair,其中第一个元素为插入位置,第二个元素为是否插入成功,因此只使用insert进行相关判断即可,不需要调用find。
并且,map的operator[]在元素不存在时同样会自动插入元素,无需判断是否存在。
下划线开头的变量名可能导致的问题
有些命名规范会使用下划线开头的变量表示私有变量,但是C++中,下划线、双下划线开头的变量实际上是保留给C++实现的(例如编译器、标准库),因此使用以下划线开头的变量名可能会导致难以预料的问题。详见本文。
模版在使用过程中的一些问题
- 模版根据调用进行编译,因此没有被调用的(全特化、实例化)模版并不会被编译,这也就是为什么模版的声明和定义要放在一起。
- 模版成员函数不能为虚函数(如果能是虚函数,基类可能并没有调用对应签名的成员函数,因此基类中可能不存在该函数,自然无法实现重载)。
- 模版全特化时,需要进行声明和定义,且定义需要放入源文件,否则会与ODR冲突。
- 万能引用(旧称为Universal Reference,目前被称为Forwarding Reference)只有在函数模版才能触发,类模版无法实现万能引用。
内存越界导致的问题
开发过程中,遇到了malloc的报错:malloc(): unsupported double linked list corrupted
。
该报错主要是由于内存越界导致的,不过报错位置可能并不是发生越界的位置,反而会发生在越界后,对堆进行分配的时候。
这是因为,在内存越界时,越界数据覆盖掉了用于管理堆的数据(在这个情况下,即覆盖掉了用于管理堆的双向链表的数据),从而导致下次需要进行分配时,无法获取管理数据。
由于该问题涉及到内存分配,因此发生位置可能不固定,不过可以通过多次报错的调用栈,定位相同的调用路径,来推断发生越界的大致位置,因为存在相同路径,即意味着越界大概率发生在相同路径之后。
没写返回语句导致的未定义行为
如果一个函数定义存在返回值,但是该函数本身没有执行返回语句,那么会导致未定义行为。
因为如果没有执行返回语句,那么函数的返回值可以视为野指针。此时,函数的返回值如果被释放,就会出现未定义的行为。
这种问题导致的报错千奇百怪,需要格外注意检查。