Effective C++ ——实现

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

当你定义一个变量的时候就要保证这个变量能够在程序中使用到,不要定义无意义的变量,这样就要求我们最好是在变量使用到的时候才做定义,因为如果一个变量定义了却不使用可能会造成效率上的降低,毕竟很多变量的构建是要调用对应的构造函数和析构函数的,考虑下面的例子:

std::string setName(std::string& name){
string name_;
if(name.length() == 0){
throw logic_error("name length is zero error");
}
name_ = name;
...
return name_;
}

在上面的这个例子中,在函数内部定义了string变量name_,但是在抛出异常的时候这个变量却成了白板,根本就没有被用到,因此我们可以将name_变量的定义放到name_被赋值的地方,也就是在抛出异常的后面!还有一点需要注意的地方是我们对变量进行定义的时候尽量的用有效的值进行初始化,在上面的例子中我们可以通过参数name进行变量的初始化!
       在一个循环中定义变量的时候,应该将变量定义在循环的外层还是内层这也需要注意下,下面例子:

Widget w;
for(int i = 0; i < 100 ; i++){
w = 取决于i的某个值;
...
}
for(int i = 0; i < 100 ; i++){
Widget w = 取决于i的某个值;
...
}

上面的例子中那个更加高效呢?第一种执行了一个构造函数+100个赋值函数+一个析构函数,第二种执行了100个构造函数和100次析构函数因此在采用哪种方法的问题上要考虑构造函数和析构函数对赋值函数效率的不同,此外后面的那种方式因为变量是定义在循环内部的因此其命名空间的影响比较小,是一种不错的方式!

请注意:

  • 尽可能延后变量定义式的出现,这样做可增加程序的清晰度和效率。

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

C++中尽量的少用类型的转化,只要有类型转化的地方就是有可能会出现问题的地方,在C++中有四种强制类型转化,如下:

1、static_cast

用法:static_cast<type-id>(expression)

该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

①用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。

②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。

③把空指针转换成目标类型的空指针。

④把任何类型的表达式转换成void类型。

注意:static_cast不能转换掉expression的const、volitale、或者__unaligned属性。

2、const_cast

用法:const_cast<type_id>(expression)

该运算符用来修改类型的const或volatile属性。除了const或volatile修饰之外,type_id和expression的类型是一样的。常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。

3、dynamic_cast

用法:dynamic_cast<type-id>(expression)

该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void*;如果type-id是类指针类型,那么

expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。在类层次间进行上行转化时,

dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。

4、reinterpret_cast

用法:reinterpret_cast<type-id>(expression)

reinterpret_cast是C++里的强制类型转换符。操作符修改了操作数类型,但仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换。

使用注意:

1、static_cast转换的类型有限制,例如不能把int转成指针等。static_cast不能从表达式中去除const、volatile等属性。进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。

2、const_cast只用来修改类型的const或volatile属性。

3、dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。有类型检查,失败的转换将返回空指针(当对指针进行类型转换时)或者抛出异常(当对引用进行类型转换时)。

4、reinterpret_cast不进行任何意义的二进制转换。reinterpret_cast只能在指针之间转换。

上面主要的介绍了C++中的四种强制类型转化,下面我们简单的介绍下如果应用强制类型转化会出现什么问题:

很多人可能认为类型转化只是告诉编译器将一个对象类型视为另外一种对象类型,没有做别的处理,其实不然,考虑下面两种情况:

1.对于基本类型的转化:

int x,y;
...
double d = static_cast<double>(x)*y;

此时对变量x做了强制类型转化,这个在编译器中肯定是做了一定的处理的因为int和double在底层肯定是不同的实现方式。

2.对于继承体系中的类型转化

class Base{...};
class Derived:public Base{...};
Derived d;
Base* b = dynamic_cast<Base*>(&d);

这里使得父类的只能指向了子类的地址,在很多编译器中会在子类指针身上会有一个偏移量,由于在Derived的指针上面取得父类的地址!换句话可以这么认为一个对象可能拥有两个地址,一个是Base*指向的地址,一个是Derive*指向的地址!
       在C++中我们尽量的通过多种方法来避免类型转化的进行,例如通过virtual函数或者在子类中通过Base:function来直接调用父类的函数!还有dynamic_cast类型转化的效率是超级的慢的!
       C++中的类型转化比C/Java/C#等有更多的坑,因此应用的时候一定要慎重,能不用尽量不要用!

请记住:

  • 如果可以尽量的不要使用类型的转化,特别是在注重效率的时候更加要慎重的选择dynamic_cast类型转化符。
  • 如果必须要使用类型转化,尽量的使用C++上面提到的类型转化,这样如果识别出来类型的转化。

条款28:避免返回handle指向对象内部成分

首先handle是指reference、指针和迭代器等都可以称之为handle,因为她们都是用来取得一个对象的,返回一个handle指向对象的内部成分主要会造成下面的两种情况:

1.增大了成员的访问权限,如下例子:

class Point{
public:
Point(int x, int y);
...
void setX(int x);
void setY(int y);
}; struct RectData{
Point ulhc;
Point lrhc;
}; class Rectangle{
public:
Point& upperLefter() const{return pData->ulhc;}
Point& lowerRight() const{return pData->lrhc;}
...
private:
std::str::shared_ptr<RectData> pData;
};

上面定义了三个类,我们在看下面的应用:

Point coord1(0,0);
Point coord2(1,1);
const Rectangle rect(coord1,coord2);
rect.upperLefter().setX(10);

在上面的例子中,rect对象的成员函数是一个const的成员函数,正常情况下是不能修改对象的任何成员的,但是在这里它却修改了对象的成员变量!之所以如此是因为函数upperLefter返回了一个reference的对象,这样通过该reference就可以获得对象内部的成员变量,从而可以对成员进行修改!因此需要注意:成员变量的封装最多只能等于"返回其reference"的函数的访问级别,在例子中;pData是private的权限,当时函数upperLefter却返回了该成员的reference并且它是public的,因此该成员函数的权限是public的。第二,如果const成员函数传出一个reference,后者所指向的数据与该对象相关联,而且该reference的对象是在对象之外,那么这个函数的调用者就可以修改那笔数据!例子防止setX被调用的一个办法就是返回一个const reference!

2

Point coord1(0,0);
Point coord2(1,1);
const Rectangle rect(coord1,coord2);
rect.upperLefter().setX(10); class GUIObject{...}
const Rectangle boundingBox(const GUIObject& obj); GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLefr());

在上面的例子中,pUpperLeft指向了右边表达式产生的临时对象的内部成员point,但是在该条语句执行结束后,后边的的语句产生的临时对象会自动的销毁,此时pUpperLeft就会指向一个无意义的空间!

请记住:

  • 避免返回handle指向对象的内部成员,这样不仅可以增加封装性并且可以防止出现"悬吊号码牌"的情况

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

在程序中抛出异常的时候,带有异常安全的函数会保证:
1.不泄露任何的资源
2.不允许数据被破坏
       对于第一种情况可以采用资源管理类的方法来保证资源的释放,我们的函数都要保证异常发生时函数的保证:
1.基本承诺,函数发生异常时,对象还能保持正常的状况,但是具体处于那种状态是不确定的
2.强烈保证,函数发生异常时,如果函数成功则完全成功,如果失败则返回到原来的状态!
3.不抛出任何的异常,这种情况是最理想的但是几乎是不可能的!

请记住:

  • 异常安全函数即使发生异常也不会出现资源泄露或者数据的破坏
  • “强烈的保护”可以通过copy-and-swap来实现,但是并不是所有的函数都可以采用强烈保护的情况
  • 函数的异常安全的级别通常是等于其所调用的所有函数的异常安全级别的最低者!

条款30:透彻了解inline函数的里里外外

inline函数是直接将函数调用用函数的本体来替换,这样就免去了函数调用时候的消耗,但是由于每次对于函数的调用都会用函数的本体来替换,因此过度的应用inline函数会对程序的代码造成膨胀,会造成指令缓冲的命中率,因此我们在应用inline函数的时候也一定要把握好度!

看下面的例子:

class Base{
public:
Base(){...}
~Base(){...}
...
private:
string strp;
string strq;
};
class Derive:public Base{
public:
Derive():Base(){...}
~Derive(){...}
string getp(){
return drivep;
}
...
public:
string drivep
string driveq;
};

在例子中我们对于getp成员函数在类定义的时候定义的,此时函数就是隐式的inline函数,该函数比较短小,适合作为inline函数,同样的对于Drive类的构造函数中,虽然我们具体的实现代码比较小,但是对于类的构造函数和析构函数,编译器都替我们做了很多的事情,在这里我们能够看到在Drived的构造函数中会包含Base的构造函数,还有其成员变量的构造函数,包括父类的,在这里就是四个string的构造函数,此外在构造函数中,还包含一些异常判断的代码,这样其实看起来基本没有代码的构造函数其实编译器给实现了很多内部的操作,这样构造函数其实就不适合作为inline函数了,对于析构函数也是一样的,因此是否将函数设置为inline函数需要慎重的考虑的!此外由于inline函数是直接对程序对函数的调用进行替换的,因此不适合程序的调试,不过编译器可以对inline后的代码进行代码的优化!

请记住:

  • 请保证inline函数只用在代码比较少,调用频繁的函数上,这样就能保证程序的优化,还能不至于造成调试的困难,也不会造成代码的膨胀。
  • 不要只因为function templates出现在头文件,就将它们声明为inline。

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

在C++中我们要将接口与实现分离开来,这样只要接口没有改变,对接口应用的客户就可以保持不变,例如:

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

在上面的例子中,如果对类Person的实现细节做了修改,则在在编译的时候所有应用到接口的客户都要重新的编译,这样效率是很低的,并且我们编译的时候通常上面都是包含#include "date.h",#define "address.h"等这样无疑就在这些文件中形成了很强的依赖关系,我们可以将Person的实现与接口分离开来,如下:

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 birthday() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
};

其中类PersonImpl是Person类的具体细节的实现类:

class PersonImpl;
class Date;
class Address;
class PersonImpl{
public:
PersonImpl(const std::string& name, const Date& birthday,
const Address& addr);
std::string name() const;
std::string birthday() const;
std::string address() const;
...
private:
std::string theName;
Date theBirthday;
Address theAddress;
};

其中:

Person::Person(const std::string& name, const Date& birthday, const Address& addr):pImpl(new PersonImpl(name,birthday,addr)){
}
std::string Person::name() const{
pImpl->name(); }

对应的Person类的接口可以通过PersonImpl的接口来实现!在上面中我们将接口实现与接口的具体的细节分离成具体的两个独立的类,在Person类中,对于Date/Address等类型我们不必在包含其类的具体实现,我们只需在前面做前置声明即可这种情况适合于文件中对该类的应用是通过指针来进行的或者该类作为函数的形式参数!在程序中我们如果使用object reference或者object pointers可以完成的任务,就不要使用object,这样我们就可以通过一个类的声明式就可以定义出指向该类型的reference和points,但是如果定义某类型的object就需要用到该类型的定义式。还有如果能够尽量的用class声明式来替换class定义式!我们可以将为声明式和定义式提供不同的头文件,例如#include "datefwd.h" 注意命名的方式。

对于接口和实现的分离我们还可以用过abstract class的形式来实现:

class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthdayDate() const = 0;
virtual std::string address() const = 0;
...
static std::tr1::shared_ptr<Person> create(const std::string& name, const Date birthday,
const Address& addr);
};

上面的Person类只是个抽象类,我们应用的时候必须实现一个具体的类才可以,因为抽象的类是不能生成对象的:

class RealPerson : public Person{
public:
RealPerson(const std::string& name, cosnt Date& birthday,
const Address& addr):theName(name),theBirthdayDate(birthday),theAddress(addr)
{}
virtual ~RealPerson(){}
std::string name() const;
std::string birthday const;
std::string address() const;
private:
std::string theName;
Date theBirthdayDate;
Address theAddress;
};

这样就可以在Person类中添加一个生成对象的接口,由于Person是抽象类是不能生成对象,因此我们这个接口必须是static的成员:

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));
}

此外我们通过该Person抽象类我们还可以创建其他的对象!这样也可以实现类的接口与具体实现的分离!

请记住:

  • 支持"编译依赖性最小"的一般构想是:相依于声明式,不要相依于定义式。
  • 程序库头文件应该以"完全且仅有声明式"的形式存在,这种做法不论是否涉及template都适合。
上一篇:gdb调试嵌入式环境搭建


下一篇:【cs229-Lecture10】特征选择