系列文章目录
文章目录
- 系列文章目录
- 前言
- 1:多语言结合体——C++
- 2:尽量以const, enum, inline替换 \#define
- 3:尽可能使用const
- 4:确定对象被使用前已先被初始化
- 5:了解C++默认编写并调用哪些函数
- 6:若不想使用编译器自动生成的函数,明确拒绝
- 7:为多态基类声名virtual析构函数
- 8:Prevent exceptions from leaving destructors
- 9:绝不在构造和析构过程中调用virtual函数
- 10:令operator=返回一个reference to *this
- 11:在operator=中处理“自我赋值”
- 12:copy all parts of an object
- 资源管理
- 13:Use objects to manage resources
- 14:Think carefully about copying behavior in resource-managing classes.
- 15:在资源管理类中提供对原始资源的访问
- 16:成对使用new和delete时要采取相同形式
- 17:Strore newed objects in smart pointers in standalone statements
- 设计与声名
- 18:Make interfaces easy to use correctly and hard to use incorrectly.
- 19:Treat class design as type design
- 20:Prefer pass-by-reference-to-const to pass-by-value
- 21:Don't try to return a reference when you must return an object
- 22:Declare data members private.
- 23:Prefer non-member non-friend functions to member functions
- 24:若所有参数皆需类型转换,请为此采用non-member函数
- 25:考虑写出一个不抛出异常的swap函数
- 实现 Implementations
- 26:尽可能延后变量定义式的出现时间
- 27:Mimimize casting
- 28:避免返回“handles”(号码牌,用来取得某个对象)指向对象内部成分
- 29:Strive for exception-safe code.
- 30:Understand the ins and outs of inlining.
- 31:将文件间的编译依存关系降至最低
- 继承于面向对象设计
- 32:Make sure public inheritance models "is-a".
- 33:Avoid hiding inherited names.
- 34:区分接口继承和实现继承
- 35:Consider alternatives to virtual functions.
- 36:Never redefine an inherited non-virtual function
- 37:Never redefine a function's inherited default parameter value.
- 38:Model "has-a" or "is-implemented-in-terms-of" through composition.
- 39:Use private inheritance judiciously.
- 40:Use multiple inheritance judiciously.
- 模板与泛型编程
- 41:Understand implicit interfaces and compile-time polymorphism.
- 42:Understand the two meanings of typename.
- 43:Know how to access names in templatized base classes.
- 44:将与参数无关的代码抽离templates
- 45:运用成员函数模板接受所有兼容类型
- 46:需要类型转换时请为模板定义非成员函数
- 47:请用traits classes表现类型信息
- 48:Be aware of template metaprogramming.
- 定制new和delete
- 49:了解new-handler的行为
- 50:了解new和delete的合理替换时机
- 51:编写new和delete时需固守常规
- 52:写了placement new 也要写placement delete
- 杂项讨论
- 53:关注编译器的警告
- 54:让自己熟悉包括TR1在内的标准程序库
- 55:Familiarize yourself with Boost
前言
前言
术语
声名:告诉编译器某个东西的名称和类型,但略去细节。表明数据类型和属性,并不分配内存。
定义:提供编译器一些声名所遗漏的细节。需要分配内存
初始化:“给对象初始”的过程。
1:多语言结合体——C++
- C
- Object-Oriented C++
- Template C++
- STL
2:尽量以const, enum, inline替换 #define
class GamePlayer
{
private:
enum {NumTurns = 5};
int scores[NumTurns];
}
若编译器不允许“int-class 初始值设定”
- 对于单纯常量,最好以const对象或enums替换#define
- 对于形似函数的宏,最好改用inline函数替换#define
3:尽可能使用const
class TextBlock
{
public:
TextBlock(string s):text(s) {}
const char& operator[](std::size_t position) const
{
return text[position];
}
char& operator[](std::size_t position)
{
return text[position];
}
private:
std::string text;
};
int main()
{
TextBlock tb("Hello");
cout << tb[0] << endl; //调用non-const TextBlock::operator[]
const TextBlock ctb("WORLD");
cout << ctb[0] << endl; //调用const TextBlock::operator[]
tb[0] = '5'; //✔
ctb[0] = '5'; //❌
}
static_cast, const_cast
4:确定对象被使用前已先被初始化
class PhoneNumber {...};
class ABEntry {
public:
ABEntry(const std::string&name);
private:
std::string theName;
};
ABEntry::ABEntry(const std::string&name):theNmae(name) //这叫初始化
{
theName = name; //着叫赋值,非初始化
}
- 为内置类型对象进行手动初始化,C++不保证初始化它们
- 构造函数最好使用成员初始值列表,而不要再构造函数本体内使用赋值操作。初始值列出的成员变量,其排列次序应该和它们再class中的声名次序相同。
- 为免除“跨编译单元之初始化次序”问题,以local static 对象替换non-local static对象。
5:了解C++默认编写并调用哪些函数
默认声名:
一个默认构造函数
copy构造函数
copy assignment操作符
析构函数
just like this
class Empty
{
public:
Empty() {}
Empty(const Empty& rhs) {}
Empty& operator=(const Empty& rhs) {}
~Empty() {}
};
Empty e1; //default 构造函数
//析构函数
Empty e2(e1); //copy构造函数
e2 = e1; //copy assignment 操作符
- 编译器可自动为class创建default constructor, copy constructor, copy assignment operator, deconstructor。
6:若不想使用编译器自动生成的函数,明确拒绝
class Empty
{
public:
Empty() = delete;
Empty(const Empty&) = delete;
Empty& operator=(const Empty&) = delete;
~Empty() = delete;
};
7:为多态基类声名virtual析构函数
class SpecialString: public std::string { // std::string有个 non-virtual析构函数
...
};
SpecialString* pss = new SpecialString("sdf");
std::string *ps;
ps = pss; //静态类型与动态类型不一致
delete ps; //❌
- polymorphic(带多态性质的) base classes 应该声名一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- classes的设计目的如果不是作为base classes使用,或不是为了具备多态特性(polymorphically),就不该声明virtual析构函数。
8:Prevent exceptions from leaving destructors
9:绝不在构造和析构过程中调用virtual函数
构造函数内的virtual函数还不是virtual的
10:令operator=返回一个reference to *this
class Widget{
public:
...
Widget& oeprator=(const Widget& rhs){
...
return *this;
}
};
11:在operator=中处理“自我赋值”
隐式的自赋值
a[i] = a[j]; *px = *py
Widget& operator=(const Widget& rhs)
{
/* 如果new Bitmap抛出异常。它不是异常安全的 */
if (this == &rhs) return *this;
delete pb; //先把原来的对象删除
pb = new Bitmap(*rhs.pb); //再new一个
return *this;
/* 异常安全,但自赋值不高效 */
Bitmap* pOrig = pb;
pb = new Bitmap(rhs->pb);
delete pOrig;
return *this;
Bitmap* tmp = new Bitmap(rhs->pb);
delete pb ;
pb = tmp;
return *this
/* x */
Widget tmp(rhs);
swap(tmp);
return *this;
}
Widget& Widget::operator=(Widget rhs)
{
swap(rhs);
return *this;
}
- 确保当对象自我赋值时operator= 有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
12:copy all parts of an object
- Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
资源管理
13:Use objects to manage resources
使用智能指针管理内存
注意:
shared_ptr<int> iptr(new int[5]); //不报错,但默认还是用delete 释放内存,会导致内存泄露
- 为防止资源泄露,使用RAII对象,它们再构造函数中获得资源并在析构函数中释放资源。
- 使用shared_ptr
14:Think carefully about copying behavior in resource-managing classes.
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
- 普遍而常见的RAII class copying行为是:抑制copying、引用计数法。
15:在资源管理类中提供对原始资源的访问
class Investment{
public:
bool isTaxFree() const;
...
};
Investment* createInvestment(); //假定返回一个堆内存指针
std::shared_ptr<Investment> pInv(createInvestment());
int daysHeld(const Investment* pi);
daysHeld(pInv.get());
pInv->isTaxFree();
(*pInv).isTaxFree();
- APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理的资源”的方法。
- 对原始资源的访问可能经由显示转换或隐式转换。一般来说显式转换比较安全,隐式转换方便。
16:成对使用new和delete时要采取相同形式
std::string* stringArray = new std::string[100];
...
delete stringArray; //❌ , delete [] stringArray
/* 当调用new时 1.通过operator new函数分配内存 2.调用构造函数
delete 与new顺序相反
*/
- new 与 delete
- new …[] 与 delete []
17:Strore newed objects in smart pointers in standalone statements
int priority();
void processWidget(std::shared_ptr<Widget> pw, int priority);
考虑
processWidget(std::shared_ptr<Widget>(new Widget), priority());
其执行顺序可能为:
1. new Widget
2. priority()
3. shared_ptr构造函数
若priority()发生异常,则会造成内存泄露
auto pw = std::shared_ptr<Widget>(new Widget);
processWidget(pw, priority());
- 将new出的对象存放在智能指针时需要单独的一行语句。否则,可能导致内存泄露
设计与声名
18:Make interfaces easy to use correctly and hard to use incorrectly.
struct Day{
explicit Day(int d): val(d) {}
int val;
};
struct Month{
explicit Month(int m):val(m) {}
int val;
};
struct Year{
explicit Year(int y):val(y) {}
int val;
};
class Date{
public:
Date(const Month&, const Day&, const Year&);
};
尽量与内置类型的行为相同,a=b, a*b = c报错等
shared_ptr要比原始指针大且慢,然而其“降低客户错误”的成效却是较为显著和方便的
- 好的接口很容易被正确使用,不容易被误用。应该在所有接口中达到。
- “促进正确使用”的方法,接口一致,以及与内置类型的行为兼容。
- “阻止误用”的方法,建立新类型、限定类型上的操作,数傅对象值,消除用户的资源管理责任。
- shared_ptr支持定制型删除器(custom deleter)。可放置DLL问题,可被用来自动解除互斥锁等。
19:Treat class design as type design
设计class时需要考虑:
- 新type的对象应该如何被创建和销毁?
- 对象的初始化和对象的赋值该有什么样的差别?
- 新type的对象如果被 passed by value,意味着什么?copy constructor函数用来定义一个type的pass-by-value该如何实现。
- 什么是新type的“合法值”?
- 新type需要配合某个继承图系(inheritance graph)吗?
- 新的type需要什么样的转换?
- 什么样的操作符和函数对此新type而言是合理的?
- 什么样的标准函数应该驳回?(private?)
- 谁该取用新type的成员
- 什么是新type的“未声名接口”(undeclared interface)?
- 新type有多么一般化?
- 真的需要一个新type?
- class的设计就是type的设计。
20:Prefer pass-by-reference-to-const to pass-by-value
class Person{
public:
Person();
virtual ~Person();
...
private:
std::string name;
std::string address;
};
class Student: public Person{
public:
Student();
~Student();
...
private:
std::string schoolName;
std::string schoolAddress;
};
- 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
- 以上规则并不适用与内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较妥当。
21:Don’t try to return a reference when you must return an object
- 禁止返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
22:Declare data members private.
论点:
- 接口一致性,接口都是函数
- 使用函数可以实现“不准访问”、“读写访问”等
- 封装
如果改变public,所有设计到public的接口倒要改变,如果改变protected,所有用到protected的子类都要变。从封装的角度看,只有private(提供封装)和其它(步提供封装)。
- 将成员变量声名为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者较高的弹性。
- protected并不比public更具封装性。就当他不存在。
23:Prefer non-member non-friend functions to member functions
class WebBrowser
{
public:
...
void clearCacke();
void clearHistory();
void removeCookies();
...
# 一:member函数
void clearEverything();
};
#二:non-member函数
void clearBrowser(WebBrowser &wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
// 头文件“webbrowser.h”这个头文件针对class WebBrowser自身及WebBrowser核心机能
namespace WebBrowserStuff{
class WebBrowser{...};
... //核心机能,如几乎所有客户都需要的 non-member函数
}
// 头文件"webbrowserbookmarks.h"
namespace WebBrowserStuff{
... //与书签相关的便利函数
}
// 头文件“webbrowsercookies.h”
namespace WebBrowserStuff{
... //与cookie相关的便利函数
}
namespace可以跨头文件,而class必须定义在一个头文件中
将多个“提供便利的函数”放在多个头文件内但隶属同一个namespace,用户可以轻松扩展这一组“提供便利的函数”。如,WebBrowser客户要添加些与影响下载相关的“提供便利的函数”,只需要再WebBrowserStuff命名空间内建立一个头文件,内含那些函数的声名即可。(若在WebBrowser命名空间中声名“提供便利的函数”,那么定义放在哪?)
- 用non-member non-friend函数替换member函数。这样可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。
24:若所有参数皆需类型转换,请为此采用non-member函数
- 如果你需要为某个函数的所有参数(包括this指针所指的的哪个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
25:考虑写出一个不抛出异常的swap函数
swap多少与异常安全性编程(exception-safe programming)相关
默认swap行为:(如果ab都在堆中该如何处理?a,b都为内置类型,先不考虑堆的情况)
namespace std {
template<typename T> void swap(T& a, T& b)
{
T temp(a); //a,b都为内置类型
a = b;
b = temp;
}
}
提供public swap成员
将std::swap特例化
class WidgetImpl
{
public:
...
private:
int a, b, c; //可能有许多数据
std::vector<double> v; //意味复制时间很长
...
};
class Widget
{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs)
{
...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& other)
{
using std::swap;
swap(pImpl, other.pImpl);
}
private:
WidgetImpl* pImpl;
};
namespace std{
template<> void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b);
}
}
std的内容完全由C++标准委员会决定,标准禁止我们膨胀那些已经声名好的东西。即,不要添加任何新东西到std里头。
所以:
namespace WidgetStuff{
...
template<typename T> class Widget {...};
...
// non-member swap function, 这里不属于std命名空间
template<typename T> void swap(Widget<T>& a, Widget<T>& b)
{ a.swap(b); }
}
template<typename T> void doSomething(T& obj1, T& obj2)
{
using std::swap; // 令std::swap在此函数内可用
...
swap(obj1, obj2); //为T性对象调用最佳swap版本
...
}
default swap, member swaps, non-member swaps, std::swap特例化版本
- 如果swap的默认实现代码对class或class template的效率较高,使用默认即可,调用swap会使用默认版本,并且效率较好。
- 如果swap默认版本效率低下(即,class或class template使用了pimpl“pointer to implementation”)尝试以下行为:
- 提供一个public swap成员函数,让它高效的swap类型的两个对象。这个函数绝对不该抛出异常。
- 在class或class template所在命名空间内提供一个non-member swap,并令它调用上述swap成员函数。
- 如果你正在编写一个class(而非class template),为你class特例化std::swap。并令它调用你的swap成员函数。
remember
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
- 如果你提供一个member swap,也提供一个non-member swap用来调用前者。对于classes(而非templates),也请特例化std::swap。
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间”。
- 为“用户定义类型”进行std templates特例化是好的,但不要在std内加入某些对std而言全新的东西。
实现 Implementations
26:尽可能延后变量定义式的出现时间
通过default构造函数构造出一个对象然后对它赋值比直接在构造时指定初值效率差。
以下哪个好?
//方法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 个析构函数
- 如果classes的一个赋值成本低于一组构造+析构成本,做法A比较高效,否则B比较高效。除非(1)知道赋值成本比“构造+析构”成本低,(2)正在处理代码中效率高度敏感的部分,否则用做法B。
- 尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序效率。
27:Mimimize casting
C风格的转型动作:
(T) expression
函数风格的转型动作:
T(expression)
C++四种新式转型:
const_cast<T>( expression )
将对象的常量属性移除。它也是唯一由此功能的C+±style转型操作符。
dynamic_cast<T>( expression )
将父类转换为子类
用于“安全向下转型”,用于决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
reinterpret_cast<T>( expression )
意图执行低级转换,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。这一类转型在低级代码以外很少见。
static_cast<T>( expression )
用来强迫隐式转换implicit conversions,例如将non-const对象转为const对象,或将int转为double等。它也可用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const——这个只有const_cast才能做。
class Widget{
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15)); //以一个int加上“函数风格”的转型动作创建一个Widget
doSomeWork(static_cast<Widget>(15)); //以一个int加上“C++风格”的转型动作创建一个Widget
int x, y;
...
double d = static_cast<double>(x) / y; //x除以y,使用浮点数除法
建立一个base class指针指向一个derived class对象,但有时候上述两个指针值并不相同。这种情况下会有个偏移量(offset)在运行期被施行于Derived指针上,用以取得正确的Base指针值
class Base { ... };
class Derived: public Base { ... };
Derived d;
Base* pb = &d; //隐喻的将Derived*转换为Base*
- 如果可以,尽量避免类型转换,尤其是在注重效率的代码中避免dynamic_casts。如果有个设计需要类型转换,尝试无需类型转换的代替设计。
- 如果转换时必要的,尝试将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
- 宁可使用C+±style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职称。
28:避免返回“handles”(号码牌,用来取得某个对象)指向对象内部成分
绝对不该令成员函数返回一个指针指向“访问级别较低”的数据成员。
- 避免返回handles(包括references、指针、迭代器)指向对象内部。遵守该条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
29:Strive for exception-safe code.
“copy-and-swap”一种实现异常安全的手段
- 异常安全函数(Exception-safe functions)即时发生异常也不会泄露资源或允许任何数据结构破坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型。
- “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
- 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
30:Understand the ins and outs of inlining.
- 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化, 使程序的速度提升机会最大化。
- 不要只因为function template出现在头文件,就将它们声明为inline。
31:将文件间的编译依存关系降至最低
“接口与实现分离”
- 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes 和 Interface classes。
- 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及template都适用。
继承于面向对象设计
32:Make sure public inheritance models “is-a”.
条款18说:好的接口可以防止无效的代码通过编译。
应该宁可采取“在编译期拒绝企鹅飞行”的设计,而不是“只在运行期才能侦测它们”的设计。
- “public继承”意味is-a。适用于base classes身上的每一件事情也一定适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
33:Avoid hiding inherited names.
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); //❌,因为Derived::mf1隐藏了Base::mf1
d.mf2(); //✔,调用Base::mf2
d.mf3(); //✔,调用Derived::mf3
d.mf3(x); //❌,因为Derived::mf3隐藏了Base::mf3
class Derived: public Base
{
public:
using Base::mf1; //让Base class 内名为mf1何mf3的所有东西
using Base::mf3; //在Derived作用域内都可见(并且public)
virtual void mf1();
void mf3();
void mf4();
...
};
这下上面的d.xx都正确了
转交函数
class Derived: private Base
{
public:
virtual void mf1() //转交函数(forwarding function)
{ Base::mf1(); }
};
- derived classes内的名称会隐藏base classes内的名称。
- 为了使被隐藏的名称可见,可用using声明式或转交函数(forwarding functions)。
34:区分接口继承和实现继承
Defferentiate between inheritance of interface and inheritance of implementation.
实现A:定义另一个默认实现函数
class Airplane
{
public:
virtual void fly(const Airport&) = 0;
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
默认行为
}
class ModelA: public Airplane
{
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
};
class ModelB: public Airplane
{
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
};
class ModelC: public Airplane
{
public:
virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport& destination)
{
C实现
}
实现B:给出纯虚函数定义
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination) //pure virtual函数实现
{
默认行为
}
class ModelA: public Airplane
{
public:
virtual void fly(const Airport& destination)
{ Airplane::fly(destination); }
...
};
class ModelB: 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的实现
}
- 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
- pure virtual函数只具体指定接口继承。
- impure virtual函数具有指定接口继承及默认实现继承。
- non-virtual函数具体指定接口继承以及强制性实现继承。
35:Consider alternatives to virtual functions.
考虑以下non-virtual interface的设计方式
class GameCharacter {
public:
int healthValue() const ;
private:
virtual int doHealthValue() const;
};
int GameCharacter::healthValue() const {
... //做一些事前工作
int retVal = doHealthValue();
... //做一些事后工作
return retVal;
}
用户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。它是Template Method设计模式的一种表现形式,可把这个non-virtual 函数称为virtual函数的wrapper 。
考虑以下设计模式:
strategy设计模式
class GameCHaracter;
int defaultHealthCalc(const GameCharacter&);
class GameCharacter{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):
healthFunc(hcf) {}
int healthValue() const
{return healthFunc(*this);}
...
private:
HealthCalcFunc healthFunc;
};
由function完成strategy模式
古典strategy模式
class GameCharacter;
class HealthCalcFunc{
public:
...
virtual int calc(const GameCharacter& gc) const
{ ... }
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc):
pHealthCalc(phcf) {}
int healthValue() const
{ return pHealthCalc->calc(*this); }
...
private:
HealthCalcFunc* pHealthCalc;
};
摘要
- 使用non-virtual interface(NVI)方法,那是Template Method设计模式的一种特殊形式。它以public non-virtual 成员函数包裹较低访问性(private or protected)的virtual函数
- 将virtual函数替换为”函数指针成员变量“,这是strategy设计模式的一种分解表现形式。
- 以function成员变量替换virtual函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是strategy设计模式的某种形式。
- 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是strategy设计模式的传统事项手法。
remember
- virtual函数的替代方案包括NVI,strategy等
- 将实现从成员函数移到class外部函数,带来的一个缺点是非成员函数无法访问class的non-public成员。
- function对象的行为就像一般函数指针。这样的对象可接纳”与给定只目标签名式(target signature)兼容“的所有可调用物(callable entities)。
36:Never redefine an inherited non-virtual function
- 绝对不要重新定义继承而来的non-virtual 函数。
37:Never redefine a function’s inherited default parameter value.
virtual函数是动态绑定,而默认参数值却是静态绑定。
class Shap{
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle: public Shape{
public:
//赋予不同的默认参数值,非常糟糕!
virtual voi draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape{
public:
virtual void draw(ShapeColor color) const;
...
};
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
pc->draw(Shape::Red); //调用Circle::draw(Shape::Red)
pr->draw(Shape::Red); //调用Rectangle::draw(Shape::Red)
pr->draw(); //调用Rectangle::draw(Shape::Red)
另一种实现:
class Shape{
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const{
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;
};
class Rectangle: public Shape{
private:
virtual void doDraw(ShapeColor color) const
{ ... }
public:
...
};
Shape* ps = new Shape;
Shape* pr = new Rectangle;
ps->draw(); //调用Shape::doDraw
pr->draw(); //调用Rectangle::doDraw
- 绝对不要重新定义一个继承而来的默认参数值,以为默认参数值都是静态绑定,而virtual函数是动态绑定。
38:Model “has-a” or “is-implemented-in-terms-of” through composition.
template<typename T> //将list应用于set
class Set{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep; //用来表述set的数据
};
- 复合(composition)的意义和public继承完全不同。
- 在应用域(application domain),复合意味着has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。
39:Use private inheritance judiciously.
Private继承意味着implemented-in-terms-of(根据某物实现出),只有实现部分被继承,接口部分应略去。D对象根据B对象实现。Private继承在软件“设计”层面上没有意义,其意义只及于软件实现层面。
- Private继承意味is-implemented-in-terms of (根据某物实现出)。它通常比复合composition的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计时是合理的。
- 和复合composition不同,private继承可以使empty base最小化。着对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
40:Use multiple inheritance judiciously.
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
- virtual继承会增加大小、速度、初始化(及赋值)复杂度等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public 继承某个Interface class”和“private继承某个协助实现的class”的两相组合。
模板与泛型编程
Templates and Generic Programming
41:Understand implicit interfaces and compile-time polymorphism.
面向对象编程以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)。
- classes和templates都支持接口interfaces和多态polymorphism
- 对classes而言接口是显式的explicit,以函数签名(函数名、参数列表、返回类型)为中心。多态则是通过virtual函数发生于运行期。
- 对template参数而言,接口是隐式的implicit,奠基于有效表达式。多态则是通过template实例化和函数重载解析function overloading resolution发生于编译期。
42:Understand the two meanings of typename.
template<typename C> // 允许使用typename或class
void f(const C& container, // 不允许使用typename
typename C::iterator iter); //一定要使用 typename
// C并非嵌套从属类型名称(它并非嵌套于任何“取决于template参数”的东西内),所以声明container时并不需要以typename开头,但C::iterator是个嵌套从属类型名,所以必须以typename为前导。
template<typename IterT>
void workWithIterator(IterT iter){
typename std::iterator_traits<IterT>::value_type temp(*iter);
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
}
- 声明template参数时,前缀关键字class和typename可互换。
- 使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列表)或member initialization list(成员初始值列表)内以它作为base class修饰符。
43:Know how to access names in templatized base classes.
学习处理模板化基类内的名称
template<typename Company> class MsgSender{
public:
... //构造、析构等
void sendClear(const MsgInfo& info)
{
std::string msg;
...
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info) //类似sendClear,唯一不同是
{ ... } //这里调用c.sendEncrypted
};
//特例化template
template<> //一个特例化的MsgSender
class MsgSender<CompanyZ>{ //它和一般template相同,差别只在与它删掉了sendClear
public:
...
void sendSecret(const MsgInfo& info)
{ ... }
};
三种方法访问 基模板类 成员的方法
/* this-> */
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
...
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log
this->sendClear(info); //成立,假设sendClear将被继承
将”传送后“的信息写至log
}
...
};
/* using声明 */
public:
using MsgSender<Company>::sendClear; //告诉编译器,请它假设sendClear位于base class内
...
void sendClearMsg(const MsgInfo& info)
{
sendClear(info); //OK,假设sendClear将被继承下来
}
...
/*指出被调用的函数位于base class内*/ 不推荐,如果调用的是virtual函数,会关闭“virtual绑定行为”
public:
...
void sendClearMsg(const MsgInfo& info)
{
MsgSender<Company>::sendClear(info); //OK,假设sendClear将被继承下来
}
...
- 可在derived class templates内通过“this->”指涉base class templates内的成员名称,或藉由一个显式的“base class资格修饰符”完成。
44:将与参数无关的代码抽离templates
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
- 因类型参数(type parameter)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representation)的实例类型(instantiation types)共享实现码。
45:运用成员函数模板接受所有兼容类型
Use member function templates to accept “all compatible types.”
member fucntion templates(成员函数模板)
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other):
heldPtr(other.get()) {...} //以other的held初始化this的heldPtr
T* get() const { return heldPtr; }
...
private:
T* heldPtr; //这个SmartPtr持有的内置(原始)指针
};
// 转化会完成,同时保证Top不会隐式转为Middle
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
- 请使用member function templates(成员函数模板)生成”可接受所有兼容类型“的函数。
- 如果你声明member templates用于”泛化copy构造“或”泛化assignment操作“,还是需要声明正常的copy构造函数和copy assignment操作符。
46:需要类型转换时请为模板定义非成员函数
Define non-member functions inside templates when type conversions are desired.
令一个函数调用一个辅助函数。
template<typename T> class Rational; //声明Rational template
// 若有必要,在头文件内定义helper template
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>( lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator() );
}
template<typename T>
class Rational{
public:
...
// 令friend调用helper
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{ return doMultiply(lhs, rhs); }
...
// 简略表达方式
// friend const Rational operator*(const Rational& lhs, const Rational& rhs) ...
};
- 当我们编写一个class template,而它所提供之”于此template相关的“函数支持”所有参数之隐式类型转换“时,请将那些函数定义为”class template内部的friend函数“。
47:请用traits classes表现类型信息
Use traits classes for information about types.
STL的5种迭代器。
- istream_iterator只能向前移动一步,且只能读
- ostream_iterator只能向前移动一步,且只能写
- forward迭代器
- 双向迭代器
- 随机访问迭代器
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 {};
tips : 如何设计并实现一个traits class
- 确认若干你希望将来可取得得类型相关信息。例如对迭代器而言,我们希望将来可取得其分类category
- 为该信息选择一个名称如iterator_category
- 提供一个template和一组特例化版本,内含你希望支持的类型相关信息
如何使用一个traits class
- 建立一组重载函数或函数模板,彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受的traits信息相应和。
- 建立一个控制函数或函数模板,它调用上述那些函数并传递traits class所提供的信息。
remember:
- Traits classes使得”类型相关信息“在编译期可用。它们以templates和”templates特例化“完成实现。
- 利用函数重载,traits classes有可能在编译期对类型执行 if…else测试。
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d)
{
// 调用的doAdvance对iter之迭代器分类而言必须是适当的
doAdvance(iter, d, std::iterator_traits<IterT>::iterator_category());
}
48:Be aware of template metaprogramming.
Template meta programming(TMP,模板元编程)是编写template-based C++程序并执行于编译期的过程。
TMP的阶乘运算示范如何通过”递归模板具现化“recursive template instantiation实现循环,以及如何在TMP种创建和使用变量:
//一般情况:Factorial<n>的值是n乘以Factorial<n-1>的值
template<unsigned n>
struct Factorial{
enum { value = n * Factorial<n-1>::value };
};
//特殊情况:Factorial<0> 的值是1
template<>
struct Factorial<0>{
enum { value = 1 };
};
- Template metaprogramming(TMP, 模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
- TMP可被用来生成”基于政策选择组合“(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
定制new和delete
49:了解new-handler的行为
#include <new>
namespace std{
typedef void (* new_handler)();
new_handler set_new_handler(new_handler p) throw();
// thrwo() 是一份异常明细,表示该函数不抛出任何异常
// - 参数为指针,指向operator new无法分配足够内存时该被调用的函数
// - 返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要被替换)的那个new-handler函数
}
// 例如:
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];
...
}
new-handler函数必须做以下事情
template<typename T> //"mixin"风格的base class,用以支持class专属的set_new_handler
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);
...
private:
static std::new_handler currentHandler;
};
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));
return ::operator new(size);
}
//以下将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
/* 妙 */ //curiously recurring template pattern;CRTP 模板化继承templatized inheritance
class Widget: public NewHandlerSupport<Widget>{
... //和先前一样,但不必声明set_new_handler或operator new
};
//以上为"class 专属之set_new_handler"
//类型参数T保证继承自NewHandlerSupport<T>的子类实例不同
- set_new_handler允许用户指定一个函数,在内存分配无法获得满足时被调用。
- Nothrow new是一个颇为有限的工具,因为它只适用于内存分配;后续的构造函数调用还是可能抛出异常。
50:了解new和delete的合理替换时机
定制new或delete的理由
- 用来检测运用上的错误。
- 为了强化效能。
- 为了收集使用上的统计数据。
alignment, computer architectures
特定的类型必须放在特定的内存地址上。如指针地址必须是4倍数(four-byte aligned)或double的地址必须是8倍数(eight-byte aligned)
- 有许多理由要写个自定义的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
51:编写new和delete时需固守常规
- operator new 应该内含一个无穷循环,并在其种尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 bytes申请。Class专属版本则还应该处理“比正确大小更大(错误)申请”。
- operator delete 应该在收到null指针时不做任何事。Class专属版本则还应该处理“比正确大小更大的(错误)申请”。
52:写了placement new 也要写placement delete
#include <new>
// 除了std::size_t外还有其它参数,placement new
void* operator new(std::size_t size, void* pMemory) throw();
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();
...
};
//如果该语句的构造函数发生异常,对应的placement delete会被自定调用,让Widget不会泄露内存
Widget* pw = new (std::cerr) Widget;
//
//默认情况下,C++在global作用域内提供以下形式的operator new
void* operator new(std::size_t) throw(std::bad_alloc); //normal new
void* operator new(std::size_t, void*) throw(); //placement new
void* operator new(std::size_t, const std::nothrow_t&) throw(); //placement new
扩充标准形式:
1.先定义一个base class,内含所有正常形式的new和delete
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); }
};
class Widget: public StandardNewDeleteForms{ //继承标准形式
public:
using StandardNewDeleteForms::operator new; //让这些形式可见
using StandardNewDeleteForms::operator delete;
// 添加一个自定的placement new
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
// 添加一个对应的placement delete
static void operator delete(void* pMemory, std::ostream& logStream) throw();
...
};
- 当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐蔽而时断时续的内存泄露。
- 当你声明placement new和placement delete,请确定不要无意识(非故意)的遮掩了它们的正常版本。
杂项讨论
53:关注编译器的警告
Pay attention to compiler warnings.
- 认真对待编译器发出的警告信息。努力在编译器最高警告级别下做到“无任何警告”。
- 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,原本依赖的警告信息可能消失。
54:让自己熟悉包括TR1在内的标准程序库
这一条可能比较老了
Familiarize yourself with the standard library, including TR1.
Technical Report 1
- C++标准程序库的主要机能由STL、iostreams、locales组成。并包含C99标准程序库。
- TR1添加了智能指针(tr1::shared_ptr)、一般化函数指针(tr1::function)、hash-based容器、正则表达式(regular expressions)以及另外10个组件的支持。
- TR1自身只是一份规范。为获得TR1提供的内容,你需要一份实物。一个好的实物来源是Boost。
55:Familiarize yourself with Boost
- Boost 是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色。
- Boost提供许多TR1组件实现品,以及其他许多程序库。