Effective C++学习笔记(2)

实现

1、尽可能地延后定义变量(包括对象),直到我们真正需要使用它。

std::string test(const std::string& p)
{
	using namespace std;
	string s;
	if(p.length() < MinimumLength)
	{
		throw logic_error("p is too short");
	}
	//string s;
    return s;
  }

注意当抛出异常时,并不会使用到字符串s,但是却耗费了时间去构造它。正确的做法应该是在确定没有异常后进行定义。
更加需要注意的是,对于对象的创建,甚至应该能够给这个对象赋初始值的时候,才去定义并构造这个对象。这样避免了先调用默认的构造函数,然后再调用copying函数去给这个对象赋值。
还有是在循环中需要使用到的变量,到底是在循环外部进行定义还是在循环内部进行定义,也是我们需要根据实际的情况作出衡量。

2、尽量避免使用转型操作,如果是特别注意效率的代码,应该减少使用dynamic_cast。尝试设计一些方法来替代转型,比如使用虚函数可以动态调用成员函数,以基类指针来管理派生类对象。

3、尽可能避免返回handlers(包括引用、指针、迭代器)指向对象内部成员,否则可能会修改私有成员,破坏封装性。同时,这也可以将发生”悬空指针“的可能降至最低。

class Point
{
private:
	int x;
	int y;
};
struct RectData
{
	Point leftU; //矩形的左上点
	Point rightD;//矩形的右下点
};
class Rectangle
{
	private:
		std::tr1::shared_ptr<RectData> pdata;
	public:
		const Point& upperleft() const { return pdata->leftU; }
};

class GUIObject
{ ... };
const Rectangle box(const GUIObject& obj);

GUIObject* p;
const Point* pLeft = &(box(*p).upperLeft()); 
//pLeft将指向一个不存在的对象,因为box函数返回
//一个临时的const Rectangle对象,假设为temp,
//然后调用这个对象的upperleft返回临时对象的矩形左上点的引用。
//当语句结束后这个临时对象将被销毁,因此pLeft将指向一个不存在的对象。

4、对于异常安全函数,提供三个保证之一:
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下,没有对象被破坏,所有对象处于内部的前后一致的状态。但是程序的实际状态不可预料。
强烈保证:如果函数成功,就不抛异常;如果函数失败,程序会恢复到”调用这个函数之前“的状态。此时程序状态只有2种,成功执行后到达的状态,或者,恢复到函数被调用前的状态。
不抛异常:保证不抛出异常。
对于,强烈保证的实例:

class PrettyMenu
{
	public:
		void changeBackground(std::istream& imgSrc);//改变背景
	private:
		Mutex mutex;//互斥器
		std::tr1::shared_ptr<Image> bgImage;//背景图的智能指针
		int cnum;//背景图像改变次数
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	lock ml(&mutex); //ml的构造函数中加锁,析构函数中解锁
	bgImage.reset(new Image(imgSrc));//假如成功new一个图像,则调用reset对旧图像删除,指向新图像;假如失败了,则不会调用reset,图像依旧是之前的样子
	++cum;//改变次数增加
}

另一个一般化的策略为copy and swap,实际上就是先对要修改的对象制作一个副本,然后在副本上进行修改。如果修改期间出现异常,则原对象时保持不变的。如果修改成功,那么再讲原对象和已修改的副本进行swap操作。代码如下:

struct PMImpl
{
	std::tr1::shared_ptr<Image> bg Image;
	int cnum;
};
class PrettyMenu
{
	private:
		Mutex mutex;
		std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
	using std::swap; //使用swap函数
	Lock ml(&mutex);
	std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));//构造原对象pImpl的副本对象
	pNew->bgImage.reset(new Image(imgSrc)); //修改副本对象
	++pNew->cnum;

	swap(pImpl, pNew);//将原对象和副本进行交换
}

值得注意的是,这种copy-and-swap的方式,并不总是具备现实意义。因为它总是要先去做出一个原对象的副本,而这也可能会消耗较大的时间和空间资源。

5、应该将inlining限制在小型、被频繁调用的函数身上。在类内部直接定义成员函数属于隐喻的inline申请,这里尤其要小心类的构造函数。很多情况下,类的构造函数往往看上去是空的,而实际上,这可能是多个深层次的派生类,在这个类的构造函数中,编译器会去调用其所有的基类构造函数。所以构造函数和析构函数不应该被inlined。
如果f 是程序库中的某一inline函数,客户端将f 编进程序后,一旦后期程序员决定改变f 函数,那么所有用到这个函数的客户端程序都必须重新编译。如果f 不是inline函数,后期就算被修改,客户端只需要重新连接即可,无需重新编译。

6、以”声明的依存性“替换”定义的依存性“,则可以让编译依存性最小化。
①在class的成员中,如果使用对象引用或者指针就可以实现需求,就没必要使用整个对象。因为使用引用和指针只需要进行前向声明,而不用将整个头文件包含进来;如果是定义某个类型的对象,那么就需要将他的头文件包含进来。当你在另一个文件总真正需要使用到那个指针所指向的内容时,才去包含这个头文件。
②class的成员函数的参数中如果采用by-value传一个对象或者返回值是一个其他的class,那么在声明的时候,也是不用包含头文件的,只需前向声明。当你真正需要调用这个函数的时候,才去包含这个类的头文件。
③使用handle class来降低文件依存关系,将一个类分成接口类和实现类。接口类只提供接口,实现类负责实现那些接口。实现类通常会被命名为~Impl,而接口类则包含两个元素,一个是private的智能指针,这个指针管理了实现类;另一个是与接口类中完全相同的成员函数(但接口类的成员函数仅仅是去调用实现类中的成员函数)。

#include <string>
#include "date.h"
 
class Person{
    public:
    	Person( const std::string& name, const Date& birthday);
    	std::string name() const;
    	std::string birthData() const;
    ...
    private:
    	std::string theName;
    	Date theBirthDate;
}

//改进后的Person.h
#include <string>
#include <memory>

class PersonImpl; //仅前向声明实现类,无需包含头文件
class Data;//仅前向声明Data类

class Person
{
	public:
		Person(const std::string& name, const Data& birthday);
		std::string name() const;
		std::string birthday() const;
	private:
	std::tr1::shared_ptr<PersonImpl> pImpl; //智能指针pImpl管理实现类PersonImpl
};

//Person.cpp
#include "Person.h"
#include "PersonImpl.h" 
Person::Person(const std::string& name, const Data& birthday)
	:pImp(new PersonImpl(name, birhday)){ } // 这里new了一个对象交由智能指针管理,离开程序块的时候,将有智能指针释放内存
std::string Person::name() const
{
	return pImpl->name(); //这里直接调用实现类的name函数
}
...

//PersonImpl.h
#include "date.h"
#include "address.h"
class PersonImpl
{
    public:
    	PersonImpl( const std::string& name, const Date& birthday, const Address& addr );
    	std::string name() const;
    	std::string birthData() const;
    ...
    private:
    	std::string theName;
    	Date theBirthDate;
}

//PersonImpl.cpp
#include "PersonImpl.h"
std::string PersonImpl::name() const {
    return theName;
}
...

这样做的优点:使Person.h不会再向下传递对上游头文件的依赖,做到了解耦合。PersonImpl.h依赖data.h等一些上游头文件,但从Person开始将不会传递这个依赖。
缺点:成员函数必须通过智能指针才能取得对象数据,访问是间接的,且增加了这个指针的内存,增加了动态内存分配的管理开销,及 bad_alloc 异常的处理难度。

④另一个制作handle class的方法是让Person类成为一个abstract class,即不能实例化的类,它包含一组纯虚函数,用来叙述整个接口。

class Person
{
	public:
		virtual ~Person();
		virtual std::string name() const = 0;//纯虚函数,在派生类中实现
		virtual std::string birthday() const = 0;
		static std::tr1::shared_ptr<Person> create(const std::string& name, const Data& birthday); //工厂函数,构造对象
};

class RealPerson: public Person
{
	public:
		RealPerson(const std::string& name, const Data& birthday)theName(name), theBirthday(birthday){ }
		virtual ~RealPerson(){ }
		std::string name() const; //实现基类中的纯虚函数
		std::string birthday() const;

	private:
		std::string theName;
		Data theBirthday;	
};

//Person.cpp
std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Data& birthday)
{
	return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday));//返回一个管理构造出来的RealPerson对象的智能指针
}
//在此,工厂函数可以创建不同类型的derived class对象,因为智能指针管理的类是抽象基类

小结:编译依存性最小化的思想是:相依于声明,而不要依赖定义式。

发布了8 篇原创文章 · 获赞 3 · 访问量 181

猜你喜欢

转载自blog.csdn.net/Longstar_L/article/details/105222669