Conclusion for Implementations

条款26:

1.尽可能避免变量被定义但是没有被使用。

std::string encryptPassword(const std::string& password){
	using namespace std;
	string encrypted;    //如果抛出异常,则可能没被使用,但是付出构造和析构成本
	if (password.length() < MinimumPasswordLength){
		throw logic_error("Password is too short");
	}
	//string encrypted; //放在此处就比较合适
	//开始加密
	return encrypted;
}
2.通过default构造函数构造出一个对象后对它赋值比直接在构造时指定初值效率差。

std::string encryptPassword(const std::string& password){
	if (password.length() < MinimumPasswordLength){
		throw logic_error("Password is too short");
	}
	string encrypted(password); //放在此处就比较合适
	//开始加密
	encrypt(encrypted);        //在其中适当地点对s加密
	return encrypted;
}
所以,尽可能延后到给它初值实参为止。

3.下面比较循环下变量定义以及初始化的情况:

//A :变量定义于循环外
Widget w;
for (int i = 0; i < n; i++){
	w = ..;
	//...
}
//B:变量定义于循环内
for (int i = 0; i < n; i++){
	Widget w(取决于i的某个值);
	//...
}
两种形式的成本如下:
A: 1个构造函数 + 1个析构函数 + n个赋值函数
B: n个构造函数 + n个析构函数

如果Widget的构造析构成本比赋值成本要高的话,无疑A的做法总体效率要高;反之则B的做法效率高。



条款27:

1.C++提供的四种新式转型

const_cast通常被用来将对象的常量性转除。它可是唯一有此技能的转型操作符。

dynamic_cast 用于继承体系下的"向下安全转换",通常用于将基类对象指针转换为其子类对象指针,它也是唯一一种无法用旧式转换进行替换的转型,也是唯一可能耗费重大运行成本的转型动作。

reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。

static_cast用来强迫隐式转换,例如将non-cast对象转为const对象,或将int转为double等等。但无法将const转为non-const(只有const_cast可以做到)
2.唯一使用旧式转型的时机是,当要调用一个explicit构造函数(禁止隐式转换)将一个对象传递给一个函数时。

class Widget{
public:
	explicit Widget(int size);
	...
};
void doSomething(Widget& w);
doSomething(Widget(15)); //"旧式转型"中的函数转型
doSomething(static_cast<Widget>(15));//"新式转型"
3.任何一个类型转换(不论是通过转型操作而进行的显式转换,或是通过编译器完成的隐式转换),往往真的令编译器编译出运行期间的执行码。

class Base{};
class Derived :public Base{};
Derived d;
Base* pb = &d;
此处建立一个base class指针指向一个derived class对象,但有时候上述两个指针值(Derived*和Base*)并不相同。这种情况下会有个偏移量在运行期被施行在Derived*指针身上,用以取得正确的Base*指针值。

#include <iostream>
using namespace std;

class Base{};
class Derived :public Base{};

int main(){
	Derived d;
	Derived* pd = &d;
	Base* pb = &d;
	cout << pd << endl << pb << endl;  //此时相同
	return 0;
}
有没有不相同的例子???

上面表明,单一对象(例如一个类型为Derived的对象)可能拥有一个以上的地址(例如“以Base*指向它”时的地址和“以Derived*指向它”是的地址)

4.许多应用框架要求derived class内的virtual函数代码的第一个动作就先调用base class的对应函数。

下面来看一个例子:

#include <iostream>
using namespace std;

class Window{
public:
	virtual void onResize(){
		size -= 1;
	}
	int size;            //为了不用提供接口函数,此处定义private
	Window(){ size = 10; }
};

class SpecialWindow :public Window{
public:
	virtual void onResize(){
		static_cast<Window>(*this).onResize();
		size -= 2;
	}
};

int main(){
	SpecialWindow s;
	s.onResize();
	cout << s.size << endl;
	return 0;
}
先调用base class的onResize函数,size减1,再执行derived class专属行为,size-3,照理最后输出应该是7,可最后输出的是8。

这段程序将*this转型为Window,对函数onResize的调用也因此调用了Window::onResize。但它调用的不是当前对象上的函数,而是稍早转型动作所建立的一个“*this对象的base class成分”的暂时副本身上的onResize。如果Window::onResize修改了对象内容,当前对象其实没被改动,改动的是副本。

解决方法是拿掉转型动作,而不是骗编译器将*this视为一个base class对象。

class SpecialWindow :public Window{
public:
	virtual void onResize(){
		Window::onResize();
		size -= 2;
	}
};
改成这样输出就是7了。

5.之所以需要dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你手上只有一个指向base的pointer或reference,你只能靠他们来处理对象。

方法一:使用容器并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要。 (不是很明白为什么要用容器)(容器类型安全??)

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Window{};

class SpecialWindow :public Window{
public:
	void blink();
};

int main(){
	typedef std::vector<std::tr1::shared_ptr<SpecialWindow>> VPSW;
	VPSW winPtrs;
	//...
	for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
		(*iter)->blink();
	return 0;
}
方法二:在base class内提供virtual函数做你想对各个Window派生类做的事。

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Window{
public:
	virtual void blink(){}
};

class SpecialWindow :public Window{
public:
	virtual void blink();
};

int main(){
	typedef std::vector<std::tr1::shared_ptr<Window>> VPW;  //注意是Window
	VPW winPtrs;
	//...
	for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
		(*iter)->blink();
	return 0;
}



条款28:

1.避免返回handle(指针,引用,迭代器)指向对象内部成分

下面来看例子:

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Point{    //使Rectangle对象尽可能小
public:
	Point(int x, int y){ xcoord = x; ycoord = y; }
	Point(){}
	void setX(int _x){ xcoord = _x; }
	void setY(int _y){ ycoord = _y; }
	int xcoord, ycoord;
};

struct RectData{
	Point ulhc;
	Point lrhc;
	RectData(){}
};

class Rectangle{
private:
	std::tr1::shared_ptr<RectData> pData;
public:
	Point & upperLeft() const
	{
		return pData->ulhc;
	}
	Point &lowerRight() const
	{
		return pData->lrhc;
	}
	Rectangle(const Point& p1, const Point& p2):pData(new RectData()) //注意只能指针的初始化
	{
		pData->ulhc = p1;
		pData->lrhc = p2;
	}
};

int main(){
	Point coord1(0, 0);
	Point coord2(100, 100);
	const Rectangle rec(coord1, coord2);
	rec.upperLeft().setX(50);
	return 0;
}
这样的设计是自相矛盾的。一方面upperLeft和lowerRight被声明为const成员函数,因为它们的目的是为了提供客户一个得知矩阵相关坐标的方法,而不是让客户修改矩阵。但另一方面两个函数却都返回references指向private内部数据,调用者于是可以通过这些reference更改内部数据。
为了解决这个问题,只需在返回的指针或引用前加上const即可。

const Point & upperLeft() const;
const Point &lowerRight() const;



条款29:

1.一个没有考虑异常安全的情形

class PrettyMenu{
public:
	//……
	void changeBackground(std::istream& imgSrc);//改变背景图片
	//……
private:
	Mutex mutex;    //互斥量
	Image* bgImage; //当前背景图片
	int imageChanges;   //背景图片改变次数
};
下面是一个可能实现:
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	lock(&mutex);
	delete bgImage;
	++imageChanges;
	bgImage = new Image(imgSrc);
	unlock(&mutex);
}

这个函数没有满足下面要说的异常安全的两个条件。

2.异常安全的两个条件
1)不泄露任何资源。上面,一旦在 lockunlock之间发生异常, unlock的调用就不会执行,互斥量永远被锁住了。
2)不允许数据败坏。上述代码中,如果 new Image(imgSrc)抛出异常,bgImage就会指向不存在的对象,但imageChanges已经累加,其实并没有新的图像安装起来。

3.解决资源泄露问题(以对象管理资源----条款13)

void PrettyMenu::changeBacground(std::istream& imgSrc) 
{ 
Lock m(&mutex); 
delete bgImage; 
++imageChanges; 
bgImage=new Image(imgSrc); 
} 
其中Lock是根据条款15定义:
class Lock{
public:
	explicit Lock(Mutex* pm) :mutexPtr(pm, unlock)
	{
		lock(mutexPtr.get());
	}
private:
	std::tr1::shared_ptr<Mutex> mutexPtr;
};
class析构函数会自动调用其non-static成员变量(本例mutexPtr)的析构函数。

4.成员函数声明后面跟上throw(),表示告诉类的使用者,我的这个方法不会抛出异常,所以在使用该方法的时候不必把它置于try/catch异常处理块中。

int doSomething() throw();  //空白的异常明细

5.强烈保证:如果异常抛出,程序状态不改变。copy and swap策略可以导致强烈保证。原则为:为打算修改的对象做出一份副本,然后在那副本身上做一切必要修改。若有任何修改抛出异常,原对象仍未改变状态。待所有改变都成功后,再将修改过得那个副本和原对象在一个不抛出异常的操作中置换。

实现上的手法被称为pimpl idiom(pointer to implementation):将所有“隶属对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象。

struct PMImpl{  //PMImpl:Pretty Menu Impl。后面说明为什么是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;//**条款**25
    Lock m1(&mutex);
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));
    pNew->bgImage.reset(new Image(imgSrc));;//修改副本
    ++pNew->imageChanges;
    swap(pImpl,pNew);
}
此处PMImpl成为一个struct而不是class,这是因为PrettyMenu的数据封装性已经由于“PImpl是private”而获得了保证。
6.copy and swap并不保证整个函数有强烈的异常安全性。

void someFunc()
{
    ……;//对local状态做备份
    f1();//
    f2();
    ……//将修改后的状态置换过来
}
如果f1或f2的异常安全性比“强烈保证”低,就很难让someFunc成为“强烈异常安全”。

如果f1和f2都是“强烈异常安全”,情况并不好转。如果f1圆满结束,程序状态在任何方面都有可能改变,因此如果f2随后异常,程序状态和someFunc被调用前并不相同。
因此不建议使用强烈保证,因为还会带来效率问题。



条款30:

1.内联函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。这样可能增加目标码大小,造成程序体积太大。

2.内联函数的申请可以是隐喻申请,也可以明确提出。隐喻方式是将函数定义于class定义式内,friend函数也可以被定义在class内,而是被隐喻为inline。明确声明inline函数的做法是在其定义式前加上关键字inline。

3.inline函数通常一定被置于头文件内,因为大多数建置环境在编译过程进行inline(有些在连接期,少量可以在运行期),而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。inline在大多数C++程序中是编译期行为。

注:templates通常也被置于头文件内,因为它一旦被使用,编译器为了将它具现化,需要知道它长什么样子。

4.大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining。而所有对virtual函数的调用也都会是Inlining落空。因为virtual意味着“等待,直到运行期才确定调用那个函数”,而inline意味着“执行前,先将调用动作替换为被调用函数的本体”。

5.编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。例如,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。

inline void f(){……}//假设编译器会inline函数f
void ( *pf)()=f;//指向函数的指针
f();//这个调用会被inlined,因为是正常调用
pf();//这个调用可能不会被inlined,因为它是通过函数指针达成
6.构造函数和析构函数往往是inlining的糟糕候选人。

class Base{
public:
    ……
private:
    std::string bm1, bm2;
};

class Derived: public Base {
public:
    Derived(){}//Derived构造函数
    ……
private:
    std::string dm1, dm2, dm3;
};
构造函数看起来是空的,其实编译器代为产生的代码安插在程序中:

Derived::Derived()//概念实现
{
    Base::Base();
    try{ dm1.std::string::string();}//构造dm1
    catch(……){
        Base::~Base();//销毁base class部分
        throw;//抛出异常
    }

    try{ dm2.std::string::string(); }//构造dm2
    catch(……){
        dm1.str::string::~string();//销毁dm1
        Base::~Base();
        throw;
    }

    try{ dm3.std::string::string();}
    catch(……){
        dm2.std::string::~string();
        dm1.std::string::~string();
        Base::~Base();
    }
}
7.大部分调试器对inline函数束手无策,因为在一个不存在的函数内设置断点。



条款32:

1.将用到的类对应的头文件包含进来会形成一种编译依存关系。

class Person{
    public:
        Person(const std::string& name, const Date& birthday, const Address& addr);
        std::string name() const;
        std::string birthDate() const;
        std::string address() const;
        ……
    private:
        //实现细目
        std::string theName;    //实现细目
        Date the BirthDate;     //实现细目
        Address theAddress;     //实现细目
    };
要想编译,还要把class中用到的string、Date、Address包含进来。在 Person定义文件的最前面,应该有:

#include<string>
#include"date.h"
#include"address.h"
这样Person定义文件和其含入的头文件形成了一种编译依存关系。如果这些头文件中有任何一个改变,或这些头文件所依赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。

2.将实现细目分开叙述

namespace std{
	class string;//前置声明
}
class Date;//前置声明
class Address;//前置声明
class Person{
public:
	Person(const std::string& name, const Date& birthday, const Address& addr);
	std::string name() const;
	std::string birthDate() const;
	std::string address() const;
	//没有实现细目(见后面)
};
上面string的前置声明错误,string不是个class,而是typedef(定义为basic_string<char>)

下面测试string的前置声明,会出错,但是Data的前置声明没问题。

#include <iostream>
#include <vector>
#include <memory>
//#include "Header.h"
using namespace std;

class string;
class Data;
int main(){
	string a;
	Data b();
	return 0;
}
3.针对Person我们可以这样做:把Person分割成两个classes,一个只提供接口,另一个负责实现该接口。如果负责实现的那个所谓implementation class取名为PersonImpl,Person的定义如下:
#include<string>
#include<memory>
class PersonImpl; //前置声明
class Date;
class Address;

class Person{
public:
	Person(const std::string& name, const Date& birthday, const Address& addr);
	std::string name() const;
	std::string birthDate() const;
	std::string address() const;
	……
private:
	//实现细目
	std::tr1::shared_ptr<PersonImpl> pImpl;//指针,指向实现
};
在这里,main class只含有一个指针成员,指向其实现类。这类设计被称为 pimpl idiom

所以以后如果面试官问:如何降低文件间的编译依存?前置声明+接口与实现分离

4.可以只靠一个类型声明式就定义出指向该类型的reference和pointers:

int main()
    {
        int x;
        Person* p;//定义一个指向Person的指针
        ……
    }
如果定义某类型的objects,就需要用到该类型的定义式。

5.如果能够,尽量以class声明式替换class定义式。

#include <iostream>
#include <vector>
#include <memory>
using namespace std;

class Data;             //class 声明式
int main(){
	Data func1();       //ok
	void func2(Data a); //ok
	Data data;          //not ok(incomplete type is not allowed)
	return 0;
}
前面两个OK是因为声明func1和func2函数不需定义Date,一旦有人调用这两个函数,调用之前Data定义式一定要先曝光才行。

6. 如果能够将提供class定义式(通过#include 完成)的义务从“函数声明所在”的头文件转移到“内含函数调用”的客户文件,便可将“并非真正必要之类型定义”与客户端之间的编译依存性去除掉。

7.为声明式和定义式提供不同的头文件。

为了严格遵守该准则,需要两个头文件,一个用于声明式,一个用于定义式。程序库客户应该总是#include一个生命文件而非前置声明若干函数,程序库作者也提供这两个头文件。(因为客户会调用这两个函数,调用之前Data定义式一定要先曝光)。

下面是Data的客户文件:

#include "data.h"  //这个头文件声明但未定义class Data
Date func1();
void func2(Data a); //ok
注意:C++标准程序库<iosfwd>内含iostream各组件的声明式,其对应定义则分布在若干不同的头文件内,包括<sstream>,<streambuf>,<fstream>,和<iostream>。
8.C++提供关键字export,允许将template声明式和template定义式分割于不同的文件内。

9.像Person这样使用pimpl idiom的class,往往被称为Handle class。这样的class一种方法是将他们的所有函数转交给相应的实现类,并由后者完成实际工作。

关于pimpl idiom见http://blog.csdn.net/tianya_team/article/details/50214833

    #include"Person.h"     //正在实现Person class,所以必须#include其class定义式
    #include"PersonImpl.h"   //我们必须#include PersonImpl的class定义式,否则无法调用其成员函数
    Person::Person(const std::string& name, const Date& birthday, const Address& addr):
            pImpl(new PersonImpl(name, birthday, addr)){}
    std::string Person::name() const
    {
        return pImpl->name();
    }
另一个制作Handle class的办法是,令Person称为一种特殊的abstract base class,称为Interface class。(条款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必须以Person的pointers和references来撰写应用程序,因为抽象类不能具现出实体。
Interface class的客户必须有办法为这种class创建新对象。他们调用一个 工厂函数(条款13),这个函数扮演“真正将被具现化”的那个derived classes的构造函数角色。

class Person{
public:
	//……
	static std::tr1::shared_ptr<Person>
		cretae(const std::string& name,
		const Data& birthday,
		const Address& addr);
		//……
};
客户这样使用它们:
std::string name;
Date dateOfBirth;
Address address;
std::tr1::shared_ptr<Person> pp(Person::create(……));
std::cout << pp->name() 
          << "was born on" 
		  << pp->birthDate() 
		  << "and now lives at" 
		  << pp->address();
支持Interface class接口的那个具体类(concrete classes)在真正的构造函数调用之前要被定义好。例如,有个RealPerson继承了Person
class RealPerson: public Person{
    public:
        RealPerson(const std::string& name, const Date& birthday, const Address& addr)
            :theName(name), theBirthDate(birthday), theAddress(addr)
            {}
        virtual ~RealPerson(){};
        std::string name() const;
        ……
    private:
        std::string theName;
        Date theBirthDate;
        Address theAddress;
    };
有了RealPerson之后,就可以写Person::create了

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




你可能感兴趣的:(Conclusion for Implementations)