C++开发时遇到的易错点

记录一些C++开发过程中遇到的一些易错问题,长期更新

auto 关键字

auto 在使用时非常方便,但是编译器自动判断的类型可能预期不符,例如 Eigen 在进行矩阵运算时,就应该尽可能避免使用 auto,否则会产生难以预期的结果。详见本博文

函数返回值为智能指针

函数返回值为智能指针时,如果对返回值直接进行寻址并将寻址结果用于初始化引用,则会导致悬空引用的问题,例如:

C++
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),类中变量的初始化顺序为声明顺序,而与初始化列表的顺序无关。

模板超出最大递归限制

如果模板中定义了递归操作,需要额外注意模板递归的停止条件,例如下述代码:

C++
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 之前,上述问题可以通过特化模板来解决,如下述代码所示:

C++
template<int N>
int foo() {
    return N + foo<N-1>();
}


template<>
int foo<1>() {
    return 1;
}

上述代码在 N = 1 时特化了模板,且该特化中不包含迭代,从而使得编译器可以在 N = 1 时停下。

C++17 之后,上述问题则可以使用 if constexpr 来解决,该指令用于在编译时判断分支是否应该被抛弃,代码如下:

C++
template<int N>
int foo() {
    if constexpr (N == 1) return 1;
    else return N + foo<N-1>();
}

需要注意,else 在这里不能省略,因为 if constexpr 只能对于 ifelse 两个分支进行判断,外部代码无法判断是否被抛弃。

单个参数包用作构造函数唯一参数时可能导致的问题

在使用参数包(parameter pack)作为构造函数的唯一参数时,默认拷贝构造函数在输入非const时会被其覆盖,可能导致一些难以排查的问题。

智能指针与普通指针的使用

智能指针的作用在于管理所有权,因此不涉及所有权的操作不应使用只能指针。

这里的所有权指的是能够决定指针指向的对象的生命周期的代码。

std::set / std::map 的插入

之前错误的使用setmap,先使用find判断元素是否存在,不存在则进行插入否则进行其他操作,实际上,insert函数会在内部进行相关判断,且其返回值为一个pair,其中第一个元素为插入位置,第二个元素为是否插入成功,因此只使用insert进行相关判断即可,不需要调用find。

并且,map的operator[]在元素不存在时同样会自动插入元素,无需判断是否存在。

下划线开头的变量名可能导致的问题

有些命名规范会使用下划线开头的变量表示私有变量,但是C++中,下划线、双下划线开头的变量实际上是保留给C++实现的(例如编译器、标准库),因此使用以下划线开头的变量名可能会导致难以预料的问题。详见本文

模版在使用过程中的一些问题

  1. 模版根据调用进行编译,因此没有被调用的(全特化、实例化)模版并不会被编译,这也就是为什么模版的声明和定义要放在一起。
  2. 模版成员函数不能为虚函数(如果能是虚函数,基类可能并没有调用对应签名的成员函数,因此基类中可能不存在该函数,自然无法实现重载)。
  3. 模版全特化时,需要进行声明和定义,且定义需要放入源文件,否则会与ODR冲突。
  4. 万能引用(旧称为Universal Reference,目前被称为Forwarding Reference)只有在函数模版才能触发,类模版无法实现万能引用。

内存越界导致的问题

开发过程中,遇到了malloc的报错:malloc(): unsupported double linked list corrupted

该报错主要是由于内存越界导致的,不过报错位置可能并不是发生越界的位置,反而会发生在越界后,对堆进行分配的时候。

这是因为,在内存越界时,越界数据覆盖掉了用于管理堆的数据(在这个情况下,即覆盖掉了用于管理堆的双向链表的数据),从而导致下次需要进行分配时,无法获取管理数据。

由于该问题涉及到内存分配,因此发生位置可能不固定,不过可以通过多次报错的调用栈,定位相同的调用路径,来推断发生越界的大致位置,因为存在相同路径,即意味着越界大概率发生在相同路径之后。

没写返回语句导致的未定义行为

如果一个函数定义存在返回值,但是该函数本身没有执行返回语句,那么会导致未定义行为。

因为如果没有执行返回语句,那么函数的返回值可以视为野指针。此时,函数的返回值如果被释放,就会出现未定义的行为。

这种问题导致的报错千奇百怪,需要格外注意检查。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部