《Effective C++》学习笔记

目录

开篇总结

其实《Effective C++》对于现在的我来说还是有点晦涩,有一些条款看起来又干又绕,所以这里只总结一下自己能理解、和对自己有用的部分条款

条款4:确定对象被使用前已先被初始化

  1. 类需要构造函数才可以初始化成员变量。

  2. 构造函数用初始化列表。如果在构造函数体内进行的话,是赋值操作。(但是vs关掉优化后反汇编都一样)
  3. const和references的成员变量,必须要用初始化列表

条款5:了解C++默默编写并调用哪些函数

  1. 编译器会为空类声明publick和inline的default构造函数、copy构造函数、copy assignment操作符和析构函数(non-virtual),取址运算符,const类型的取值运算符,移动构造,移动赋值构造,直到当需要被调用时才会创建出来
  2. 当声明了一个构造函数后,编译器就不会再创建default构造函数
    • 原因:避免需要一个必须有实参的构造函数,但是编译器会给你调用无参的default构造函数

条款6:若不想使用编译器自动生成的函数,就该明确拒绝

  1. 做法:在private中声明函数,无需定义;C++11提供delete关键字

条款7:为多态基类声明virtual析构函数

  1. 当需要抽象类,但又没有纯虚函数时,可以将析构函数声明为纯虚函数,但特殊的是,这里的纯虚析构函数必须定义(类内类外都可以,只是这和纯虚函数的概念有点矛盾)

条款8:别让异常逃离析构函数

  1. C++不禁止,但不鼓励析构函数吐出异常。析构函数要捕捉异常,然后吞下(不传播)或者结束程序
    • 原因:①C++不能同时处理两个异常,通常发生异常,系统会调用析构函数,如果析构函数又抛出异常;②内存泄漏,异常点后的程序不执行,假如释放资源放的操作在异常之后,就会发生内存泄漏;③vsstudio默认就不允许析构函数传播异常

    • 吞下(不传播):程序继续执行

    • 结束程序:调用abort(),程序退出

  2. 如果一定要执行某个会抛出异常的操作函数,那就让用户处理异常,提供一个会吐出异常的操作函数供用户使用。用户没有调用该操作函数,就由析构函数来调用,但是抛出异常时,执行1

条款9:绝不在构造和析构过程中调用virtual函数

  1. 可以调用,但是不会实现多态的功能
  2. 在构造函数中调用virtual函数,构造Derived对象之前会构造Base对象,Base构造函数调用virtual函数的版本是Base自己的,因为Derived对象还没完成构造
  3. 在析构函数中调用virtual函数,析构Derived对象之后会析构Base对象,Base构造函数调用virtual函数的版本是Base自己的,因为Derived对象部分已经被析构

条款10:令operator=返回一个reference to *this

  1. 拷贝赋值运算符的重载,返回类型是类名&,return是*this

条款11:在operator=中处理“自我赋值”

  1. 在管理资源的类中要注意自我赋值的行为

条款12 复制对象时勿忘其每一个成分

  1. copying函数确保复制包括base类的成员变量,可以在初始化列表里使用base类的拷贝构造函数
  2. 不要用某个copying函数调用另一个copying函数,而是把共同的功能放到另一个函数,由两个copying函数调用

条款13:以对象管理资源(RAII资源取得时机便是初始化时机)

  1. 把资源放进对象内,利用“析构函数自动调用机制”确保资源被释放
  2. 在构造函数中获得资源并在析构函数中释放资源
  3. 例子:[shared_ptr和unique_ptr的简单实现](https://www.cnblogs.com/wasi-991017/p/14284830.html)

条款15:在资源管理类中提供对原始资源的访问

  1. 智能指针会提供get()获取内部原始指针

条款16:成对使用new和delete时要采取相同形式

  1. new和delete底层

条款17:以独立语句将newed对象置入智能指针

  1. shared_ptr作为函数的第一个参数,第二个参数是可能抛出异常的形参。
  • 如果shared_ptr以new的形式作为实参,即shared_ptr(new x)
  • 执行顺序可能是new-->第二个形参(函数形式传进)-->shared_ptr构造
  • 如果第二个形参抛出异常,那么new出来的就发生内存泄漏了
  1. 类似于make_shared的例子

条款20:宁以pass-by-reference-to-const替换pass-by-value

  1. 先考虑前者,若确实不能使用才pass-by-value。前者通常比较高效,也可以避免切割问题(因为引用底层是指针)
  2. 内置类型、STL的迭代器和函数对象往往使用pass-by-value

条款21:必须返回对象时,别妄想返回其reference

  1. 绝不要返回pointer或reference指向一个localStack对象,或返回reference指向一个heap对象,或返回pointer或reference指向一个localStatic对象而有可能同时需要多个这样的对象

条款22:将成员变量声明为private

  1. 切记将成员变量声明为private,通过函数来读写,实现封装
    • 有时需要更改成员变量的属性,但是使用的人并不知道内部已经改变,他只是单纯的使用函数获取值
  2. protected并不比public更具封装性,因为修改一个protected,所有派生类都要进行修改

条款23:宁以non-member、non-friend替换member函数

  1. 面向对象守则是要求数据尽可能被封装,数据是指private成员变量
  2. 封装性体现在有很少人可以看到数据,而friend和member函数都会增加访问class数据的函数数量,使封装性变差

条款24:若所有参数皆需类型转换,请为此采用non-member函数

  1. 构造函数不使用explict关键字,因为explict关键字表明不允许隐式转换
  2. 不需要成为friend函数,因为会导致类封装性变差(条款23)。前提是遵循条款22第1点

条款26:尽可能延后变量定义式的出现时间

  1. 延后到需要它的时候再定义
  2. 循环中变量什么时候定义,除去功能需求外就是取决于成本
    • 循环内:n个构造函数+n个析构函数
    • 循环外:1个构造函数+1个析构函数+n个赋值操作

条款27:尽量少做转型动作(部分)

  1. 新式转型的优点
    • 查找容易,方便debug
    • 有各自的目标操作,编译器更容易找到错误
  2. 使用了类型转换,编译器并不是什么都没做,会编译出不一样的目标代码(比如int和double的底层就不一样)

条款30:透彻了解inlining的里里外外

  1. inline函数的功能是以函数本体替换函数调用
  2. 优点:可以消除函数调用的额外开销
  3. 缺点:①可能会导致目标码大小增加,从而导致额外的换页行为,降低效率;②无法随程序库升级而升级,用到inline函数的程序必须重新编译,如果是non-inline函数,程序只需要重新连接。如果是动态链接,甚至可以什么事情都不用做
  4. inline只是对编译器的申请,而不是强制命令
    • 显式提出:函数加inline关键字
    • 隐喻提出:函数定义于class定义式内
    • 拒绝inline:①函数太复杂,有循环或递归;②virtual函数,virtual意味着运行期确定调用,而inline意味着编译器确定替换(不过inline和virtual是可以一起声明的);③被通过函数指针进行调用,因为指针要指向存在的函数,inline相当于不存在

条款32:确定你的public继承塑模出is-a关系

  1. “public继承”意味is-a。适用于baseClass身上的每一件事情一定也适用于derivedClass身上,因为每个derivedClass对象同时也是baseClass对象

条款33:避免遮掩继承而来的名称

  1. derivedClass内的名称会遮掩baseClass内的名称
  2. 解决遮掩问题:使用using声明式或转发函数

条款34:区分接口继承和实现继承

  1. pureVirtual函数只具体指定接口继承
  2. virtual函数具体指定接口继承和缺省实现继承(缺省实现意思就是Derived可以不重写,会使用Base的实现)
  3. non-virtual函数具体指定接口继承和强制性实现继承
  4. pureVirtual函数也可以定义作为缺省实现,不过派生类还是必须去重写,可以通过作用域::访问

条款36:绝不重新定义继承而来的non-virtual函数

条款37:绝不重新定义继承而来的缺省参数值

  1. virtual函数是动态绑定的,而缺省参数值是静态绑定的
    • 原因:如果缺省参数值是动态绑定,就会降低执行速度。因为要实现动态绑定,都需要某些机制来完成,就像virtual函数要有虚表
  2. 静态绑定的问题:调用一个定义于derivedClass内的virtual函数,使用baseClass为它所指定的缺省函数值缺调用derivedClass的函数定义
  3. 解决方法:baseClass使用带缺省参数的non-virtual函数,调用privateVirtual函数。因为条款36,所以多态方式调用non-virtual函数时,再转发virtual函数,都可以拥有缺省参数值

条款38:通过复合塑模出has-a或“根据某物实现出”

  1. 复合(组合):某种对象内含另一种对象。和条款31对比
  2. 例子:比如set底层用到list,如果用is-a,list可以重复,但set不可以,那set就不是一个list;所以要用has-a,根据list的部分机能实现出set

条款39:明智而审慎地使用private继承

  1. private继承不意味is-a关系,意味着“根据某物实现出”,和复合一样(条款38);继承实现部分、忽略接口部分(条款34)
  2. 如何选择复合和private继承:
    • 尽可能使用复合
    • 当protected成员或virtual函数牵扯进来时才使用private继承
    • 空间问题:C++规定,凡是独立对象(包括空类)都必须有非零大小。

条款40:明智而审慎地使用多重继承

  1. 歧义1:两个基类都有同名(同名就不行,就算是参数列表不一样)的两个函数,即便一个是public另一个是private也会有这个问题
  2. 歧义2:菱形(钻石)继承问题,函数或成员变量都会有问题
    • 解决方法:虚继承,注意是连接基类的两个类进行虚继承,最终派生类虚继承没有用
    • 虚继承的问题:对象体积大(多了虚基类表指针),访问速度慢(需要访问虚基类表)
    • 关于虚继承:①尽量避免使用;②要使用虚继承时避免在虚基类放置数据
    • 虚继承底层
      • 直接继承类会在原基础上(即还是有虚基类的成员变量)增加一个虚基类表指针(指向虚基类表)
      • 间接继承类只有一份虚基类的拷贝
      • 虚基类表记录的是虚基类相对直接继承类的偏移
      • 下图例子,因为虚基类表指针是属于两个直接继承类B、C的,间接继承类D也有虚基类表指针但是都是B、C的,所以虚基类表记录的是虚基类在间接继承类D中关于直接继承类B、C的偏移

条款41-48:模板

条款49:了解new-handler的行为(部分)

  1. 当operator new无法满足某一内存分配需求时,它会抛出异常;在抛出异常之前,会先调用一个客户指定的错误处理函数
    • 指定方法:set_new_handler(funcPtr);

  2. new-handler函数可以做的处理:
    • 让更多内存可被使用。调用了new-handler函数说明内存不足,可以释放部分内存,让下一次new可以成功
    • 安装另一个new-handler。自己本身无法让程序取得更多内存,但有别的new-handler函数可以,调用set_new_handler(funcPtr);更换
    • 卸载new-handler。调用set_new_handler(funcPtr);将nullptr指针传入,下一次new失败就会抛出异常
    • 抛出异常。这样异常不会被operator new捕捉,而是传播到内存索求处
    • 不返回。调用abort()或exit()
  3. C++不支持class专属的new-handlers,不像operator new和delete

条款50:了解new和delete的合理替换时机

  • 1-3为常见的理由
  1. 检测运用上的错误
    • 分配额外空间放置特定标记,delete时查看是否越界(标记是否完整)
    • new时添加记录,delete时删除记录,避免内存泄漏(没delete)或不确定性行为(delete多次)。可以用map或链表实现
  2. 强化效能
    • 缺省的new主要用于一般目的,采取中庸之道,即能满足大多情况,但都是适当的好,没有最好的表现
    • 面对一些特殊情况,定制的new会比缺省的new有更好的表现
  3. 收集使用上的统计数据
#include<iostream>
#include<map>
using namespace std;
class Test {
public:
	static void print() {
		for (auto a : rec) {
			cout << "memLeak: line" << a.second << endl;
		}
	}
	//正常版本
	static void* operator new(size_t size) throw(bad_alloc) {
		return ::operator new (size);
	}
	//正常版本
	static void operator delete(void* ptr) throw() {
		if (ptr == nullptr) return;
		rec.erase(static_cast<int*>(ptr));
		::operator delete(ptr);
	}
	//placement
	static void* operator new(size_t size, int line) throw(bad_alloc) {
		void* ptr = ::operator new (size);
		rec[static_cast<int*>(ptr)] = line;
		return ptr;
	}
	//placement
	static void operator delete(void* ptr, int line) throw() {
		::operator delete(ptr);
	}
	//nothrow
	static void* operator new(size_t size, nothrow_t& nt) throw() {
		return ::operator new(size, nt);
	}
	static void operator delete(void* ptr, nothrow_t& nt) throw() {
		::operator delete(ptr);
	}
private:
	static map<int*, int> rec;
};
map<int*, int> Test::rec = map<int*, int>();

int main()
{
	Test* i = new(__LINE__) Test;
	Test* ii = new(__LINE__) Test;
	delete i;
	Test::print();

	return 0;
}


  1. 增加分配和归还的速度
    • 比如你的程序是单线程程序,而缺省的内存管理器是线程安全的,就可以定制一个不具线程安全的分配器改善速度
  2. 降低缺省内存管理器带来的额外开销
    • 泛用型内存管理器往往不止比定制型慢,而且还是用更多内存,因为它们常常在每个区块添加记录
  3. 将相关对象成簇集中
    • 类似linux里得slab机制,成簇集中在尽可能少的内存页

条款51:编写new和delete时需墨守成规

  1. operator new应该内含一个无穷循环,并在其中尝试分配内存,如果无法满足内存需求,就调用new-handler。它也应该有能力处理0bytes申请
  2. operator delete应该在收到nullptr时不做任何事情
  3. 结合条款50的代码

条款52:写了placement new也要写placement delete

  1. 构造函数抛出异常(对应条款8)。new的过程是调用operator new再到构造函数,运行时出现异常系统会自动调用operator new对应的operator delete(和第3点对应)。如果没有对应的,就会发生内存泄漏
  2. placement new是接收参数除了size_t类型外还有额外参数的operator new;placement delete是接收参数除了一个指针外还有额外参数的operator delete
    • 对应的operator new和operator delete就是额外参数个数、类型都相同(和第1点对应)
  3. placement operator new要有对应的placement operator delete,用于构造期间调用placement operator new抛出异常时使用。
  4. 声明了plactment new和placement delete,请确定不要无意识地遮掩了它们的正常版本
    • 声明了placement operator new和delete会遮掩正常版本的operator new和delete
    • 所以要加上正常版本的operator new和operator delete。使用placement new正常情况下(即构造函数没有抛出异常)是调用正常版本的delete
  5. 还有nothrow版本(书本在条款49),像malloc一样,失败了不抛异常,只是返回nullptr
    • 用处不大,因为构造函数会抛异常,还是需要处理异常
posted @ 2021-01-29 14:49  肥斯大只仔  阅读(210)  评论(0编辑  收藏  举报