Effective C++ 条款1-25
构造函数
- 对用户自定义的对象而言,初始化由构造函数执行
- default构造函数是一个可被调用而不带任何实参者
- explicit用来阻止构造函数进行隐式类型转换,但是仍可以用来执行显式的类型转换
- 拷贝构造函数可以用来以同类型对象初始化自我对象,copy assignment操作符被用来从同一个类型对象中拷贝其值到自我对象
- =也可以用来调用拷贝构造函数
1. 习惯C++
条款1:View C++ as a federation of languages
- C++是一个多重泛型编程语言:1)过程形式;2)面向对象;3)函数;4)泛型;5)元编程
- 有四个次语言组成的联邦库,C、Object-Oriented C++、Template C++/STL
条款2:尽量以const/enum/inline替代#define
- 用编译器替代预处理器
- 用常量替换#define:1)定义常量指针
const string authorName();
;
2)class专属常量:声明式static const int NumTurns=5;
定义式const int GamePlayer::NumTurns;
前面已经赋过初值了,所以不需要再赋初值 - #define不能用来创建class专属常量,以及所有封装性的
- 常量用const对象或者enums替换#defines
- 形似于函数的宏,最好改用inline函数替换#defines
条款3:尽可能使用const
- const指定语义约束,
const char* p; //p指向的数据是const
&char* const p; //常量指针
const Widget* pw; 和 Widget const pw; //含义相同
指针pw指向的数据是常量- 声明迭代器const和声明指针const一样,迭代器不能指向不同的对象,但是对象的值可以改动。
std::vector<int> vec;
const std::vector<int>::iterator iter=vec.begin();
- 声明迭代器指向的对象的值不可改变,使用const_iterator
std::vector<int>::const_iterator iter=vec.begin();
- const成员函数的作用:1)可以使class接口更容易被理解(哪个函数可以改动对象,哪个不能);2)可以用来操作const对象
- 两个函数只是常量性不同,可以被重载
- 程序中的const对象大多用于引用传递或指针传递
- bitwise constness不更改对象的任何成员变量,const成员函数不能更改对象内任何non_static成员变量,但是如果有指针指向对象,不能更改指针,但是可以更改指针的内容,这种情况会导出logical constness
- logical constness一个const成员函数可以更改对象内的某些bits,可以使用mutable释放掉non_static成员变量的bitwise constness
- 常量性转除:用non_const operator[]调用const兄弟,将*this转型为const类型用static_cast,并且将去除const operator[]返回值中的const用const_cast完成
条款4:确定对象在使用前已经被初始化
- 单用一次拷贝构造函数效率较高
- 如果成员是const或者是引用,必须被初始化,而不是赋值
- 编译单元是产生单一目标文件的源码,两个源码文件中的两个non_local static对象,其中一个的初始化可能用到另一个编译单元的未初始化对象
- 将每个non_local static对象搬到自己的函数中,该对象在这个函数中被定义为static,函数返回一个引用指向对象,本质上是non_local static对象被local static替换了
- C++保证在函数调用期间,首次遇上local static对象的定义式被初始化
- 但是也在多线程系统中带有不确定性,在单线程启动阶段手动调用所有reference-returning函数,消除与初始化有关的”竞速形势“
- 只要对对象有良好的初始化顺序就能有效防止reference-returning函数的初始化次序问题
- 手工初始化内置型non_member对象,使用成员初值列对付对象的所有成分,加强设计
2. 构造/析构/赋值运算
条款5:了解C++默认编写并调用的函数
- 编译器生成的默认构造函数和析构函数是public和inline的,只有这些函数被调用的时候才会被编译器创建出来
- 编译器产生的析构函数不是虚函数,除非这个类的基类的析构函数是虚函数
- 默认拷贝构造函数只是将来源对象的每一个非静态成员变量拷贝到目标对象
- 如果有条件不符合,则编译器会拒绝生成默认赋值运算符
条款6:可以拒绝编译器自动生成的函数
- 可以将拷贝构造函数和赋值运算符声明为private的
条款7:为多态基类声明virtual析构函数
- factory函数,返回指针指向派生类动态分配的对象,在完成之后delete
- 如果继承类对象需要由基类的析构函数完成删除,会出现为定义行为,对象的继承成分没有被销毁
- 给基类声明一个虚析构函数,可以消耗整个对象
- 虚函数的目的是允许继承类的实现可以客制化
- 只有当类中至少含有一个virtual函数时,才为他声明virtual析构函数
- 纯虚析构函数可能导致抽象类,不能被实体化,不能为这种类创建对象,定义纯虚析构函数
virtual ~AWOV()=0;
,必须为纯虚析构函数提供定义,最深层的派生类的析构函数首先被调用,然后沿着派生链往上 - 带多态性质的基类应该声明一个虚析构函数,反之就不使用虚析构函数
条款8:别让异常逃离析构函数
- 多个异常产生导致程序过早结束,或产生不明确的行为,剩下的对象没有释放将会导致内存泄露
- 创建一个资源管理类,在析构函数中调用close函数,通过调用abort制作转运记录,记录close调用失败
- 程序在发生异常时继续执行,使用try/catch记录close调用失败
- 重新设计资源管理类接口,使客户可以对出现的问题作出反应,检查链接是否关闭,可以调用析构函数关闭防止遗失数据库链接
- 析构函数不应该突出异常,对于客户需要对某个操作函数运行期间抛出的异常作出反应,在class中编写普通函数执行该操作
条款9:不在构造和析构过程中调用虚函数
- 构造派生类对象首先调用基类的构造函数,在构造过程中派生类的成员变量尚未初始化,在基类构造期间,对象是基类对象,虚函数会被解析成基类
- 要解决这种问题,将基类的虚函数改为非虚函数,利用private辅助函数创建一个值传给基类的构造函数,这个函数是static不会意外指向尚未完成初始化的成员变量
条款10:令operator=返回一个reference to *this
- 实现赋值连锁形式
x=y=z=15;
,返回操作符左侧对象
条款11:在operator=中处理自我赋值
- 自我赋值安全,证同测试,如果是自我赋值就不做任何事
- 异常安全,可能会返回一个指针指向被删除的bitmap,无法安全读取也无法安全删除
- 在赋值pb之前删除pb,如果newBitmap抛出异常,pb保持原状
- 使用copy and swap技术,
Weight temp(rhs); swap(temp);
条款12:复制对象时不要忘记每一个成分
- 设计良好的面向对象系统会将对象的内部封装起来,只留两个函数负责对象拷贝,拷贝构造函数和拷贝赋值运算符
- 自定义的拷贝构造函数,编译器不会提醒出错,拷贝构造函数没有复制基类的成员变量,没有指定实参传递给基类构造函数,调用基类的默认构造函数。
- 派生类无法访问基类构造函数的private成分,必须用派生类的拷贝函数调用相应的基类函数
- 确保复制所有的local变量,调用所有的基类适当的拷贝函数
- 不要使用拷贝运算符函数调用拷贝构造函数,也不要用拷贝构造函数调用拷贝运算符函数,可以建立一个新的成员函数给两个函数调用,这种函数是private,可以消除两个函数之间的代码重复
3. 资源管理
条款13:以对象管理资源
- 过早的return语句,函数因为continue、goto语句过早退出,或者函数抛出异常将会造成投资对象保存的资源内存泄露
- 将资源放进对象,析构函数会自动释放资源
- 资源被存放在堆中,当控制流离开那个区块或函数时被释放
- 智能指针的析构函数自动对其所指对象调用delete,避免潜在的资源泄露的可能性
auto_ptr<Investment> pInv(creatInvestment());
creatInvestment()返回的资源被当做智能指针的初值,赋值而不是初始化。控制流离开区块,对象被销毁,析构函数自动被调用- 不能让多个auto_ptr同时指向同一个对象,否则对象会被删除一次以上,产生未定义行为
- 如果使用拷贝构造函数或者拷贝赋值运算符赋值auto_ptr会变成null
- 引用计数型智慧指针RCSP,持续追踪共有多少个对象指向某个资源,在无人指向它时,释放该资源,但是RSCP无法打破环状引用
- tr1::shared_ptr,上述的两种智能指针在析构函数内做delete而不是delete[]
条款14:在资源管理类中小心copying行为
- 可以使用禁止复制,将copying操作声明为private
- 使用引用计数,关于互斥器解除锁定,引用计数为0时,以unlock为删除器,进行互斥器解锁,lock类不声明析构函数,当因此计数为0时自动调用系统自动生成的析构函数
- 复制资源管理对象时,复制其所包覆的资源,进行的是深度拷贝
- 转移底部资源的拥有权,确保只有RAII对象指向一个未加工的资源,资源的拥有权会从被复制物转移到目标物
条款15:在资源管理类中提供对原始资源的访问
- shared_ptr和auto_ptr都提供get成员函数,用来执行显式转换,返回智能指针内部的原始指针的复件
- 指针取值操作符operator->和operator*,允许隐式转换至底部原始指针
- 显式转换比较安全,隐式转换对客户比较方便
条款16:承兑使用new和delete时采取相同形式
- 调用new,内存会被分配出来,调用构造函数
- 调用delete,首先调用析构函数,然后释放内存
- 最好不要对数组形式做typedef动作
条款17:以独立语句将newed对象置入智能指针
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
执行new Widget
要在tr1::shared_ptr
执行之前执行,因为这个结果要被传给tr1::shared_ptr
作为实参- 如果在上述两个操作中间执行
priority
,调用失败会造成new Widget
返回的指针遗失,在processWidget
的调用过程中引发资源泄露 - 使用分离语句先将
new Widget
单独传递给一个智能指针,然后将这个智能指针传递给processWidget
4. 设计与声明
条款18:让接口更容易被正确使用,不易被误用
- 限制值得合理范围,使用安全类型,以函数替换对象,以const修饰operator*的返回类型
- 促进正确使用的办法包括接口的一致性,以及内置类型的行为兼容
- 阻止误用的办法建立新类型,限制类型上的操作,束缚对象值,消除用户的资源管理责任
- 定制删除器,防止动态连接程序库问题,用来自动解除互斥锁
条款19:设计class犹如设计type
- 新type对象如何被创建和销毁
- 对象初始化和对象赋值有什么区别,这决定了构造函数和赋值运算符的差别,初始化和赋值对应于不同的函数调用
- 拷贝构造函数用来定义一个类型的值传递是如何实现的
- 对class的成员变量而言,通常只有某些数值集是有效的
- 派生类的类型收到基类的设计的束缚
- 是否需要类型转换操作符或者可被单一实参调用的构造函数
- 什么样的操作符和函数对新type是合理的,什么标准函数应该被驳回,谁该采用新type成员(决定成员的public/protected/private,决定类或者函数是友元,以及嵌套的合理性)条款23、24、46
- 未声明接口,条款29,对效率、异常安全性以及资源运用提供何种保证
- 在某些情况下定义一个或多个非成员函数或者模板就能得到想要的机能
条款20:用引用常量代替值传递
- 值传递以实际实参的副本为初值,调用端返回的是函数返回值的一个复件,这些副本由对象的拷贝构造函数产出,费时
- 用常量引用传递参数,
const Student& s
函数不会改变传入的对象的初值 - 值传递时,基类构造函数会分割对象的派生类部分和基类部分,可能会使派生类对象编程基类对象。使用引用常量传递参数,参数类型由传进来的参数决定,不会被改变
- 使用内置类型时值传递效率更高,但是并不意味着复制内置类型更快,诸如此类
- 用户自定义类型的大小容易发生变化,内置类型、STL的迭代器和函数对象使用值传递效率较高
条款21:必须返回对象时不要返回引用
- 返回对象的引用时这个对象必须已经存在
- 为了避免调用构造函数,函数返回的可能是local对象,在函数返回前就已经被销毁了
- 在堆内构造一个对象并返回reference指向它,调用了构造函数,又不能准确的delete引用背后隐藏的指针,造成内存泄露
- 定义在函数内部的static对象,造成多线程安全
Rational(lhs.n*rhs.n, lhs.d*rhs.d)
构造成本和析构成本没有省略,但是保证正确
条款22:将成员变量声明为private
- 使用函数访问成员变量可以用某个计算替换这个成员变量
- 将成员变量隐藏在函数接口后面为所有可能的实现提供弹性
- 封装成员变量,确保class的约束条件总是被维护,只有成员函数可以影响他们
- public意味着不封装,不封装意味着不可改变
- protected成员变量和public成员变量一样缺
- 少封装性,在这种情况下如果成员变量被改变将会造成大量代码被破坏
条款23:以非成员、非友元函数替换成员函数
- 面向对象的守则是尽可能的封装,非成员函数的封装性较高
- 非成员函数允许对类相关性能有较大的包裹弹性,最终导致较低的编译相依度,增加类的可延伸性
- 封装性越高,我们越能自由的改变对象数据,我们使用非成员、非友元函数替换成员函数,不会增加访问私有成员变量的函数数量,不会降低封装性,非成员非友元函数可以是另一个类的静态成员函数
- 让这个非成员函数放在class的同一个命名空间中
- 将不同的相关便利函数声明在不同的头文件中但隶属同一个命名空间,允许客户只对他们所用的那一部分形成编译相依,客户可以扩展这一组遍历函数,可以将非成员非友元函数添加到这个头文件里
- 可以增加封装性、包裹弹性、机能扩充性
条款24:如果所有参数都需要类型转换,为此采用非成员函数
- 例如建立一个数值类型的类,有一个重载乘法运算符作为成员函数,类类型和整型变量混合运算时,不能使用交换率,因为没有满足这样参数组合的成员函数或是在同一个作用域下的非成员函数
- 只有参数位于参数列时隐式类型转换才是合法的
- 将重载乘法运算符称为一个非成员函数:
const Rational operator*(const Rational& lhs, const Rational& rhs);
条款25:写一个不抛出异常的swap函数
1 | namespace std{ |
- 只要T支持拷贝操作,缺省的swap代码会置换类型为T的对象
- swap缺省行为导致变慢
template<> void swap<Widget>(Widget& a, Widget& b);
template<>全特化的std::swap版本施行于Widget身上- 通常我们不允许改变std命名空间内的任何东西,但是可以为标准模板制造特化版本,使它专属于我们的类
- 令类声明一个swap的public成员函数,然后将std::swap特化调用成员函数
- C++只允许对类模板偏特化
- 客户可以全特化std内的模板,但是不能添加新的模板到std里
- 我们可以定义一个非成员swap函数来调用swap成员函数
- 如果T是Widget并且位于命名空间WidgetStuff,编译器采用使用查找原则找到专属的swap,如果类的std::swap已经被特化,特化版会被挑中,如果没有则调用std::swap
- public swap成员函数不能抛出任何异常