《Effective C++》 笔记

文章目录

  • 0、导读
    • 命名习惯
  • 1、让自己习惯C++
    • 条款01
    • 条款02:尽量以const、enum、inline替换 #define
    • 条款03:尽可能使用const
      • 1、const与函数声明式关联
      • 2、在const和non-const成员函数中避免重复
    • 条款04:确定对象被使用前已先被初始化
      • 1、不同编译单元内定义的non-local static对象的初始化次序
      • 2、函数内含staic对象时,在多线程系统中带有不确定性。
  • 2、构造/析构/赋值运算
    • 条款05:了解C++默默编写并调用哪些函数
    • 条款06:若不想使用编译器自动生成的函数,就该明确拒绝
    • 条款07:为多态基类声明虚析构函数
    • 条款08:别让异常逃离析构函数
    • 条款09:尽量不在构造和析构过程中调用虚函数
    • 条款10:operator= 的时候返回*this
    • 条款11:在operator=中实现“自我赋值”
    • 条款12:复制对象时勿忘其每一个成分
  • 3、资源管理
    • 条款13:以对象管理资源
    • 条款14:在资源管理类中小心copying行为
    • 条款15:在资源管理类中提供对原始资源的访问
    • 条款16:成对使用new和delete时要采取相同形式
    • 条款17:以独立语句将newed对象置入智能指针
  • 4、设计与声明
    • 条款18:让接口容易被正确使用,不容易被误用
    • 条款19:设计class犹如设计type
    • 条款20:宁以传递const引用替换传递值
    • 条款21:必须返回对象时,别妄想返回其引用
    • 条款22:将成员变量声明为private
    • 条款23:宁以non-member、non-friend替换member函数
      • 1、问题引出
      • 2、为什么写成non-member、non-friend 好?
      • 3、关于上述两个原因的进一步解释一:封装性
        • (1)为什么强调封装性?
        • (2)如何衡量封装性?
      • 4、关于上述两个原因的进一步解释二:编译相依度、包裹弹性
        • (1)原因一:可以降低文件间的编译相依度。
        • (2)原因二:可以轻松的扩展像这样的便利函数。(体现包裹弹性)
    • 条款24:若所有参数皆需类型转换,请为此采用非成员函数
      • 1、问题引出
      • 2、写成member函数引发的问题
        • 第一句为什么能够通过编译呢?
        • 为什么第二句的参数2不能发生隐式转换呢?
      • 3、是否应该写成friend函数?
      • 4、注意事项
    • 条款25:考虑写出一个不抛异常的swap函数
      • 1、内容引出
      • 2、“pimpl”手法
      • 3、widget 和widgetImpl 都是class时,如何写出高效的swap()
      • 4、如果widget 和widgetImpl 都是类模板,而非类的话,怎么实现?
      • 5、用户角度
      • 6、总结
      • 7、最后,还有一点关于成员版本的swap:绝对不可以抛出异常!
    • 条款26:尽可能延后变量定义式的出现时间
      • 1、尽可能延后变量定义直至能够给他初值实参
      • 2、该在循环内定义变量还是循环外?
    • 条款27:尽量少做转型操作
      • 1、关于C++的转型动作
      • 2、C和C++的三种形式的转型语法
      • 3、倡导使用新式转型
      • 4、转型动作的原理
      • 5、关于dynamic_cast
      • 6、最后的话
    • 条款28:避免返回handles 指向对象内部成分
      • 1、什么是handles?
      • 2、“返回一个handle代表对象内部成分”总是危险的原因:
    • 条款29 :为“异常安全”而努力是值得的
      • 1、异常安全条件
      • 2、异常安全函数的保证
      • 3、copy-and-swap策略
    • 条款30:透彻了解inlining的里里外外
      • 1、inline的优缺点
      • 2、隐式内联、显式内联
      • 3、编译器拒绝内联的情况
      • 4、总结
    • 条款31:将文件间的编译依存关系降至最低
      • 1、文件间依存度高的话带来的影响?
      • 2、出现上述问题的原因
      • 3、解决文件间依存关系的正确手法一:handle class
        • (1)基本思想
        • (2)好处
        • (3)关键
      • 4、解决文件间依存关系的正确手法二:interface class手法
        • (1)基本思想
        • (2)C++ 接口类与其他语言接口类的不同
        • (3)interface class 类创建对象的方式
      • 5、两种手法带来的代价
      • 6、两种手法的适用场景
  • 6、继承与面向对象设计
    • 条款32:确定你的public继承塑模出is-a关系
    • 条款33:避免遮掩/隐藏继承而来的名称
      • 1、C++的名称遮掩规则
      • 2、派生类的成员函数内查找名称的顺序
      • 3、隐藏基类的全部函数
      • 4、解决办法
        • 1)使用using声明式
        • 2)使用转交函数
        • 3)显示使用基类成员函数
    • 条款34:区分接口继承和实现继承
      • 1、`public`继承细分
      • 2、三种继承对应的成员函数的写法
        • 1)只继承接口
        • 2)同时继承接口和实现,且继承而来的实现能够被覆盖
        • 3)同时继承接口和实现,且继承而来的实现不能够被覆盖
        • 比较virtual和non-virtual
    • 条款35:考虑virtual函数以外的其他选择
      • 1、考虑virtual函数以外的其他选择是什么意思?
      • 2、第一种方案:通过Non-Virtual Interface (NVI)手法实现Template Method 模式
        • (1)Non-Virtual Interface 是Template Method 设计模式的一个独特的表现形式。
        • (2)这里把non-virtual的函数 healthValue()称为doHealthValue()函数的外覆器。
        • (3)关于对派生类对象中重新定义 private virtual函数的担心
        • (4)virtual函数的访问控制
      • 3、第二种方案:通过Function Pointers完成Strategy模式
        • (1)具体做法
        • (2)优点
      • 4、第三种方案:通过tr1::function完成Strategy模式(使用函数对象,而不是使用函数指针)
        • (1)实现方法:
        • (2)优点:
      • 5、第四种方案:传统的Strategy模式
        • (1)传统的Strategy的做法是:
        • (2)优点:
      • 6、二、三、四方案存在的问题
        • (1)实现方案存在的问题
        • (2)解决上述问题的方法:
        • (3)三种方案的缺点
    • 条款36:绝不重新定义继承而来的non-virtual函数
    • 条款37:绝不重新定义继承而来的缺省参数值
      • 1、静态/动态类型
      • 2、静态/动态绑定
      • 3、为什么不能重新定义继承而来的缺省参数值?
      • 4、在遵守该规则的前提下,如何做到同时给base class 和derived class 提供缺省参数?
        • 方案一:一般手法,基类和所有派生类提供一模一样的缺省参数。
        • 方案二:NVI手法替代方案
    • 条款38:通过复合塑模出has-a 关系或 is-implemented-in-terms-of(根据某物实现出)关系
      • 1、什么是复合?
      • 2、复合描述的关系的细分
        • (1)复合意味着两种关系
        • (2)细分依据
        • (3)应用域和实现域
    • 条款 39:明智而审慎地使用private继承
      • 1、private继承的两条规则
      • 2、private 继承所描述的关系
      • 3、复合与private继承的选择
        • 疑问:
        • (1)首先大部分情况下应该尽可能的使用复合。
        • (2)在下述几种情况使用private继承:
      • 4、复合与private继承的优缺点比较
    • 条款40:明智而审慎地使用多重继承
      • 1、多重继承可能引发的问题:接口调用歧义
        • (1)举例
        • (2)为什么引发歧义?
        • (3)解决名称歧义的方法
      • 2、菱形继承
        • 虚继承的代价
        • 对于`virtual base classes`的建议
      • 3、多重继承的应用
      • 4、 有关多重继承的建议
  • 7、模板与泛型编程
    • 条款41:了解隐式接口和编译期多态
      • 1、显式接口和运行期多态
        • (1)简介
        • (2)所谓的显式接口
        • (3)所谓的运行期多态
      • 2、 隐式接口和编译期多态
        • (1)泛型编程与面向对象编程的不同之处
        • (2)所谓的隐式接口
        • (3)所谓的编译期多态
      • 3、隐式接口和显式接口的不同之处
    • 条款42:了解typename的双重意义
      • 1、意义一
      • 2、意义二
        • 解析规则
    • 条款43:学习处理模板化基类内的名称
      • 1、引出问题
      • 2、原因
      • 3、解决办法
    • 条款44:将与参数无关的代码抽离template
      • 1、代码重复与对应策略
      • 2、模板中代码重复
        • 1)非类型参数引起的代码重复
        • 2)类型参数引起的代码重复
    • 条款45:运用成员函数模板接受所有兼容类型
      • 1、问题引出
      • 2、成员函数模板
        • 解决方法
    • 条款46:需要类型转换时请为模板定义非成员函数
      • 1、函数模板中参数隐式转换失效
      • 2、原因
      • 3、解决办法
      • 4、friend关键字在模板中的重要作用
    • 条款47:请使用traits classes表现类型信息
      • 1、STL迭代器回顾
        • Tag 结构体
      • 2、Traits
        • 1)用户自定义类的迭代器
        • 2)基本数据类型的指针
      • 3、advance 的实现
      • 4、使用说明
    • 条款48:认识template元编程
      • 1、模板元编程
      • 2、TMP的用途
  • 8、定制new和delete
    • 条款49:了解new handler的行为
      • 1、类型相关new-handler
        • 1)重载operator new
        • 2)使用
        • 3)通用基类
      • 2、nothrow new
    • 条款50:了解new和delete的合理替换时机
      • 1、替换原因
      • 2、替换new和delete的时机
    • 条款51:编写new和delete时需固守常规
      • 1、operator new
        • 成员operator new函数
      • 2、operator delete
        • 成员operator delete函数
    • 条款52:写了placement new也要写placment delete
      • 1、placement new和placement delete
      • 2、成对的new/delete
      • 3、名称隐藏
          • 解决办法
  • 9、杂项讨论
    • 条款53:不要轻忽编译器的警告
    • 条款54:让自己熟悉包括TR1在内的标准程序库

0、导读

被声明为explicit的构造函数通常比其non-explici兄弟更受欢迎,因为它们禁止编译器执行非预期(往往也不被期望)的类型转换。**除非我有一个好理由允许构造函数被用于隐式类型转换,否则我会把它声明为explicit。**我鼓励你遵循相同的政策。

class Widget {
public:
    Widget();                              // default构造函数
    Widget(const Widget& rhs);             // copy构造函数
    Widget& operator=(const Widget& rhs);  // copy assignment操作符
};
Widget w1;      //调用default构造函数
Widget w2(w1);  //调用copy构造函数
w1 = w2;        //调用copy assignment操作符
Widget w3 = w2; //调用copy构造函数

copy构造和copy赋值的区别:
如果一个新对象被定义(例如以上语句中的w3),一定会有个构造函数被调用,不可能调用赋值操作。如果没有新对象被定义(例如前述的"w1=w2"语句),就不会有构造函数被调用,那么当然就是赋值操作被调用。

函数Pass-by-value 意味“调用copy构造函数”。以by value传递用户自定义类型通常是个坏主意,Pass-by-reference-to-const往往是比较好的选择,详见条款20。

命名习惯

Ihs和rhs。它们分别代表"left-hand side"(左手端)和"right-hand side"(右手端)。可以作为二元操作符(binary operators)函数如operator==和operator*的参数名称。

对于成员函数,左侧实参由this指针表现出来,所以单独使用参数名称rhs。

将“指向一个T型对象”的指针命名为pt,意思是"pointer to T"。

Airplane* pa;		//pa ="ptr to Airplane".
GameCharacter* pgc;	//pgc="ptr to GameCharacter"

对于references:rw可能是个reference to widget,ra则是个reference to Airplane。

1、让自己习惯C++

条款01

C++可视为:

  • C:以C为基础。
  • 面向对象的C++:添加面向对象特性。
  • 模板C++:泛型编程概念,使用模板。
  • STL:使用STL的容器、迭代器、算法、及函数对象。

四者的集合。
c++高效编程守则视状况而变化,取决于你使用c++的哪一部分。

比如:
传值还是传引用?

  • 对内置(也就是C-like)类型而言pass-by-value通常比pass-by-reference高效;
  • 但当你从C part of C++移往Object-Oriented C++,由于用户自定义(user-defined)构造函数和析构函数的存在,pass-by-reference-to-const往往更好;
  • 运用Template C++时尤其如此,因为彼时你甚至不知道所处理的对象的类型;
  • 然而一旦跨入STL你就会了解,迭代器和函数对象都是在C指针之上塑造出来的,所以对STL的迭代器和函数对象而言,旧式的C pass-by-value守则再次适用(参数传递方式的选择细节请见条款20)。

条款02:尽量以const、enum、inline替换 #define

enum hack:

void fun(){
    enum { x =5 };//不限定作用域枚举
    int arr[x];
}

一个属于枚举类型的数值可权充ints被使用,类似于局部的#define而不像const,该数值无法取地址。
如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。Enums和#defines一样绝不会导致非必要的内存分配。

建议:
对于单纯常量,尽量以const对象或enums枚举来代替#define。
对于形式函数的宏,用inline函数代替#define。

条款03:尽可能使用const

char greet[] = "hello";
const char* p = greet; //const data,non-const pointer;
char* const p = greet; //non-const data,const pointer;
const char* const p = greet; //const data,const pointer;

如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量; 如果出现在星号两边,表示被指物和指针两者都是常量。

如果被指物是常量,有些程序员会将关键字const写在类型之前,有些人会把它写在类型之后、星号之前。两种写法的意义相同,所以下列两个函数接受的参数类型是一样的:

void f1(const Widget* pw);
void f2(Widget const* pw);

STL送代器系以指针为根据塑模出来,所以迭代器的作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改动的。如果你希望迭代器所指的东西不可被改动(即希望STL模拟一个const T*指针),你需要的是const-iterator

std::vector<int> vec;
 
//iter2类型为int* const
const std::vector<int>::iterator iter2 = vec.begin();//iter类似于T* const     
*iter2 = 1; //正确
iter2++;    //错误!iter是const
 
//iter3类型为const int*
std::vector<int>::const_iterator iter3 = vec.cbegin();//iter类似于const T* 
*iter3 = 1; //错误!*iter3是const
iter3++;    //正确
int x=2;
const int *p = &x;
*p=2;       //报错:表达式必须是可修改的左值
x=3;
cout<<*p;   //输出为3

const只是限制 *p 不可变,但x依然可以改变。

1、const与函数声明式关联

const最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联:

  • const返回值

    令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。举个例子,考虑有理数(rational numbers,详见条款24)的operator*声明式:

    class Rational{....}; 
    const Rational operator* (const Rational& lhs, const Rational& rhs);
    

    返回const 可以避免诸如if(a * b = c)之类的错误,编译直接报错。

  • const参数
    至于const参数,没有什么特别新颖的观念,它们不过就像local const对象一样,你应该在必要使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们声明为const。只不过多打6个字符,却可以省下恼人的错误,像是“想要键入’==’,却意外键成’=’ ”的错误,一如稍早所述。

  • const 成员函数
    将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。这一类成员函数之所以重要,基于两个理由。第一,它们使class接口比较容易被理解。这是因为,得知哪个函数可以改动对象内容而哪个函数不行,很是重要。第二,它们使“操作const对象”成为可能。这对编写高效代码是个关键,因为如条款20所言,改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。

编译器对const是“bitwise”(逐位)的不变检查,但编程时应该以“逻辑级”的不变思路来做,一个const成员函数可以修改它所处理的对象内的某些bits,通过使用mutable修饰这些变量,mutable释放掉non-static成员变量的bitwise constness约束。

2、在const和non-const成员函数中避免重复

由于函数有重载特性,当const和non-const成员函数有实质等价的实现时,用non-const版本调用const版本来避免代码重复,但是要做好类型转换,否则会出现自己调用自己的现象。也不能反向调用,这不符合逻辑。

class CTextBlock{
public:
	const char& operator[](std::size_t position)const
	{
        ...
        ...
        ...
		return pText[position];
	}
	char& operator[](std::size_t position)
	{
		//static_cast添加了const以调用const版本的[],第二次则从返回值中移出const
		return const_cast<char&>(static_cast<const CTextBlock&>(*this)[position]);
	}
	string pText;
	int length;
};

建议:

  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。尽可能使用const。
  • 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

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

1、不同编译单元内定义的non-local static对象的初始化次序

所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。

所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。

问题是:如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。

解决办法:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。

这个手法的基础在于:**C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。**所以如果你以“函数调用”(返回一个reference指向local static对象)替换“直接访问non-local static对象”,你就获得了保证,保证你所获得的那个reference将指向一个历经初始化的对象。更棒的是,如果你从未调用该改造后的函数,就绝不会引发构造和析构成本!

例子:https://blog.csdn.net/chgaowei/article/details/6001433?utm_source=jiancool

2、函数内含staic对象时,在多线程系统中带有不确定性。

任何一种non-const static对象,不论它是local或non-local,在多线程环境下“等待某事发生”可能会产生竞态条件,比如:
《Effective C++》 笔记_第1张图片
建议:

  • 为内置类型对象进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初值列表列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
  • 为免除跨编译单元之初始化次序问题,请以local static对象替换non-local static对象。

2、构造/析构/赋值运算

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

如果类没有声明定义构造函数、拷贝构造函数、拷贝赋值运算符,则会在必要时自动生成需要的函数,比如在有虚函数时自动生成合成的默认构造函数(因为有虚函数时需要构造函数生成虚表指针),需要用到拷贝构造函数时自动生成合成拷贝构造函数,需要用到拷贝赋值运算符时自动生成合成拷贝赋值运算符。

class Empty{}
Empty e1;//如今的编译器并不会生成合成的默认构造函数,因为没有必要。

如何证明?使用 nm -C a.out查看可执行文件a.out的符号表就可以看到这些函数是否存在。或者使用objdump反汇编。

编译器并不是一定会生成这些函数,如果生成的这些函数会违反C++规则的话,编译器会拒绝编译。

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

如果不想使用编译器自动生成的函数:

  1. 可以将这些函数声明为private且不定义。
  2. 为1专门设计一个类,让他被我们想要的类继承:
class Uncopyable{
{
protected:
	Uncopyable(){}
	~Uncopyable(){};
private:
	Uncopyable(const Uncopyable&);
	Uncopyable& operator=(const Uncopyable&);
}
class HomeForSale : public Uncopyable{
	...
};

也可以使用boost提供的版本:

#include

class HomeForSale : public boost::noncopyable{
	...
};
  1. 使用=delete
class HomeForSale{
	...
	HomeForSale(const HomeForSale &) = delete;
    HomeForSale &operator=(const HomeForSale &) = delete;
};

条款07:为多态基类声明虚析构函数

C++指出,当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁。

解决办法:给base class一个virtual析构函数。此后删除derived class对象就会如你想要的那般。是的,它会销毁整个对象,包括所有derived class成分。(因为此时的析构函数被放在虚函数表中,调用的实际上是派生类的析构函数)

任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。有虚函数的类的对象会自动增加一个vptr(virtual table pointer,虚函数表指针) ,增加至少 32bits或 64bits。

含有纯虚函数的抽象类不能被实例化,但即便虚析构函数是纯虚函数,他还是需要被定义,因为派生类析构时会按相反顺序调用其继承的基类的析构函数,这些析构函数必须被定义。

class AWOV
{
public:
	virtual ~AWOV()=0;
};
AWOV::~AWOV(){}//这一步是必要的

请记住:

  • polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  • Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。

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

析构函数绝对不要吐出异常。
如果一个被析构函数调用的函数可能跑抛出异常,析构函数应该捕捉任何异常,然后

  1. 吞下他们,或者
  2. 结束程序。
class DBConn{     // 这个class用来管理DBConnection对象
public:
	~DBConn()     // 确保数据库连接总是会被关闭;
	{
		try{
			db.close();
		}
		catch(...){
			// 制作运转记录,记下对close的调用失败
			//选择1:结束程序,如下
			abort();	
			//选择2:吞下异常
		}
	}
private:
	DBConnection db;
};

更好的办法,是让用户自己选择:

  1. 主动调用close来对可能出现的错误做出响应;
  2. 让析构函数自己处理。
class DBConn{     // 这个class用来管理DBConnection对象
public:
	void close()//供客户使用的新函数
	{
		db.close();
		closed = true;
	}
	~DBConn()     // 确保数据库连接总是会被关闭;
	{
		if(!closed)  // 判断是否关闭
		{
			try{
				db.close();
			}
			catch(){
				// 制作运转记录,记下对close的调用失败;或吞下异常
				abort();	//close()抛出异常的处理,
			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

请记住:

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

条款09:尽量不在构造和析构过程中调用虚函数

基类中的构造函数调用的虚函数永远是基类的虚函数(因为此时派生类部分还没有生成),不能指望派生类使用基类构造函数时,该基类构造函数使用派生类的虚函数。
析构函数同理。

class A{
public:
	A(){
		//do sth;
		// ······
		// final
		print();     //调用A的方法,而不是B的。
	}
	virtual void print() const;
};

class B:public A{
public:
	virtual void print() const;
};

解决办法:

  1. 在派生类的构造函数中调用派生类的虚函数:
class A{
public:
	A(){
		//do sth;
		// ······
		// final
	}
	virtual void print() const;
};

class B:public A{
public:
	virtual void print() const;
    B(){
        print();
    }
};
  1. 令派生类将必要的构造信息向上传递至基类构造函数来,不再需要虚函数:
class A{
public:
	explict A(string& loginfo)
	{
		//do sth;
		······
		// final
		print(loginfo);     //调用A的方法,而不是B的。
	}
	void print(string& loginfo) const;
};

class B:public A{
public:
	B(param):A(createlogstring(param))
	{···}
private:
	string createlogstring(param);
};

如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或虚函数所属类型相对应的虚函数版本。——《C++primer第五版》P556


条款10:operator= 的时候返回*this

为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议。

class Widget
{
public: 
	Widget& operator=(const Widget& rhs)
	{
		···
		return *this;
	}
};

Widget x, y, z;
//连锁赋值
x=y=z;
//返回引用是为了如下正确。
(x=y)=z;

条款11:在operator=中实现“自我赋值”

由于变量有别名的存在(多个指针或引用指向同一个对象),所以可能出现自我赋值的情况。比如 a[i] = a[j],可能是同一个对象赋值。这时就需要慎重处理赋值操作符以免删除了自己后再用自己来赋值,比如:

class Widget{
public:
	Widget& operator=(const Widget& rhs){
		delete p;//如果p之前就已经释放掉了,再次释放会被报错
		p=new int(ths.p);
		return *this;
	}
	int *p;
};

解决方法:

  1. “证同测试”:
class Widget{
public:
	Widget& operator=(const Widget& rhs){
		if(this==&rhs)//证同测试
			return *this;
		delete p;
		p=new int(rhs.p);
		return *this;
	}
	int *p;
};

2.我们只需注意在复制p所指东西之前别删除p:

Widget& operator=(const Widget& rhs){
	Bitmap* pOrig = p;		 // 记住原先的pb
	p = new Bitmap(*rhs.p);  // 使用rhs bitmap的副本
	delete pOrig;                // 删除原先的pb
	return *this;
}

该版本同时具备之前版本不具备的“异常安全性”。

3.使用copy and swap技术:

class Widget{
public:
	void swap(const Widget& rhs);//交换rhs和this
	Widget& operator=(const Widget& rhs){
		Widget tmp(rhs);//赋值一份数据
		swap(tmp)//交换
		return *this;//临时变量会自动销毁
	}
	int *p;
};

请记住:

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

当你编写一个copying函数(拷贝构造函数、拷贝赋值运算符),请确保:

  1. 复制所有local成员变量,
  2. 调用所有base classes内的适当的copying函数。
class A {
public:
    int A_mem;
    A() {}
    A(const A& rhs) :A_mem(rhs.A_mem) {}
    A& operator=(const A& rhs) { A_mem = rhs.A_mem; }
};
class B : public A {
public:
    int B_mem;
    B() {}
    B(const B& rhs) :A(rhs), B_mem(rhs.B_mem) {}
    B& operator=(const B& rhs) {
        A::operator=(rhs);
        B_mem = rhs.B_mem;
        return *this;
    }
};

如果B的copy函数漏掉了基类A部分,则A部分会使用默认构造函数,而不是copy。

请记住:

  • Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用。



3、资源管理

条款13:以对象管理资源

我们可以将资源放进局部对象内,当对象离开作用域时,该对象的析构函数会自动释放资源。shared_ptr就是一种这样的对象。

实际上“以对象管理资源”的观念常被称为“资源取得时机便是初始化时机"(Resource Acquisition Is Initialization;RAll),因为我们几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值(而非初始化)某个管理对象,但不论哪一种做法,每一笔资源都在获得的同时立刻被放进管理对象中。

请记住:
为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

条款14:在资源管理类中小心copying行为

并不是所有资源都是开辟在堆上,有时候我们需要自己建立资源管理类:

class Lock{
public:
	explicit Lock(Mutex* mu):mutexPtr(mu)
	{
		lock(mutexPtr);
	}
	~Lock()
	{
		unlock(mutexPtr);
	}
private:
	Mutex* mutexPtr;
};

客户对Lock的如下使用方法符合RAII:

Mutex m;//定义互斥器
……
{//建立区块来定义critical section
	Lock(&m);
	……//执行critical section 内的操作
}//在区块末尾,自动解除互斥器的锁

但是,当一个RAII对象被复制,会发生什么?有以下做法:

  1. 禁止复制,将coping函数设置为私有或=delete,条款6。

  2. 对底层资源使用引用计数法。复制的时候引用计数加1,为0时销毁。

    通常只要内含一个shared_ptr成员变量,RAII classes便可实现出引用计数行为。如果前述的Lock打算使用reference counting,它可以改变mutexPtr的类型,将它从Mutex*改为shared_ptr< Mutex > 。
    但是shared_ptr默认释放动作为delete,我们需要制定专门的删除器,一个函数或函数对象。

    class Lock{
    public:
    	//以某个Mutex初始化shared_prt,unlock作为删除器
    	explicit Lock(Mutex* mu):mutexPtr(mu,unlock){
    		lock(mutexPtr);
    	}
    private:
    	shared_prt<Mutex> mutexPtr;
    };
    

    这个类中不需要自己编写析构函数,因为mutexPtr是类中的普通成员变量,编译器会自动生成析构函数类析构这样的变量。

  3. 复制底部资源
    如果这种资源可以任意复制,我们只需编写好适当的copying函数即可。确保复制时是深拷贝。

  4. 转移底层资源的拥有权
    有时候资源的拥有权只能给一个对象,这时候当资源复制时,就需要剥夺原RAII类对该资源的拥有权,比如使用unique_ptr的release()。

请记住:

  • 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAIl对象的copying行为。
  • 普遍而常见的RAIl class copying行为是:抑制copying、施行引用计数法。不过其他行为也都可能被实现。

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

  • APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。尽量用显示转换,不提供隐式转换。

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

对内置数组的new,数组所用的内存通常还包括“数组大小”的记录,以便delete知道需要调用多少次析构函数。很多编译器如下实现:
《Effective C++》 笔记_第2张图片
不要对内置数组做typedefs动作,很容易让人delete时忘记添加[]。

建议:
如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。

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

C++参数列表中表达式的计算顺序并没有在C/C++中明确定义,每个编译器可以自由发挥,有自己的实现。
比如:

int processWidget(shared_ptr<Widget>(new Widget), int priority());

如果按如下顺序:
1、执行new Widget
2、执行priority()函数
3、执行shared_ptr构造函数

并且在priority()时抛出异常,则会发生资源泄漏。
因此,最好使用make_shared()代替shared_ptr(new Widget)或者将如下将其独立出去:

shared_prt<Widget> pw(new Widget);
processWidget(pw,priority());

请记住:
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。



4、设计与声明

条款18:让接口容易被正确使用,不容易被误用

欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。假设你为一个用来表现日期的class设计构造函数:

class Date {
public:
    Date (int month, int day, int year);
    ...
};

客户很可能以错误的次序传递参数。许多客户端错误可以因为导入新类型而获得预防。在防范“不值得拥有的代码"上,类型系统是你的主要同盟国。既然这样,就让我们导入简单的外覆类型(wrapper types)来区别天数、月份和年份,然后于Date构造函数中使用这些类型:

《Effective C++》 笔记_第3张图片
令Day,Month和Year成为成熟且经充分锻炼的classes并封装其内数据,比简单使用上述的structs好(见条款22)。但即使structs也已经足够示范:明智而审慎地导入新类型对预防“接口被误用”有神奇疗效

预防客户错误的另一个办法是,限制类型内什么事可做,什么事不能做。常见的限制是加上const。

下面是另一个一般性准则“让types容易被正确使用,不容易被误用”的表现形式:
除非有好理由,否则应该尽量令你的types的行为与内置types一致”。客户已经知道像int这样的type有些什么行为,所以你应该努力让你的types在合样合理的前提下也有相同表现。例如,如果a和b都是ints,那么对a*b赋值并不合法,所以除非你有好的理由与此行为分道扬镳,否则应该让你的types也有相同的表现。是的,一旦怀疑,就请拿ints做范本。

避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。很少有其他性质比得上“一致性”更能导致“接口容易被正确使用”。比如STL容器的接口十分一致,没一个STL容器都有size成员函数。

任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用"的倾向。比如需要用户自己delete释放内存,更好的方法是返回智能指针对象。

shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的"cross-DLL problem" 。这个问题发生于“对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁”。在许多平台上,这一类“跨DLL之new/delete成对运用”会导致运行期错误。shared_ptr没有这个问题,因为它默认的删除器是来自"shared_ptr诞生所在的那个DLL"的delete。

虽然shared_ptr比原始指针大且慢,但是额外执行成本并不明显,并且降低客户错误的成效很好。

请记住:

  • 好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutexes;见条款14)等等。

条款19:设计class犹如设计type

​ 要注意解决以下问题:

  • 新type的对象应该如何被创建和销毁?
  • 对象初始化和对象赋值该有什么样的区别? 条款4
  • 新type的对象如果被pass by value,意味着什么
  • 什么是新type的合法值?
  • 新type需要配合某个继承图系(inheritance graph)吗? (条款34和条款36)
  • 什么样的操作符和函数对此新type而言是合理的?
  • 什么样的函数应该被驳回?
  • 谁该取用新type的成员?
  • 什么是新type的“未声明接口”(undeclared interface)?
  • 你的新type有多么一般化?
  • 你真的需要一个新type吗?

条款20:宁以传递const引用替换传递值

在默认情况下,C++函数传递参数是继承C的方式,是值传递(pass by value)。这样传递的都是实际实参的副本,调用端所获得的也是函数返回值的副本,这些副本都是通过调用复制构造函数来创建的。有时候创建副本代价非常昂贵。

使用pass-by-reference-to-const的好处:

  1. 引用传参,省去了构造和析构;const使内容不会改变,并且const引用可以绑定临时量。
  2. 引用传参还可以避免对象切割问题,因此可以展现多态。

例外:引用往往以指针实现,对于编译器会给予优化的内置类型,以及STL迭代器和函数对象,使用传值效率更高

但是,对于小型用户自定义类型,对象小并不意味着copy构造函数代价小,编译器也不一定会对其优化,应该使用pass-by-reference-to-const。

请记住:

  • 尽量以pass-by-reference-to-const替换pass-by-value,前者通常比较高效,并可避免切割问题(slicing problem)。
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

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

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。

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

成员变量应该是private,原因:

  1. 语法一致性:如果成员变量不是public,客户唯一能够访问对象的办法就是通过成员函数。如果public接口内的每样东西都是函数,客户就不需要在打算访问class成员时迷惑地试着记住是否该使用小括号(圆括号)。

  2. 可细微划分访问控制:使用函数可以让你对成员变量的处理有更精确的控制。如果你令成员变量为public,每个人都可以读写它,但如果你以函数取得或设定其值,你就可以实现出“不准访问”、“只读访问"以及“读写访问”。
    《Effective C++》 笔记_第4张图片

  3. 提供弹性:将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如这可使得成员变量被读或被写时轻松通知其他对象、可以验证class的约束条件以及函数的前提和事后状态、可以在多线程环境中执行同步控制…等等。**被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。**只要类的接口不变,用户代码就无需改变。

  4. protected同理,因为他影响的是派生类。从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。

请记住:

  • 切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
  • protected并不比public更具封装性。

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

1、问题引出

(1)假设有一个WebBrower类,代表浏览器,其中有的成员方法有:

class WebBrower
{
public:
    void ClearCach();
    void ClearHistory();
    void RemoveCookies();
};

(2)现在有一个需求:同时使用这三个函数,但是我们实现上述需求有两种方式:
方式一:在WebBrower类中再写一个成员方法,直接在其内部调用其他成员方法。

class WebBrower
{void ClearEverything()
    {
        ClearCach();
        ClearHistory();
        RemoveCookies();
    }};

方式二:写一个不属于这个类的函数,在函数内调用这个类的三个成员方法。

void ClearWebBrowser(WebBrower& w)
{
    w.ClearCach();
    w.ClearHistory();
    w.RemoveCookies();
}

(3)现在的问题是,哪一种实现方式更好?也就是把它写成成员函数好,还是写成 non-member 、non-friend 函数好呢?
答案:写成:non-member、non-friend 好。

2、为什么写成non-member、non-friend 好?

首先,对于面向对象的一个误解:数据应该和操作数据的函数绑定在一起。如果按照这种解释,那么应该写成member的。但是实际上,面向对象强调的是封装性。

  • 面向对象真正强调的是封装性,对于上述问题,non-member、non-friend函数的封装性要比member函数好。
  • non-member函数允许对浏览器类有较大的包裹弹性,较大的包裹弹性,将导致较低的编译相依度,增加浏览器类的可延展性。

3、关于上述两个原因的进一步解释一:封装性

(1)为什么强调封装性?

所谓封装就是不可见,越多东西被封装,能够看见它的人越少。越少的人看到它,我们就能够更大弹性的修改它。因此,封装性越好,我们改变实现的能力就越高。推崇封装的原因:我们能够改变事物,而只影响有限的客户

(2)如何衡量封装性?

我们计算能够访问该数据的成员函数以及其它函数的数量,作为一种粗糙的衡量。越多的函数能够访问它,它的封装性就越低
例如:public数据,所有的函数都可以访问它,它就是毫无封装性的。private数据,只有friend和member函数可以访问它,它的封装性的高低,就和能够访问它的friend函数和member函数数量有关,数量越大,代表封装性越低,数量越小,代表封装性越高。

换句话说,member函数可以访问类的private数据,而non-member、non-friend函数无法访问,所以说后者封装性更好

总之,在实现同一机能的情况下,面对使用member函数和non-member、non-friend函数的抉择时,后者提供更好的封装性。(这个member只是该类的member,不包括其他类的member函数,其他类的member函数也提供封装性)

4、关于上述两个原因的进一步解释二:编译相依度、包裹弹性

虽然可以将其写到其它类中,但是C++比较自然的做法是,将它写成一个non-member函数,并让它和浏览器类在同一个命名空间中。
这样的做法是由原因的:

(1)原因一:可以降低文件间的编译相依度。

namespace 和class 是不同的,namespace是可以跨越多个文件的,但是class却不能。class 内的是核心技能,但是便利函数只是提供便利的,可有可无的。即使没有便利函数,用户可以通过访问class进行相关的操作。因此说便利函数时外覆的。

一个类可以由不同的机能分化出拥有多个便利函数,与cookies管理有关的、与书签有关的、与打印有关的:
《Effective C++》 笔记_第5张图片

用户可能只对其中一部分感兴趣,那就没有必须让他们之间存在编译相依的关系。可以将他们进行分离,与不同模块相关的便利函数写到不同的头文件中,这样用户对哪个模块感兴趣,就包含哪个头文件就可以了。

(2)原因二:可以轻松的扩展像这样的便利函数。(体现包裹弹性)

将所有便利函数放在多个头文件中但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数。他们需要增加什么便利函数时,也添加到这个命名空间就好。class就不能这样扩展。

注意: 原因一 组织代码的 方式也正是C++标准程序库的组织方式。C++并没有将所有的功能都写到一个头文件里,而是写成数十个头文件,每个头文件中包含某些机能。用户需要什么,就包含什么。这样形成一个编译相依的小系统。 这种分割机能的方式不适用于class成员函数,因为class必须整体定义,不能被分割成片段。

转自:
https://blog.csdn.net/lintianyi9921/article/details/103793070

记住:
宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。
更好的做法是将这些non-member non-friend函数放在同一命名空间,但分模块位于不同头文件,这样编译时只需加入需要的模块头文件,降低了编译依赖性,而且便于客户扩展自己的函数。

条款24:若所有参数皆需类型转换,请为此采用非成员函数

1、问题引出

通常,令class支持隐式转换是不好的设计。但是也有例外,最常见的例外是在建立数值类型时。例如设计一个类表示有理数时,允许整数隐式转换为有理数是合理的。

class Rational{
public:
    Rational(int numerator = 0, int denominator = 1); //刻意不为explicit;允许int-to-Rational隐式转换
    int numerator()const;
    int denominator()const;
};

我需要为有理数类实现加法、乘法等,是该写成member函数、non-member、non-friend函数还是non-member、friend函数呢?

答案是:non-member、non-friend函数。

2、写成member函数引发的问题

class Rational
{
public:
    Rational(int numerator = 0,int denominator = 1) : x(numerator), y(denominator){}
    //构造函数不为explicit,允许int-to-Rational隐式转换 
    int numerator() const; //分子访问函数
    int denominator() const; //分母访问函数
    const Rational operator*(const Rational& rhs) const //有理数的乘法
    {
    	return Rational(x*rhs.x, y*rhs.y);
    }
private:
	int x;
	int y;
};
Rational oneEight(1, 8);
Rational oneHalf(1, 2);
//执行混合运算时会发生错误:
Rational result = oneHalf * 2; //很好
result = 2 * oneHalf; //错误
//函数形式重写:
result = oneHalf.operator*(2);
result = 2.operator*(oneHalf);

第一句为什么能够通过编译呢?

答案就是发生了隐式转换,首先oneHalf有成员方法operator* ,它需要一个参数Rational 类对象,但是传递的参数确是一个int 型的2,于是编译器开始查找Rational 的类型转换构造函数,看能否把2转换成Rational 类对象,它找到了,于是产生了一个临时的对象,然后将这个对象传递给了operator*,因此编译通过。这便是所谓的隐式转换。

为什么第二句的参数2不能发生隐式转换呢?

只有当参数被列于参数列内,这个参数才是隐式转换的合格参与者。地位相当于this的隐喻参数不是隐式转换的合格参与者。

如果将类型转换构造函数写成non-explicit 的话,就达成了二者的一致性,即:都不能通过编译。

显然我们的有理数类是应该支持混合运算的,那么我们的设计就需要改进。

将operator* 写成non-member、non-friend函数,使两个参数都列于参数列表:

const Rational operator*(const Rational& lhs, const Rational& rhs){
	return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());
}

3、是否应该写成friend函数?

不应该,因为使用public接口完全可以完成任务。无论何时,我们都应该尽量避免non-member函数写成友元函数以增强封装性(条款23)。当然,也有必须写成friend的场景,friend是有用的。

4、注意事项

当进入泛型编程时,本条款可能就不太适用了。

转自:https://blog.csdn.net/lintianyi9921/article/details/103794852

记住:
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。因为只有参数列表内的参数会被隐式转换。

条款25:考虑写出一个不抛异常的swap函数

注:C++11后出现了移动语意,因此现在的swap只要类提供移动语意,效率应该不低。

1、内容引出

swap()属于STL的一部分(算法),而后成为异常安全性编程的脊柱(因此必须不抛异常),以及用来处理自我赋值的可能性的一个常见的机制。它是如此的有用,适当的实现就显得十分重要。然而在C++11前它的实现的复杂度也比较高。这个条款就在讲swap()函数的实现问题。

C++11前swap实现:

namespace std {
	template<typename T>
	void swap(T&a, T&b) {
		T temp(a);
		a = b;
		b = temp;
	}
}

C++11后(大致实现):

template<class _Ty> inline
	void _Swap(_Ty& _Left, _Ty& _Right)
	{
		_Ty _Tmp = std::move(_Left);
		_Left = std::move(_Right);
		_Right = std::move(_Tmp);
	}

2、“pimpl”手法

但本节主要讲了“pimpl”手法,以及如何在std中全特化原有的templates。

“以指针指向一个对象,内含真正数据”的类型的,就是“pimpl手法”的表现形式:

class WidgetImpl {
private:
	int i;
	vector<double> v;
	...
};

class Widgt {
	Widgt(const Widgt&rhs);
	Widgt&operator = (const Widgt&rhs) {
		*imp = *rhs.imp;
	}
private:
	WidgetImpl* imp;
};

3、widget 和widgetImpl 都是class时,如何写出高效的swap()

在这种情况下,实际上我们只交换两个指针的指向便可,没有必要交换所指物。但是怎么才能告诉标准库的swap呢?
答案:在std空间内全特化一个swap函数,然后在widget类内写一个成员函数swap,调用该全特化的swap函数。具体代码如下:

class WidgetImpl {
public:
	int i;
};
class Widgt {
public:
	Widgt() = default;
	Widgt(const Widgt&rhs);
	Widgt&operator = (const Widgt&rhs) {
		*imp = *rhs.imp;
	}
	void swap(Widgt& other) {//member swap
		using std::swap;	//令std::swap在函数内可用
		swap(imp, other.imp);
	}
public:
	WidgetImpl* imp;
};
 
namespace std {
	template<>
	void swap<Widgt>(Widgt&a, Widgt&b) {//std::swap 特化版本
		a.swap(b);
	}
}

"template<>"表示它是std::swap的一个全特化(total template specialization)版本。换句话说当一般性的swap template施行于widgets身上便会启用这个版本。通常我们不能够(不被允许)改变std命名空间内的任何东西,但可以(被允许)为标准templates(如swap)制造特化版本,使它专属于我们自己的classes(例如widget)。
这种做法的好处:

  • 提高了swap函数的效率
  • 与STL容器达成了一致性。(STL容器类也提供public swap函数,也全特化了对象的std::swap)

4、如果widget 和widgetImpl 都是类模板,而非类的话,怎么实现?

C++只允许对类模板偏特化,在函数模板上偏特化是行不通的,因此不能用3的方法。
正确方法:
在一个命名空间中定义一个swap 和 Widget WidgetImpl 等等模板, 在这个空间中的swap调用类中的成员函数。但与此同时建议提供一个std的特化Swap版本。

为了简化起见,假设Widget的所有相关机能被置于命名空间WidgetStuff,于是:

namespace WidgetStuff {
    ...                         //模板化的WidgetImpl等等
    template<typename T>        //和前面一样,内含swap成员函数
    class Widget { ... };
    ...

    template<typename T>        //non-member swap函数
    void swap(Widget<T>& a,     //这里并不属于std命名空间
              Widget<T>& b)
    {
        a.swap(b);
    }
}   

此时调用swap,C++的名称查找法则(name lookup rules;更具体的说是所谓argument-dependent lookup或Kobeig lookup法则)将会找到WidgetStuff内的Widget专属版本,这正是我们所希望的。

注意:虽然上面的做法对于class和classtemplate都行得通,但我们还是应该为class特化std::swap。所以,如果我们想让“class专属版”的swap在尽可能多的语境下被调用,我们就应该同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。

5、用户角度

从用户角度考虑,假设正在写一个function template,其内需要置换两个对象值。

template<typename T>
void doSomething(T& obj1, T& obj2)
{
    ...
    swap(obj1, obj2);
    ...
}

(1)此时,调用了swap,但是调用的是哪一个一般化版本?

  • std既有的一般化版本?
  • 某个可能存在的特化版本?
  • 存在的T专属版本而且可能存在与某个命名空间内(非std内)

我们希望的是调用T专属版本,并在该版本不存在的情况下,再去调用std内的一般化版本,那么正确的写法如下:

template<typename T>
void doSomething(T& obj1, T& obj2)
{
    usint std::swap;    //令std::swap在此函数内可用
    ...
    swap(obj1, obj2);   //为T型对象调用最佳swap版本
    ...
}

一旦编译器看到了对swap的调用,它们便查找适当的swap并加以调用。C++的名称查找法则会确保将找到global作用域或者T所在的命名空间内的任何T专属的swap。

(2)C++的名称查找法则的具体做法:

  • 如果T是Widget并位于命名空间WidgetStuff内,编译器就会使用“实参取决的查找规则”(argument-dependentlookup)找出WidgetStuff内的swap。
  • 如果没有T专属的swap存在,编译器就会使用std内的swap——由using std::swap这条语句,使得这个选择被曝光。

6、总结

首先,如果swap的缺省实现对我们的class或class template提供可接受的效率,那么我们并不需要做其他的事情。

其次,如果swap的缺省版本效率不足(通常就是因为class或者class template使用了某种pimpl手法),则:

  • 提供一个public swap成员函数,让它高效地置换对应类型的两个对象值。这个函数绝不能抛出异常!
  • 在我们的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
  • 如果我们正在编写一个class(而非classtemplate),为我们的class特化std::swap。并令它调用我们的swap成员函数。

最后,如果我们调用swap,请确定包含一个using声明式,以便让std::swap在我们的函数内部可以曝光可见,然后不加任何namespace修饰符,直接去调用swap。

7、最后,还有一点关于成员版本的swap:绝对不可以抛出异常!

  1. 原因

    swap的一个最好的应用就是为了帮助class(和class template)提供强烈的异常安全性(exception-safety)保障。

  2. 适用
    当然,这一约束只施行于成员版!不可实施于非成员版,因为swap缺省版本是以copy构造函数和copy assignment操作符为基础的,在一般情况下是允许抛出异常的。
    因此,当我们写一个自定义版本的swap时,往往需要提供以下两点:

    • 高效置换对象值的办法
    • 不抛出异常

    一般而言,上面这两个特性是连在一起的,因为高效率的swap几乎总是基于对内置类型的操作(例如pimpl首发的底层指针),而内置类型上的操作绝不会抛出异常。

记住:

  • 如果std::swap不高效时,提供一个swap成员函数,并且确定这个函数不抛出异常。
  • 如果提供一个member-swap,也应该提供一个non-member swap来调用前者。对于class(非class template),要特化std::swap。
  • 调用swap时,针对std::swap使用using形式,然后调用swap并且不带任何命名空间资格修饰。
  • 为“用户定义类型”进行std template全特化时,不要试图在std内加入某些对std而言是全新的东西。

参考:https://blog.csdn.net/lintianyi9921/article/details/103799093

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

1、尽可能延后变量定义直至能够给他初值实参

原因:

  • 直到必须使用变量的时候才对其进行定义,这样可以减少定义对象时候需要的构造函数和销毁变量时析构函数的开销。太早定义变量可能导致还未使用就return或抛出异常,白白做了构造和析构。
  • 有初值实参的时候再进行定义变量,可以跳过无意义的默认构造过程,而且附带说明变量的目的。

2、该在循环内定义变量还是循环外?

如果变量只在循环内使用,那么把它定义于循环外并在每次循环迭代时赋值给它比较好,还是该把它定义于循环内?也就是说下面左右两个一般性结构,哪一个比较好?
《Effective C++》 笔记_第6张图片
在widget函数内部,以上两种写法的成本如下:

  • 做法A:1个构造函数+1个析构函数+n个赋值操作
  • 做法B:n个构造函数+n个析构函数

当类的一个赋值成本低于一组构造+析构成本,A大体比较高效,否则做法B更好。另外A版本造成变量的作用域比B的大,这可能对于程序的易理解性、易维护性有一定的冲击。
因此,除非

  • 你知道赋值成本比”析构+构造”成本低
  • 或者你正在处理代码中效率高度敏感的部分

否则应该选择做法B。

条款27:尽量少做转型操作

1、关于C++的转型动作

C++中转型破坏了类型系统。那可能导致任何种类的麻烦,有的容易辨识,有些非常隐晦。C、java、c#语言中可能转型是必要的、无法避免的,相比于C++也比较不那么危险。但是C++中,应该尽量少的做转型,C++中使用转型比较危险,应该尽量将转型动作使用不转型的手法给化解掉。

C++的转型并不是在原变量地址上做转型,而是创建了一个临时对象

2、C和C++的三种形式的转型语法

(1)形式一:C语言风格的转型语法:

 (T)expression     //将expression转换为T类型

(2)形式二:函数风格的转型:

T(expression)     将expression转换为T类型

(3)形式三:C++风格的转型语法

  • cpp const_cast(expression);//const->non const
    const_cast用来将对象的const属性去掉,功能单一,使用方便。
  • dynamic_cast(expression);
    将一个基类对象指针(或引用)转换到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理(如果否,则返回空指针)。它也是唯一一种无法用旧式转换进行替换的转型,也是唯一可能耗费重大运行成本的转型动作。比使用static_cast多了类型检查的功能。
  • reinterpret_cast(expression);
    对二进制数据进行重新解释,通常用于指针转换。
  • static_cast(expression);
    static_cast 用来进行强制隐式转换,我们平时遇到的大部分的转型功能都通过它来实现.例如将int转换为double,将void*转换为typed指针,将non-const对象转换为const对象,反之则只有const_cast能够完成.

注意:形式一、二并无差别,统称旧式转型,形式三称为新式转型。

3、倡导使用新式转型

新式转型的优点

  • 在代码这种容易被识别出来(无论是人工识别还是使用工具如grep),因而简化“找出类型系统在哪个点被破坏”的过程(简化找错的过程)。
  • 各种转型动作的目标越窄化,编译器越能判断出出错的运用。例如:如果你打算将常量性去掉,除非使用新式转型的const_cast否则无法通过编译。

4、转型动作的原理

任何一种转型动作往往真的令编译器额外地编译出运行期间执行的代码,非指针转型会创建一个额外的副本,而不是在原对象的内存空间上操作

class A{
public:
    int A_mem;
	A(){ cout<<"A构造"<<endl; }
    A(const A&a):A_mem(a.A_mem){   cout<<"A复制构造"<<endl;}
    ~A(){ cout<<"A析构"<<endl; }
};
class B:public A{
    int B_mem;
};
int main()
{
    B b;
    static_cast<A>(b);
    cout<<"---"<<endl;
}
//输出:A构造 A复制构造 A析构 ---
//A析构

5、关于dynamic_cast

dynamic_cast在运行期执行,很多实现都是使用strcmp比较class名称,执行速度相当的慢。尤其是在深度继承和多重继承中,速度更慢,因此应尽量避免,尤其是循环中的dynamic_cast,更要避免。

6、最后的话

优良的C++代码很少使用转型,若说要完全摆脱它们又太过不切实际,但是我们应该尽量避免转型。就像面对众多蹊跷可疑的构造函数一样,我们应该尽可能隔离转型动作,通常是把它隐藏在某个函数内,函数的接口会保护调用者不受函数内部任何肮脏龌龊的动作的影响。

请记住:

  • 应该尽量少使用转型,尤其是在注重效率的代码中使用dynamic_cast。如果需要转型,试着设计无需转型代替。
  • 如果必须使用转型,把它隐藏在函数内,客户通过接口使用,而不是由客户来实现转型。
  • 使用新式的C++ style转型来代替旧的转型,因为新式的转型很容易辨识出来,而且它们有分类。

条款28:避免返回handles 指向对象内部成分

1、什么是handles?

reference、指针、迭代器系统都是所谓的handles(号码牌,用来获得某个对象)。函数返回一个handle,随之而来的便是“减低对象封装性”的风险。它也可能导致:虽调用const成员函数却造成对象状态被更改的风险。

class Rectangle{
……
public:
    Point& upperLeft()const{return pData->ulhc;}//返回了引用,可以修改所致的对象
    Point& lowerRight()const{return pData->lrhc;}
};

上面两个函数就是返回handle的成员函数。

2、“返回一个handle代表对象内部成分”总是危险的原因:

不论这所谓的handle是个指针或迭代器或reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里的唯一关键是**,有个handle被传出去了,一旦如此你就是暴露在"handle比其所指对象更长寿”的风险下**。当使用handle成员函数的对象是个临时量时,语句结束,该临时量消失,handle就变成了空悬。

这并不意味你绝对不可以让成员函数返回handle。有时候你必须那么做。例如operator[]就允许你“摘采”strings和vectors的个别元素,而这些operator[]s就是返回references指向“容器内的数据”(见条款3),那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。

条款29 :为“异常安全”而努力是值得的

《Effective C++》 笔记_第7张图片
这个函数没有满足异常安全的任何一个条件。

1、异常安全条件

“异常安全”有两个条件,即当异常被抛出时,带有异常安全性的函数会:

  • 不泄漏任何资源。上述代码没有做到这一点,因为一旦"newImage(imgSrc)"导致异常,对unlock的调用就绝不会执行,于是互斥器就永远被把持住了。
  • 不允许数据败坏。如果"new Image(ingSrc)"抛出异常,bgImage就是指向一个已被删除的对象,inageChanges也已被累加,而其实并没有新的图像被成功安装起来。

解决资源泄漏的问题,使用RAII即可:
《Effective C++》 笔记_第8张图片

2、异常安全函数的保证

异常安全函数提供以下三个保证之一:

  • 基本保证:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的class约束条件都继续获得满足)。然而程序的现实状态(exact state)恐怕不可预料。举个例子,我们可以撰写changeBackground使得一旦有异常被抛出时,Prettylenu对象可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么。
  • 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态
  • 不抛掷(nothrow)保证,承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。注:throw()空异常说明或者noexcept说明符并不保证绝不抛掷,而是说若抛掷则是严重错误,一般会终止。

异常安全码(Exception-safe code)必须提供上述三种保证之一。如果它不这样做,它就不具备异常安全性。

对于changeBackground()函数而言,为了尽量接近强烈保证,可以更改为下面的代码:

class PrettyMenu {
//...
private:
    std::tr1::shared_ptr<Image> bgImage;
};
 
//重写修改PrettyMenu的成员函数
void PrettyMenu::changeBackground(std::istream& imgSrc) {
	Lock ml(&mutex);
	bgImage.reset(new Image(imgSrc));
	++imageChanges;
}

这两个改变几乎足够让changeBackground提供强烈的异常安全保证。美中不足的是参数imgSrc.如果Image构造函数抛出异常,有可能输入流(input stream)的读取记号(read marker)已被移走,而这样的搬移对程序其余部分是一种可见的状态改变。所以changeBackground在解决这个问题之前只提供基本的异常安全保证。
因此,需要使用copy-and-swap策略,这个一般化的设计策略很典型地会导致强烈保证。

3、copy-and-swap策略

原则:为你打算修改的对象(原件)做一份副本,然后在副本身上做修改:

  • 如果在副本的身上修改抛出了异常,那么原对象未改变状态;
  • 如果在副本的身上修改未抛出异常,那么就将修改过的副本与原对象进行置换(swap)。

实现上通常是使用pimpl idiom(pimpl惯用法):将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。

//将bgImage和imageChanges从PrettyMenu独立出来,封装到一个结构中
struct PMImpl {
    std::tr1::shared_ptr<Image> bgImage;
    int imageChanges
};
 
class PrettyMenu {
    //...
private:
    std::tr1::shared_ptr<PMImpl> pImpl; //创建一个该结构
};

//使用copy and swap
void PrettyMenu::changeBackground(std::istream& imgSrc) {
    using std::swap; //见条款25
 
    Lock ml(&mutex);
	
    //以pImpl为原件,创建一个副本,然后在副本上做修改
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc));
    pNew->imageChanges++;
 
    //如果上面副本的修改没有抛出异常,那么交换副本与原件
    swap(pImpl, pNew);
}

注意:copy and swap只能保证这部分有强烈的异常安全性,如果调用的其他函数异常安全性比“强烈保证”低,则不能保证整体强烈异常安全;
又或者将多个具有强烈异常安全性的函数顺序调用,则会有“连带影响”:前面函数调用成功,后面的函数抛出异常,也不能恢复整体状态,因此也不能提供“强烈保证”。

此外,copy and swap创建副本的消耗可能无法接受,所以“强烈保证”并非任何时刻都显得实际。

记住:

  • 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型。
  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高值等于其所调用之各个函数的“异常安全保证”中的最弱者。

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

inline只是对编译器的一个建议,不是强制命令,编译器可能接受也可能忽略。

1、inline的优缺点

优点:

  • 免除函数调用的开销,类似宏一样提高效率,但比宏安全;
  • 可以享受编译器语境的优化(一般编译器不会对outlined函数调用执行最优化 )。

缺点:

  • 以函数本体代替函数调用,可能造成代码膨胀,膨胀太大的话可能会造成额外的换页行为,降低指令高速缓存装置的集中率,以及伴随效率的损失;
  • 不利于调试;
  • 如果日后该函数被改变,则所有用到该函数的客户端程序都必须重新编译;而如果f不是inline函数,客户端只需重新链接,如果是动态链接,甚至不需要管。

2、隐式内联、显式内联

  • 隐式内联:
    当函数定义在类的内部时(包括友元函数),这个函数是隐式inline的(隐式内联只有这一种情况,构造、析构、虚函数除外)。

    class Person {
    public:
        //隐式内联(编译器自动申请),这个函数不仅在类中声明,还在类中进行了定义
        int age()const { return theAge; }
    private:
        int theAge;
    };
    

    构造函数与析构函数除外,因为一般编译器会在其中添加很多隐藏代码。虚函数在运行期确定,因此也不内联。

  • 显式内联:
    我们也可以通过inline关键字显式的指出一个函数作为内联函数。例如:

    template<typename T>
    inline const T& std::max(const T& a, const T& b)
    {
        return a < b ? b : a;
    }
    

注意:inline函数通常被置于头文件内,因为大多数建置环境在编译过程中进行inlining,编译器需要知道内联函数长什么样子。inlining在大多数C++程序中是编译期的行为(但是也有少数情况是在运行期链接期完成inlining)。

3、编译器拒绝内联的情况

即使你将函数声明为inline的,但是在有些情况下编译器会拒绝将函数作为inlining。例如:

  • 太过复杂的函数:例如带有循环或递归,因为函数调用的消耗比起循环和递归太小;
  • 对virtual函数的调用(除非是最平淡无奇的):因为virtual意为“等待”,直到运行期才确定调用哪个函数,而inline意味着在编译期就能够确定调用函数本体。因此virtual函数将被编译器拒接生成为inline的。
  • 需要显式或隐式地取函数地址(存放函数代码)时,编译器必须为函数代码分配内存地址。

注意:g++是否内联可能和编译选项中的优化等级相关:不优化时不进行内联,开优化才内联。

4、总结

将大多数inlining限制在小型、被频繁调用的函数身上。这可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

条款31:将文件间的编译依存关系降至最低

1、文件间依存度高的话带来的影响?

假如你修改了C++ class实现文件,修改的仅仅是实现,而没有修改接口,而且只修改private部分。此时,重新构建这个程序时,会发现整个文件、以及用到该class 的文件都被会被重新编译和连接,这不是我们想要看到的。

2、出现上述问题的原因

问题出在C++没有把关于接口与实现相分离这件事做好。C++ 的class 的定义式中不仅定义了接口,还定义了实现细目(成员变量)。
例如:

class Person{ 
public: 
    Person(const std::string& name, const Date& birthday, const Address& addr); 
    std::string name() const; 
    std::string birthDate() const; 
    std::string address() const; 
    ... 
private: 
    std::string theName;        //实现细目 
    Date theBirthDate;          //实现细目 
    Address theAddress;         //实现细目 
};

当编译器没有取得实现代码所需要的class string,Date和Address的定义式时,它无法通过编译它所需要的这样的定义式往往由#include <>提供(里面有class string,Date和Address的实现代码)。例如本例中需要:。

#include  
#include "date.h" 
#include "address.h"

如果这些头文件中(或头文件所依赖的头文件)的一个的实现被改变了,那么每一个用到class 类的文件都得重新编译。这就是所谓的文件间的依存度比较高。

有两个手法可以降低编译依存关系:

3、解决文件间依存关系的正确手法一:handle class

(1)基本思想

当编译器看到一个自定义类型对象时,它必须通过该类的定义得知为该对象分配多少内存,但是如果看到的是该对象的指针,只需要清楚为该指针分配多少内存。
因此,可以将对象的实现细目隐藏到一个指针(通常是一个智能指针)背后。
例如:

#include  
#include  
class PersonImpl; 
class Date; 
class Address; 

class Person{ 
public: 
    Person(const std::string& name, const Date& birthday, const Address& addr); 
    std::string name()const; 
    std::string birthDate() const; 
    std::string address()const; 
    ... 
private: 
    std::tr1::shared_ptr<PersonImpl> pImpl; // 指向实现物的指针 
};

上述程序中,将原本的Person 类写成两个部分,接口那部分是主要的部分,其中含了一个智能指针,指向实现细目。而实现细目另外定义了一个类:PersonImpl。这种设计手法被称为:pimpl idiom
注意:pimpl 指的是 pointer to implementation。这种class内的指针往往被称为:pImpl指针。上述class的写法 往往被称为handle class。

(2)好处

实现了接口与实现的分离。即:Person的客户与 Date、Address、以及Person的实现细目就分离了。
实现接口与实现的分离所带来的的好处:

  • 这些class的修改,都不需要Person客户进行重新编译。
  • 而且客户无法看到实现细目,提高了封装性。

(3)关键

这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。其他每一件事都源自于这个简单的设计策略:

  • 如果用 object reference 或 object pointer 可以完成任务,就不要用 objects。
    可以只靠声明式定义出指向该类型的 pointer 和 reference;但如果定义某类型的 objects,就需要用到该类型的定义式。

  • 如果能够,尽量以 class 声明式替换 class 定义式。
    当你声明一个函数而它用到某个 class 时,你并不需要该 class 的定义式,纵使函数以 by value 方式传递该类型的参数(或返回值)亦然:

    class Date;
    Date today();
    void clearAppointments(Date d);
    

    只有使用了这两个函数的客户才需要定义Date。将“提供class定义式”(通过#include完成)的义务从“函数声明所在”之头文件移转到“内含函数调用”之客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性去除掉。

  • 为声明式和定义式提供不同的头文件。
    两个头文件应该包吃一致性,其中一个头文件发生改变,另一个就得也改变。一个内含了class 接口的定义,另一个仅仅内含声明。

    例如:只含声明式的Date class 的头文件应该命名为datefwd.h(有一定的命名规则),该命名规则出自C++标准程序库头文件
    注意:本条款适用于template 也适用于 non-template。template如果想要分离声明式和定义式,则需要使用关键字export。

4、解决文件间依存关系的正确手法二:interface class手法

(1)基本思想

令Person class 成为一种特殊的abstract base class (抽象基类),称为interface class。这样的类通常:没有成员变量,也没有构造函数,只有一个virtual 的析构函数以及一组pure virtual 用来描述接口

(2)C++ 接口类与其他语言接口类的不同

像.net和java 的接口,他们不允许在接口类中定义成员函数和成员变量。但是C++的接口类并不禁止,这样的规则使得C++语言具有更大的弹性。

(3)interface class 类创建对象的方式

由于这样的类往往没有构造函数,因此通过工厂函数或者virtual构造函数创建,他们返回指针,指向动态分配对象所得的对象,这样的对象支持interface class的 接口,这样的函数在interface class往往被声明为 static,例如:

class Person{ 
public: 
    ... 
    static std::tr1::shared_ptr<Person> 
    create(const std::string& name, const Date& birthday, const Address& addr); 
};

客户使用他们像这样:

std::string name; 
Date dateBirth; 
Address address; 
std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address)); 
... 
std::cout << pp->name() 
            << "was born on " 
            << PP->birthDate() 
            << " and now lives at " 
            << pp->address(); 
...

当然支持 interface class 接口的那个具象类(concrete classes)必须被定义出来,而真正的构造函数必须被调用。
假设有个 derived class RealPerson,提供继承而来的 virtual 函数的实现:

class RealPerson : public Person{ 
public: 
    RealPerson(const std::string& name, const Date& birthday, const Address& addr) 
    : theName(name), theBirthDate(birthday), theAddress(addr) 
    {} 
    virtual ~RealPerson(){} 

    std::string name() const; 
    std::string birthDate() const; 
    std::string address() const; 

private: 
    std::string theName; 
    Date theBirthDate; 
    Address theAddress; 
};

有了 RealPerson 之后,写出 Person::create 就真的一点也不稀奇了:

std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr) 
{ 
    return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr)); 
}

一个更现实的 Person::create 实现代码会创建不同类型的 derived class 对象,取决于诸如额外参数值、独自文件或数据库的数据、环境变量等等。

RealPerson 示范实现了 Interface class 的两个最常见机制之一:从 interface class 继承接口规格,然后实现出接口所覆盖的函数。第二个实现设计条款40的多重继承。

5、两种手法带来的代价

handle classes 和 interface classes 解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。

  • handle classe

    成员函数必须通过 implementation pointer 取得对象数据。那会为每一次访问增加一层间接性。每个对象消耗的内存必须增加一个 implementation pointer 的大小。 implementation pointer 必须初始化指向一个动态分配的 implementation object,所以还得蒙受因动态内存分配带来的额外开销。

  • Interface classe

    由于每个函数都是 virtual,必须为每次函数调用付出一个间接跳跃。此外 Interface class 派生的对象必须内含一个 vptr(virtual table pointer)。

6、两种手法的适用场景

在程序开发过程中使用 handle class 和 interface class 以求实现码有所改变时对其客户带来最小冲击。

而当他们导致速度和/或大小差异过于重大以至于 class 之间的耦合相形之下不成为关键时,就以具象类(concrete class)替换 handle class 和 interface class。

记住:

  • 支持 “编译依存最小化” 的一般构想是:相依于声明式,不相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
  • 程序库头文件应该以 “完全且仅有声明式” 的形式存在。



6、继承与面向对象设计

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

“is-a”的概念:

  • 以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系
  • 如果你令class D以public形式继承class B,你便是告诉编译器:
    • 每一个类型为D的对象同时也是一个类型为B的对象。反之不是。
    • B对象可使用的地方,D对象一样可以使用。反之不是。
  • 编程时,is-a关系可能是违背由现实生活得来的经验直觉的。

记住:
“public”继承意味着is-a。适用于base classes身上的每一件事一定也适用于derived身上,因为每一个derived对象也是一个base class 对象。

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

1、C++的名称遮掩规则

当不同作用域有相同名称的变量、函数时,需要用到名称遮掩/隐藏。
名称遮掩规则做的事就是:遮掩名称。至于名称是否应和相同或不同的类型对应,并不重要。

2、派生类的成员函数内查找名称的顺序

  • 先在成员函数体内查找(local作用域)。
  • 找不到的情况下,在派生类内查找。
  • 找不到的情况下,在派生类所继承的基类内查找。
  • 找不到的情况下,在派生类所属namespace查找。
  • 找不到的情况下,在global作用域查找。
  • 找不到的情况下,则会报错。

只要找到名称相同的函数,无论参数是否相同,都算查找完毕,如果参数不同则出错。

3、隐藏基类的全部函数

  • 如果基类中含有一些列重载函数,只要派生类定义了一个与基类同名的函数,那么基类中的所有重载函数对于派生类来说全都被隐藏(即使参数列表不一致也是),但可以使用基类指针来调用(多态还是可以实现)。
  • 设计该规则的理由:防止你在程序库或应用框架内建立新的派生类时附带地从疏远的基类继承重载函数。

示例:
《Effective C++》 笔记_第9张图片

class Base
{
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};
 
class Derived :public Base
{
public:
    virtual void mf1(); //基类中的所有mf1()都被隐藏
    void mf3();         //基类中的所有fm3()都被隐藏
    void mf4();
};

调用代码:

Derived d;
int x;
 
d.mf1();  //正确
d.mf1(x); //错误,Base::fm1(int)被隐藏了

Base *b=new Derived;
b->mf1(x);//正确,多态还是可以实现
    
d.mf2();  //正确
 
d.mf3();  //正确
d.mf3(x); //错误,Base::mf3(double)被隐藏了

4、解决办法

主要有两种办法可以解决名称遮掩问题:

1)使用using声明式

class Base
{
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};
 
class Derived :public Base
{
public:
    using Base::mf1; //Base所有版本的mf1函数在派生类作用域都可见
    using Base::mf3; //Base所有版本的mf3函数在派生类作用域都可见
 
    virtual void mf1(); 
    void mf3();         
    void mf4();
};

Derived d;
int x;
 
d.mf1();  //正确,调用Derived::mf1()
d.mf1(x); //正确,调用Base::mf1(int)
 
d.mf2();  //正确,调用Derived::mf2()
 
d.mf3();  //正确,调用Derived::mf3()
d.mf3(x); //正确,调用Base::mf3(double)

如果你继承base class并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。
注:using只能声明该类能访问(protected或public访问级别)的成员。using详细介绍

2)使用转交函数

当我们只想继承一系列重载函数的部分函数,而不是全部时,不处理会导致全部继承,使用using会导致某给定名称的所有同名函数在派生类中都可见,因此需要用到转交函数(转交函数能够同时取消私有继承的限制):

class Base
{
public:
    virtual void mf1();
    virtual void mf1(int);
};
 
class Derived :private Base	//私有继承
{
public:
    //这是一个转交函数
    virtual void mf1() {
        Base::mf1(); //调用基类的mf1()函数
    }
};
 

Derived d;
int x;

d.mf1();  //正确,虽然调用的是Derived::mf1(),但是本质上调用的是Base::mf()
d.mf1(x); //错误,Base::mf(double)被隐藏了

此外,转交函数可以实现uisng的功能。

3)显示使用基类成员函数

class Base {
   private:
    int x;

   public:
    virtual void mf1() = 0;
    virtual void mf1(int);
};

class Derived : public Base {
   public:
    virtual void mf1();
};

Derived d;
int x;
d.mf1();         //正确,调用Derived::mf1()
d.Base::mf1(x);  //正确,调用Base::mf1(int)

记住:

  • derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding functions)。

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

1、public继承细分

实际上细分为:函数接口继承函数实现继承。这两种细分更像是函数声明和函数定义之间的差异。

从这两个角度出发,public继承可以分为:

  • 只继承接口
  • 同时继承接口和实现,且继承而来的实现能够被覆写
  • 同时继承接口和实现,且继承而来的实现不能够被覆写

2、三种继承对应的成员函数的写法

1)只继承接口

  • 对应的成员函数的写法为:pure virtual
  • 特点:此类继承派生类必须自己写实现。

注意:pure virtual 函数是可以写定义的,但是只能通过类名调用(Base::fun())。

2)同时继承接口和实现,且继承而来的实现能够被覆盖

  • 对应的成员函数的写法:virtual
  • 特点:此类继承派生类可以缺省实现(不对该虚函数重新声明),缺省的话,就会继承基类的实现。也可以自己手动写实现,这样相当于是覆盖了基类的实现。

3)同时继承接口和实现,且继承而来的实现不能够被覆盖

  • 对应的成员函数的写法:non-virtual
  • 特点:此类继承,派生类必须继承缺省和实现,且不能够修改。

比较virtual和non-virtual

  • virtual函数主要考虑的是特异性,即派生类可能和基类拥有不同的性质的时候应该写成virtual。
  • non-virtual则主要考虑的是不变形,即所有的派生类都应该拥有这一性质的话,就应该写成non-virtual。

记住:

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。

条款35:考虑virtual函数以外的其他选择

1、考虑virtual函数以外的其他选择是什么意思?

大多时候,我们会自然而然的想到使用virtual手法来塑模现实中的类。但是,实际上也有别的方案可以替代virtual手法的,即:考虑virtual函数以外的其他选择。下面介绍的便是几种可以替代virtual的方案。

2、第一种方案:通过Non-Virtual Interface (NVI)手法实现Template Method 模式

(1)Non-Virtual Interface 是Template Method 设计模式的一个独特的表现形式。

class GameCharacter {
public:
    //1、派生类不可改变的算法骨架
    int healthValue() const
    {
        ...//做一些事前工作
        int retVal = doHealthValue();//核心点
        ...//做一些事后工作
        return retVal;
    }
private:
	//2、派生类可以改变的算法细节
    virtual int doHealthValue() const //derived class 可以重新定义它。
    {
        ...//缺省计算,计算健康指数
    }
};

(2)这里把non-virtual的函数 healthValue()称为doHealthValue()函数的外覆器。

这个实现方法的优点:可以在核心点前做准备工作,和核心点后做善后工作。

(3)关于对派生类对象中重新定义 private virtual函数的担心

  • 派生类有权利决定实算法的某一步的具体细节
  • 派生类无权决定派生类实现的步骤和时机(这些由基类决定。)
  • 这是合情合理的,因此不必担心。

(4)virtual函数的访问控制

  • NVI手法中,virtual函数可以是private的
  • NVI手法中,virtual函数也可以是protected的。这样的写法使得派生类可以调用其父类中的对应的函数。
  • NVI手法中,virtual函数不可以是public的。

注意:准备工作可以是:锁定互斥器、验证class约束条件、验证函数先决条件等等。善后工作可以是:解除互斥器锁定、验证函数的事后条件、再次验证class约束条件等等。

3、第二种方案:通过Function Pointers完成Strategy模式

(1)具体做法

在游戏角色类中添加一个指针,指向一个计算健康指数的函数。

(2)优点

通过Funtion Pointers完成Strategy与使用virtual函数实现的比较:

  • 同一人物类型的不同实体可以有不同的健康计算函数。
  • 某个已知人物的健康指数极端函数可以在运行期变更。

4、第三种方案:通过tr1::function完成Strategy模式(使用函数对象,而不是使用函数指针)

(1)实现方法:

上面的方法是让游戏角色类拥有一个指向函数的指针。而这个方案是让游戏角色类拥有一个像函数一样的函数对象。

(2)优点:

  • 这种方案比上述方案的效果更好,因为函数对象允许我们做的事更多。可以调用任何兼容可调用物(函数指针、函数对象或成员函数指针)计算健康指数。(兼容意指 function对象可以持有 任何 (参数和返回类型)可以隐式转换为该function对象签名式的 可调用物)

注意:对于宣称只接收一个参数的成员函数,实际上接收两个参数,包括隐式的(*this),因此转化为function对象时得先用bind函数将 *this 对象绑定到成员函数上:

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
 
class GameCharacter {
public:
    //其余部分同上
    //只是将函数指针改为了function模板,其接受一个const GameCharacter&参数,并返回int
    typedef function<int(const GameCharacter&)> HealthCalcFunc;
	
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) 
        :healthFunc(hcf) {}
 
    int healthValue() {
        return healthFunc(*this);
    }
private:
    HealthCalcFunc healthFunc;
};
class EvilBadGuy :public GameCharacter {
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
        :GameCharacter(hcf) {}
    //..
};
//其提供一个成员函数,用以计算健康
class GameLevel {
public:
    float health(const GameCharacter&)const{}
};
int main()
{
    //人物,其使用GameLevel类的health()成员函数来计算健康指数
    GameLevel currentLevel;
    EvilBadGuy ebg2(bind(&GameLevel::health, currentLevel, placeholders:: _1));
    //或者EvilBadGuy ebg2(bind(&GameLevel::health, ¤tLevel, placeholders:: _1));
}

5、第四种方案:传统的Strategy模式

(1)传统的Strategy的做法是:

  • 设计一个游戏角色类的继承体系,其中可以派生多个类。并且在其基类中包含一个指向计算健康指数类的指针。
  • 将计算健康指数设计成一个继承体系,其中可以派生多个类。

(2)优点:

  • 设计模式容易辨认。
  • 增加新的健康指数计算类比较容易,只需要在健康指数继承体系中派生一个新的计算类即可。

6、二、三、四方案存在的问题

(1)实现方案存在的问题

实际上也就是说,健康指数计算函数不再是游戏角色类的成员函数了,这也意味着他们不能直接访问游戏角色类的成员了。

(2)解决上述问题的方法:

弱化游戏角色类的封装,这也是这种替代方案的缺点,这是下面Strategy设计模式都要需要面临的问题。
方案一:游戏角色类可以将该函数声明为友元。
方案二:将游戏角色类的某些成员声明为public。

(3)三种方案的缺点

弱化了封装。

记住:

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  • function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物。

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

class B {
public:
    virtual void mf();
};
class D :public B {
public:
    virtual void mf();
};
D x;

B *pB = &x;
pB->mf();   //调用B::mf()

D *pD = &x;
pD->mf();   //调用D::mf()

原因:
non-virtual函数都是静态绑定(程序编译过程中,把函数调用与响应调用所需的代码结合的过程称之为静态绑定)的,即:假如P是基类指针,无论P指向的基类对象还是派生类对象,它发起函数调用的版本都是基类的版本。不同于与virtual的动态绑定。
注:如果不通过指针而是直接通过对象调用,也属于静态绑定。

记住:
绝对不要重新定义继承而来的non-virtual函数。

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

1、静态/动态类型

  • 静态类型:对象在声明时采用的类型,在编译期已经确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的,可在程序执行过程中改变。

2、静态/动态绑定

  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期,又称前期绑定;
  • 动态绑定:绑定的是动态类型,所对应的函数或属性取决于对象的动态类型,发生在运行期,又称后期绑定。

3、为什么不能重新定义继承而来的缺省参数值?

因为virtual 函数体是动态绑定的,但是virtual 函数的缺省参数是静态绑定的。假如通过指针或者引用访问重新定义继承而来的virtual函数的缺省参数值,则会造成错误。(例如:函数体为派生类版本,但是缺省参数却是基类版本。)

注:C++编译器将 virtual函数体设为动态的,而将virtual的缺省参数设为静态的是出于效率的考虑。如果缺省参数设为动态的,程序运行机制会变得更慢、更复杂。

4、在遵守该规则的前提下,如何做到同时给base class 和derived class 提供缺省参数?

方案一:一般手法,基类和所有派生类提供一模一样的缺省参数。

class Shape {
public:
    enum ShapeColor {Red, Green, Blue};
    virtual void draw(ShapeColor color = Red) const = 0;
    ...
};
class Rectangle: public Shape{
public:
    virtual void draw(ShapeColor color = Red) const;
    ...
};

代码的缺点:

  • 重复度大。(每个派生类都要指定缺省参数ShapeColor color等于Red )
  • 灵活性差。(当基类的缺省参数改变时,所有派生类的缺省参数也要跟着修改。)

方案二:NVI手法替代方案

class Shape {
public:
    enum ShapeColor {Red, Green, Blue};
    void draw(ShapeColor color = Red) const            //如今它是non-virtual
    {
        doDraw(color);                                 //调用一个virtual
    }
    ...
private:
    virtual void doDraw(ShapeColor color) const = 0;    //真正的工作在此处完成
};

class Rectangle: public Shape{
public:
    ...
private:
    virtual void doDraw(ShapeColor color) const;        //注意,无须指定缺省参数值。
    ...
};


Shape * rec = new Rectangle();
rec->draw();			//  缺省参数调用,缺省参数为Red  矩形版本draw()
rec->draw(Green);		//  带参数调用					 矩形版本draw()

优点:

  • 基类缺省发生改变,派生类不需要修改参数。

记住:
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual 函数——你唯一应该覆盖的东西——却是动态绑定。

条款38:通过复合塑模出has-a 关系或 is-implemented-in-terms-of(根据某物实现出)关系

1、什么是复合?

复合是类型之间的一种关系,当某种类型的对象包含其它类型对象时,便是这种关系。复合有很多同义词:layering(分层),containment(内含),aggregation(聚合),embedding(内嵌)。

2、复合描述的关系的细分

(1)复合意味着两种关系

  • has-a 关系
  • is-impemented-in-terms-of关系

(2)细分依据

区分这两种关系的依据:你正打算在你的软件中处理两个不同的领域:

(3)应用域和实现域

  • 应用域:应用域里的对象相当于你所塑造的世界中的某些事物,例如人、汽车、视频画面。当复合发生于应用域内的对象之间,表现出has-a的关系;
  • 实现域:其他对象则纯粹是实现细节上的人工制品(比如:缓冲区、互斥器、查找树)。当复合发生于实现域内则是表现is-implemented-in-terms-of(根据某物实现)的关系。

记住:

  • 复合(composition)的意义和public继承完全不同。
  • 在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。

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

1、private继承的两条规则

  • 规则一:如果classes之间是private继承关系,那么编译器不会自动将一个derived class对象转换为一个base class对象(无法类型转换)。
  • 规则二:由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protectedpublic属性。

2、private 继承所描述的关系

  • private继承 意味implemented-in-terms-of
    如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。
  • private继承意味只有实现部分被继承,接口部分应略去

3、复合与private继承的选择

疑问:

复合和private继承都是描述implemented-in-terms-of 关系,那么在类设计时应该使用复合还是private继承该如何选择呢?

(1)首先大部分情况下应该尽可能的使用复合。

(2)在下述几种情况使用private继承:

  • 主要是在protected成员或virtual函数牵扯进来的时候(比如派生类想要使用基类protected成员,或需要重新定义继承而来的virtual函数)。

  • 另一种更激进的情况:当基类为空类时,可以起到节约内存空间的作用。(如果客户对于内存空间的要求极高的话,可以采用private继承),示例:

    • 使用复合:
      class Empty {}; //空类
       
      class HoldsAnint :private Empty {
      private:
          int x;
          Empty e;
      };
      
      sizeof(HoldsAnint); //8
      
      之所以是8而不是4,是因为C++标准要求凡是独立(非附属)对象必须有非0大小(通过默默安插一个char到空对象内)。

    非附属/独立对象,指的是不以“某对象之base class成分”存在的对象。

    • 使用private继承,该优化称为EBO(empty base optimization)空白基类最优化:
      class Empty {};
       
      class HoldsAnint :private Empty {
      private:
          int x;
      };
       
      sizeof(HoldsAnint); //4
      
      上面的约束不适用于derived class对象内的base class成分,因为它们并非独立(非附属)。EBO不适用于多重继承。

注意:“empty class”:这里的空类是指不包含non-static成员,但可以包含:typedefs,enums,static成员变量或者non-virtual函数。

4、复合与private继承的优缺点比较

  • 复合比private继承更容易理解。
  • 复合的耦合性(依存性,需要包含头文件,需要注意文件加载顺序等问题)要比private继承低(通过复合一个对象指针代替对象)。这对大型程序设计非常重要。
  • private继承在某些对内存要求比较高的场合可以起到节约内存的作用。

记住:

  • Private继承意味is-implemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected基类的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合(composition)不同,private继承可以造成empty base最优化(EBO)。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

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

1、多重继承可能引发的问题:接口调用歧义

(1)举例

    clas BorrowableItem{
    public:
        void checkOut();
        ……
    };
    class ElectronicGadgent{
    private:
        bool checkOut() const;
        ……
    };

    class MP3Player: public BorrowableItem, public ElectronicGadget
    {
        ……
    };
    MP3Player mp;
    mp.checkOut();//歧义,调用哪个checkOut?

(2)为什么引发歧义?

虽然上面两个函数一个是public,一个是private,但还是有歧义。

这与C++用来解析(resolving)重载函数调用的规则相符:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用是最佳匹配。找出最佳匹配函数后才检验其可取用性
本例的两个checkouts有相同的匹配程度,没有所谓最佳匹配。因此ElectronicGadget::checkout 的可取用性也就从未被编译器审查。

(3)解决名称歧义的方法

调用时写清楚调用的函数版本,符合编译器规则就能够被正确调用。

mp.BorrowableItem::checkOut();	    //成功调用

2、菱形继承

多重继承的意思是继承一个以上的base classes,但这些base classes并不常在继承体系中又有更高级的base classes,因为那会导致要命的“菱形多重继承”:
《Effective C++》 笔记_第10张图片

class File { ... };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... };

任何时候如果你有一个继承体系而其中某个基类和某个派生类之间有一条以上的相通路线,你就必须面对这样一个问题:是否打算让基类内的成员变量经由每一条路径被复制?

两个方案C++都支持——虽然其缺省做法是执行复制。如果不想复制,必须令那个带有此数据的基类(也就是File)成为一个虚基类,令所有直接继承自它的派生类采用虚继承:

class File { ... };
class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile { ... };

虚继承的代价

  • virtual 继承的classnon-virtual继承的class体积要大、访问成员变量的速度也相比较慢,此种细节因编译器不同而异。
  • virtual 继承的初始化及赋值情况复杂,且不直观:virtual base的初始化责任是由继承体系中的最低层(most derived)class负责,这暗示
    • classes若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些bases距离多远;
    • 当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

对于virtual base classes的建议

  • 如非迫不得已,不使用虚继承;
  • 如果你必须使用virtual base classes,尽可能避免在其中放置数据。这么一来你就不需担心这些classes身上的初始化(和赋值)所带来的诡异事情了。

3、多重继承的应用

多重继承有一个通情达理的应用:将"public继承自某接口”和"private继承自某实现”结合在一起:

class IPerson {
public:
    virtual ~IPerson();
    virtual std::string name()const = 0;
    virtual std::string birthDate()const = 0;
};
 
class DatabaseID {};
 
class PersonInfo {
public:
    explicit PersonInfo(DatabaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName()const;
    virtual const char* theBirthDate()const;
private:
    virtual const char* valueDelimOpen()const;
    virtual const char* valueDelimClose()const;
};
 
class CPerson :public IPerson, private PersonInfo {
public:
    explicit CPerson(DatabaseID pid) :PersonInfo(pid) {}
 
    virtual std::string name()const = 0{
        return PersonInfo::theName();
    }
    virtual std::string birthDate()const = 0 {
        return PersonInfo::theBirthDate();
    }
private:
    virtual const char* valueDelimOpen()const;
    virtual const char* valueDelimClose()const;
};

4、 有关多重继承的建议

多重继承只是面向对象的一个工具。和单一继承比较,它通常比较复杂,使用上也比较难以理解,所以如果你有个单一继承的设计方案,而它大约等价于一个多重继承设计方案,那么单一继承设计方案儿乎一定比较受欢迎。
如果你唯一能够提出的设计方案涉及多重继承,你应该更努力想一想——几乎可以说一定会有某些方案让单一继承行得通
然而多重继承有时候的确是完成任务之最简洁、最易维护、最合理的做法,果真如此就别害怕使用它。

请记住:

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。尽量避免让虚基类含有数据成员。
  • 多重继承的确有正当用途。其中一个情节涉及"public继承某个Interface class"和“private继承某个协助实现的class"的组合。



7、模板与泛型编程

条款41:了解隐式接口和编译期多态

1、显式接口和运行期多态

(1)简介

面向对象编程总是以显式接口和运行期多态来解决问题。例如:

class Widget{ 
public: 
    Widget(); 
    virtual ~Widget(); 
    virtual std::size_t size() const; 
    virtual void normalize(); 
    virtual swap(Widget& other); 
}; 

void doProcessing(Widget& w) 
{ 
    if (w.size() > 10 && w != someNastyWidget){ 
        Widget temp(w); 
        temp.normalize(); 
        temp.swap(w); 
    } 
}

(2)所谓的显式接口

由于w的类型被声明为Widget,因此w需要Widget接口,并且我们可以在源码中找到这个接口,看到源码的样子,所以称为是显式接口

(3)所谓的运行期多态

由于Widget的某些函数是虚函数,因此w的某些函数在运行期间才可以根据w的类型动态调用相关版本的函数,这就是所谓的运行期多态

2、 隐式接口和编译期多态

(1)泛型编程与面向对象编程的不同之处

在泛型编程中,显式接口与运行期多态仍有使用,但是其主要应用的是隐式接口和编译期多态。

例如将刚才的函数改为函数模板:

template<typename T> 
void doProcessing(T& w) 
{ 
    if (w.size() > 10 && w != someNastyWidget){ 
        T temp(w); 
        temp.normalize(); 
        temp.swap(w); 
    } 
}

(2)所谓的隐式接口

void doProcessing(T& w)   //w需要支持的操作都是隐式接口

这个时候w发生了什么样的改变呢?
w所需要支持的接口需要当函数模板具现化时执行于w身上的操作决定(执行了什么操作,说明w一定需要支持这些接口),例子中w使用了size、normalize、swap函数、copy构造函数、不等比较。并且if语句中还有一个长表达式。这所有的函数与长表达式便是T必须支持的一组隐式接口(其实就是w需要被约束的东西)。

(3)所谓的编译期多态

使用到w的任何函数调用,都可能会造成模板具现化,这样的函数具现化发生在编译期。“以不同的template参数具体化函数模板”会导致调用不同的函数,这便是所谓的编译期多态

3、隐式接口和显式接口的不同之处

通常显式接口是由函数的签名式(函数名称、参数类型、返回类型)构成。

但是隐式接口不是基于签名式的,而是由一组有效表达式构成。
例如:

template<typename T>
void doProcess(T& w) {
         if(w.size()>10&&w!=someNastyWidget)
         ……
}
w.size()>10&&w!=someNastyWidget//这就是所谓的隐式接口之一,是一组有效表达式。

w的隐式接口似乎有下述的约束:

  • 提供size()函数,返回整数值
  • 支持!= 操作符重载,用来比较两个T对象

由于运算符重载的也行,上面的两个约束都不需要满足:

  • 对于size()来说,size()可能从基类继承而来,这个成员函数不需要返回一个整数值,甚至不需要返回一个数值类型,其甚至不需要返回一个定义有operator>的类型。它唯一要做的是返回一个类型为X的对象,而X对象加上一个int(10的类型)必须能够调用一个operator>
  • T并不需要支持operator!=,因为以下这种情况也是可以的:operator!=接受一个类型为X的对象和一个类型为Y的对象,T可被转换为X而someNastyWidget的类型可被转换为Y,这样就可以有效调用operator!=。
  • 注意:上述并未考虑这样的可能性:operator&&被重载,从一个连接词改变或许完全不同的某种东西,从而改变上述表达式的意义。

此外,隐式表达式还包括:上述表达式需要与bool兼容,copy构造函数、normalize和swap 也 都必须对T型对象有效。

记住:

  • classes和templates都支持接口(interfaces)和多态(polymorphism)。
  • 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
  • 对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

条款42:了解typename的双重意义

1、意义一

typename可以在template中声明类型参数,此时等价于class:

//两者是等价的
template<class T> class Widget;
template<typename T> class Widget;

2、意义二

可以用来告诉编译器后面跟随的名称是一个类型名。

//编译错误,无法通过
template<typename C>
void print2nd(const C& container)
{
    if (container.size() >= 2) {
        C::const_iterator iter(container.begin()); //初始化迭代器,绑定到第一个元素上
        ++iter;
        int value = *iter;
        std::cout << value;
    }
}

关于“嵌套从属名称”和“非从属名称”的概念:

  • 从属名称:
    • 模板内的一个名称依赖于template的某个参数,那么其就是从属名称,比如vector
    • 当一个从属名称位于class作用域内时(即处于作用域限定符::后面),我们称其为嵌套从属名称。因此上面的const_iterator就是一种嵌套从属名称
  • 非从属名称:上面的value变量,其类型就是int,不依赖于模板参数,因此我们称int为非从属名称。

解析规则

在我们知道C是什么之前,没有任何办法可以知道C::const-iterator是否为一个类型或是静态变量等等。而当编译器开始解析template print2nd时,尚未确知C是什么东西。
C++有个规则可以解析(resolve)此一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。此规则有个例外,稍后我会提到。

解决的方法就是在其之前放置关键字tyname:

template<typename C>
void print2nd(const C& container)
{
    if (container.size() >= 2) {
        //使用typename,显式告诉编译器,const_iterator是一个类型
        typename C::const_iterator iter(container.begin());
        ++iter;
        int value = *iter;
        std::cout << value;
    }
}

C++11后还有其他办法,比如使用auto代替C::const_iterator

一般性规则:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename
但有两个例外:typenane不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列表)中作为base class修饰符。例如:

template<typename T>
class Derived :public Base<T>::Nested //此处不可以使用typename
{
public:
    explicit Derived(int) 
        :Base<T>::Nested(x)//此处不可以使用typename
    {
        typename Base<T>::Nested temp; //此处可以使用typename
    }
};

总之,typename关键字告诉了编译器把一个特殊的名字解释成一个类型。保险起见,你应该在所有编译器可能错把一个type当成一个变量的地方使用typename。

记住:

  • 声明template参数时,前缀关键字class和typename可互换;
  • 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。

条款43:学习处理模板化基类内的名称

1、引出问题

在继承模板基类时,C++拒绝在模板化基类(templatized base classes)内寻找继承而来的名称,例如,对于以下模板基类:

template<typename T>
class Base{
public:
    void fun(){
        ...
    }
}

以下代码通不过编译:

template<typename T>
class Derived:public Base<T>{
public:
    void useFun(){
        fun();  //通不过编译,因为编译器拒绝在Base类模板中查找fun
    }
}

2、原因

因为基类模板(base classe templates)有可能被特化,而那个特化版本可能不提供和一般性template相同的接口(如下),因而C++拒绝在模板化基类中查找继承而来的名称。

全特化版本(不再有fun函数):

template<>
class Base<int>{	
public:
    void foo(){//fun()被 foo()替代
        ...
    }
}

3、解决办法

我们有三种办法令C++"不进入templatized base classes观察”的行为失效:

  1. 在基类函数调用动作之前加上this->,即将对fun的调用改为如下:

    template<typename T>
    class Derived:public Base<T>{
    public:
        void useFun(){
            this->fun();  //通不过编译,因为编译器拒绝在Base类模板中查找fun
        }
    };
    
  2. 使用using 声明式,使编译器在模板作用域中查找改名字:

    template<typename T>
    class Derived:public Base<T>{
    public:
        void useFun(){
            this->fun();  //通不过编译,因为编译器拒绝在Base类模板中查找fun
        }
    };
    

    这里using声明式的作用和条款33不同,它解决的并不是基类名字被派生类掩盖的问题,而是编译器不进入base class作用域内查找的问题。

  3. 明确指出被调用的函数位于base class内:

    template<typename T>
    class Derived:public Base<T>{
    public:
        void useFun(){
            Base<T>::fun();  //通不过编译,因为编译器拒绝在Base类模板中查找fun
        }
    };
    

    这往往是最不让人满意的一个解法,因为如果被调用的函数是虚函,上述的"明确资格修饰"(explict qualification)会关闭"virtual绑定行为"。

记住:
可在derived class templates内通过"this->"指涉base class templates内的成员名称,或通过使用using声明式,或藉由一个明白写出的"base class资格修饰符”完成。

条款44:将与参数无关的代码抽离template

1、代码重复与对应策略

  • 对于函数之间的代码重复,可以将这些函数共同的部分抽取出来,然后放在另一个函数中,让原本那些函数调用这个新函数;
  • 对于类之间的代码重复,可以将这些类中共同的部分抽取出来,建立一个新类,然后让原本那些类使用继承或符合;
  • 然而在模板中,重复时隐晦的,因为只有template被实例化时,你才知道template产生了什么内容。例如:

2、模板中代码重复

1)非类型参数引起的代码重复

以下是用于操作方矩阵的类模板,该矩阵还支持一个逆矩阵运算的方法。

//矩阵的元素类型为T,矩阵的大小为n*n类型(模板第二个类型为非类型参数)
template<typename T, std::size_t n>
class SquareMatrix {
public:
    void invert(); //求逆矩阵
};

注:n为非类型参数
那么对于以下代码:

SquareMatrix<double,5> sm1;
sm1.invert();
 
SquareMatrix<double,10> sm2;
sm2.invert();

具现化sm1和sm2时,尽管它们类型参数相同,都为double,但由于非类型参数不同,SquareMatrix会产生两份实体,包括其成员函数invert等,而它们的差别仅在于非类型参数n,这明显产生了大量重复的代码,初步改进方法如下:

//不论SquareMatrix产生多少粉,SquareMatrixBase在代码中只产生一份
template<typename T>
class SquareMatrixBase {
protected:
    void invert(std::size_t matrixSize);
};
 
template<typename T,std::size_t n>
class SquareMatrix :private SquareMatrixBase<T>	//私继承:is-implemented-in-terms of关系
{
private:
    using SquareMatrixBase<T>::invert; //避免派生类隐藏基类的invert函数
public:
    void invert() {
        this->invert(n); //为什么使用this,参阅条款43
    }
};

以上代码中,base class SquareMatrixBase含有一个带大小参数的invert,与之前SquareMatrix不同,它只对矩阵元素类型参数化,并不对矩阵大小参数化。因此对于同一类型参数,SquareMatrixBase只具现化出一份实体,结果就是类型参数相同,非类型参数不同的SquareMatrix虽然具现化出不同实体,但继承的是同一个SquareMatrixBase。SquareMatrixBase只是用来帮助derived classes实现,两者关系为is-implemented-in-terms of,因而采用priva继承。
至于矩阵内容,可以储存在派生类对象中或堆中,基类包含指向矩阵内容的指针。

效率比较

  • 最初的版本中,尺寸是个编译期常量,因此可藉由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数,这在共享版本中无法做到。
  • 代码共享版本中,不同大小的矩阵只拥有单一版本的invert()(SquareMatrixBase中的),可减少执行文件大小,也就因此降低程序的work set大小,并强化指令告诉缓存区内的引用集中化。这些都可能使程序执行得更快速 ,超越前者的优化效果。

所谓work set是指对一个在“虚内存环境”下执行的进程而言,其所使用的那一组内存页。

2)类型参数引起的代码重复

本条款是讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀,比如:

  • 许多平台上int和long有着相同的二进制表述, 所以vectorvector的成员函数有可能完全相同——造成代码重复/膨胀。有些链接器(linkers)会合并相同的函数实现码,但有些不会。后者意味着模板被具体化为int和long两个版本,并因此造成代码重复/膨胀。
  • 大多数平台上,所有指针类型有相同的二进制表述,因此template持有指针者(例如list,list,list*>等等)常常应该为每一份成员函数提供唯一一份被不同类型参数共享的底层实现。也就是说,如果某些成员函数操作强类型指针(T*)等,应该令它们调用另一个操作无类型指针void*的函数,由后者完成实际工作。某些C++标准程序库的实现版本的确为vector,list等templates做了这件事。

记住:

  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
  • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
  • 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。

条款45:运用成员函数模板接受所有兼容类型

1、问题引出

class Top {};
class Middle :public Top {};
class Bottom :public Middle {};
 
//自己设计的智能指针类
template<typename T>
class SmartPtr
{
public:
    explicit SmartPtr(T* realPtr);
};
 
int main()
{
    //下面是我们希望能完成的,但是还没有实现
    SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
    SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
    SmartPtr<const Top> pct2 = pt1;
 
    return 0;
}

对于上述不同参数的智能指针之间相互转换,如何实现?

2、成员函数模板

解决方法:使用成员函数模板。

template<typename T>
class SmartPtr
{
public:
    //拷贝构造函数,是一个成员函数模板
    typename<typename U>
    SmartPtr(const SmartPtr<U>& other);
};
  • 但是还有一些问题没有解决:
    • 那就是,对于类继承来说,派生类指针可以转换为基类指针,但是基类指针不能转换为派生类指针
    • 类似的,对于普通类型来说,我们不能将int转换为double
  • 因此,即使我们设计了成员函数模板,那么还需要考虑一些转换的特殊情况

解决方法

  • 我们可以为自己的只能指针类提供一个类似于shared_ptr的get()成员函数,这个函数返回智能指针锁封装的那个原始指针

  • 设计的代码如下:

    template <typename T>
    class SmartPtr{
    public:
        typename<typename U>
        SmartPtr(const SmartPtr<U>& other)
            : heldPtr(other.get()) { ... }
        T* get() const { return heldPtr; }
    private:
        T* heldPtr;
    };
    

    这样就把检查底层指针能否转换的任务交由底层指针来自行检查。

同理,成员模板函数(member template function)可用于支持赋值操作:

template<typename T>
class shared_ptr{
public:
    template<class Y>    //声明类型参数时class和typename含义相同
        explict shared_ptr(Y* p);
    template<class Y>
        shared_ptr(shared_ptr<Y> const& r);
    template<class Y>
        explict shared_ptr(weak_ptr<Y> const& r);
    template<class Y>
        shared_ptr& operator=(shared_ptr<Y> const& r);
    ...
}

以上所有构造函数都为explict,唯有泛化copy构造函数除外,这意味着shared_ptr允许shared_ptr之间的隐式类型转换,而禁止原始指针或其他智能指针类型向shared_ptr的转换。

注意事项:
虽然成员函数模板(member function template)可以实例化出用于shared_ptrshared_ptr转换的copy构造函数,但如果没有声明copy构造函数,编译器还是会合成一个,所以不能依赖于成员函数模板的实例化,要手动声明一个普通的copy构造函数,copy赋值操作符也是一样。

记住:

  • 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
  • 如果你生命member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。

条款46:需要类型转换时请为模板定义非成员函数

1、函数模板中参数隐式转换失效

对于条款24Rational的例子,变为模板类时,规则不再成立:

template<typename T>
class Rational {
public:
    Rational(const T& numerator = 0, const T& denominator = 1)
        :x(numerator), y(denominator) {}
    const T numerator() const { return x; }
    const T denominator()const { return y; }
private:
    T x;
    T y;
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {
   return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * lhs.denominator());
}

Rational<int> oneHalf(1,2);
Rational<int> oneThird(1,3);
Rational<int> result=oneHalf*oneThird;
result=oneHalf*2;   //出错

看起来 oneHalf*2 似乎应该使函数模板operator*具现化并调用它,但实际上编译不通过。

2、原因

根本原因在于"编译器在template实参推导过程中从不将隐式转换纳入考虑":转换函数在函数调用过程中的确被使用(如果operator*是一个函数而不是函数模板的话),但在调用一个函数之前,必须要知道那个函数存在,而为了知道它,必须先为相关的function template推导出参数类型(然后才可将适当的函数具现化出来)。
然而template实参推导过程中并不考虑采纳"通过构造函数而发生的"隐式类型转换!

3、解决办法

只要利用一个事实就可以改变这种现状:template class内的friend声明式可以指涉该特定函数。也就是说Rational可以声明operator为它的一个friend函数,class template并不依赖于template实参推导,因此编译器总是能够在class Rational具现化时得知T,因此,令Rational class声明适当的operator为其friend函数可简化整个问题:

template<typename T>
class Rational{
public:
    friend Rational operator*(const Rational& lhs,
                                           const Rational& rhs);
    ...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
                                             const Rational<T>& rhs){
    ...
}

现在对operator*的调用就可以通过编译了,发生的改变是:oneHalf被声明时,class Rational被具现化出来,而作为过程的一部分,friend函数operator*也就被作为一个函数而非函数模板自动声明出来,因此编译器可以在调用它时使用隐式转换函数.

此时还未结束,以上代码虽然通过了编译,但却无法链接——Rational operator*(const Rational&lhs,const Rational&rhs)已经被声明出来,但却没有定义。使用template是行不通的,因为此当Rational被具现化时,operator*只是作为一个普通的函数声明被具现化出来,编译器不会认为它和operator*函数模板有关联而为它具现化出一个函数实体。

解决办法就是将operator*函数本体合并至其声明式内:

template<typename T>
class Rational {
public:
    friend const Rational operator*(const Rational& lhs, const Rational& rhs){
        return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * lhs.denominator());
    }
    Rational(const T& numerator = 0, const T& denominator = 1)
        :x(numerator), y(denominator) {}
    const T numerator() const { return x; }
    const T denominator()const { return y; }
private:
    T x;
    T y;
};

此时对operator*的调用可编译链接并执行。

4、friend关键字在模板中的重要作用

使用模板函数不能实现隐式转换,因此必须使用普通函数代替模板函数:

在本条款中,friend的作用不再是提升函数或类的访问权限,而是使类型转换发生在所有参数身上为了使该函数被自动具现化,需要把它声明在类内部,而在类内部声明non-member函数的唯一办法就是令它成为一个friend

优化:
由于operator*需要在Rational内部定义,它被默认声明为inline,为了使这种inline声明带来的冲击最小(本例中operator*已经是一个单行函数,但更复杂的函数也许需要这样),可以使它调用一个定义域class外部的辅助函数,由该辅助函数完成实际功能:

template<typename T>
const Rational<T> doMultiply(const Rational& lhs,
                                             const Rational& rhs){
    ...
}
template<typename T>
class Rational{
public:
    friend Rational operator*(const Rational& lhs,
                                           const Rational& rhs){
        doMultiply(lhs,rhs);
    ...
};

doMultiply()不支持混合式操作(例如将Rational对象和一个int相乘),但是该函数只由operator*()调用,而operator*()支持混合操作,因此功能完善。

在一个class template内,template名称可被用来作为"template和其参数”的简略表达方式,所以在Rational内我们可以只写Rational而不必写Rational,本例中这只节省我们少打几个字,但若出现许多参数,或参数名称很长,这可以节省我们的时间,也可以让代码比较干净。

记住:
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”,因为模板函数不能实现参数隐式转换,而friend可以使函数具现化(将模板函数具现化为具体函数)

条款47:请使用traits classes表现类型信息

traits:一种技术,允许你在编译期间取得某些类型信息。

本条款以 iterator_traits 为例介绍了如何实现traits类,以及如何使用traits类以实现advance

STL提供了很多的容器、迭代器和算法,其中的 advance 便是一个通用的算法,可以让一个迭代器移动n步:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);     // 如果d小于0,就逆向移动

1、STL迭代器回顾

  • 最简单的迭代器是输入迭代器(input iterator)和输出迭代器(output iterator), 它们只能向前移动,可以读取/写入它的当前位置,但只能读写一次。比如 ostream_iterator 就是一个输出迭代器。
  • 比它们稍强的是前向迭代器(forward iterator),可以多次读写它的当前位置。 单向链表(slist,STL并未提供)和TR1哈希容器的迭代器就属于前向迭代器
  • 双向迭代器(bidirectional iterator)支持前后移动,支持它的容器包括 set, multiset, map, multimap
  • 随机访问迭代器(random access iterator)是最强的一类迭代器,可以支持 +=, -= 等移动操作,支持它的容器包括 vector, deque, string 等。

Tag 结构体

对于上述五种迭代器,C++ 提供了五种专属卷标结构(tag struct) 来标识迭代器的类型,它们之间是“is-a”的关系:

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};

现在回到 advance 的问题,它的实现方式显然取决于 Iter 的类型:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
  if (iter is a random access iterator) {
    iter += d;                                      // use iterator arithmetic
  }                                                  // for random access iters
  else {
    if (d >= 0) { while (d--) ++iter; }              // use iterative calls to
    else { while (d++) --iter; }                     // ++ or -- for other
  }                                                  // iterator categories
}

怎么得到 Iter 的类型呢?这正是 traits 的作用。

2、Traits

Traits 不是关键字,也不是 std 类型或模板,它只是 C++ 中的一种编程惯例,允许我们在编译期得到类型的信息。 用 Bjarne 的话讲,Traits 是一个用来携带信息的很小的对象(或结构), 在其他对象或算法中用这一信息来确定策略或实现细节。

traits的另一个需求在于 advance 对与基本数据类型也能正常工作,比如 char*。所以traits不能借助类来实现, 于是我们把traits放到模板中。比如:

template<typename IterT>          // template for information about
struct iterator_traits;           // iterator types

iterator_traits 将会标识 IterT 的迭代器类别。iterator_traits 的实现包括两部分:

  • 用户定义类型的迭代器
  • 基本数据类型的指针

1)用户自定义类的迭代器

在用户定义的类型中,typedef 该类型支持迭代器的 Tag,例如 deque 支持随机迭代器:

template < ... >                    // template params elided
class deque {
public:
  class iterator {
  public:
    typedef random_access_iterator_tag iterator_category;
  };
};

然后在全局的 iterator_traits 模板中 typedef 那个用户类型中的 Tag,以提供全局和统一的类型识别。

template<typename IterT>
struct iterator_traits {
  typedef typename IterT::iterator_category iterator_category;
};

2)基本数据类型的指针

上述办法对基本数据类型的指针是不起作用的,我们不能在指针里面 typedef 一个 Tag 。
解决办法: 偏特化 iterator_traits,因为内置类型指针都是可以随机访问的:

template<typename IterT>               // partial template specialization
struct iterator_traits<IterT*>{
  typedef random_access_iterator_tag iterator_category;
};

你已经看到了实现一个traits类的整个过程:

  1. 确定你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将来可取得其分类(category);
  2. 为那个信息起一个名字。比如 iterator_catetory
  3. 提供一个template和一组特化版本,内含希望支持的类型信息。

3、advance 的实现

我们已经用 iterator_traits 提供了迭代器的类型信息,是时候给出 advance 的实现了。

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
  if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
    typeid(std::random_access_iterator_tag))
  ...
}

由于iterator_traits::category在编译期即可确定,但if语句的判断却要在运行期核定,这不仅浪费时间,也会造成可执行文件膨胀。

实际上,C++提供了在编译期完成核定的方法:函数重载。

当你重载某个函数,你必须详细叙述各个重载件的参数类型。当你调用f,编译器便根据传来的实参选择最适当的重载件。编译器的态度是“如果这个重载件最匹配传递过来的实参,就调用这个f;如果那个重载件最匹配,就调用那个f;如果第三个f最匹配,就调用第三个f!”依此类推。这正是一个针对类型而发生的“编译期条件句”。

因此,我们可以产生两种重载函数,但接受不同的iterator_category对象,由它们完成advance的实际功能,因此advance的最终实现版本如下:

template<typename IterT,typename DistT>
void advance(Iter& iter,Dist d){
    doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
}                                              

// 随机访问迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) {
  iter += d;
}

// 双向迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) {
  if (d >= 0) { while (d--) ++iter; }
  else { while (d++) --iter; }
}

// 输入迭代器
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) {
  if (d < 0 ) {
     throw std::out_of_range("Negative distance");    // see below
  }
  while (d--) ++iter;
}

其中,由于之前iterator卷标结构的继承关系,doAdvance的input_iterator版本也可以被forward iterator调用。

4、使用说明

总结一下上面代码是如何使用traits类的:

  1. 建立一组重载函数(身份像劳工)或函数模板(例如doAdvance),彼此之间的差异仅在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。
  2. 建立一个控制函数(身份像工头)或函数模板(例如advance),它调用上述"劳工函数”并传递traits classes所提供的信息。

STL库中有类似的char_traits用于保存字符类型的相关信息,numeric_limits用于保存数值类型相关信息等等。

C++11头文件导入了许多新的traits classes用以提供类型信息,包括is_fundamental(判断T是否为内置类型),is_array(判断T是否为数组类型),is_base_of(判断T1,T2是否相同,抑或T1是T2的base classes)等。

记住:

  • Traits classes使得“类型相关信息”在编译期可用。它们以templates和"templates特化”实现。
  • 整合重载技术(overloading)后,traits classes有可能在编译期对类型执行if…else测试。

条款48:认识template元编程

模板元编程(Template Metaprogramming,TMP)就是利用模板来编写那些在编译时运行的C++程序。 模板元程序(Template Metaprogram)是由C++写成的,运行在编译器中的程序。当程序运行结束后,它的输出仍然会正常地编译。

C++并不是为模板元编程设计的,但自90年代以来,模板元编程的用处逐渐地被世人所发现。

  • 模板编程提供的很多便利在面向对象编程中很难实现;
  • 程序的工作时间从运行期转移到编译期,可以更早发现错误,运行时更加高效。
  • 在设计模式上,可以基于不同的策略,自动组合而生成具体的设计模式实现。

1、模板元编程

TMP后来被证明是图灵完全的,这意味着TMP可以用来计算任何可计算的问题。你可以声明变量、执行循环、编写和调用函数等等。 但它的使用风格和普通C++完全不同。

我们来看看TMP中如何执行一个循环:

template<unsigned n>
struct Factorial{
    enum{ value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0>{
    enum{ value = 1 };
};

int main(){
    cout<<Factorial<5>::value;
}

2、TMP的用途

为了更好地理解TMP的重要性,我们来看看TMP能干什么:

  1. 确保量纲正确。在科学计算中,量纲的结合要始终保持正确。比如一定要单位为”m”的变量和单位为”s”的变量相除才能得到一个速度变量(其单位为”m/s”)。 使用TMP时,编译器可以保证这一点。因为不同的量纲在TMP中会被映射为不同的类型。
  2. 优化矩阵运算。比如矩阵连乘问题,TMP中有一项表达式模板(expression template)的技术,可以在编译期去除临时变量和合并循环。 可以做到更好的运行时效率。
  3. 自定义设计模式的实现。设计模式往往有多种实现方式,而一项叫基于策略设计(policy-based design)的TMP技术可以帮你创建独立的设计策略(design choices),而这些设计策略可以以任意方式组合。生成无数的设计模式实现方式。

记住:

  • Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
  • TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。



8、定制new和delete

条款49:了解new handler的行为

当operator new无法满足某一内存分配需求时,它会先调用客户指定的错误处理函数(如果客户未指定,它就会抛出异常),即new-handler,并且会一直调用直到内存足够。
为了指定这个"用以处理内存不足"的函数,客户必须调用set_new_handler,那是声明于的一个标准库函数:

namespace std{
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

throw()是一个异常声明,表示保证不抛任何异常,但不是绝对。如果抛出异常则是严重错误。

该函数用于指定当无法分配足够内存时调用的函数,返回的函数指针指向在此之前的处理函数。使用:

void outOfMem(){
    std::cout<<"Unable to alloc memory";
    std::abort();
}
int main(){
    std::set_new_handler(outOfMem);
    int *p = new int[10000000000000L];
}

一个设计良好的new-handler应该做以下事情:

  1. 让更多内存可被使用.实现这个目的的策略之一是,在程序一开始就分配一大块内存,当new-handler第一次被调用(说明内存不足)时,就把它们归还给程序使用.

  2. 安装另一个new-handler,如果当前new-handler无法获取更多内存但它知道另一个new-handler有此能力,它可以调用set_new_handler将那个new-handler设为默认new-handler使得下一次operator new调用的new-handler是最新的那个(令一种策略是令当前new-handler修改自己的行为,方法是让它修改会影响当前new-handler行为的static数据,namespace数据,global数据等)

  3. 卸除new-handler.将NULL指针传给set_new_handler,当operator new分配内存不成功时抛出异常.

  4. 抛出bad_alloc(或派生自bad_alloc的)异常.这样的异常不会被operator new捕捉,因而会被传递到内存索求处.

  5. 不返回.调用abort或exit.(正如outOfMem所做的那样)

1、类型相关new-handler

std::set_new_handler设置的是全局的bad_alloc的错误处理函数,C++并未提供类型相关的bad_alloc异常处理机制。 但我们可以重载类的operator new,当创建对象时暂时设置全局的错误处理函数,结束后再恢复全局的错误处理函数。

比如Widget类,首先需要声明自己的set_new_handleroperator new

class Widget{
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void * operator new(std::size_t size) throw(std::bad_alloc);
private:
    static std::new_handler current;
};

// 静态成员需要定义在类的外面
std::new_handler Widget::current = 0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
    std::new_handler old = current;
    current = p;
    return old;
}

关于abort, exit, terminate的区别:abort会设置程序非正常退出,exit会设置程序正常退出,当存在未处理异常时C++会调用terminate, 它会回调由std::set_terminate设置的处理函数,默认会调用abort

最后来实现operator new,该函数的工作分为三个步骤:

  1. 调用std::set_new_handler,把Widget::current设置为全局的错误处理函数;
  2. 调用全局的operator new来分配真正的内存;如果分配内存失败,Widget::current将会抛出异常;
  3. 不管成功与否,都卸载Widget::current(通过RAII),并安装调用Widget::operator new之前的全局错误处理函数。

1)重载operator new

结合资源管理类实现Widget::operator new如下:

class NewHandlerHolder {
public:
    explicit NewHandlerHolder(std::new_handler nh) :handler(nh) {}
    ~NewHandlerHolder() { set_new_handler(handler); }
private:
    new_handler handler;  //用于保存当前global new-handler
    NewHandlerHolder(const NewHandlerHolder&);
    NewHandlerHolder& operator=(const NewHandlerHolder&);
};
void* Widget::operator new(std::size_t size) throw(std::bad_alloc) {
    NewHandlerHolder h(std::set_new_handler(current));//安装new-handler并使h保存global new-handler
    return ::operator new(size);  //h析构时恢复global new-handler
}

2)使用

客户使用Widget的方式也符合基本数据类型的惯例:

void outOfMem();
Widget::set_new_handler(outOfMem);

Widget *p1 = new Widget;    // 如果失败,将会调用outOfMem
string *ps = new string;    // 如果失败,将会调用全局的 new-handling function,当然如果没有的话就没有了
Widget::set_new_handler(0); // 把Widget的异常处理函数设为空
Widget *p2 = new Widget;    // 如果失败,立即抛出异常

3)通用基类

以上代码具有一般性,因此可以考虑将其设为模板,由其他类继承,从而继承这种"可以设定类之专属new-handler"的能力:

template<typename T>
class NewHandlerSupport{
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static std::operator new(std::size_t size) throw(std::bad_alloc);
    ...
private:
    static std::new_handler currentHandler;
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler=0;
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw(){
    std::new_handler oldHandler=currentHandler;
    currentHandler=p;
    return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::::operator new(std::size_t size) throw(std::bad_alloc){
    NewHandlerHolder h(std::set_new_handler(currentHandler));//安装new-handler并使h保存global new-handler
    return ::operator new(size);  //h析构时恢复global new-handler
}

有了这个class template,就可以为Widget和其他类添加set_new_handler支持能力了——只要令Widget继承自NewHandlerSupport就好:

class Widget:public NewHandlerSupport<Widget>{
    ...
}

类模板NewHandlerSupport看起来相当奇怪,因为参数T从未被使用,实际上参数T只是用来区分不同的derived class,使继承自NewHandlerSupport的每一个class都有自己独立的static成员变量currentHandler。

至于Widget继承自一个以Widget为参数的类模板具现化的类,这是一个有用的技术,叫做——“怪异的循环模板模式”(curiously recurring template pattern;CRTP)。

2、nothrow new

1993年之前C++的operator new在失败时会返回null而不是抛出异常。如今的C++仍然支持这种nothrow的operator new:

Widget *p1 = new Widget;    // 失败时抛出 bad_alloc 异常
assert(p1 != 0);            // 这总是成立的

Widget *p2 = new (std::nothrow) Widget;
if(p2 == 0) ...             // 失败时 p2 == 0

由于以上new Widget表达式发生两件事:调用nothrow版的operator new,调用Widget的默认构造函数,因而尽管nothrow版的operator new保证不抛出异常,但这并不能阻止Widget的默认构造函数使用普通new再抛出异常,因而具有局限性。

记住:

  • set_new handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
  • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。

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

1、替换原因

替换标准库提供的operator new或operator delete通常基于以下三个理由:

  1. 用来检测运用上的错误。
    new得到的内存如果没有delete会导致内存泄露,而多次delete又会引发未定义行为。如果自定义operator new来保存动态内存的地址列表,在delete中判断内存是否完整,便可以识别使用错误,避免程序崩溃的同时还可以记录这些错误使用的日志。

  2. 为了强化效能,提高效率。
    全局的new和delete被设计为通用目的(general purpose)的使用方式,通过提供自定义的new,我们可以手动维护更适合应用场景的存储策略。

  3. 为了收集使用上的统计数据。
    在定制new之前,你可能需要先自定义一个new来收集地址分配信息,比如:软件内存区块大小分布,寿命分布,内存归还次序,最大动态分配量等信息。

示例:

struct const int signature=0xDEADBEEF;
typedef unsigned char Byte;
//这段代码还有若干小错误,详下
void* operator new(std::size_t size) throw(std::bad_alloc){
    using namespace std;
    size_t realSize=size+2+sizeof(int);
    void* pMem=malloc(realSize);
    if(!pMem)
        throw bad_alloc();
    *(static_cast<int*>(pMem))=signatrue;
    *(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int)))=signature;
    return static_cast<Byte*>(pMem)+sizeof(int);
}

"这个operator new的主要缺点在于它疏忽了身为这个特殊函数所应该具备的’坚持C++规矩’的态度.例如,条款51提到所有operator news都应该内含一个循环,反复调用某个new-handling函数,这里却没有."此外,还有一个更加微妙的主题:对齐(alignment)。

许多计算机体系结构要求特定的类型必须放在特定的内存地址上。例如它可能会要求doubles的地址必须是8倍数。如果没有奉行这个约束条件,可能导致运行期硬件异常。有些体系结构没有这么严苛,而是宣称如果对齐条件获得满足,便提供较佳效率。例如Intelx86体系结构上的doubles可被对齐于任何byte边界,但如果它是8-byte对齐,其访问速度会快许多。

C++要求所有operator news返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作,所以令malloc返回一个得自malloc的指针是安全的
然而上述operator new并未提供这样的保证,存在不安全性,可能导致程序崩溃或变慢。

因此写一个能优良运作的内存管理器可能并不容易。其实我们还有别的选择:比如去读编译器文档、内存管理的商业工具、开源内存管理工具等

许多编译器已经在它们的内存管理函数中切换到调试状态或志记状态,许多平台已有可以替代编译器自带之内存管理器的商业产品,开放源码领域的内存管理器(如Boost库的Pool内存池)也都可用,因而可能并不需要自己定制operator new和operator delete。

2、替换new和delete的时机

本条款的主题是,了解何时可在“全局性的”或"class专属的”基础上合理替换缺省的new和delete:

  • 为了检测运用错误(如前所示)
  • 为了收集动态分配内存之使用统计信息(如前所示)
  • 为了增加分配和归还速度
    • 系统提供的new往往(虽然并不总是)比自己定义的new慢,特别是当自定义的new针对于某特定类型的对象设计时。
    • class专属分配器是“区块尺寸固定”的分配器实例,例如Boost提供的Pool程序库便是。
    • 如果你的程序是个单线程程序,但你的编译器所带的内存管理器具备线程安全,你或许可以写个布局线程安全的分配器而大幅改善速度。
    • 当然,在获得new和delete有加快速度的价值之前,首先分析的程序,确认程序瓶颈是否发生在这些函数身上。
  • 为了降低缺省内存管理器带来的空间额外开销
    • 系统提供的new往往(虽然并不总是)比自己定义的new慢,它们往往还是用更多内存,因为它们常常在每一个分配区块身上带来一些额外的开销。
    • 针对小型对象而开发的分配器(例如Boost提供的Pool程序库),本质上消除了这样的额外开销。
  • 为了弥补缺省分配器中的非最佳齐位
    • 如前面所说,在x86体系上,double的访问速度最快的原则是它们以8bytes对齐。但是编译器自带的operator new并不保证动态分配而得的double采取8bytes对齐。在这种情况下,将缺省的operator new替换为一个8bytes齐位版本,可以使程序大幅度提升。
  • 为了将相关对象成簇集中
    • 如果你知道将某个数据结构常常放在一起被使用,而你又希望这样数据将“内存页错误”的频率降至最低,那么可以为此数据结构创建另一个heap,这么一来它们就可以被成簇集中在尽可能少的内存页上。
    • new和delete的“placement版本(见条款52)”有可能完成这样的集簇行为。
  • 为了获得非传统的行为
    • 有时候你希望operator new和delete做编译器附带没有的事情:例如希望分配和归还共享内存内的区块,但唯一能够管理该内存的只有C API函数,那么可以写一个定制版的new和delete(很可能是placement版本,见条款52),你可以为此C API套上一件C++外套,你也可以写一个自定义的delete,在其中将所有归还内存内容覆盖为0,增加安全性。

记住:
有许多理由需要写个自己的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。

条款51:编写new和delete时需固守常规

1、operator new

operator new需遵循的规则有:

  • 正确处理size为0的内存申请需求,通过将0改为1;

  • 内含无穷循环,内存不足时:

    • 若new-handler函数为空,抛出bad_alloc异常;
    • 否则调用new-handler函数,并循环 分配内存、调用处理函数。

伪代码如下:

void* operator new(std::size_t size) throw(std::bad_alloc){
    using namespace std;
    if(size==0)
        size=1;
    while(true){
        尝试分配size bytes;
        if(分配成功)
            return(一个指针,指向分配而来的内存);
        new_handler globalHandler=set_new_handler(0);
        set_new_handler(globalHandler);
        if(globalHandler)
            (*globalHandler)();
        else
            throw std::bad_alloc();
    }
}

成员operator new函数

重载operator new为成员函数通常是为了对某个特定的类进行动态内存管理的优化,但会被其派生类继承,因此需要在实现Base::operator new()时,基于对象大小为sizeof(Base)来进行内存管理优化的。

class Base{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    ...
};
class Derived:public Base{
   ...
};

void * Base::operator new(std::size_t size) throw(std::bad_alloc){
    if(size!=sizeof(Base))
        return ::operator new(size);	//使用默认operator new处理
    ...
}

Derived*p =new Derived; //这里调用的是Base::operaotr new!

(当然,有些情况你写的Base::operator new是通用于整个class及其子类的,这时这一条规则不适用。)

对于operator new[](被称为array new),那么operator new[]唯一要做的就是分配一块内存,而无法对array内尚未存在的元素做任何事情,设置无法计算每个元素对象有多大。因为:

  1. 你不知道对象大小是什么。上面也提到了当继承发生时size不一定等于sizeof(Base)
  2. size实参的值可能大于这些对象的大小之和。因为数组的大小可能也需要存储。

2、operator delete

operator delete的情况比较简单,唯一需要注意的是C++保证"删除null指针永远安全",下面是non-member operator delete的伪码:

void operator delete(void* rawMemory) throw(){
    if(rawMemory==0)
        return;
    归还rawMemory所指内存;
}

成员operator delete函数

operator delete的member版本也比较简单,只需要多加一个动作检查删除数量.万一class专属的operator new将大小有误的分配行为转交::operator new执行,大小有误的删除行为也必须转交::operator delete执行:

class Base{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
    static void operator delete(void* rawMemory,str::size_t size) throw();
    ...
};
void Base::operator delete(void* rawMemory,std::size_t size) throw(){
    if(rawMemory==0)
        return;
    if(size!=sizeof(Base)){
        ::operator delete(rawMemory);
        return;
    }
    现在,归还rawMemory所指内存;
    return;
}

当我们将operator delete或operator delete[]定义成类的成员时,该函数可以包含另外一个类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给operator delete的字节数将因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的operator delete函数版本也由对象的动态类型决定。——《C++primer 5th》

记住:

  • operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new_handler。它也应该有能力处理0bytes申请。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
  • operator delete应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。

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

1、placement new和placement delete

placement new(定位new)和place ment delete指的是正常的operator new和operator delete的重载版本,所谓的正常的operator new和delete,指的是拥有以下正常签名式的版本:

void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();  //global作用域中的正常签名式
void operator delete(void*,std::size_t) throw(); //class作用域中的正常签名式

由于C++允许对重载的operator new和operator delete添加额外参数,因而具有额外参数的operator new和operator delete就是placement new和placement delete
众多placement new版本中比较常见的一个是"接受一个指针指向对象该被构造之处"(大多数时placement new特指这一版本),其主要用途是接受一个指针,然后将其返回以供构造函数在其上构造对象,签名式为:

void* operator new(std::size_t,void* pMemory) throw();

2、成对的new/delete

对于以下语句:

Widget* pw1=new Widget;  //Widget是一个类

共有两个函数被调用:一个是用以分配内存的operator new,另一个是用于构造Widget的Widget default构造函数。
假设第一个函数调用成功,第二个却抛出异常,那么步骤1所申请的内存必须被释放,否则就是内存泄露.这个任务客户端无法做到,因此由C++运行期系统来完成:运行期系统会调用步骤一所调用的operator new对应的operator delete版本。所谓"对应",与具有正常签名式的operator new对应的版本就是具有正常签名式的operator delete(1中所列),而对于placement new,"对应"指的是额外参数相同的placement delete。

假设Widget的定义如下:

class Widget{
public:
    ...
    static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc); //placement operator new
    static void operator delete(void* pMemory,std::size_t size) throw();   //正常operator delete
    ...
};

对于以下语句:

Widget* pw=new (std::cerr) Widget;

如果该语句在Widget default构造函数中抛出异常,那么运行期系统有责任取消operator new的分配并恢复旧观,正如以上所言,它需要找到与placement new对应的placement delete:

static void operator delete(std::size_t size,std::ostream& logStream) throw();

结果就是"如果一个带额外参数的operator new没有’带相同额外参数’的对应版operator delete,那么当new的分配动作需要取消并恢复旧观时就没有任何operator delete会被调用",内存泄露也就不可避免。
解决办法就是:为Widget声明并定义一个与之前带额外参数的operator new对应的operator delete:

class Widget{
public:
    ...
    static void* operator new(std::size_t size,std::ostream& logStream) throw(std::bad_alloc); //placement operator new
    static void operator delete(void* pMemory,std::size_t size) throw();   //正常operator delete
    static void operator delete(std::size_t size,std::ostream& logStream) throw(); //对应的operator delete
    ...
};

此时可避免内存泄漏。

需要注意的是,只有当抛出异常时,调用的才会是对应的placement delete,如果以上语句执行正常,那么执行:

delete pw;

调用的是正常版本的operator delete。

3、名称隐藏

在条款33中提到,类中的名称会隐藏外部的名称,子类的名称会隐藏父类的名称。 所以当你声明一个”placement new”时:

class Base{
public:
    static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc);
};
Base *p = new Base;     // Error!
Base *p = new (std::cerr) Base;     // OK

普通的new将会抛出异常,因为”placement new”隐藏了外部的”normal new”。同样地,当你继承时:

class Derived: public Base{
public:
    static void* operator new(std::size_t size) throw(std::bad_alloc);
};
Derived *p = new (std::clog) Derived;       // Error!
Derived *p = new Derived;       // OK

这是因为子类中的”normal new”隐藏了父类中的”placement new”,虽然它们的函数签名不同。条款33中提到,按照C++的名称隐藏规则会隐藏所有同名(name)的东西,和签名无关。

解决办法

为了避免隐藏全局”new”,你在创建自定义的”new”时,需要分别声明这些签名的”new”并调用全局的版本。
为了方便,我们可以为这些全局版本的调用声明一个父类StandardNewDeleteForms

class StandardNewDeleteForms {
public:
  // normal new/delete
  static void* operator new(std::size_t size) throw(std::bad_alloc) { return ::operator new(size); }
  static void operator delete(void *pMemory) throw() { ::operator delete(pMemory); }

  // placement new/delete
  static void* operator new(std::size_t size, void *ptr) throw() { return ::operator new(size, ptr); }
  static void operator delete(void *pMemory, void *ptr) throw() { return ::operator delete(pMemory, ptr); }

  // nothrow new/delete
  static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); }
  static void operator delete(void *pMemory, const std::nothrow_t&) throw() { ::operator delete(pMemory); }
};

然后在用户类型Widgetusing StandardNewDeleteForms::new/delete即可使得这些函数都可见:

class Widget: public StandardNewDeleteForms {           // inherit std forms
public:
   using StandardNewDeleteForms::operator new;         
   using StandardNewDeleteForms::operator delete;     

   static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc);   // 自定义 placement new
   static void operator delete(void *pMemory, std::ostream& logStream) throw();            // 对应的 placement delete
};

记住:

  • 当你写一个placement operator new时,请确定也写出了对象的placement operator delete。如果没有这样,你的程序可能会发生隐微而时断时续的内存泄漏。
  • 当你声明placement new和placement delete,请确定不要无意识(非故意)地掩盖了它们的正常版本。



9、杂项讨论

条款53:不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最苛刻)警告级别下争取“无任何警告”的荣誉。
  • 不要过度依赖编译器的警告能力,因为不同的编译器对待事情的态度不同,一旦移植到另一个编译器上,你原本依赖的警告信息可能会消失。

条款54:让自己熟悉包括TR1在内的标准程序库

  • 智能指针在非环形数据结构中防止资源泄漏很有帮助,但如果两个或多个对象内含trl::shared_ptrs并形成环状,这个环形会造成每个对象的引用次数都超过0-即使指向这个环形的所有指针都已被销毁(也就是这一群对象整体看来已无法触及)。这就是为什么又有个weakptrs的原因。
    weak ptrs的设计使其表现像是“非环形shared ptr-based数据结构”中的环形感生指针。weakptrs并不参与引用计数的计算;当最后一个指向某对象的shared ptr被销毁,纵使还有个weak ptrs继续指向同一对象,该对象仍旧会被删除。这种情况下的weakptrs会被自动标示无效。
  • mem_fn:这是个语句构造上与成员函数指针一致的东西。提供了统一的方式来适配成员函数指针。
  • TR1::reference_wrapper:使得引用更像一个对象,原本在容器中只能存储指针和对象的。
    ……



参考:

《Effective C++》
《C++primer 5th》
https://www.cnblogs.com/reasno/archive/2015/09.html
https://blog.csdn.net/lintianyi9921/category_9564213.html
https://blog.csdn.net/qq_41453285/category_9690724.html
https://harttle.land/tags.html#Effective-C++

你可能感兴趣的:(C++,读书笔记)