effective C++笔记——实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/rest_in_peace/article/details/83990933

尽可能延后变量定义式的出现时间

. 当定义了一个变量并且它的类型存在构造函数和析构函数,那么程序需要承受它的构造成本和析构成本,即使这个变量并不被使用,仍需要耗费这些成本,所以应该尽量避免这种情况的发生。
  通常我们不会定义一个不被使用的变量出来,但是定义变量过早可能会应为程序的运行情况而没有被使用到,比如:

string entrypassword(const string& password){
	string entryret;									//过早的定义
	if(password.length() < 5){
		throw logic_error("password is too short!");
	}
	...														//其他操作
	entryret = password;
	return entryret;
}

. 假设在其中抛出了异常,变量entry热帖就没有被使用,但是仍然要承担entryret的构造成本和析构成本。另一个问题是定义变量的操作是使用的默认的构造函数,然后再使用赋值符号进行赋值,这样显得效率低下,应该尽量跳过默认的默认的构造:

string entrypassword(const string& password){
	if(password.length() < 5){
		throw logic_error("password is too short!");
	}
	...														//其他操作
	string entryret(password);				//使用拷贝构造函数完成定义和初始化					
	return entryret;
}

. 另外一种比较常见的定义变量的情况是在循环中的变量定义,是应该将变量定义在循环内部还是定义在循环外部每次进行赋值?两种情况分别对应的成本如下:
  做法1:n个构造函数+n个析构函数
  做法2:1个构造函数+1个析构函数+n个赋值操作
  所以通常除了在知道赋值操作的成本比“构造+析构”的成本低,或则正在处理代码中效率的高敏感部分时,都应该使用做法1。

尽量少做转型操作

. 转型破坏了类型系统,可能带来任何种类的麻烦,有些容易辨识,有些则会非常隐晦。所以这个特性应当得到重视。
  先回顾两种旧式转型的形式:

//C风格的转型动作
(T)expression;      //将expression转型为T
//函数风格的转型动作
expression(T);			//将expression转型为T

两种形式并无差别,知识括号的摆放位置不同,C++还提供四中新式转型:
1.const_cast<T>(expression)
 const_cast通常被用来将对象的常量性转除,它也是唯一有此能力的C++风格的转型操作符。
2.dynamic_cast<T>()expression)
 dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的动作。
3.reinterpret_cast<T>(expression)
 reinterpret_cast意图执行低级转型,实际动作及结果可能取决于编译器,这也就表示它不可移植,例如将一个指向整型的指针转为一个整型。
4.static_cast<T>(expression)
 static_cast用来强迫隐式转换,例如将非常量对象转换为常量对象、将void指针转为typed指针,或者将int转为double等。
  新式的转型较受欢迎,因为容易在代码中辨识出来,也更加目标明确。
  转型操作并不是简单的告诉编译器将变量视作另一种类型,而是往往真的产出需要执行的代码,比如:

int x,y;
...
double d = static_cast<double>(x)/y;		//使用浮点数除法

. 将int转型为double几乎肯定会产生一些代码,因为大部分的计算器体系中,int的底层描述不同于double的底层描述。又比如在继承体系中父类指针指向子类对象的时候,实际上是隐式的将子类指针转型为父类指针,这操作在运行期间有一个偏移量的存在,确保能够取得正确的Base部分。
  另一种情况是可能会写出事实而非的代码,比如:

class Window{
public:
	virtual void onResize(){...}
};

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

. 在子类中做了一个转型操作,将this指针指向的对象转型为Window,燃火调用了父类的onResize函数,看起来想法没什么问题,但是调用的却不是当前对象上的函数,而是转型操作时所建立的Base部分的副本身上的函数,也就是说这样调用后并不对该对象的基类部分做改动,只对该基类部分的一个副本进行了改动,所以正常情况还是应该这么写:

class SpecialWindow : public Window{
public:
	virtual void onResize(){
		Window::onResize();			//调用onResize作用于this指针指向的对象
		...
	}
	...
};

优良的C++代码很少使用转型,但是要完全摆脱他们又不太实际,通常应该将转型动作尽可能隔离开,将它隐藏在某个函数中,函数的接口会保护调用者不受内部的动作所影响。

避免返回handles指向对象的内部成分

我的理解是不要将私有变量直接用引用的方式返回,这样外部将能对他们进行修改,破坏了封装性。

为“异常安全”而努力是值得的

. 异常安全性对函数来说是很重要的,对一个异常安全函数应该提供三个保证之一:
1.基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或是数据结构被破坏,所有对象应当处于一种内部前后一致的状态。
2.强烈保证:如果异常被抛出,程序状态不改变。也就是说如果函数调用成功,那么就应该完全成功,如果调用失败,程序应该恢复到调用函数之前的状态。关于这点,往往能够以copy-and-swap实现出来,即修改对象数据的副本,然后在不抛出异常的函数中将修改后的数据和原件进行置换。
3.不抛掷保证:承诺绝不抛出异常,因为他们总是能够完成他们原先承诺的功能(没懂,是说尽量不抛出异常,让函数完成它的工作的意思吗?)

透彻了解inlining的里里外外

. inline函数看起来像是函数,动作像是函数,比宏要更加高效,不要承担函数调用所招致的额外开销,编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,所以当inline某个函数时,编译器就有能力对它执行语境相关最优化。
  但是天下没有白吃的午餐,inline函数也不例外,inline函数的背后理念是,将“对此函数的每一次调用”都以函数本体替换之,这样做可能会增加目标码的大小,在一台内存有限的机器上,过度使用inlining会造成程序体积太大,带来效率损失。
  隐喻声明inline函数的方式是将函数定义在class内部,明确的定义inline函数是在其定义式前加上inline关键字,但是一个函数被你声明为inline后是否真的是inline的,取决于编译器环境,通常编译器会拒绝将过于复杂的函数inlining,而所有对virtual函数的调用,也会使inlining落空。幸运的是大多数编译器提供了一个诊断级别:如果不能对函数进行inline化,会给出一个警告信息。
  虽然编译器有意愿将某个函数inlined,但还是有可能为函数声明一个函数本体,比如当程序要取某个inline函数的地址的时候(函数指针啊之类的),编译器通常必须为这个函数生成一个函数本体,因为编译器没有能力提出一个指针指向并不存在的函数。
  实际上构造函数和析构函数往往是inline的糟糕人选,这是因为C++的设计原理中对“对象被创建和销毁的时候做了什么”要做出各种保证,即使你的构造函数或析构函数中什么都没有写,但是可以知道的是这些情况中肯定是有事情发生的,程序内一定有某些代码让这些保证发生,而这些代码,是编译器在编译期间代为产生并安插到程序中的,有时候就放在构造函数与析构函数中。
  程序设计者应当评估将函数声明为inline带来的冲击:inline函数无法随着程序库的升级而升级,一旦需要修改这个inline函数,所有用到它的客户端程序都必须重新编译。
  还有比较实际的一个问题是:大部分调试器对inline函数都束手无策,因为无法在不存在的函数内打断点。
  所以建议将inline限制在小型、被频繁使用的函数身上,降低代码膨胀为题,提高程序运行速度。

将文件间的编译依存关系降到最低

. 往往在编写代码的时候会分成很多的文件,比如某个类的定义和实现会分别在一个头文件和cpp文件中,其他文件包含了头文件就能定义这个类并调用这个类的方法,但是当修改了这个类的某个属性的时候,或者这个类所依赖的头文件中有改变时,任何包含这个类的文件都需要进行重新编译,这将大大增加编译的时间。
  可以使用一种“将对象的实现细节隐藏于一个指针背后”,很像java中的接口类这种东西,对C++来说可以将类分为两个class,一个只提供接口,一个负责实现接口,例如:

class PersonImpl;				//Person实现类的前置声明
class Date;							//Person类用到的class的前置声明
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;			//指向实现物的指针
}

. 以上的Person类中只含有一个指针成员,指向其实现类,这样的设计下,Person的客户就与Date、Address以及Person的实现细节分离了,那些class的任何修改都不需要Person客户端重新编译。
  这个分离的关键在于以“声明的依存性”替换“定义的依存性”,这就是确定编译依存性最小化的本质:现实中让头文件尽可能自我满足,如果做不到,则让它与其他文件内的声明式相依:
  如果使用引用或指针能完成任务,就不要直接使用对象。可以只靠一个类型的声明式就定义出指向该类型的引用或指针,但如果定义某类型的对象,就需要该类型的定义式;
  如果可以,尽量以class声明式替换class定义式。比如说在一个函数的声明中,它的参数或者返回值是一个类类型的对象,只需要在前面做这个类的声明,而不需要这个类的定义。你调用这个函数的时候才需要这个类的定义;
  为声明式和定义式提供不同的头文件。根据以上两点,需要两个头文件,一个用于声明式,一个用于定义式。这两个文件需要保证一致性。这样不需要大量的做前置声明,而是包含这个声明式的头文件就可以了。
  像之前的Person类,往往被称为Handle classes,如何制作这样的类呢,办法之一是将它的所有函数转交给相应的实现类并由后者完成实际工作,例如:

#include "Person.h"					
#include "PersonImpl.h"				//两个类有相同的成员函数
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 classes的办法是,令Person成为一种特殊的抽象基类,这种class的目的是描述派生类的接口,因此它通常没有成员变量,也没有构造函数,只有一个virtual析构函数和一组虚函数,用来叙述整个接口,听起来很像java的接口(Interfaces),但不同的地方在于java不允许在interface内实现成员变量或是成员函数,但C++不禁止,这样的弹性是有用途的,比如非虚函数的实现对继承体系内的所有类都应该相同。例如一个针对Person的interface class看起来像是这样:

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的pointer或reference来撰写应用程序,因为不能对有纯虚函数的类具现出实体,通常通过一个特殊函数(工厂函数或虚构造函数)返回指针来指向动态分配所获得的这个类的对象,而该对象支持interface接口。
  Handle class 和interface class解除了接口和实现之间的耦合关系,从而降低文件间的依存性,不过这样的代价是:运行期将丧失若干速度,又让你对每个对象超额付出若干内存。

猜你喜欢

转载自blog.csdn.net/rest_in_peace/article/details/83990933