Effective C++ 笔记

《Effective C++》第三版的笔记

让自己习惯C++

尽量以const, enum, inline 替换 #define

#define 是预处理命令,预处理器只会对文本进行简单的替换,不会进行类型检查等等操作。同时,预处理命令定义的变量并不会在编译阶段出现,因此当这个预处理命令出问题时,很难进行排查和定位。

const 用来替换常量,inline 用来替换 #defineenum 则是为了补充 const 的一个使用场景。

当在类内定义一个静态的常量时,这个常量必须要在类外才能初始化。

1
2
3
4
5
6
struct Test {
// static const int length = 0;
// static constexpr int length = 1;
enum { length = 1 };
std::array<int, length> people;
};

尽可能使用const

重载

注意,两个函数参数的 const 不同,这两个函数不能被重载,也就是说

1
2
void print(int val) { cout << val << endl; }
void print(const int val) { cout << val << endl; }

这两个函数在编译器眼中是一样的

const成员函数则可以重载

1
2
void Test::print(int val) { cout << val << endl; }
void Test::print(int val) const { cout << val << endl; }

bitwise constness 和 logical constness

const来说,有两种

  • bitwise constness:对于这个常量,它的二进制不改变
  • logical constness:对于这个常量,它的外在表现不会改变

编译器执行的是 bitwise constness,而写程序时就需要保证是 logical constness。

当类里面出现了指针,就会出现两种 const 不同的情况

1
struct Test { int a[10] = {0, 1, 2}; }

constnon-const避免重复

const成员函数和non-const成员函数有相同的行为时,可以让non-const调用const版本实现代码的复用

1
2
3
4
5
6
7
8
9
10
11
12
class Test {
public:
Test(int val) : m_val(val) {}
int &print() {
return const_cast<int &>(
static_cast<const Test *>(this)->print()
);
}
const int &print() const { cout << m_val << endl; return m_val; }
private:
int m_val;
};

确定对象被使用前已经初始化

尽量使用列表初始化,这样效率更高

为了免除编译时多个文件初始化顺序的问题,尽量使用 local static 对象代替 non-local static 对象

构造、析构赋值

编译器默默编写的函数

1
2
3
4
5
6
7
8
9
//class Empty {};
class Empty {
public:
Empty() {}
~Empty() {}
Empty(const Empty &other) {}
Empty &operator=(const Empty &other) {}
Empty(const Empty &&other) {}
};

注意:这些函数一开始都没有,只有需要用到的时候才会被构造出来

不使用自动生成的函数,就应该拒绝

比如说单例模式,如果不删掉或者自由化拷贝构造函数,编译器就会自动生成,然后就会违反单例模式。

将基类析构函数声明为虚函数

老面试题了,核心问题是当父类指针指向基类的对象时,会发生内存泄露

不要在构造函数和析构函数中调用虚函数

因为子类的在构造自身前需要调用基类的构造函数,此时基类中构造函数调用的虚函数是基类版本的虚函数,会很难排查。

一种可能的方式是,这样会发生混乱

1
2
3
4
class Base() {
Base() { init(); }
virtual init() { cout << "Create Base" << endl; }
}

operator= 返回一个reference to *this

这样就可以做到连续赋值了 test3 = test2 = test1

1
2
3
4
5
6
7
8
9
struct Test
{
Test(int t_val) : val(t_val) {}
Test &operator= (const Test &others) {
val = others.val;
return *this;
}
int val;
};

operator=中处理自我赋值

对用户来说,很可能发生自己给自己赋值的情况,当进行删除操作的时候,需要额外的注意,可以采用

  • 比较源和目标的地址

  • 调整语句顺序(让删除操作在构造新对象之后进行)

    1
    2
    3
    4
    5
    6
    Test &operator= (const Test &others) {
    Data *temp = other.data;
    data = new Data(temp);
    delete temp;
    return *this;
    }
  • copy-and-swap(较好的方法)

复制时不要忘记每一个成员

当在派生类写复制构造函数或赋值运算符重载函数的时候,经常会忘记构造基类的对象,应该在派生类的函数中调用基类对应的函数

1
2
3
4
5
class D {
public:
D(const D &other) : B(D) {}
D &operator= (const D &other) { B::operator=(D); }
};

资源管理

资源用了要还回去,但是还回去并不简单,很容易出现资源的泄露。

以对象管理资源

以对象管理资源有两个关键的思路

  • 资源获取后立即放入管理对象中
  • 管理对象利用析构函数确保资源的释放

为了防止资源泄露,尽量使用RAII对象

在资源管理类中小心对象拷贝

资源很多时候是互斥的,当对一个RAII对象进行拷贝时,他们的行为会有不同,主要包括

  • 禁止拷贝(unique_ptr
  • 允许拷贝,通过引用计数,确保安全释放(shared_ptr
  • 复制底部资源
  • 转移底部资源的拥有权

在资源管理类中提供对原始资源的访问

RAII对象很好,但是有些API只允许传入原始的对象,如指针,因此需要提供一个访问原始资源的接口。可以通过函数显示的调用,也可提供隐式类型转换

使用newdelete要使用相同的形式

deletenew [] 构造的对象会造成资源的泄露

delete[]new 构造的对象会造成未定义的行为

通过new构造的智能指针需要设置为独立的语句

对于下面的语句

1
2
3
void getNumber();
void setNumber(std::shared_ptr<Person>, int);
setNumber(std::shared_ptr<Person>(new Persion), getNumber());

编译器的执行顺序为下图,但是 new PersongetNumber 的执行顺序无法确认。

graph LR
	执行newPerson --> 调用shared_ptr构造函数 --> 执行setNumber
	调用getNumber --> 执行setNumber

new Person 执行后再执行 getNumber() 此时一旦 getNumber() 中出现异常,new 出的对象将会造成内存泄露

设计与声明

让接口容易正确使用

  • 构造新类型避免接口被误用
  • 对输入进行限制
  • 让自定义类型的行为与内置类型的行为一致
  • 消除用户管理资源的责任

用传常量引用代替传值

  • 对于自定义对象,使用传常量引用代替传值
  • 对于内置类型和STL迭代器和函数对象,使用传值的方式就更加高效一些

Effective C++ 笔记
https://fu-qingchen.github.io/2021/08/26/HUST/CPPConcurrency/
作者
FU Qingchen
发布于
2021年8月26日
许可协议