TR1 和 Boost

条款 54 Tr1

“Technical Report 1”, 是一份规范,描述加入C++标准程序库的诸多新机能.

条款 55 Boost

Boost 是个组织/网站, 提供开源的C++程序库

条款 01 :

1.C

2.Object-Oriented C++ (oop)

3.Template C++

4.STL

条款 02 : 使用const,enum,inline替换#define

1.const

2.enum 类内使用相当于常量

3.inline 替换 #define 定义的函数

条款 03 : 尽可能使用const

1.对于不做更改打算的,尽量使用const,防止出现 a*b == c写成赋值的错误

2.mutabel 释放掉const的约束

3.constnon-const 一个op[] 的例子,说明可以籍由non-const 调用const,但绝对不能反向操作,籍由const 调用non-const,

注意事项:

1.声明为const有助于编译器侦察错误用法

2.编译器强制实施bitwise constness(const一切都不能更改),用户应该使用”概念上的常量性”,用mutable实现

3.用const 来避免代码复用,通过non-const 调用 const

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

1.对于C part of C++non-C parts of C++ 初始化规则有点不同,最好的办法是对于所有的对象都初始化

2.对于构造函数可以使用初始化列表技术,对于部分构造可以选择性的在构造函数体中使用赋值来替换初始化

3.“成员初始化次序” ,base classes更早于其他derived classes,而class的成员变量总以其声明次序被初始化

4.“不同编译单元内定义non-local static对象”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#1.h
class FileSystem
{
public:
...
std::size_t numDisks()const;
...
};
extern FileSystem tfs; // 另一翻译单元中的non-local static 对象

#2.cpp
class Directory
{
public:
Directory( params );
...
};
Directory::Directory( params )
{
...
std::size_t disks = tfs.numDisks(); // 使用 tfs 对象
...
}

如果想调用Directory的构造 则 tfs 的初始必须在之前,但C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序无明确定义.

一个小小的设计:将每个non-local static对象搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象,用户调用这些函数,而不直接涉猎这些对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#1.h
class FileSystem { ... };
FileSystem & tfs()
{
static FileSystem fs;
return fs;
}
#2.cpp
class Directory { ... };
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory & tempDir()
{
static Directory td;
return td;
}

注意事项:

1.为内置对象手动初始化,C++不保证初始化他们

2.对构造函数使用初始化列表技术

3.对于跨编译单元的non-local static 对象的处理

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

1.default构造,copy构造,copy assignment赋值,析构

2.编译器提供的copy为浅拷贝

3.编译器产出的析构为non-vitrual析构

4.如果已声明构造,编译器不会再提供default构造

5.对于内含reference成员的类,应提供自定义copy assignment

6.如果将base classcopy assignment 声明为 private,则 derived class 编译器不会提供copy assignment

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

1.如何拒绝使用编译器提供的copy构造和copy assignment

2.将其声明为private(但不定义),对其进行copy行为时会给出编译错误或连接性错误,编译错误源自private类外不能访问,链接性错误源自member 或 friend 函数链接不到其定义

注意事项:

1.为驳回编译器自动提供的机能,可将其对应的成员函数声明为private 并且不予定义,或使用像 Uncopyable 这样的 base class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#第一种方式
class HomeForSale
{
public:
...
private:
...
HomeForSale(const HomeForSale&); //只有声明
HomeforSale & operator=(const HomeForSale&);
};
#第二种方式
class Uncopyable
{
protected:
Uncopyable(){} //允许derived 对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable & operator=(const Uncopyable&); // 组织 copy
};
//为防止HomeForSale对象被拷贝,只需继承Uncopyable
class HomeForSale:private Uncopyable
{
...
};

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

1.如果base class 析构不是vitrual,当derived class 对象经由一个base class 指针删除,通常发生derived 成分没销毁,导致”局部销毁”,内存泄露,应给base class 一个virtual析构函数,这样就能正常销毁整个对象,包括derived class 成分

1
2
3
4
5
6
7
8
9
10
#例子
class TimeKeeper
{
public:
virtual ~TimeKeeper();
...;
};
TimeKeeper * ptk = getTimeKeeper(); //getTimeKeeper 返回一个指向derived 对象的base 指针
...
delete ptk; //现在行为正确

2.如果没有意图用于base class 则不应该令其析构为virtual,virtual声明会导致类创建vptr(虚表指针),会导致不必要的内存开支,只有当class内至少含有一个virtual函数,才为它声明virtual析构函数

1
2
3
4
5
6
7
8
9
10
#例子
class Point
{
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x,y;
};
//如果int占32位,则point对象刚好可塞入64-bit缓存器,此时如果声明位virtual,多余的开支导致其不能恰好塞入

3.pure virtual (纯虚)->abstract(抽象) classes — 不能被实例化的class

1
2
3
4
5
6
class AMOV
{
public:
virtual ~AMOV()=0; //声明位 pure virtual 析构函数
};
AMOV::~AMOV(){} //pure virtual 析构函数的定义

只要class含有一个pure virtual 函数,则这个类为抽象类,注意事项:你必须为这个pure virtual 析构函数提供一份定义

4.析构函数的运作规则,由最外层的 derived class 开始析构,然后是其每一个 base class的析构函数被调用.编译器会在AMOV的 derived classes 的析构函数中创建一个对~AMOV的调用动作,所以你必须为这个函数提供一份定义(第3点),否则连接器会报错

注意事项:

1.对于具有多态性质的base classes 应该声明一个virtual 析构函数,或者class带有任何virtual函数,它也应该拥有virtual 析构

2.如果class的目的不是作为base class 则不应该声明virtual析构函数

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

1.如果析构吐出异常,则会导致程序提前结束或出现不明确的行为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#例子:class 负责数据库连接
class DBConnection
{
public:
...
static DBConnection creat();


void close(); //关闭联机,失败则抛出异常
};
#为防止客户忘记调用close(),一个合理的想法是创建一个class用来管理
class DBConn
{
public:
...
~DBConn() //析构函数确保数据库连接总是会被关闭
{
db.close();
}
private:
DBConnection db;
};

{
DBConn dbc(DBConnection::creat());
//建立一个 DBConnection 对象并交给DBConn对象管理
//通过DBConn的接口使用DBConnection对象
//在区块结束时,DBConn对象销毁调用析构函数,自动为DBConnection对象调用close
}

2.如果调用失败则会抛出异常,导致问题,两种解决方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#1.如果close抛出异常就结束程序,通过调用abort完成
DBConn::~DBConn()
{
try { db.close(); }
catch (...){
制作运转记录,记下对close的调用失败;
std::abort();
}
}
//在导致"不明确行为"前,强迫结束程序
#2.吞下close导致的异常
DBConn::~DBConn()
{
try { db.close(); }
catch (...){
制作转运记录,记下对close的调用失败;
}
}

3.上述方法用处不大,一个更好的解决方法为为客户提供一个处理发生异常的机会

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DBConn
{
public:
...
void close() //为客户提供的处理接口
{
db.close();
closed = true;
}
~DBConn()
{
if (!closed)
{
try { db.close(); }
catch (...){ //如果抛出异常
制作运转记录,记下对close的调用失败; //记录下来并结束程序或吞下异常
...
}
}
}
private:
DBConnection db;
bool closed;
};

如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那这个异常必须来自析构函数以外的某个函数,因为析构函数抛出异常很危险,会导致提前结束程序的风险,或发生不明确行为

注意事项:

1.析构函数不应抛出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,吞下它或提前结束程序

2.可以给客户提供一个处理程序中异常的接口,一个普通函数

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

1.在base class 构造期间,virtual 函数不是 virtual 函数

2.在derived class 对象的base class 构造期间,对象类型是base class,不是derived class

3.确定你的构造函数都没有(在对象被创建和被销毁期间)调用virtual函数,而它们调用的函数也都服从同一约束,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#错误示例1
class Transaction
{
public:
Transaction()
{
init(); //调用 non-virtual
}
virtual void logTransaction()const = 0;
...
private:
void init()
{
...
logTransaction(); //调用virtual!
}
};
#正确示例2
class Transaction
{
public:
explicit Transaction(const std::string & logInfo);
void logTransaction(const std::string & logInfo)const; //现为non-virtual函数
...
};
Transaction::Transaction(const std::string & logInfo)
{
...
logTransaction(logInfo);
}
class BuyTransaction:public Transaction
{
public:
BuyTransaction( parameters )
:Transaction(createLogString( parameters )) //将log信息向上传给base class构造
{ ... }
...
private:
static std::string creatLogString( parameters );
};

注意本例中比起 初始化列表内直接给予数据 利用辅助函数创建一个值传给构造函数更方便(更可读),声明为static 防止出现 “那些成员变量处于为定义状态”

注意事项:

1.在构造和析构期间不要调用virtual函数,因为这类调用不会下降至derived class,换句话说在base class 构造期间,virtua函数调用属于base的那个

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

1.不仅仅适用于=,也适用于+=,-=,*=等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//理由 为了实现连锁赋值,实现和内置数据类型一样的功能
//int x,y,z; x = y = z = 15;
#示例1
class Widget
{
public:
...
Widget& operator=(const Widget& rhs)
{
...
return * this;
}
...
};
#适用于其他赋值相关运算
class Widget
{
public:
...
Widget& operator+=(const Widget& rhs)
{
...
return * this;
}
Widget& operator=(int rhs)
{
...
return * this;
//此函数也适用,即使此一操作符的参数类型不符协定
}
...
};

2.这仅仅是个协议,无强制性,如果你有自己的实现需求,可以不遵守它

注意事项:

1.令assignment(赋值)操作符返回一个 reference to * this

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

1.不处理自我赋值的后果,在“停止使用资源前意外释放了它”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#示例
class Bitmap { ... };
class Widget
{
...
private:
Bitmap * pb; //指向一个从heap分配而得的对象
};
Widget& Widget::operator=(const Widget & rhs) //一份未防止自我赋值的 assignment copy
{
delete pb; //释放当前的内存
pb = new Bitmap(*rhs.pb); //使用rhs的副本
return * this;
}
//没有处理"自我赋值" *this 和 rhs 可能为同一对象,

2.解决方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//#证同测试
Widget& Widget::operator=(const Widget & rhs)
{
if (this == &rhs) return * this;
delete pb;
pb = new Bitmap(*rhs.pb);
return * this;
}
//缺陷:不具备"异常安全性",如果"new Bitmap"导致异常,Widget最终会持有一个被删除的Bitmap
//#恰当的语句顺序 另外让operator= 具备"异常安全性"往往自动获得"自我赋值安全"
Widget& Widget::operator=(const Widget & rhs)
{
Bitmap * pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return * this;
}
//缺陷:不是效率最高的方法,但行得通
//#copy and swap 技术
class Widget
{
...
void swap(Widget & rhs); //交换*this 和 rhs 的数据
...
};
Widget& Widget::operator=(const Widget & rhs)
{
Widget temp(rhs); //为 rhs 数据制作一份副本
swap(temp); //将*this 和 temp 交换
return * this;
}
//该方法的另一版本,copy assignment "以 by value 方式接受实参" ,该方式传递会早成一份副本
Widget& Widget::operator=(Widget rhs)
{
swap(rhs); //pass by value
return * this;
}
//缺陷:牺牲了代码的清晰性,但更高效

注意事项:

1.确保处理”自我赋值”,包括上述三种方式的技术可以处理

2.确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为任然正确,就像第一个示例中 赋值 和 被赋值 的对象是同一个

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

1.往类中新添成员,同时也需要修改copying函数,编译器不会提醒,导致可能局部拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
//#1
void logCall(const std::string & funcName); //制作一个log entry
class Customer
{
public:
...
Customer(const Customer & rhs);
Customer& operator=(const Customer & rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer & rhs):name(rhs.name) //复制
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer & rhs)
{
logCall("Customer copy assignment operator");
name = rhs.name; //复制
return * this;
}

//#2 往类中新添成员
class Date { ... }; //日期
class Customer
{
public:
... //同前
private:
std::string name;
Date lastTransaction;
}

//#3 如果发生继承,可能导致严重的潜藏危机
class PriorityCustomer:public Customer //derived class
{
public:
...
PriorityCustomer(const PriorityCustomer & rhs);
PriorityCustomer& operator=(const PriorityCustomer & rhs);
...
private:
int priority;
};
//没有指定实参传给 base class 的构造函数,base class 将调用不带参数的构造(default构造)
PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
:Priority(rhs.Priority)
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer & rhs)
{
logCall("PriorityCustomer copy assignment operator");
Priority = rhs.Priority;
return * this;
}

//#4
PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
:Customer(rhs),Priority(rhs.Priority) //调用base class 的 copy构造函数
{
logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer & rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs); //对base class 成分进行复制动作
Priority = rhs.Priority;
return * this;
}

2.当你编写一个copying函数,确保复制所有local成员变量,调用所有base class内适当的copying函数

3.你不该令copy assignment操作符 调用 copy构造函数,反过来也不能令copy构造函数调用copy assignment操作符

注意事项:

1.copying函数应确保复制”对象内的所有成员变量”及”所有base class 成分”

2.不要尝试以某个copying函数实现另一个copying函数,应将共同机能放进第三个函数,并由两个copying函数共同调用

条款 13 : 以对象管理资源

1.为何需要以对象管理资源

1
2
3
4
5
6
7
8
9
10
class Investment { ... };                     //继承体系中的base class
//进一步假设,通过一个 factory function (条款7) 提供特定的 Investment 对象
Investment * createInvestment(); //返回指针,指向继承体系中的动态分配对象
void f()
{
Investment * pInv = createInvestment(); //调用 factory function
...
delete pInv; //释放pInv所指对象
}
//此处的风险在于,如果...因为一些原因提前结束,像是return,或者continue or goto导致跳过了delete,导致内存泄漏,谨慎编写程序可以防止这类错误,但更好的做法是将资源放入对象,以析构函数来自动释放这些资源

2.利用auto_ptr(pointer-like),也就是智能指针,其析构函数会自动调用delete

1
2
3
4
5
6
//#示例
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
... //经由auto_ptr析构函数自动释放pInv
}

“以对象管理资源”的两个关键想法

a.获得资源后立刻放进管理对象,该观念被称为“资源取得时机便是初始化时机”(RAII)
b.管理对象运用析构函数确保资源被释放

3.由于其自动销毁,注意别让多个auto_ptr指向同一对象,否则会导致”未定义行为”,对于这个问题,auto_ptr有一个不寻常的性质: 若通过copy构造或copy assignment 复制它们,它们会变成null,复制所得指针获取资源的唯一拥有权

1
2
3
4
//#示例
std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向对象
std::auto_ptr<Investment> pInv2(pInv1); //pInv2指向对象,pInv1被设为null
pInv1 = pInv2; //pInv1指向对象,pInv2被设为null

“受auto_ptr管理的资源必须绝对没有一个以上的auto_ptr同时指向它”

4.另一替代方案“引用计数型智能指针”(RCSP).RCSP持续追踪共有多少对象指向某资源,并在无人指向它时自动删除该资源

Tr1的tr1::shared_ptr 就是个RCSP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//#1
void f()
{
...
std::tr1::shared_ptr<Investment> pInv(createInvestment());
...
}
//使用与auto_ptr基本一致,区别在于不同于auto_ptrd的复制行为
//#2
void f()
{
...
std::tr1::shared_ptr<Investment> pInv1(createInvestment()); //pInv1指向对象
std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1,pInv2指向同一对象
pInv1 = pInv2; //同上
...
}

5.auto_ptr和shared_ptr都在析构函数内做delete而不是delete[],这意味着动态分配的array不能在auto_ptr或shared_ptr上使用(如有需求见条款55)

注意事项:

1.为防止资源泄漏,应使用RAII对象,它们在构造时获取资源,析构时释放资源

2.两个常用的RAII对象,tr1::shared_ptr 和 auto_ptr,两只区别在于复制行为的不同

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

1.对于并非heap_based的资源,auto_pt 或 tr1::shared_ptr 往往不适合,因此你需要建立自己的资源管理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//例如,假设 有一个 类型为 Mutex的互斥器对象,共有lock 和 unlock 两函数可用
void lock(Mutex * pm); //锁定pm所指的互斥器
void unlock(Mutex * pm); //将互斥器解除锁定
//建立一个class用来管理机锁,由RAII守则支配,"资源在构造期间获得,在析构期间释放".
class Lock
{
public:
explicit Lock(Mutex * pm)
:mutexPtr(pm)
{
lock(mutePtr); //获得资源
}
~Lock(){ unlock(mutexPtr); } //释放资源
private:
Mutex * mutexPtr;
};
//客户对lock的用法符号RAII方式:
Mutex m; //定义你所需要的互斥器
...
{
Lock m1(&m); //以Lock来管理Mutex m ,锁定互斥器
...
} //在区块末尾自动解除互斥器锁定

2.考虑如果Lock对象被复制,会发生什么

1
2
Lock m11(&m);                     //锁定m
Lock m12(m11); //将m11 复制到 m12 上
a.禁止复制,许多时候允许RAII对象被复制并不合理.如果复制动作对RAII class 并不合理,应该将其禁止,通过将copying操作声明为private(条款6)
b.对底层资源祭出“引用计数法”,如 tr1::shared_ptr.
通常内含一个tr1::shared_ptr成员变量,RAII class 便可实现 reference-counting copying 行为,如 若前述的Lock打算使用reference couting ,它可以改变mutexPtr的类型,改为 tr1::shared_ptr<Mutex.>,tr1::shared_ptr 的默认行为为“当引用次数为0时删除其所指物”,这不是Lock想要的行为,但tr1::shared_ptr允许指定所谓的”删除器”,那是一个函数或函数对象,当引用次数为0的时候调用
1
2
3
4
5
6
7
8
9
10
11
12
13
//#示例
class Lock
{
public:
explicit Lock(Mutex * pm)
:mutexPtr(pm,unlock) //将unlock 函数 作为删除器
{
lock(mutexPtr.get()); //条款 15 谈到"get"
}
private:
std::tr1::shared_ptr<Mutex> mutexPtr; //使用shared_ptr 替换 raw pointer
}
//此处没有声明析构,条款5 说过,class的析构(无论是编译器生成的,或是用户自定义的)会自动调用其non-static成员变量的析构函数(本例为mutexPtr)
c.复制底层资源 ,复制资源管理类对象时,进行的应是 “深度拷贝”
d.转移底部资源的拥有权,如条款13 ,这是auto_ptr的复制意义

注意事项:

1.复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为

2.常见的的RAII class copying 行为为:抑制copying(禁止复制) , reference counting

(引用计数法), 其他行为也可能被实现如 c,d.

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

1.为什么引入对原始资源的直接访问:

下述例子想返回一个int型的天数却通不过编译,因为daysHeld需要的是 Investment*指针

但传给它的却是个类型为tr1::shared_ptr<.Investment>的对象

1
2
3
4
//#示例
tr1::shared_ptr<Investment> pInv(createInvestment()); //智能指针管理对象
int dayHeld(const Investment * pi); //返回天数
int days = dayHeld(pInv); //错误

2.两种做法可以达成目的,显示转换和隐式转换. tr1::shared_ptr和auto_ptr都提供一个get函数,用来执行显示转换,也就是它会返回智能指针内部的原始指针(的复件).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//#显示转换的例子
int days = dayHeld(pInv.get()); //利用get返回原始指针
//#tr1::shared_ptr auto_ptr 重载了->和* ,它们允许隐式转换至底部原始指针
class Investment
{
public:
bool isTaxFree()const;
...
};
Investment* createInvestment(); //factory函数
std::tr1::shared_ptr<Investment> pil(createInvestment());//令pi1管理一笔资源
bool taxable1 = !(pi1->isTaxFree()); //经由operator->访问资源
...
std::auto_ptr<Investment> pi2(createInvestment()); //令pi2管理一笔资源
bool taxable2 = !((*pi2).isTaxFree()); //经由operator*访问资源
...

3.显示转换和隐式转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//#示例
FontHandle getFont();
void releaseFont(FontHandle fh);
class Font //RAII class
{
public:
explicit Font(FontHandle fh //获得资源 pass-by-value
:f(fh)
{
}
~Font(){ releaseFont(f); } //释放资源
private:
FontHandle f; //raw(原始) 资源
};
//#假设将Font转换为FontHandle 很频繁则需要提供一个显示转换函数
class Font
{
...
FontHandle get()cosnt{ return f; } //显示转换函数
...
}
void changeFontSize(FontHandle f,int newSize);
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize) //显示的将Font转换为FontHandle
//#或者提供隐式转换
class Font
{
...
operator FontHandle()const;
{ return f; }
...
}
Font f(getFont());
int newFontSize;
...
changeFontSize(f,newFontSize) //隐式转换
//#缺陷:增加错误发生的机会
Font f1(getFont());
... //原意是拷贝一个Font对象,却将其隐式转换后才复制
FontHandle f2 = f1;

4.通常显示转换用的更多,隐式转换会增加错误的发生.RAII class 并不是为了封装而存在:

其目的为”确保一个特殊行为–资源释放–会发生”,所以RAII class中返回原始资源的函数并非设计灾难

注意事项:

1.APIs往往要求访问原始资源,所以每一个RAII class 应该提供一个”获取原始资源”的方法

2.通过显示转换或隐式转换都可以访问,但显示更安全,隐式增加了错误的发生但更方便

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

1.new 与 delete 应该成对配套出现 , new 与 delete , new [] 与 delete[]

2.原因,数组所有内存通常还包括”数组大小”的记录,以便delete知道调用多少次析构,单一内存则没有这笔记录

单一对象 Object
对象数组 n Object Object Object

上述大概描述了编译器中的实现,但并非所有编译都是如此,但足够解释为什么配套使用

3.对typedef也是如此,考虑下述例子

1
2
3
4
5
6
typedef std::string AddressLines[4];
//typedef 定义 数组 AddressLines a; 相当于 string a[4];
std::string* pal = new AddressLines; //所以是以new [] 创建的,需以delete [] 释放

delete pal; //行为为定义
delete [] pal; //很好

注意事项:

1.new 和 delete ,new [] 和 delete [] 需成对配套出现使用

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

1.对于下述例子

1
2
3
4
5
6
7
8
9
int priority();
void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);
//由于谨记"以对象管理资源"(条款13),processWidget决定以智能指针处理其动态分配而来的Widget
//现在考虑调用processWidget
processWidget(new Widget, priority());
//该形式无法通过编译,由于tr1::shared_ptr需要一个原始指针,但该构造是explicit构造,无法进行隐式转换
//将"new Widget“转换为tr1::shared_ptr
processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
//这样即可通过编译,但出现了另一个问题,即使运用了 RAII 对象,仍存在泄漏资源的可能

2.原因在于调用processWidget之前,必须先核算被传递的实参,上述第二实参有一个priority函数调用,但第一实参由两部分组成,“执行new Widget 表达式”,”调用tr1::shared构造函数”

所以再调用processWidget之前有三件事

a.调用priority
b.执行”new Widget”
c.调用tr1::shared_ptr 构造函数

编译完成这些事情的次序具有弹性,如果最终获得这样的操作序列

a.执行”new Widget”
b.调用priority
c.调用tr1::shared_ptr 构造函数

如果priority的调用抛出异常,则会导致 “new Widget” 返回的指针遗失,引发资源泄漏

3.如何处理:

1
2
std::tr1::shared_ptr<Widget> pw(new Widget)     //分出单独语句
processWidget(pw,priority()); //这个动作不会导致泄漏

注意事项:

1.以独立语句将newd对象存储于智能指针,如果不这么做,一旦抛出异常,则可能导致难以察觉的资源泄漏

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

1.导入简单的外覆类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//#示例
class Data
{
public:
Date(int month,int day,int year);
...
};
Date d(30,3,1995) //应该为 3,30 而不是 30,3
//外覆类型
struct Day{
explicit Day(int d)
:val(d){

}
int val;
};
struct Month{
explicit Month(int d)
:val(d){

}
int val;
};
struct Year{
explicit Year(int d)
:val(d){

}
int val;
};
class Date
{
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30,3,1995); //error!
Date d(Day(30),Month(3),Year(1995)); //error!
Date d(Month(3),Day(30),Year(1995)); //right

2.提供行为一致的接口,如果STL容器每个都有一个size成员函数,告诉调用者容器内有多少个对象

3.对于下述例子

1
2
3
4
Investment* createInvestment();
//返回的指针可能导致两种错误:1.没有删除指针 2.删除多次
//条款13 表面可以用智能指针来更好的管理指针的销毁 , 本例可令其返回一个智能指针
std::tr1::shared_ptr<Investment> createInvestment();

4.假设调用者期许将指针传给一个特定的函数来执行特定的销毁而不是delete可以尝试将其绑定一个删除器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//shared_ptr 接受两个实参:1.被管理的指针 2.引用次数为0时调用的"删除器"
//如果像下述一样创建一个 null tr1::shared_ptr
std::tr1::shared_ptr<Investment> pInv(0,getRidOfInvestment); //无法通过编译
//第一参数必须为指针,而0是个int , 虽然可以被转换但不够好 tr1::shared_ptr 需要一个完全的指针
std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*>(0),getOfInvestment);

//现在可以将createinvestment 函数 实现成我们想要的,即返回一个传给特点函数删除的指针
std::tr1::shared_ptr<Investment> createInvesment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment);
reVal = ...;
return reVal;
}

5.防范“cross-DLL-problem” (不清楚DLL是什么,似乎与多线程有关)

注意事项:

1.应尽量在接口中实现,不容易误用的性质

2.实现接口的一致性,以及与内置类型的行为兼容

3.”阻止误用” a.建立新类型 b.限制类型上的操作 c.束缚对象值 d.消除客户的资源管理自认

4.tr1::shared_ptr 可以自定义删除器 , 这可防范DLL 问题

条款 19 : 设计class犹如type

1.新type的对象该如何创建和销毁?

考虑构造和析构,以及new 和 delete 重载的设计

2.初始化和对象的赋值该有什么差别?

不要混淆对象的构造函数和operator=

3.新type对象如果 pass-by-value 意味着什么?

通常用copy构造函数来实现pass by value

4.新type的合法值?

对class范围的约束,对成员函数需要进行的错误检测工作

5.你的新type需要配合某个继承图系吗?

对于继承中non-virtual 和 virtual 声明的考虑,尤其是析构函数是否为virtual (条款 34,36)

6.你的新type需要声明样的转换?

types之间的转换考虑, 隐式转换 或 i按时转换函数的设计

7.什么样的操作符和函数对此新type是合理的?

考虑该type需要什么函数和操作符,如list需要insert (条款 23,24,46)

8.什么样的标准函数该驳回?

如你不需要编译器默认提供的copying 函数,即应该将其声明为private (条款 6)

9.谁该取用type的成员?

考虑成员的访问权,是声明为private , public, 还剩 protected

10.什么是新type的”未声明接口”?

不理解(见条款29)

11.你的新type有多么一般化?

考虑是否需要使用 template class

12.你真的需要一个新的type吗?

考虑继承 自 base class 即在 derived class 上添加新机能

注意事项:

1.再设计class 之前确定你已经考虑过本条款所有讨论主题

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

1.考虑下述继承体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person
{
public:
Person(); //为求简化,省略参数
virtual ~Person(); //声明为virtual (条款7)
...
private:
std::string name;
std::string address;
};
class Student:public person
{
public:
Student();
~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
bool validateStudent(Student s); //pass-by-value
Student plato;
bool platoIsOK = validateStudent(plato); //调用函数

上述函数实现上没有问题,但 pass-by-value 时构造一个临时的Student,Student内又包含两个string,同时继承自Perosn Base-Class 又要构建Person ,Person又包含两个string,总体上进行了六次构造和六次析构,效率极低

2.对此的解决方式即为pass-by-reference

1
bool validateStudent(const Student& s);

3.by-reference可以避免slicing(对象切割),此问题产生于将derived class 以 by-value的方式传递给形参为base-class 的函数,考虑下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Window
{
public:
...
std::string name()const; //返回窗口名称
virtual void display()const; //显示窗口及其内容
};
class WindowWithScrollBars:public Window
{
public:
...
virtual void display()const;
};
//错误示例
void printNameAndDisplay(Window w) //不正确,参数可能被切割
{
std::cout << w.name();
w.display();
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
//wwsb中的windowWithScrollBars部分将被切割,只剩下Window
//正确示例 pass-by-reference
void printNameAndDisplay(const Window& w)
{
std::cout << w.name();
w.display();
}

4.pass-by-refernce 往往比 pass-by-value 更高效

5.对于大部分类型应该选用by-reference 但对于内置类型,及STL迭代器和函数对象选择pass-by-value并非没有道理

注意事项:

1.尽量以pass-by-reference 替换 pass-by-value ,高效且避免切割问题

2.以上规则不适用于内置类型,以及STL迭代器和函数对象

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

1.返回 reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Rational
{
public:
Rational(int numerator = 0,
int denominator = 0);
...
private:
int n,d; //分子 numerator , 分母 denominator
friend Rational
operator* (const Rational& lhs, const Rational& rhs);
};
//当前版本返回一个计算结果的对象 pass-by-value
//如果改为 byr-reference
Rational a(1,2); //a = 1/2
Rational b(3,5); //b = 3/5
Rational c = a * b; //c = 3/10
//期望"原本就存在一个其值为3/10的Rational"不合理,它必须自己创建那个Rational对象

2.在stack 或 heap 空间创建并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
Rational result(lhs.n * rhs.n , lhs.d * rhs.d);
return result;
}
//result 为 local 对象,函数退出前被销毁了,导致"无定义行为",然后函数如果返回一个reference指向某个local对象,都将一败涂地
//如果在heap 内呢?考虑如下
const Rational& operator*(const Rational& lhs, const rational& rhs)
{
Rational* result = new Rational(lhs.n * rhs.n , lhs.d * rhs.d);
return * result;
//这将导致: 谁该对着被你 new 出来的对象 实施 delete
}
Rational w,x,y,z;
w = x * y * z;
//这里两次new 就需要 两次 delete,但没有合理的办法让他们获得operator*返回的reference背后隐藏的指针,导致资源泄漏

3.如果使用static 呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
static Rational result;
result = ...;
retur retsult;
}
bool operator==(const Rational& lhs, const Rational& rhs);
Rational a,b,c,d;
if ((a*b)==(c*d))
{
...;
} else
{
...;
}
//这将导致 (a*b)==(c*d) 永远被判别为TRUE , 因为返回的引用都指向static同一个值,同一个值当然和自己相等

4.综上,一个”必须返回新对象”的函数正确写法是:return 一个新对象 而不是refernce

1
2
3
4
inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Ratioanl(lhs.n*rhs.n,lhs.d*rhs.d);
}

注意事项:

1.绝不要返回pointer或reference指向local stack 对象 ,或返回reference 指向 heap-allocated 对象, 或返回 reference 指向local-static 对象.(条款4)为”在单线程环境中合理返回reference指向一个local statci对象”提供了一份设计实例

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

1.保证语法一致性

2.对成员变量进行更精确的控制

3.保证封装性,从封装的角度出发,其实只有两种访问权限:private 和 其他

4.条款23:某些东西的封装性于”当其内容改变时造成的代码破坏量”成反比

注意事项:

1.切记将成员变量声明为private,赋予客户访问数据的一致性(即通过Public方法来访问),可细微划分访问控制,允诺约束条件获得保证,提供class 作者充分的实现弹性

2.proteced 并不比 public 更具封装性

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

1.对于下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class WebBrowser
{
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
//如果想统一执行这些动作,可以改写成下述
class WebBrowser
{
...
void clearEverything(); //调用clearCache,clearHistory,removeCookies
...
};
//同样也可以改写为non-member
void clearBrowser(WebBrowser& wb)
{
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}

clearBrowser具有更大的封装性

2.C++中更自然的做法是让clearBrowser 作为一个non-member 且与其在同一namespace

1
2
3
4
5
6
namespace WebBrowserStuff
{
class WebBrowser { ... };
void clearBrowser(WebBrowser& wb);
...
}

3.namespcae 的好处: a.可跨越多个源码文件,而后者不能 b.将所有便利函数放在多个头文件但隶属于同一个namespace 意味着可以更轻松的扩展 ,class 对于客户是不能扩展的

注意事项:

1.以non-member-non-friend 替换 member 函数,依次增加封装性,包裹弹性?机能扩充性

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

1.对于下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Rational   //有理数类
{
public:
Rational(int numerator=0, int denominator=1); //non-explicit 允许隐式转换
int numerator()const; //分子和分母的访问函数
int denominator()const;
private:
...
};
//假设想实现 operator*
class Rational
{
public:
...
const Rational operator*(const Rational& rhs)const;
};
//return by-value 但接受一个 pass-by-reference-to-const (条款3,20,21)
Rational oneEighth(1,8);
Rational oneHalf(1,2);
Rational result = oneHalf * oneEighth;
result = result * oneEighth;
//但如果想实现混合算数呢?
1.result = oneHalf.operator*(2); //valid
2.result = 2.operator*(oneHalf); //invalid
3.result = operator*(2,oneHalf); //invalid
//对于成功的 1 2传给operator*时发生了隐式转换 , 类似于下述的行为
const Rational temp(2);
result = oneHalf * temp;
//对于下述例子
result = oneHalf * 2; //valid (在non-explicit构造下)
result = 2 * oneHalf; //invalid(甚至在non-explicit构造下)
//结论:只有参数被列于参数列内,这个参数才是隐式转换的合格参与者

2.对此可以以non-member函数实现

1
2
3
4
5
6
7
8
9
10
class Raional { ... };
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1,4);
Rational result;
result = oneFourth * 2; //valid
result = 2 * oneFourth; //invalid

3.member函数的反面是non-member函数,不是friend函数

注意事项:

1.如果你需要为某个函数的所有参数 进行类型转换,那么这个函数必须是个non-member

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

1.考虑下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace std
{
template<typename T>
void swap(T& a, T& b) //std::swap
{
T temp(a);
a = b;
b = temp;
}
}
//只要类型支持copying函数,即可使用默认的swap代码
//"pimpl手法" "以指针指向一个对象,内含真正数据"
class WidgetImpl
{ //针对Widget数据设计的class
public:
...
private:
int a,b,c; //可能有许多数据,意味着赋值时间很长
std::vector<double> v;
...
};
class Widget //使用 pimpl 手法
{
public:
Widget(const Wdiget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl);
... //省略实现细节 见(条款10,11,12)
}
...
private:
WidgetImpl * pImpl; //指针,所指对象内含Widget数据
}

2.一旦要置换两个Widget对象值,需要做的是置换pImpl指针,但默认的swap算法不知道这点,默认的会赋值三个Widgets 和 三个WidgetImpl 效率极低,对此可以对std::swap提供针对Widget的特化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace std
{
template<> //针对Widget的特化,但目前无法通过编译
void swap<Widget>(Widget& a, Widget& b)
{
swap(a.pImpl, b.pImpl); //置换Wdigets时只需要置换它们的指针
}
}
//template<>表面它是一个std::swap的全特化
//无法编译的原因:该函数无法访问private变量,解决将其声明为friend 或 member
class Widget
{
public:
...
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
...
};
namespace std
{
template<>
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); //调用a的swap成员函数
}
}

3.假设Widget和WidgetImpl 都是class templates 而不是 classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget { ... };
//如果尝试像 2 中一样的实现对std::swap 的特化
namespace std
{
template<typename T> //错误 不合法
void swap< Widget<T> >(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
//此处的错误是企图 偏特化 一个function template , 而C++只允许对class template 偏特化
//对此的解决办法是,为它添加一个重载版本
namespace std
{
template<typename T> //std::swap的一个重载版本
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b); //稍后为告诉你,这任然不合法
}
}

4.一般而言重载function template 没有问题,但std是个特殊的namespace ,其管理规则比较特殊, 客户可以全特化 其中的templates 但不可以添加新的templates,即为此处为什么重载std::swap不合法的原因,所以请不要添加新的东西到std里头

5.所以对 3 中的解决办法为 声明一个non-member-swap,让他调用member-swap,但不再将这个swap声明为std::swap的特化或重载版本

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace WidgetStuff
{
... //模板化的WidgetImpl 等等 用...省略
template<typename T>
class Widget { ... }; //同前,省略细节,参考前述例子
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b)
{
a.swap(b);
}
}
//现在任何地点的任何代码如果打算置换两个Widget对象,因而调用 name lookup rules: 所谓的"argument-dependent lookup 或 Koenig lookup"法则,找到WidgetStuff内的Widget专属版本

6.应该调用哪个swap? std::swap的一般化,还是可能存在的特化版本,栖身于某个命名空间的T专属版本?

1
2
3
4
5
6
7
8
9
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap; //令std::swap 在此函数内可用
...
swap(obj1,obj2); //为T 调用最佳swap版本
...
}
//编译器优先std::swap的T专属版本,而非一般的template,所以如果你已针对T将std::swap特化,特化版会被编译器挑中

7.此处已经讨论过 default swap,member swap,non-member swap,std::swap 特化版本,以及对swap的调用,如果swap默认版本效率不够,尝试做以下事

a.提供一个public swap 成员函数,此函数绝不该抛出异常
b.在你的class 或 template 所在的命名空间内提供一个non-member swap
c.如果你在编写一个class ,为你的class 特化std::swap
d.调用时确定包含一个using 声明式,使std::swap在函数内曝光

注意事项:

1.当std::swap 效率不高时,提供一个绝不抛出异常的swap成员函数

2.如果你提供一个member swap,也该提供一个non-member swap来调用函数,队与classes(而非templates) 请特化std::swap

3.调用swap时应该针对std::swap使用using 声明

4.不要在std namespcae 中添加新的东西,但可以提供template的特化

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

1.对于下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//下述函数计算通行密码的加密版本并返回,前提是密码够长,若太短则抛出异常,类型为logic_error
//这个函数过早定义变量"encrypted"
std::string encryptPassword(const std::string& password)
{
using namespace std;
string encrypted;
if (password.length() < MinmumPasswordLength){
throw logic_error("Password is too short");
}
... //必要动作,加密密码,并置入变量encrypted内
return encrypted;
}
//encrypted并没有被完全使用,如果抛出异常,则白白承担了encrypted的构造和析构成本
//下述函数延后"encrypted"的定义,直到需要它
std::string encryptPassword(const std::string& password)
{
using namespace std;
if (password.length() < MinmumPasswordLength){
throw logic_error("Password is too short");
}
string encrypted;
... //必要动作,加密密码,并置入变量encrypted内
return encrypted;
}
//仍然不够秾纤合度,条款4曾解释为什么"通过default构造函数构造出一个对象然后赋值"比"直接在构造时指定初值"效率差
//假设加密部分在以下函数中进行
void encrypt(std::string& s);
std::string encryptPassword(const std::string& password)
{
...
std::string encrypted;
encrypted = password;
encrypt(encrypted);
return encrypted;
}
//更受欢迎的做法是以password直接作为encrypted的初值,跳过无意义的default构造
std::string encryptPassword(const std::string& password)
{
...
std::string encrypted(password);
encrypt(encrypted);
return encrypted;
}

2.对于循环来说呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//#示例A
Widget w;
for (int i = 0; i < n; ++i)
{
w = i;
...
}
//#示例B
for (int i = 0; i < n; ++i)
{
Widget w(i);
...
}
//两种写法成本如下
//A : 1个构造函数 + 1个析构函数 + n个赋值操作
//B : n个构造函数 + n个析构函数

大体上A更高效,尤其当n很大的时候,否则做法B或许比较好.A造成名称w的作用域比B大,可能对程序的可理解性和易维护性造成冲突,因此除非你知道:

a.赋值成本比”析构+构造“低
b.你正在处理代码中效率高度敏感的部分

注意事项:

1.尽可能延后变量定义式的出现,以增加程序清晰度,并改善效率

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

1.”旧式转换”: (T)expression 和 T(expression) , C++提供四种新式类型转换

1
2
3
4
5
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
//各有不同的目的
a. const_cast 通常用来将对象的常量性转除 (cast away the constness),也是唯一有次能力的C++-style操作符
b. dynamic_cast 用来执行”安全向下转型” (safe downcasting) ,也就是来决定某对象是否归属继承体系中的某个类型,唯一无法由旧语法执行的动作,而且开销巨大
c. reinterpret_cast 意图执行低级转型
d. static_cast 用来强迫隐式转换,例如将non-const 转换为 const对象(条款3),或像int 转为 double,但他无法做到const转为non-const

2.C++新式转换很受欢迎,但旧式转换仍然有更适合的使用时机 ,参考下述例子

1
2
3
4
5
6
7
8
9
class Widget
{
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15)); //旧式函数风格
doSomeWork(static_cast<Widget>(15)); //C++-style

对此蓄意的”对象生成”,动作上不像”类型转换”,所以使用旧式转换可能更恰当,但始终使用新式转换也是好的习惯

3.对于类型转换,编译器什么都没做吗?参考下述例子

1
2
3
4
5
6
class Base { ... };
class Derived:public Base { ... };
Derived d;
Base * pb = &d; //隐喻的将Derived* 转换为 Base*
//这里建立一个 base class 指针指向一个 derived class 对象,但有时候上述的两个指针值并不相同,这种情况下会有个偏移量(offset)在运行期被施行于Derived* 指针上,用以获取正确的Base* 指针值
//这个例子表面,但一对象可能拥有一个以上的地址(C,JAVA,C# 都不可能发生这种事),但C++可能

4.参考下述例子,一个base class 和 derived class ,两者都定义了virtual 函数,要求先在derived class 的virtual 中 调用 base class 的virtua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Window
{
public:
virtual void onResize(){ ... }
...
};
class SpecialWindow:public Window
{
public:
virtual void onResize()
{
static_cast<Window>(*this).onResize();// 将*this转为Window 调用其onResize这不可行
... //执行SpecialWindow的行为
}
...
};
//此处将*this转为 Window ,对函数的调用因此调用了 Window::onResize ,但他调用的不是当前对象上的函数,
//" 而是稍早转型动作所建立的一个 ‘*this 对象的base class 成分’ 的暂时副本身上的onResize "
//换句话说就是 转型动作导致生成了一个副本,此处的调用是对副本执行的,使当前对象进入一种"伤残"状态:
//其base class 成分的更改没有执行(执行到一个副本上去了),而derived class 成分的更改落实了

5.解决的办法应该是拿掉转型动作,采用下述例子的方法

1
2
3
4
5
6
7
8
9
10
class SpecialWindow:public Window
{
public:
virtual void onResize()
{
Window::onResize(); //调用Window::onResize 作用于当前对象身上
...
}
...
};

6.对于dynamic_cast 应该注重他对效率的影响,之所以需要使用:dynamic_cast ,通常是因为你想要在一个你认定为derived class 对象身上执行derived class操作,但你的手上只有一个”指向base”的pointer或reference,一般两种做法可以解决:

第一,使用容器并在其中存储指向 derived class 对象的指针(通常是智能指针,条款13)
假设先前的Window / SpecialWindow 继承中,只有SpecialWindow 支持闪烁
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//试着不要这么做
class Window { ... };
class SpecialWindow:public Window
{
public:
void blink();
...
}; //关于tr1::shared_ptr 见条款13
typedef std::vector<std::tr1::shared_ptr<Window> > VPM;
VPM winPtrs;
...
for (VPM::iterator iter = winPtrs.begin();iter != winPtrs.end(); ++iter)
{ //不希望使用
if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>( iter->get() ) )
psw->blink();
}
//应该改成这样做 1.
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
... //不使用dynamic_cast
for (VPSW::iterator iter = winPtrs.begin();iter != winPtrs.end(); ++iter)
(*iter)->blink();

//这种做法的弊端:无法在同一个容器内存储指针"指向所有可能的Window派生类"
//另一种做法可以通过base class 接口处理所有可能
// 2.
//那就是在base class 中提供virtual函数.举个例子 在不需要闪烁的派生中提供一份什么也不做的闪烁函数
class Window
{
public: //默认实现代码什么也不做,条款34告诉你为什么默认实现代码可能是个馊主意
virtual void blink(){}
...
};
class SpecialWindow:public Window
{
public:
virtual void blink() { ... };
...
};
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs; //Window 指向所有可能
...
for (VPM::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
(*iter)->blink();

无论哪种写法 1.使用类型安全容器 2.将virtual函数往继承体系上方移动 —-都并非完美的方案,但在许多情况下提供了一个可行的dynamic_cast替代方案

7.绝对需要避免的是所谓的 “连串 dynamic-cast”

1
2
3
4
5
6
7
8
9
10
11
12
class Window { ... };
...
typedef std::vector<std::tr1::shared_ptr<Window> > VPM;
VPM winPtrs;
...
for (VPM::iterator iter = winPtrs.begin();iter != winPtrs.end(); ++iter)
{
if (SpecialWindow1 * psw1 = dynamic_cast<SpecialWindow1>(iter->get())) { ... }
else if (SpecialWindow2 * psw2 = dynamic_cast<SpecialWindow2>(iter->get())) { ... }
else if (SpecialWindow3 * psw3 = dynamic_cast<SpecialWindow3>(iter->get())) { ... }
...
}

这样的代码又大又慢,且一旦base class 发生改变,需要花费大量的时间检查是否需要修改,应当采用上述替代方案,将其替换

注意事项:

1.尽量避免转型动作,尤其是在效率敏感的代码中执行 dynamic_cast

2.如果转型是必要的,试着将它隐藏于某个函数背后,客户通过调用该函数达到目的,而不是直接将他放入代码

3.使用新式而不用旧式,新式方便辨认目的

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

1.成员变量的封装性最多只等于”返回其reference”的函数的访问级别,如返回一个pirvate变量的引用,将导致该变量的访问权限降到public

2.如果const成员函数传出一个reference ,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据, 这正式bitwise constness 的附带结果(见条款3)

3.上述事情都是发生于”函数返回reference”.如果它们返回的式指针或迭代器,同样如此.

reference,指针,迭代器 都是所谓的 handles ,返回它们将导致”降低对象封装性

4.对于成员内部,变量或函数,绝不应该令public 中的成员函数返回一个指针指向它们,这样会导致后者的访问级别提高

5.返回handles 易导致 指向一个不存在的对象,即该指针变为 空悬,虚掉的

6.这并不意味着绝对不可以让函数返回handle, operator[] 就是一个允许返回引用的例子,但这是例外,并不是常态

注意事项:

1.避免返回handles(reference,pointer,iterator)指向内部,将发生指针悬吊的可能性降到最低

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

1.基本承诺:如果抛出异常,程序内的任何事物仍然保存在有效状态,没有任何对象或数据结构会因此损坏,例如 对于changeBackground 一旦对象被抛出, 对象可能继续拥有原背景图像,也可能拥有一个缺省背景图像,但客户无法预期哪一种情况,如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么

2.强烈保证:如果异常被抛出,程序状态不改变,即如果成功,就是完全成功,如果失败,则会回复到”调用函数前的状态”

3.不抛掷保证,承诺绝不抛出异常,总是能完成它们承诺的功能

4.对于强烈保证的一个一般化设计策略:copy and swap

实现上通常是将所有”隶属于对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象,这种手法通常被称为 pimpl idiom 条款31 详细描述了它
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct PMImpl                                     //声明为struct 后面解释
{
std::tr1::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu
{
...
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap;
Lock m1(&mutex);
std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); //修改副本
pNew->imageChanges++;
swap(pImpl,pNew); //置换swap,释放mutex
}
//让PMImpl成为一个struct 而不是一个 class,因为PrettyMenu 的数据封装性,已经将其声明为private
//"copy-and-swap" 策略是对象状态做出"全有或全无"改变的一个很好办法

5.但一般而言它并不保证整个函数有强烈的异常安全性

1
2
3
4
5
6
7
8
9
void someFunc()
{
... //对local状态做一份副本
f1();
f2();
... //将修改后的的状态置换过来
}
//很显然如果f1, f2 的异常安全性比"强烈保证低",就很难让somefunc成为"强烈异常安全"
//如果f1,f2都是"强烈异常安全",情况并不就此好转,假设f1圆满执行,但f2抛出异常,此时f1发生变化,f2不变,程序状态与被调用前并不相同,所以不属于"强烈异常安全"

6.问题出在“连带影响”,只操作局部性状态很容易提供强烈保证,但是当函数对非局部性数据有连带影响时,提供强烈保证就困难的多

7.上述议题会组织你为函数提供强烈保证,即使你想这么做.另一个原因是效率,copy-and-swap的关键在于”修改对象数据的副本”,然后再不抛出异常的前提下置换,因此必须为每一个将被改动的对象做一个副本

8.当”强烈保证”不切实际时,应当提供”基本保证

注意事项:

1.异常安全函数(即使发生异常也不会泄漏资源或允许任何数据结构败坏):这样的函数分为三种级别:基本型,强烈型,不抛异常型

2.”强烈保证”通常的实现策略为copy-and-swap ,但并非所有函数都可以实现或具备实现意义

3.函数提供的”异常安全保证”,通常只等于其调用各个函数的”异常安全保证”中的最弱者,短板效应?

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

1.inline 虽然“免除函数调用成本”,但导致程序体积增大,适用于本体很小的函数

2.inline 是对编译器提出的一个申请,而不是强制命令.可以隐喻提出,也可以明确提出

1
2
3
4
5
6
7
8
9
10
//隐喻方式是将函数定义于class定义式内
class Person
{
public:
...
int age()const { return theAge; } //一个隐喻的inline申请
...
private:
int theAge;
};

3.inlining在大多数C++程序中是编译期行为

4.有时候即使inline但还是会生成函数本体(是否意味着inline代表着没有函数本体?),如下述例子

1
2
3
4
5
6
//代码要取某个inline的地址,编译器通常必须为此函数生成一个Outline的函数本体
inline void f() {...}
void (*pf) () = f; //pf 指向 f
...
f(); //这个调用inlined
pf(); //这个调用 或许 不被inlined,因为它通过函数指针达成

5.构造和析构函数不适用于inline

6.inline 不适用于 virtual ,virtual 运行时确定, inline编译时确定,互相冲突

7.inline 函数难以调试(不知道现在的编译器解决这个问题没)

注意事项:

1.将inline 限制在小型,调用频繁的函数身上

2.不要只因为function template 出现在头文件,就将它们声明为 inline

3.重点 inline 是个申请,最终是否为inlined函数取决于编译器(自己的理解)

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

1.编译时定义,往往会包含其他文件中的定义式,如果其他文件中的定义被更改,那么则该文件需要重新编译,这既是 “编译依存关系” , 不过通常包含标准头文件不会造成问题

2.采用pimpl idiom 设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <string>
#include <memory>

class PersonImpl; //Person实现类的前置声明
class Date; //Person接口用到的classes的前置声明
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; //指针,指向实现物,且采用shared_ptr (条款13)
};

这样的设计下,Person的客户完全与Dates,Addresses以及Person的实现细节分离,”接口与实现分离”

这个分离关键在于,”声明的依存性” 替换 “定义的依存性”, 那正是编译依存最小化的本质,这个简单的设计策略:
a.如果使用 object references 或 object pointers 可以完成任务,则不用使用objects
b.如果能够,尽量以class 声明式替换class 定义式
c.为声明式和定义式提供不同的头文件 (类似于我自己写的?将声明与定义分文件编写,声明包含定义头文件)

3.这样的pimpl设计,使classes 往往被称为Handle classes ,这种做法不会改变它要做的事情,只会改变它做事的方式.另一种制作Handle class 的方法是,令Person class 成为abstract class ,称为interface class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Person
{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate()const = 0;
virtual std::string address()const = 0;
...
};
//通常这种class的目的是详细描述derived 的接口,为了有办法为这种class创建新对象,往往采用facrtory函数
//(条款13)或virtual构造函数.
class Person
{
public:
...
static std::tr1::shared_ptr<Person>create(const std::string& name,
const Date& birthday,
const Address& addr);
...
};
//使用ing
std::string name;
Date dateOfBirth;
Address address;
...
//创意一个对象,支持Person接口
std::tr1::shared_ptr<Person> pp(Person::create(name,dateOfBith,address));
...
std::cout << pp->name()
<< " was born on "
<< pp->birthDate()
<<" and now lives at"
<< pp->address();
...

当然支持interface class 接口的具象类必须被定义出来(p147)

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

注意事项:

1.支持”编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式

2.程序库头文件,应该以”完全且仅有声明式”,(类似于main->声明.h->实现.cpp ?)

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

注意事项:

1.”public”继承,意味着 is-a 关系,适用于base class 身上的每一件事情也一定适用于derived class 身上

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

1.同名的函数会导致隐藏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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();
void mf3();
void mf4();
...
};
Derived d;
int x;
...
d.mf1(); //Derived::mf1
d.mf1(x); //invalid Derived::mf1 隐藏了 Base::mf1
d.mf2(); //Base::mf2
d.mf3(); //Derived::mf3
d.mf3(x); //invalid Derived::mf3 隐藏了 Base::mf3

2.覆盖 / 隐藏

3.通过using 声明取消隐藏

4.转交函数 forwarding funciton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base
{
public:
virtual void mf1()=0;
virtual void mf1(int);
...
};
class Derived:private Base
{
public:
virtual void mf1()
{ Base::mf1(); }
...
};
...
Derived d;
int x;
d.mf1(); // Base::mf1()
d.mf1(x); //Base::mf1(int) 被隐藏了

注意事项:

1.deirved class 内的名称会隐藏base class 内的名称

2.可用using 声明或转交函数来让被隐藏的名称再见天日

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

1.继承时可能出现三种我们希望的情况,”只继承接口”,”同时继承接口和实现,但希望能override”,”继承接口和实现,但不允许override”.

2.成员函数的接口总会被继承

3.pure virtual函数的目的是为了第一种情况,只继承函数接口(Interface class 类似条款31?)

4.impure virtua函数目的是为了第二种情况,继承函数的接口和默认实现

1
2
3
4
5
6
7
class Shape
{
public:
virtual void error(const std::string& msg);
...
};
//该接口表示每个derived class 都必须支持"遇上错误时可调用",若某个class不打算针对错误做出任何特殊行为,它可以退回到Shape class 的默认错误处理

但这种情况可能出现危险

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Airport { ... };
class Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{
default 实现
}
class ModelA::public Airplane { ... };
class ModelB::public Airplane { ... };
//由A B 继承 Airplane 并 override 默认实现 , 但如果出现下述情况呢
class ModelC::public Airplane
{
... //未声明fly函数
};
Airport PDX (...); //目的地
Airplane* pa = new ModelC;
...
pa->fly(PDX) //调用 Airplane::fly

如果忘记override,则采用默认实现,可能导致错误,问题不在于采用了默认实现,关键在于ModelC在没有override的时候,继承了fly,获得了一个它没有的行为,切断virtual接口和其默认实现之间的联系,可以解决这种问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Airplane
{
public:
virtual void fly(const Airport& dstination) =0;
...
protected:
void defalutFly(const Airport& destination);
};
void Airplane::defaultFly(const Airplane& destination)
{
default 实现
}
//现在fly 被改为 pure-virtual 函数,只提供接口,即接口于实现分离!
class ModelA::public Airplane
{
public:
virtual void fly(const Airplane& destination)
{ defaultFly(destination); }
...
};
class ModelB::public Airplane
{
public:
virtual void fly(const Airplane& destination)
{ defaultFly(destination); }
...
};
//现在不可能意外继承不正确的实现了,因为pure-virtual函数只提供了接口,ModelC必须override提供自己的版本

由于pure-virtual函数一定要被override,则可以采用另一种方式

(如果不覆盖,则派生类也会由于继承了纯虚函数变为抽象类–个人理解)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Airplane
{
public:
virtual void fly(const Airport& destination)=0;
...
};
void Airplane::fly(const Airport& destination) //pure-virtual函数实现
{
default 实现
}
class ModelA::public Airplane
{
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelC:public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
C的实现
}

5.non-virtual函数用于令derived classes 继承函数的接口以及一份强制性实现

1
2
3
4
5
6
7
class shape
{
public:
int objectID()const;
...
};
//就如上述例子,所有shape都采用统一的方式计算ID

6.pure-virtual , simple(impure)virtual , non-virtual :”只继承接口”, “继承接口和一份默认实现” ,”继承接口和一份强制实现”

注意事项:

1.接口继承和实现继承的区别,在public继承下,derived class 总是继承base class的接口

2.pure-virtual 只继承接口

3.impure-virutal 继承接口和默认实现

4.non-virtual 继承接口和强制实现

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

对于下述例子 , 通常会用virtual 来overred它,不妨考虑其他设计

1
2
3
4
5
6
class GameCharacter
{
public:
virtual int healthValue()const; //返回人物的健康值,derived class 可以override它
...
};

1.籍由non-virtual-Interface 实现 Template Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GameCharacter
{
public:
int healthValue()const //derived classes不重新定义它
{ //见条款36
... //做一些事前工作
int reVal = doHealthValue(); //真正的工作
... //做一些事后工作
return reVal;
}
...
private:
virtual int doHealthValue()const //derived classes可重新定义它
{
... //默认实现
}
};

这一设计”令客户通过public non-virtual成员函数间接调用 private virtual 函数”,称为non-virtual interface (NVI)手法. 它是所谓Template Method 设计模式(与c++templa

es并无关联),的一个独特表现形式,这个non-virtual 被称为 virtual 的外覆器

此手法优点: “做一些事前工作”和”做一些事后工作”, -> 确保得以在一个virtual函数被调用前设定好适当场景,并在调用结束之后清理场景

2.籍由Function Pointer 实现 Strategy模式

例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GameCharacter;         //前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue()const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};

与virtual 做法相比,它提供了弹性

a.同一类型不同实体,可以有不同的计算函数
b.已知类型的计算函数可以在运行期变更,例如提供一个成员函数setHealthCalculator,替换当前计算函数

3.籍由tr1::function 完成Strategy模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
//HealthCalcFunc可以是任何"可调用物",可被调用并接受任何兼容于
//GameCharacter之物,返回任何兼容于int 的东西
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthClacFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue()const
{ return healthFunc(*this);}
...
private:
HealthCalcFunc healthFunc;
};

和前一个设计相比,几乎相同.但如果需要更惊人的弹性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
short calcHealth(const GameCharacter&);    //健康计算函数  其返回类型为 non-int

struct HealthCalculator //为计算健康而设计的函数对象
{
int operator()(const GameCharacter*)const
{...}
};

class GameLevel
{
public:
float health(const GameCharacter&)const; //成员函数用以计算健康
...
};
class EvilBadGuy:public GameCharacter
{
...
};
class EyeCandyCharacter:public GameCharacter
{
...
};
EvilBadGuye ebg1(calcHealth); //人物 1 使用"函数"计算
EyeCandyCharacter ecc1(HealthCalculator()); //人物 2 使用"函数对象"计算
GameLevel currentLevel;
...
EvilBadGuy ebg2( //人物 3 使用"成员函数"计算
std::tr1::band(&Game::health,
currentLevel,
_1)
);

前两个都很好理解,对于第三个的理解因为此处的构造只接受一个参数,但此处有两个参数,所以用tr1::bind 将currentLevel绑定为GameLevel对象,让它在”每次GameLevel被调用计算ebg2的健康”时被使用,那正是tr1::bind的作为:它指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象

4.古典的Strategy

将健康计算函数做成一个分离的继承体系中的virtual函数. 对于上述例子,GameCharacter 是某个继承体系的根类,体系中的EvilBadGuy 和 EyeCandyCharacter 都是derived class;HealthCalcFunc是另一个继承体系的根类,体系中的SlowHealthLoser 和 FastHealthLoser 都是derived class, 每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象(参考p176图)

5.摘要

a.使用non-virtual interface (NVI)手法,这是 Template Method 设计模式的一种特殊形式.
b.将virtual函数替换为”函数指针成员变量”,这是Strategy设计模式的一种分解表现形式
c.以tr1::function成员变量替换virtual函数,这也是Strategy设计模式的某种形式
d.将继承体系中的virtual函数替换为另一个继承体系内的virtual函数,这是Strategy设计模式的传统实现手法

注意事项:

1.virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式

2.将机能从成员函数移到class外部函数

3.tr1::function对象的行为就像一般函数指针

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

1.non-virtual函数的性质为”不变性凌驾于特异性”,如果打算重定义,则应当使用virtual函数,当使用non-virtual函数并进行重定义,会导致指针/引用 指向一个实际的类时,所指的函数不是有实际的类决定,而是由指针/引用的类型决定

注意事项:

1.绝对不要重新定义继承而来的non-virtual函数

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

1.virtual函数是动态绑定(dynamically bound),而缺省参数值却是静态绑定(statically)

2.考虑以下继承体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Shape
{
public:
enum ShapeColor { RED, GREEN, BLUE};
virtual void draw(ShapeColor color = RED)const=0;
...
};
class Rectangle:public Shape
{
//注意,赋予不同的缺省参数值
virtual void draw(ShapeColor color = GREEN)const;
...
};
class Circle:public Shape
{
public:
virtual void draw(ShapeColor color)const;
//若以对象调用该函数,则一定要指明参数值,但若以指针(或引用)调用此函数,则可以不指明
//因为动态绑定下这个函数会从其base继承缺省参数值
}
//现在考虑这些指针
Shape* ps; //静态类型为Shape*
Shape* pc = new Circle; //静态类型为Shape*
Shape* pr = new Rectangle; //静态类型为Shape*
//本例中ps,pc和pr都被声明为pointer-to-shape类型,所以它们都以它为静态类型.
//所谓的动态类型则是指"目前所指对象的类型",对上例pc为Circle*,pr为Rectangle*

virtual函数系动态绑定而来,调用哪一份实现代码,取决于动态类型

1
2
pc->draw(Shape::RED);            //调用Circle::draw
pr->draw(Shape::RED); //调用Rectangle::draw

但对于缺省参数值,由于是静态绑定,所有derived类的缺省参数值全部取决于base中的

3.对于这些问题,当你想令virtual函数表现出你想要的行为但却遭遇麻烦,可以考虑virtual函数的替代设计(条款35)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Shape
{
public:
enum ShapeColor { Red, Green, Blue};
void draw(ShapeColor color = Red)const
{
do Draw(color);
}
...
private:
virtual void doDraw(ShapeColor color)const=0;
};
class Rectangle:public Shape
{
public:
...
private:
virtual void doDraw(ShapeColor color)const;
};
//由于non-virtual 函数绝不应该被override(条款36),这个设计很清楚的使得draw函数类的color缺省参数值总为Red

注意事项:

1.绝不要重定义一个继承而来的缺省参数值,因为缺省参数值是静态绑定,而virtual函数却是动态绑定

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

1.复合是类型之间的一种关系,当某种类型的对象含有它种类型的对象,便是这种关系

1
2
3
4
5
6
7
8
9
10
11
12
class Address { ... };
class PhoneNumber { ... };
class Person
{
public:
...
private:
std::string name;
Address address;
PhoneNumber voiceNumber;
PhoneNumber faxNumber;
};

2.条款32曾说,”public继承”带有 is-a的意义. 复合也有它的意义即:

a.has-a
b.根据某物实现出(“is-implemented-in-terms-of”)

当复合发生于应用域(人,汽车,一张张视频画面等)内的对象之间,表现出has-a关系;当它发生于实现域(缓冲区,互斥器,查找树等)则是表现出 “is implemented-in-terms-of”关系

3.has-a 和 is-a 的区别很好区分,关键在于is-a 和 is-implemented-in-terms-of(根据某物实现出),书中给出一个由list 实现 set 的例子,由于set并不是list,只是借由list的机能复用代码实现set,所以不符合is-a关系,采用 is-implemented-in-terms-of关系

注意事项:

1.复合(composition)的意义和public继承完全不同

2.在应用域,复合意味着 has-a,在实现域,复合意味着 is-implemented-in-terms-of

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

1.private继承下,编译器不会自动将一个derived class 对象转换成一个 base class 对象

2.private继承意味这implemented-in-terms-o(根据某物实现出),如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了

private在设计层面上没有意义,其意义只存在软件实现层面

3.考虑下述例子,设定某种计时器,记录Widget每个成员函数被调用的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Timer
{
public:
explicit Timer(int tickFrequency);
virtual void onTick()const;
...
};
//为了让Widget重定义Timer中的virtual函数,Widget必须继承自Timer
//因为Ontick并不是Widget接口的一部分,只是内部计数的实现细节,所以public继承不是好策略,会导致违反条款18
//的忠告:"让接口容易被正常使用,不易被误用"
//所以必须以private继承
class Widget::private Timer
{
private:
virtual void onTick()const;
...
};

这是个好设计,但private继承绝非必要,另一种做法是以复合取而代之

1
2
3
4
5
6
7
8
9
10
11
12
class Widget
{
private:
class WidgetTimer:public Timer
{
public:
virtual void onTick()const;
...
};
WidgetTimer time;
...
};

4.如果考虑将Widget的编译依存性降至最低,可以将class WidgetTimer分离出去,Widget内只留一个WidgetTimer* 指针

5.何时优先选择”private继承”而不是”继承加复合”?,这一种激进情况涉及空间最优化

只适用于你所处理的class不带任何数据是.这样的class没有non-static变量,没有virtual函数(会带来vptr,条款7),也没有virtual base classes(也会导致体积上的额外开销,条款40), 这种所谓的 Empty Class 不使用任何空间,但C++官方会安插一个char到空对象中,裁定凡是独立(非附属)对象都必须有非零大小
1
2
3
4
5
6
7
8
class Empty { };
class HoldAnInt
{
private:
int x;
Empty e;
};
//会出现 sizeof(HoldAnInt) > sizeof(int)

此时采用复合的话,HoldAnInt的对象不仅仅变大,还可能不止获得一个char的大小,由于齐位需求(条款50),如在clion下,HoldAnInt的大小变为 8

1
2
3
4
5
6
7
class HoldAnInt:private Empty
{
private:
int x;
};
//前面提到了"独立(非附属)"的对象大小不一定为0,但这个约束不适用于derived class对象的base class
//所以此处 sizeof(HoldAnInt) == sizeof(int),这是所谓的空白基类最优化 (EBO)

这种情况较少,所以通常采用复合

注意事项:

1.private 意味着 根据某物实现出.它通常比复合的级别低.

但是当derived class 需要访问 protected base class 的成员,或需重新定义继承而来的virtual 函数时,可以这样

2.private继承可以实现,EBO(空白基类最优化),这对于程序库开发者可能很重要

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

1.不同base class 中含有同名函数会导致调用发生歧义 p192

2.菱形继承时,应采用virtual 继承 防止含有多份成员变量

3.virtual 继承会导致额外的开销代价

4.p195给出一个良好的多重继承例子

5.多重继承只是一个工具,是否采用取决于你的设计方案,采用多重还是单一哪种效果更好

注意事项:

1.多重继承比单一继承复杂,他可能导致新的歧义性,以及对virtual函数的需要

2.virtual继承会增加大小,速度,初始化复杂度等成本.

3.多重继承的确有正当用途. 其中一个情节涉及”public继承某个Interface class” 和 “private 继承某个协助实现的class”的两者组合,参考 4 中的例子

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

1.对于oop 一般以显示接口和运行期多态解决问题,如下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Widget
{
public:
Widget();
virtual ~Wdiget();
virtual std::size_t size()const;
virtual void normalize();
void swap(Widget& other);
...
};
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
//1.由于w的类型被声明为Widget,所以w必须支持Widget接口,可以在源码中找出这个接口,所以称此为显式接口,也就
//是它在源码中明确可见
//2.由于Widget的某些成员函数为virtual,对这些函数的调用表现出运行期多态,即根据W的动态类型(条款37)决定掉
//用哪一个函数

2.对于Template及泛型编程,显式接口和运行期多态仍存在,但重要性变低,隐式接口和编译期多态移到前头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
//1.w必须支持哪一种接口,系由template中执行在w身上的操作来决定,本例来看好像必须支持 size,normalize和
//swap成员函数,copy构造函数,不等比较,但这并非完全成员,总而言之这一组表达式便是T必须支持的隐式接口
//2.凡涉及w的任何函数调用,有可能造成template具现化,这样的行为发生在编译期."以不同的template参数具现化"
//导致调用不同的函数,这既是所谓的编译期多态

注意事项:

1.classes 和 templates 都支持接口(interfaces) 和 多态(ploymorphism)

2.对class,接口是显式的,多态则是通过virtual函数发生于运行期

3.对template,接口是隐式的,奠基于表达式.多态则是通过template具现化和函数重载解析发生于编译期.

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

1.参考下述例子

1
2
3
4
5
6
7
8
9
10
11
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;
}
}

上述例子中出现的两个变量, iter 和 value. iter的类型是C::const_iterator , 实际是什么取决于template参数C. template内出现的名称如果相依于某个template参数,称之为从属名称.如果从属名称在class内呈嵌套状,称它为嵌套从属名称,此处的C::const_iterator就是这样的名称,实际上它还是个嵌套从属类型名称

2.嵌套从属名称可能导致解析困难

1
2
3
4
5
6
7
8
template<typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
...
}
//看起来好像好像是 const_iterator 指向一个 x
//但如果const_iterator 是 C里面的一个变量 , x是一个global变量名称,则上述代码变为相乘动作

3.解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是类型,除非你告诉它是.所以缺省情况下,嵌套从属名称不是类型,但有例外

4.想要告诉解析器是,需要在前面加上关键字

1
2
3
4
5
6
7
8
9
10
11
12
template <typename C>
void print2nd(const C& container)
{
if (container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
}
}
//typename只用来验明嵌套从属类型名称,其他名称不该有它存在,如下述例子
template <typename T> //允许使用 "typename" 或 "class"
void f(const C& container, //不允许使用 "typename"
typename C::iterator iter); //一定要使用 "typeneme"

5.例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list 中作为base class 修饰符

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
class Derived : public Base<T>::Nested //不允许
{
public:
explicit Derived(int x)
:Base<T>::Nested(x) //不允许
{
typename Base<T>::Nested(temp); //需要
...
}
...
};

注意事项:

1.声明template 参数时,前缀关键字class 和 typename 可以呼唤

2.请使用关键字typename标识嵌套从属类型名称,但不得在base class lists(基类列) 或 member initialization list(成员初值列)内以它作为base class 修饰符

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

1.对于下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
sendClear(info); //调用base class函数,这段无法通过编译
}
...
};
//当编译器遭遇class template 时,并不知道它继承的什么class.当然它继承的是MsgSender<Company>,但其中的
//company是一个template参数,不到后来(被具现化)不知道它是什么.
//而如果不知道Company是什么,就无法知道class MsgSender<company>看起来像什么,更明确的说是没办法知道它
//是否有个sendClear函数

2.对于下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//首先提供一个全特化版本
template <>
class MsgSender<CompanyZ>
{
public:
... //与一般template的差距在于它没有sendClear
void sendSecret(const MsgInfo& info){...}
};
//现在再次考虑derived class
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
...
sendClear(info); //如果Company == CompanyZ,这个函数不存在
...
}
...
};

如注释所言,base class 被指定为MsgSender<.CompanyZ>不合理.以为那个特化中没有提供sendClear函数,这就是为什么拒绝调用.

个人理解:也是为什么会出现遭遇class template时不清楚它继承了什么,因为它知道base class templates可能被特化,而那个特化版本不提供和一般性template相同的接口. 因此它往往拒绝在 templatized base classes(模板化基类)内寻找继承而来的名称

3.对此有三种办法使得C++”不进入 templatized base classes观察”的行为失效

a.使用this->
1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
...
this->sendClear(info); //成立,假设sendClear将被继承
...
}
...
};
b.使用using 声明式(条款33)
虽然使用using也可以在这里有效运作,但两处解决的问题不相同,这里的情况并不是base class名称被遮掩,而是编译器不进入base class作用域查找,于是我们通过using告诉它,让它这么做
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>
{
public:
using MsgSender<Company>::sendClear; //告诉编译器,请他假设sendClear位于baseclass内
...
void sendClearMsg(const MsgInfo& info)
{
...
endClear(info); //假设sendClear将被继承
...
}
...
};
c.明白指出被调用的函数位于base class 内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename Company>
class LoggingMsgSender:public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
...
MsgSender<Company>::sendClear(info); //假设sendClear将被继承
...
}
...
};
//但这往往是最不让人满意的一个解法
// 如果被调用的是virtual函数,上述的 明确资格修饰 会关闭"virtual"绑定行为

4.上述的每个解法做的事情都相同,对编译器承诺: “base class templates的任何特化版本都将支持其一般(泛化)版本所提供的接口”

1
2
3
4
5
//举个例子
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
...
zMsgSender.sendClearMsg(msgDate); //错误,特化版本中不存在sendClear函数

注意事项:

1.可在derived class templates 内通过 “this->”指涉base class templates 内的成员名称,或籍由一个明白写出的”base class 资格修饰符”完成

条款 44 : 将于参数无关的代码抽离templates

1.对于non-template代码中,重复十分明确:你可以”看”到两个函数或两个classes之间有重复,但在template代码中,重复很隐晦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//考虑下述例子
template <typename T,std::size_t n>
class SqueareMatrix
{
public:
...
void invert(); //求逆矩阵
};
//现在再看下述例子
SquareMatrix<double,5> sm1;
...
sm1.invert(); //调用SquareMatrix<double,5>::invert
SquearMatrix<double,10> sm2;
...
sm2.invert(); ////调用SquareMatrix<double,10>::invert

这导致具现化了两次invert函数

2.对此可以将重复的代码抽离出,共享一份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
class SquareMatrixBase
{
protected:
...
void invert(std::size_t matrixSize);
...
};
template <typename T,std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>
{
private:
using SquareMatrixBase<T>::invert; //避免隐藏 条款33
public:
...
void invert() { this->invert(n); }
};

此处的几个细节

a.private继承,因为这里的base class 只是用来帮助实现derived class ,不是is-a关系
另外几点的理解我的和书上的有出入
书上:此处的using声明用来避免隐藏, 对于this->若不用,模板化基类的函数名会被derived classes遮盖
我的:此处的using声明避免隐藏的同时,让不进入模板化基类的行为失效,this->在此处有点多余了,且无法做到排除derived的遮盖,此处的例子刚好参数不同导致了重载,若变为无参,调用的还是derived 中的函数

3.SquareMatrixBase::invert如何知道该操作什么数据,完整的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
template <typename T>
class SquareMatrixBase
{
protected:
SquareMatrixBase(std::size_t n,T* pMem)//存储矩阵大小和一个指针,指向矩阵数值
:size(n),pData(pMem){}
void setDataPtr(T* ptr){ pData = ptr; }//重新赋值给pData
...
private:
std::size_t size; //矩阵的大小
T* pData; //指针,指向矩阵内容
}
//这允许derived class 决定内存的分配方式
template <typename T,std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>
{
public:
SquareMatrix()
:SquareMatrixBase<T>(n,data){}
...
private:
T data[n*n];
}
//另一种做法是,通过new来分配内存到heap上
template <typename T,std::szie_t n>
class SquareMatrix:private SquareMatrixBase<T>
{
public:
SquareMatrix()
:SquareMatrixBase<T>(n,0) //将base class 的数据指针设为null
,pData(new T[n*n]) //为derived内的指针分配内存
{ this->setDataPtr(pData.get());}//将它的一个副本交给base class
...
private:
boost::scoped_array<T> pData; //boost::scoped_array 见条款13
};

4.p216面下半部分未理解 大概意思是1.抽取公共代码到新的类导致对象大小的增加,此处就是多了一个data指针2.此处的n在模板参数中的话可能会有更多的优化,但作为函数参数的话就失去了这些优化 是否应该抽取公共代码,需要根据实际做出选择

5.目前这个条款只讨论了non-type template parameters (非类型模板参数)带来的膨胀,也就是代码的膨胀是由于相同算法多次具现化导致的,在本例就是矩阵的逆运算算法带来的.类型参数(type parameters)也会导致膨胀,例如许多平台上 int 和 long 有完全相同的二进制表述, 所以像vector<int.>和vector<long.>的成员函数有可能完全相同,这就导致了膨胀

注意事项:

1.Templates生成多个classes和多个函数,所以任何template代码都不该于某个造成膨胀的template参数产生相依关系(理解:使用模板时应避免产生代码膨胀)

2.因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数

3.因类型参数而造成的代码膨胀,往往可以降低,做法是带有完全相同的二进制表述的具现类型共享实现码

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

1.本条款给出下述例子,定义智能指针,指向某继承体系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//三层继承体系
class Top { ... };
class Middle:public Top { ... };
class Bottom:public Middle { ... };
Top* pt1 = new Middle; //将Middle* 转换为 Top*
Top* pt2 = new Bottom; //将Bottom* 转换为 Top*
const Top* pct2 = pt1; //将Top* 转换为 const Top*

//定义的智能指针
template <typename T>
class SmartPtr
{
public:
explicit SmartPtr(T* realPtr); //以内置(原始)指针完成初始化
...
};
//如果改为自定义的智能指针
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle); //将SmartPtr<Middle>转换为SmartPtr<Top>
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom); //下同
SmartPtr<Top> pct2 = pt1;

对于上述例子,同一个template的不同具现体之间不存在什么关系

这里意指如果以带有base-derived 关系的B,D两类型分别具现化某个template,产生的两个具现体,并不带有base
derived关系,所以完全为不同的两个classes

2.对此需要的应该是member function template ,需要一个构造模板

1
2
3
4
5
6
7
8
template <typename T>
class SmartPtr
{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other);
...
};
上述例子的意思,对任何类型T和任何类型U,这里可以根据SmartPtr<.U>生成一个Smart<T.>

3.由此可以实现我们想要的

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
class SmartPtr
{
public:
template <typename U>
SmartPtr(const SmartPtr<U>& other)
:heldPtr(other.get()) { ... } //关键 heldPtr(get())
T* get()const { return heldPtr; }
...
private:
T* heldPtr; //原始指针
}
以初值列标来初始化,这个行为仅当U指针转为一个T指针时才能通过编译

4.泛化copyi构造并不阻止编译器生成自己的copy构造,你必须同时声明泛化版本和正常版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class T>
class shared_ptr
{
public:
//copy构造 和 泛化copy构造
shared_ptr(shared_ptr const & r);
template<class Y>
shared_ptr(shared_ptr<Y> const& r);
//普通copy assignment 和 泛化版本
shared_ptr& operator=(shared_ptr const& r);
template<class Y>
shared_ptr& operator=(shared_ptr<T> const& r);
...
};

注意事项:

1.请使用member function templates 生成 “可接受所有兼容类型”的函数

2.如果你声明的member template 用于”泛化copy构造”或”泛化assignment”操作,你还是需要声明正常的copy构造和copyassignment操作符

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

1.对于下述例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>
class Rational
{
public: //pass-by-referenece 条款20
Rational(const T& numerator=0,const T& denominator=1);
const T numerator()const;//pass-by-value 条款28 返回引用(handle)会导致修改private变量
const T denominator()const;
... //使用 const 条款3
};
template <typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs)
{ ... }
//像条款24 (此例子的普通版本来自于条款24) 区别:改为template
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2; //错误 无法通过编译
//问题在于编译器不知道该如何具现化出这个函数,为了完成这一动作,必须先算出T是什么,但编译器不行
//对于第一实参,能够顺利推导出T为int,但对于第二实参类型为 int ? 你可能期盼编译器使用Rational<int>的non-explicit 构造函数 将 2 转化为内 Rational<int> , 进而将T推到为int,但它们不这么做

因为在template实参推到过程中从不将隐式类型转换函数纳入考虑

2. e.g

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
class Rational
{
public:
...
friend const Rational operator*(const Rational& lhs,const Rational& rhs);
...
};
template <typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs)
{
...
}

此处编译可以通过,frined 函数在class被声明的时候,随着class同时被具现出来,后者由此作为一个函数而非模板函数,因此编译器可以调用它时用隐式转换函数

但此处,无法链接,因为具现化的时候class内只是声明了,但没有定义,类外那个template和它关系不大,所以连接器链接不上

3.对此最简单的解决方法

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
class Rational
{
public:
...
friend const Rational operator*(const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rbs.denominator());
}
};

这里的friend与传统的目的不同,传统的friend是用来让non-member函数访问non-public成员,但此处是为了实现类型转换,而为了实现类型转应采用non-member函数,而为了在class内声明non-member函数,我们需要friend

4.另一种做法是为了减小inline的影响,采用辅助函数的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T> class Rational;
template <typename T>
const Rational<T> doMultiply(const Rational<T>& lhs,const Rational<T>& rhs);

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

这里是让operator*只是用来进行转换,用来支持混合式乘法,实现交给doMultiply,有种各司其职的感觉,因为doMultiply因为是template class 不支持混合式乘法,但operator提供了类型转换

5.还有一种做法,教官提出来的,但我的编译器上实现有问题

注意事项:

1.当编写的class template , 而它提供的template相关的函数支持”所有参数的隐式转换”时,请将那些函数定义为class template 内的 friend函数

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

首先为更好的理解下述例子,给出不同迭代器的定义:
  1. Input迭代器:只能向前移动,一次一步,只读
  2. Output迭代器:只能向前移动,一次一步,只写
  3. forward迭代器:做前两种迭代器能做的事情,可读可写
  4. Bidirectional迭代器:比前一个能做的事更多,既可以向前还可以向后
  5. random access迭代器:可以移动任意距离
它们之间具有一系列的继承关系:
1
2
3
4
5
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag{};
sturct bidirectional_iterator_tag:public forward_iterator_tag{};
struct random_access_iterator_tag:public bidirectional_iterator_tag{};

1.文中给出一个advance函数,用来对某个迭代器移动某个给定距离,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename IterT,typename DistT>
void advance(IterT& iter,DistT d)
{
if (iter is a random access iterator)
{
iter += d; //针对random access迭代器使用迭代器算数运算
}
else
{ //针对其它迭代器分类
if ( d>=0 ) { while (d--) ++iter; }
else { while (d++) --iter; }
}
}

“traits必须能够施行于内置类型”意味着”类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原始指针内,因此类型的traits信息必须位于类型自身之外.标准技术是把它放进一个template及其一个或多个特化版本中,这样的templates在标准程序库中有若干个,其中针对迭代器的被命名为iterator_traits

对于这段话的理解,一般实现traits技术时,是在每个类型中,此例子中就是每个迭代器类型中,声明一个typedef这也就是”类型内的嵌套信息”,而指针也相当于迭代器,但普通指针不是类,也就不存在”类型内的嵌套信息”,对于这种类型,tratis就需要提供特化版本

2.iterator_traits的运作方式,针对每一个类型IterT,在struct iterator_traits<IterT.>内一定声明某个typedef名为iterator_category,这个typedef用来确认IterT的迭代器分类

同时Iterator_traits以两个部分来实现,首先它要求自定义的迭代器类型必须嵌套一个typedef ,名为iterator_ category
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//deque的例子
template < ... >
class deque
{
public:
class iterator
{
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
//list的例子
template < ... >
class list
{
public:
class iterator
{
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};

而对于iterator_traits的另一部分实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename IterT>
struct iterator_traits
{
//嵌套从属名称,用typename来表示它是一个类型 参考条款42
typedef typename IterT::iterator_category iterator_category;
...
};
//而对于指针迭代器则需要一个特化版本
template <typename IterT>
struct iterator_traits
{
typedef random_access_iterator_tag iterator_category;
}

3.这就是如何实现traits class

  1. 确认若干你希望将来可取得的类型相关信息.例如对于迭代器,我们希望将来可取得其分类
  2. 为该信息选择一个名称(如iterator_category)
  3. 提供一个template 和 一些特化版本

4.接下来就是如何使用tratis class了,对于先前的代码可以如下改写

1
2
3
4
5
6
7
8
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) ))
...
}
//目前看起来很好,但会导致编译问题,条款48会讨论这点

5.还有一个问题是,IterT类型可以在编译期确定,但if要在运行期才被核定,为解决这个问题,引出来另一做法,通过重载来决定,因为重载会优先选择最匹配的重载件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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::bidirectionla_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");
while (d--) ++iter;
}
template<typename IterT,typename DistT>
void advance(IterT& iter,DistT d)
{
doAdvance{iter,d,typename std::iterator_traits<IterT>::iterator_category()};
}
上述即是怎么使用traits class
  1. 建立一组重载函数,根据不同的traits信息,给出不同实现
  2. 建立一个控制函数,调用上述重载函数,并传递tratis class 所提供的信息

注意事项:

1.Tratis classes使得”类型相关信息”在编译期可用.它们以templates和 “templates 特化” 完成实现

2.整合重载技术后,tratis classes可能在编译期对类型执行if…else测试,如同书中上述例子

条款 48 : 认识template元编程

1.以C++写成,执行与C++编译期内的程序.一旦TMP程序结束执行,其输出,也就是从template具现出来的若干C++源码,便会一如既往地被编译

2.TMP的两个优势 a.让某些事情更容易,如果不用TMP甚至不可能 b.TMP执行于编译期,编译期执行带来的好处,较小的文件,较短的运行期,较少的内存需求,但是编译时间变长.

3.条款47曾说过下属例子,会编译错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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))
{
iter += d;
}
else
{
if ( d>=0 ) {while (d--) ++iter; }
else {while (d++) --iter; }
}
}
//这里就体现出了TMP与之相比的优势
std::list<int>::iterator iter;
...
advance(iter,10);
//无法通过编译,有上述调用可用产生以下实现
void advance(std::iter<int>::iterator& iter,int d)
{
if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category)
== typeid(std::random_access_iterator_tag))
{
iter += d; //错误
}
else
{
...
}
}
//此处尝试在一个 bidirectional迭代器上使用 += 导致错误

TMP是一个**”函数式语言”**

4.一个TMP程序的例子(魔法)

1
2
3
4
5
6
7
8
9
10
template<unsigned n>
struct Factorial
{
enum {value = n*Factorial<n-1>::value};
};
template<>
struct Factorial<0>
{
enum{value = 1};
};

为何使用TMP的三个原因(p237)

  1. 确保量度单位正确
  2. 优化矩阵运算
  3. 可用生成客户定制的设计模式

注意事项:

1.TMP (模板元编程)可讲工作由运行期移至编译期,由此可用实现早期错误侦测,以及更高的效率

2.TMP可被用来生成”基于政策选择组合”的客户定制代码,也可以避免生成对某些特殊类型并不适合的代码

条款 49 : 了解new-handler的行为

new-handler相当于一个处理错误的函数,当new遇到分配内存失败的情况时就会调用这个函数,new_handler也能通过set_new_handler来设置我们自己的

1.这是一个声明于<new.>的标准程序库函数

1
2
3
4
5
6
7
namespace std
{
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
//该函数的参数是个指针,目的是接受传进去的用来设置遇到错误时调用的函数,返回值也是一个指针,返回设置前的错误处
//理函数也就是默认的那个 , 后面会给出使用例子,就能理解为什么返回也是一个函数指针了

2.下面是一些简单的使用例子

1
2
3
4
5
6
7
8
9
10
11
12
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray = new int[100000000L]; //开辟一块不能满足的内存
...
}
//当operator new 无法满足内存申请时,就会不断调用 new_handler 函数,即错误处理函数

对此一个设计优秀的new_handler函数,必须做以下事情

  1. 让更多内存可被使用,我的理解就是清理内存,或者提前准备一部分内存以防发生分配不足的情况
  2. 安装另一个new_handler,当前new_handler不能处理,那么交给另一个能处理的new_handler
  3. 卸除new_handler,将null指针传给set_newhandler,一旦没有安装任何new_handler,在不足时会抛出异常
  4. 抛出bad_alloc异常,这样的异常不会被operator new 捕捉,因此会传播到内存索求处
  5. 不返回,通常调用abort或eixt

3.C++并不支持class专属的new_handler,但你可以自己实现,只需要自己提供set_new_handler和operator new即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//1中提到的例子
//首先是一个自定义的class
class Widget
{
public:
//注意不是std::set_new_handler此处的只是用来设定Widget中的new_handler而不是全局中的
static std::new_handler set_new_handler(std::new_handler p)throw();
//operator new 中调用的才是std::那个 用来改变全局中的new_hander
static void* operator new(std::size_t size)throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
}

//Widget中函数的实现 下例函数也是标准版中的作为
std::new_handler Widget::set_new_handler(std::new_handler p)throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return olHandler;
}
void* Widget::operator new(std::size_t size)throw(std::bad_alloc)
{
//此处用一个资源管理类保存装载Widget类的new_handler之前的全局new_handler也就是 null
//在结束时恢复全局new_handler 通过资源管理类中的析构函数实现
NewHandlerHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}

//其次需要一个资源处理类用来管理new_handler的释放于归还
class NewHandlerHolder
{
public: //获得没装载前的全局new_handler
explicit NewHandLerHolder(std::new_handler nh)
:handler(nh){}
~NewHandlerHolder() //析构时恢复全局new_handler
{ std::set_new_handler(handler);}
private:
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder&); //阻止copying,也可以用delete
NewHandlerHolder& operator=(const NewHandlerHolder&)
}

//使用如下
void outOfMem();

int main()
{
Widget::set_new_handler(outOfMem); //为Widge指定 new_handler
Widget* pw1 = new Widget; //如果分配失败调用 outOfMem

std::String* ps = new std::string; //如果分配失败调用全局new_handler,
//因为恢复了原先的new_handler
Widget::set_new_handler(0);
Widget* pw2 = new Widget; //如果失败,抛出异常
}

4.另一种做法是采用继承和模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <typename T>          //"mixin"风格
class NewHandlerSupport
{
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);
... //其它operator new 版本 见条款52
private:
static std::new_handler currentHandler;
};
... //省略实现, 见 p245例子
//对此Widget只需要继承
class Widget:public NewHandlerSupport<Widget>
{
...
};
//其他调用基本如前

该技术被称为 “怪异的循环模板模式” CRTP

使用”mixin”风格的继承肯定导致多重继承的争议,见条款40

5.nothrow new 和 普通的new , nothrow-new在分配失败时返回null

nothrow 的一些理解见p247最上面一段

注意事项:

1.set_new_handler允许客户指定一个函数,在内存分配失败时被调用

2.Nothrow new 是一个颇为局限的工具.

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

就是为啥不用编译期提供的而要自己定义

  1. 用来检测运用上的错误,能够实现将使用出错时记录下来
  2. 为了强化效能,编译期提供的主要用于一般目的,而在例如(网页服务器,web servers)则可能需要进行替换
  3. 为了收集使用上的统计数据,收集分配删除时的一些信息

e.g 是一个例子,促进并协助检测”overruns”(写入点在分配区块尾端之后)或”underruns”(写入点在分配区块起点之前)的定制 operator new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static 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) //增加大小,使得能够塞入两个signature

void* pMem = malloc(realSize); //使用malloc分配内存
if (!pMem) throw bad_alloc();
//将signature写入最前和最后段 由于不清楚这个定制operator new是干啥的,此处不需要过度考虑
//此operator new 只是用来了解怎样定制一个 opeator new
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) = signature;

//返回指针,指向第一个signature之后
return static_cast<Byte*>(pMem)+sizeof(int);
}

此处的一些问题

  1. “没有坚持c++的规矩”, 条款51说所有operator new 都应该内含一个循环,反复调用某个new_handing函数,z这里却没有,另一个问题 就是 齐位
  2. “齐位”,C++要求所有的operator new 返回的指针都有适当的齐位,否则不安全,具体的详细介绍见p249

摘要:

  1. 为了检测运用错误(如前所述)
  2. 为了收集动态分配内存的使用统计信息(如前所述)
  3. 为了增加分配和归还的速度
  4. 为了降低默认版本带来的空间额外开销
  5. 为了弥补默认分配器中的非最佳齐位
  6. 为了将相关对象成簇集中
  7. 为了获得非传统行为

注意事项:

1.你有许多理由需要写个自定的new和delte,本条款就是介绍了一些使用的理由

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

条款50说过了你何时需要写一个自己的new和delete,本条款主要用来说明写自己的new和delete时需要遵守的一些规范

  1. 一个循环,不断尝试分配内存并在失败时调用new_handler
  2. 有能力处理0bytes申请
  3. class的专属版本还应处理 继承时 “比正确大小更大的错误申请”
  4. delete在收到null时应该不做任何事
  5. class的版本还应处理”比正确大小更大的错误申请”

1.下面是一些伪码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void* operator new(std::size_t size)throw(std::bad_alloc)
{ //你的可能还有一些别的参数,这里只是给出一些需要遵守的共同部分
using namespace std;
if (size == 0){
size = 1; //将0-byte申请 视为 1-byte申请
}
while (true)
{
尝试分配;
if (分配成功)
return (指针,指向分配的内存)
//分配失败
new_handler globalHandler = set_new_hander(0);
set_new_handler(globalHandler);

if (globalHandler)
(*globalHandler)();
else
throw std::bad_alloc();
}
}

用set_new_handler找出new_handler指针,因为没有别的好方法,这样虽然拙劣,但有效

2.在出现继承时的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base
{
public:
static void* operator new(std::size_t size)throw(std::bad_alloc);
...
};
class Derived:public Base { ... }; //假设Derived未声明自己的operator new
Derived* p = new Derived; //这里将调用 Base::operator new

//由于 Base 的operator new 只是针对Base 的,对于Derived的意外继承应采取措施
void* operator new(std::size_t size)throw(std::bad_alloc)
{
if (size != sizeof(Base)) //如果大小错误,则采取标准的.此处理同时将0的情况也处理了,所
return ::operator new(size); //以不需要额外对0的情况进行处理
...
}

对于 new [] 详细见 p254下面段落

3.对于delete情况

1
2
3
4
5
void operator delete(void* rawMemory)throw()
{
if (rawMemory == 0)return;
归还内存;
}

4.对于class情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base
{
public:
...
static void operator delete(void* rawMemory,std::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(rawMrmory);
return;
}
归还内存;
return;
}
//欠缺virtual析构可能出现的一些问题,见条款7

注意事项:

1.operator new 应该内含一个无穷循环,不断尝试分配内存,若失败则调用new_handler,有能力处理0 byte申请,class版本还应该处理”并正确大小更大的申请”

3.operator delete 应该在收到null时不做任何事情,class版本还应该处理”并正确大小更大的申请”

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

1.条款首先引入一条例子,告诉我们为什么要如此做

Widget* pw = new Widget;

此处公有两个函数被调用,一是用以分配内存的operator new , 另一个是 Widget的构造函数

问题就出在此,假设第一个函数调用成功,而构造的时候抛出异常,这个时候就需要取消分配的内存,否则会造成内存泄漏,但Widget抛出异常导致**,pw并未被赋值**,也就是客户没有能力归还内存,所以取消内存的责任落在C++运行期系统身上,运行期系统会调用与operator new 相应的 operator delete版本,如果是正常形式的new 和 delete的话,运行期系统毫无问题可以知道该用那个delete

2.首先简单介绍一下术语placement new

placement new 实际上不只是一个特殊的new,之前我以为只是一个能在指定区域分配的内存的new,实际上placement new 是一系列特殊new的统称,意味着带任意额外参数的new,而那个能在指定区域分配内存的new,是最早的placement new 版本,这也是这个函数命名的根据,由此placement new 有多重定义,大多数时候是这个**”接受一个指针指向对象被构造之处”**,也就是下面这个C++标准库中的placement new

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

3.一个非正常 operator new 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Widget
{
public:
...//如果operator new接受的参数除了size_t华友其他的,这便是所谓的placement new
static void* operator new(std::size_t size,std::ostream& logStream)
throw(std::bad_alloc); //非正常形式的new
static void operator delete(void* pMemory,std::size_t size)
throw(); //正常的class专属delete
...
};
//这个设计有问题
Widget* pw = new(std::cerr)Widget;
//如果如1一样构造时抛出异常,则运行期系统则会寻找对应的delete"参数个数和类型都与operator new相同的"
//也就是如下的delete
void operator delete(void*,std::ostream&)throw(); //placement delete

4.对此应该提供对应的operator delete

1
2
3
4
5
6
7
8
9
10
11
12
13
class Widget
{
public:
...
static void* operator new(std::size_t size,std::ostream& logStream)
throw(std::bad_alloc);
static void operator delete(void* pMemory)throw();
static void operator delete(void* pMemory,std::ostream& logStream)
throw();
...
};
Widget* pw = new(std::cerr)Widget; //不再泄漏
delete pw; //调用正常的operator new

在与placement new 相关内存泄漏宣战时,必须同时提供一个正常的operator new(用于构造无任何异常被抛出)和一个placement 版本(用于构造期间有异常抛出)

5.注意名称遮盖问题,比较好理解就是如果class只声明了一种则会遮掩其他的,解决方法就是声明一个base class 储存正常的,让derivde继承,并using 使其在derived中可见

注意事项:

1.当你写一个placement new 请确定写出placement operator delete,否则回导致隐微的内存泄漏

2.当你声明placement new 和 placement delete时,注意不要遮掩它们的正常版本

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

编译期提示的警告信息,可能与你想的含义不一样,不能忽略它们,要了解编译期真正想告诉你的是什么,并从中吸取经验,尽量做到no-warming,不同的编译器警告可能不同,在这个编译器无警告的代码,在另一可能会有警告

1
2
3
4
5
6
7
8
9
10
11
class B
{
public:
virtual void f()const;
};
class D:public B
{
public:
virtual void f();
};
//warming: D::F() hides virtual B::f()

上述代码你可能觉得想得和你一样,D::f 遮掩了 B::f ,实际上编译器是想告诉你,B中的f没有被override,而是被遮掩了,因为B中的签名式const 而D中没有

注意事项:

1.严肃对待编译器发出的警告,尽量在编译期的最高警告级别下争取”no-warming”

2.不要过度依赖编译器的警告,因为不同的编译器对待事情的态度并不相同

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

各种标准程序库

注意事项:

1.C++标准程序库主要由STL,iostreams,locales组成.并包括c99标准程序库

2.TR1添加了智能指针,一般化函数指针(tr1::function 包装器),hash-based容器,正则表达式(regular expression)以及另外10个组件

3.TR1自身只是一份规范.为获得TR1提供的好处,你需要一份实物,来自Boost

条款 55 : 让自己熟悉Boost

Boost一个社区,由于和C++委员会重合很大,所以与其他的C++社区相比比较有分量

注意事项:

1.Boost是一个社群,也是一个网站,提供免费,开源的C++程序库

2.Boost提供许多TR1组件实现品