Effective C++

条款01:视C++为一个语言联邦

将C++视为一个由相关语言组成的联邦而非单一语言

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

#define处理与预处理阶段,而非编译阶段,因此此条款也可称为“宁可以编译器替换预处理器比较好”

  1. 对于单纯变量,最好以const对象或enums替换#define
  2. 对于形似函数的宏,最好用inline函数替换#define

条款03:尽可能使用const

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

bitwise constness又称为physical constness,是相对于logical constness的一个概念。

前者认为:成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const,也就是说它不更改对象内任何一个bit;
后者认为:一个const成员函数可以修改它所处理的对象内的某些bit,但只有在客户端侦测不出的情况下才可如此。

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

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

赋值(assignmen)与初始化(initialization)不可混淆。
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。

class PhoneNumber{...}
class ABEntry{
public:
	ABEntry(const string& name,const string& address,const list<Phonenumber>& phone);
private:
	string theName;
	string theAddress;
	list<PhoneNumber> thePhone;
	int numTimesConsulted;
};
ABEntry::ABEntry(const string& name,const string& address,const list<Phonenumber>& phone)
{
	//这些均为赋值,而非初始化
	theName = name;	
	theAddress = address;
	thePhone = phone;
	numtimesConsulted = 0;
}

初始化发生的时间更早,发生于这些成员的default构造函数被自动调用之时。
使用成员初值列替换赋值动作,效率更高

//此处均为初始化动作,函数体内不需要任何动作
ABEntry::ABEntry(const string& name,const string& address,
const list<Phonenumber>& phone)
:theName(name),theAddress(address),thePhone(phone),numtimesConsulted(0){}

执行上一个构造函数时,实际上首先调用default构造函数初始化成员变量,然后立刻对他们进行赋值,而使用成员初值列时,初值列中针对各个成员变量而设的实参,被拿去作为各成员变量之构造函数的实参,实际执行了copy构造。

“ 不同编译单元内定义之non-local static对象”的初始化顺序

函数内的static对象称为local static对象,因为他们对于函数而言是local,其他static对象称为non-local static 对象;编译单元是指产出单一目标文件的那些源码,基本上它是单一源码文件加上其所含入的头文件。

当某一编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的 non-local static对象时,这个对象可能还未初始化,因为C++对“ 不同编译单元内定义之non-local static对象”的初始化顺序并没有明确规定。
对于此问题,可采用一个小设计:==将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。==然后用户调用这些函数,而不直接指涉这些对象,换句话说non-local static对象被local static对象替换了。这也是**单例模式(Singleton)**的一个常见手法。

这个手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间”“首次遇上该对象的定义式时被初始化”

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

若自己不声明,C++会自动为一个类声明一个copy构造函数、一个copy assignment操作符以及一个析构函数。此外如果没有声明任何构造函数,编译器也会为你声明一个defaul构造函数。所有这些函数都是public且inline的。

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

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。也可以定义一个基类,在基类中将不想使用的函数例如copy构造函数,声明为private,当此类的派生类尝试调用copy构造函数时,编译器会尝试生成一个copy构造函数,而此构造函数会尝试调用其base class的对应函数,而这些调用会被编译器拒绝,因为其base class的拷贝函数时private

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

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

  1. 析构函数绝对不要吐出异常。如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们或者结束程序
  2. 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作

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

当drive class对象的base class 构造期间,对象的类型是base class 而不是derived class。

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

为了实现连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参。

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

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

如果声明自己的copying函数,意思就是告诉编译器你并不喜欢默认实现中的某些行为。此后,当你的实现代码几乎必然出错时,编译器不会告知你。
为drived class编写copying函数时,应该让drived class 的copying函数调用相应的base class 函数

编写一个copying函数时,请确保:

  1. 复制所有local成员变量
  2. 调用所有base class内的适当的copying函数。

条款13:以对象管理资源

  1. 为防止资源泄露,请使用RAII(资源获取就是初始化)对象,他们在构造函数中获得资源并在析构函数中释放资源。
  2. 两个常被使用的RAII classes分别是shared_ptr 和 auto_ptr。

手动释放资源容易发生某些错误

//定义一个函数用来释放资源
class A{...};
A* createA();		//返回指向A对象的指针

void f()
{
	A* temp = createA();
	...
	delete A;	
}
//在...处可能会发生异常,或者由于某一个return语句而提前结束,导致delete失败
  • 获得资源后立刻放进管理对象内。
  • 管理对象运用析构函数确保资源被释放。

例如使用智能指针管理资源,当智能指针被销毁时会自动删除它所指之物
auto_ptrs有一个不同寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成nullptr,而赋值所得的指针将取得资源的唯一所有权。

 class A{...};
 A* createA();		//返回指向A对象的指针
 
 auto_ptr<A> ptr1(createA());
 auto_ptr<A> ptr2(ptr1); 	//此时ptr1=nullptr,ptr2指向对象;
 ptr1 = ptr2;				//此时ptr2=nullptr,ptr1指向对象;

因此,提出了一种“计数型智慧指针RCSP”(例如shared_ptr),它持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。
其缺点是无法打破环状引用,例如两个其实已经没被使用的对象彼此互指,因而好像还处在被使用状态。

auto_ptr和shared_ptr两者在析构函数内都使用delete而不是delete[ ]动作。因此,在动态分配的array身上使用智能指针似乎是个馊主意

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

RAII(Resource Acquisition Is Initialization):资源取得时机便是初始化时机
为确保绝不会忘记一个被锁住的Mutex解锁,可以建立一个class用来管理机锁,这样的class的基本结构由RAII守则支配。

class Lock{
public:
	explict Lock(Mutex* pm):mutexPtr(pm)
	{lock(mutexPtr);}	//获得资源
	~Lock()
	{unlock(mutexPtr);}	//释放资源
private:
	Mutex * mutexPtr;
};

//客户对Lock的用法符合RAII方式:
Mutex m;
...
{					//建议一个区块用来定义critical section(临界区)
	Lock m1(&m);	//锁定互斥器
	...				//执行critical section内的操作
}		//在区块最末尾,自动解除互斥器锁定

当Lock对象被复制时,会发生什么?

1. 禁止复制。将Lock的copying操作声明为private

2. 对底层资源祭出“引用计数法”。

有时候我们希望保有资源,直到它的最后有一个使用者被销毁。这种情况下复制RAII对象时,应该将资源的“被引用数”递增。share_ptr即是如此。

shared_ptr初始化时允许传入一个删除器操作,用来定义当其引用次数为0时的操作,于是,Lock可被定义如下:

class Lock{
public:
	explict Lock(Mutex* pm):mutexPtr(pm,unlock)		//以unlock作为删除器
	{lock(mutexPtr.get());}	//获得资源
private:
	shared_ptr<Mutex> mutexPtr;
};

此时声明析构函数已经没有必要。条款5说过,class析构函数会自动调用其non-static成员变量的析构函数(此处为mutexPtr)而mutexPtr的析构函数会在互斥器的引用次数为0时自动调用shared_ptr的删除器(此处为unlock)

3. 复制底部资源

对对象进行拷贝时,进行深拷贝

4. 转移底部资源的拥有权。

一如auto_ptr奉行的复制意义

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

class A{
	...
	bool isTax() const;
};
A* creatA();

shared_ptr<A> ptr1(creatA());
//但是当有另一个函数需要使用createA()的返回值时
void f(A*);
//如果使用指针传入会直接报错,因为形参实参的类型不匹配
f(ptr1);	//报错
//于是需要一个函数将RAII class对象转换为其所内含的原始资源。
//有两个方法可以达成目标:显示转换和隐式转换

两种智能指针中都提供了一个get()成员函数,用来执行显示转换,它会返回智能指针内部的原始指针(的复件)

//显示转换
f(ptr1.get());

几乎所有智能指针都重载了指针取值操作符(operator->和operator*),他们允许隐式转换至底部原始指针:

//隐式转换
bool temp = ptr1->isTax();
bool temp1 = (*ptr1).isTax();

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

单一对象的内存布局一般而言不同于数组的内存布局。更明确的说,数组所用的内存通常还包括数组大小的记录,以便delete知道需要调用多少次析构函数。单一对象的内存则没有这笔记录。

因此,当使用delete[ ]时,delete便认定指针指向一个数组,否则他便认定指针指向单一对象

在new表达式中使用[ ],必须在相应的delete表达式中也使用[ ]。

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

int priority();
void processWidget(shared_ptr<Widget> pw,int priority);
//错误,不能通过编译,智能指针的构造函数需要一个原始指针,但该构造函数是个explicit构造函数,无法进行隐式类型转换
processWidget(new Widget,Priorty());
//如此可通过编译但可能造成资源泄露
processWidget(shared_ptr<Widget>(new Widget),Priorty());

在调用此函数前,编译器需做三件事情:

  1. 执行 new Widget;
  2. 调用Priorty()
  3. 调用shared_ptr构造函数

new Widget 一定在智能指针构造函数之前被调用,其他顺序不定。
于是,当按以上顺序执行时,若调用priority()导致异常,则new Widget 返回的指针将遗失,因为它尚未置入智能指针内,由此造成资源被泄露。

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

任何接口如果要求客户必须记得做某件事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事

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

  • 新type的对象应该如何被创建和销毁
  • 对象的初始化和对象的赋值应该有什么样的差别
  • 新type的对象如果被以值传递,意味着什么
  • 什么是新type的“合法值”
  • 你的新type需要配合某个继承图系吗
  • 你的新type需要什么样的转换
  • 什么样的操作符和函数对此新type而言是合理的
  • 什么样的标准函数应该驳回
  • 谁该取用新type的成员
  • 什么是新type的“未声明接口”
  • 你的新type有多么一般化
  • 你真的需要一个新type吗

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

默认情况下C++以by-value方式传递对象至函数。除非另外指定,否则函数参数都是以实际实参的副本为初值,而调用端所获得的亦是函数返回值的一个副本。这些副本由对象的copy构造函数产出,这可能是的pass-by-value称为费时的操作
以by-reference方式传递参数效率高的多,不会涉及任何构造函数以及析构函数,并且也可以避免==slicing(对象切割)==问题。
以by-reference方式传递参数时,为了不让函数改变参数的值,需定位为const

对象切割

当一个派生类对象以by-value的方式传递并被视为一个基类对象,基类的copy构造函数会被调用,而造成此对象的行为像个派生类对象的那些特化性质全被切割掉了,仅仅留下一个基类对象

class Window{
public:
	...
	string name() const;
	virtual void display() const;
};

class WindowWithScrollBars:public Window{
public:
	...
	virtual void display() const;
};

//窗口打印函数
void printName(Window w)		//参数可能被切割
{
	cout<<w.name();				
	w.display();
}
//当给此打印函数传入一个WindowWithScrollBars对象时,w会被构造成一个Window对象

void printName(const Window& w)		//现在传入什么类型就是什么类型
{
	cout<<w.name();				
	w.display();
}

对于内置类型,有事pass-by-value的效率更高。
对象小并不意味着copy构造函数不昂贵。许多对象,包括大多数STL容器,内含的东西只比一个指针多一些,但复制这些对象却需承担“复制那些指针所指的每一样东西”,那将非常昂贵。

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

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

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

1.语法一致性

2.封装

对客户隐藏成员变量,可以确保class的约束条件总是会获得维护,因为只有成员函数可以影响它们。public意味着不封装,而不封装意味着不可改变==(意味着如果你宣布一个公共成员并在你的代码中的许多其他地方使用它,那么如果你以后决定改变那个成员那么改变所有这些地方将是一项艰苦的工作。)==
protected与public的封装性一致,并不会优于public,因此其实只有两种封装性,private(提供封装)和其他(不提供封装)

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

考虑两种情况

class WebBrowser{
public:
	...
	void clearCache();
	void clearHistory();
	void removeCookies();
	...
};

//调用member函数
class Webbrowser{
public:
	...
	void clearAll();	//调用clearCache(),clearHistory(),removeCookies()
	...
};

//也可由一个non-member函数调用适当的menber函数来提供此功能
void clearBrowser(WebBrowser& w)
{
	w.clearCache();
	w.clearHistory();
	w.removeCookies();
}

使用non-member函数调用更加好,封装性更强
为什么推崇封装?因为它使我们能够改变事物而只影响有限客户。
对于对象内的数据,越少代码访问它,越多的数据可被封装,而我们也就越能自由的改变对象数据。
若使用member函数,则增加了可访问数据的代码量,而使用non-member函数(non-friend函数),则没有增加这个代码量,因此non-member函数(non-friend函数)具有更好的封装性,包裹弹性和机能扩充性。
这里的non-member函数,non-friend函数只针对同一个class而言,A class的non-member函数可以是B class的member函数

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

class A{
public:
	const operator*(const A& rhs) cosnt;
};
A a;
A result;
result = a * 2;	//正确,类似于a = a.operator*(2);
result = 2 * a;	//错误,2(int)中没有相应的operator*成员函数

编译器也会在全局范围内寻找可以如此调用的non-member operator函数:
result = operator
(2,a); //错误,全局中并没有这样的函数

回过头来看上述中正确的语句:
result = a * 2; //正确,类似于a = a.operator*(2);
其中发生了隐式类型转换:
const A temp(2);
result = a * temp;
可以发生隐式类型转换的前提是A class 的构造函数为non-explicit的;

只有当参数别列于参数列内,这个参数才是隐式类型转换的合格参与者。

若把operator* 声明为non-member函数,便可允许编译器在每一个实参上执行隐式类型转换

class A{
public:
	...
};
const A operator*(const A& lhs,const A& rhs)
{
	...
}

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

待补充

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

只要定义了一个带构造和析构的变量,那么当程序控制流到达这个变量定义式时,便得承担构造成本;变量离开作用域时,便得承担析构成本。即使这个变量并未使用,我们应该竭力避免这种情形

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

  • const_cast:通常被用来将对象的常量性转除。它也是唯一有此能力的C+±style转型操作符
  • dynamic_cast;主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。
  • reinterpret_cast:意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。
  • static_cast:用来强迫隐式转换,例如将non-const 转换为const,但是不可将const转换为non-const,只有const_cast才可以。
class Window{										//base class
	public:
		virtual void onResize(){...}				//base onResize实现代码
		...
};
class SpecialWindow: public Window{					//derived class
public:
	virtual void onResize(){						//derived onResize实现代码
		static_cast<Window>(*this).onResize();		//将*this转型为Window,
													//然后调用其onResize;
													//这不可行!
			...		//这里进行SpecialWindow专属行为。
	}
	...
}

上述代码描绘了一种情形:
有一个Window base class和一个SpecialWindow derived class,两者都定义了virtual函数 onResize。进一步假设SpecialWindow的onResize函数被要求首先调用Window的Resize,上述代码描绘了一种看起来对,实际是错的实现方式。

在SpecialWindow的onResize函数中将*this指针强制转换为Window,然后调用Window的onResize

实际上,转型动作static-cast创建了一个“this对象的class成分的暂时副本”,调用的实际上是这个副本的onResize!因此,上述代码最终的执行结果是在副本上执行Window::onResize,然后在当前对象上执行SpecialWindow专属动作。

解决之道是拿掉转型动作

class SpecialWindow: public Window{					//derived class
public:
	virtual void onResize(){						//derived onResize实现代码
		Window::onResize();
			...		//这里进行SpecialWindow专属行为。
	}
	...
}

条款28:避免返回handles(号码牌,用来取得某个对象)指向对象内部成分

//定义一个Point类
class Point{
public:
	Point(int x,int y);
	...
	void setX(int newVal);
	void setY(int newVal);
	...
};

struct RecData{
	Point ulhc;
	Point lrhc;
};

class Rectangle{
public:
	...
	Point& upperLeft() const {return pData->ulhc;}		//常函数,不允许改变变量的值,返回reference
	Point& lowerRight() const {return pData->lrhc;}
private:
	shared_ptr<RecData> pData;
}

Point coord1(0,0);
Point coord2(100,100);
const Rectangle rec(coord1,coord2);

rec.upperLeft().setX(50);	//upperLeft()函数是常成员函数,本意是不允许改变变量的值,
						 	//现在却通过其返回的reference改变了Rectangle的私有变量的值

避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降到最低

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

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

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。但是程序的现实状态不可预料
  • 强烈保证:如果异常被抛出,程序状态不改变——如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前的状态”。
  • 不抛掷(nothrow)保证:承诺绝不抛出异常,因为他们总是能够完成他们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。

有一个一般化的设计策略很典型地会导致强烈保证——copy and swap

  • 基本原则:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
  • 实现手段:将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(implementation object,即副本)。这种常被称为pimpl idiom。

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

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使得日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  • 不要只因为function templates出现在头文件,就将它们声明为inline。

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

在一个文件中加入#include时,此文件便与其含入文件之间形成了一种编译依存关系

#include <string>
#include "data.h"
#include "address.h"

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

由上述代码所示,当Person定义文件和其含入文件之间形成了一种编译依存关系。如果这些头文件中有一个任何一个被改变,或这些头文件所依赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person的文件也必须重新编译。

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

public inheritance(公开继承)意味“is-a(是一种)”的关系

  • 如果令class D以public形式继承class B,则是告诉C++编译器:每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。
  • 适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象都是一个base class对象

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

int x;	//global变量
void someFunc()
{
	double x;		//local变量,此变量遮掩了global变量x
	cin>>x;			//对local变量赋值
}

class Base{
private:
	int x;
public:
	virtual void mf1() = 0;
	virtual void mf2();
	void mf3();
	...
};
class Derived:public Base{
public:
	virtual void mf1();
	void mf4();
	...
};
void Derived::mf4()
{
	...
	mf2();		//当编译器看到这里的mf2,必须估算它指涉(refer to)什么东西。编译器的做法是查找各作用域
	...			//编译器的做法是查找各作用域,看看有没有某个名为mf2的声明式。
}
  • 与作用域类似,在继承关系中,Derived class作用域被嵌套在base class 作用域内
  • 当编译器看到这里的mf2,必须估算它指涉(refer to)什么东西。编译器的做法是查找各作用域编译器的做法是查找各作用域,看看有没有某个名为mf2的声明式。
  • 编译器首先查找local作用域(也就是mf4覆盖的作用域),在那没找到任何东西名为mf2,于是查找其外围作用域,也就是class Derived覆盖的作用域。还是没找到,继续往外围移动,本例为base class,找到了mf2,查找动作停止。
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();
	...
};
//Base class 内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。
//从名称查找观点来看,Base::mf1和Base::mf3不再被继承
Derived d;
int x;
d.mf3(x);	//错误!Derived::mf3()遮掩了Base::mf3
d.mf1(x);	//错误!

//可以用using声明式解决上述问题
class Derived:public Base{
public:
	using Base::mf1;
	using Base::mf3;
	virtual void mf1();
	void mf3();
	void mf4();
	...
};
  • 当不想继承base classes的所有函数,在public继承下,这绝对不可能发生,因为它违反了“is-a”关系。
  • 假设Derived以privated形式继承Base,而Derived唯一想继承的mf1是那个无参数的版本,那么using声明式在这里不起作用,因为using声明式会令继承而来的某给定名称的所有同名函数在derived class中都可见。
  • 此时需要一种新技术——一个简单的转交函数(forwarding function)
class Derived:public Base{
public:
	virtual void mf1()	//转交函数
	{Base::mf1();}
	void mf3();
	void mf4();
	...
};

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

public继承由两部组成:函数接口继承和函数实现继承。

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口
  • 声明一个impure virtual函数的目的,是让derived classes继承该函数的接口和默认实现
  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现

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

藉由Non-Virtual Interface手法实现Template Method模式

class base{
public:
	void dosomething()
	{
		...			//做一些事前工作	
		justDo();
		...			//做一些事后工作
	}
private:
	void justDo();
};

令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法,他是Template Method设计模式的一个独特的表现形式。
此方法的优点在于上述代码中的“做一些事前工作”和“做一些事后工作”

  • 事前工作可以包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件
  • 事后工作可以包括互斥器解除锁定、验证函数的事后条件等等

略,待补充

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

  • non-virtual 函数是静态绑定的
  • virtual 函数是动态绑定的

public继承意味着“is-a”:

  • 适用于Base对象的每一件事,也适用于Derived对象,因为每一个Derived对象都是一个Base对象
  • Derived对象一定会继承Base对象中的non-virtual函数的接口与实现

因此,如果在Derived对象中重新定义继承而来的non-virtual函数,便违反了以上原则

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

继承一个带有缺省参数值的virtual函数

  • virtual函数系动态绑定
  • 缺省参数值确实静态绑定

静态绑定(前期绑定),动态绑定(后期绑定)

Shape
Rectangle
Circle

考虑如上继承体系

Shape* ps;
Shape* pc = new Circle;		//动态类型为Circle*
Shape* pr = new Rectangle;		//动态类型为Rectangle*
//静态类型均为Shape*

本例中ps,pc和pr都被声明为pointer-to-Shape类型,即它们的静态类型均为Shape*。
对象所谓的动态类型则是指“目前所指对象的类型”。也就是说,动态类型可以表现出一个对象将会有什么行为。动态类型可在程序执行过程中改变。

virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。

当virtual函数带有默认参数值时,参数值为静态绑定的,意思是当调用派生类的virtual函数时,却使用了基类为他指定的默认参数值
当通过pc调用某virtual函数时,调用的是Circle内定义的函数,但若此函数带有默认参数,则使用Shape内为其指定的默认参数

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

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

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

多重继承:继承一个以上的base class

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

编译时多态:以不同的template参数具现化function templates会导致调用不同的函数

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

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

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

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值