Effetive C++读书笔记-第5章

5 实现

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

变量应在使用的时候进行构造,并且通过default构造函数构造出一个对象然后对它赋值,比直接在构造时指定初值效率差。

27 尽量少做转型动作

1 四种C++式转型

const_cast:通常被用来将对象的常量性去除。

dynamic_cast:主要用来执行安全向下转型(基类到派生类),也就是用来决定某对象是否归属于继承体系中的某个类型。

reinterpret_cast:仅仅重新解释类型,但没有进行二进制的转换,执行低级转型,执行结果取决于编译器,不可移植,例如将指针转换为int类型。

  • 转换的类型必须是一个指针、引用、算术类型、函数指针或者成员指针。
  • 在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。但不能将非32bit的实例转成指针。
  • 最普通的用途就是在函数指针类型之间进行转换。

static_cast:类似于C风格的强制转换。无条件转换,静态类型转换。没有运行时类型检查来保证转换的安全性。

  • 基类和子类之间转换:其中子类指针转换成父类指针是安全的;但父类指针转换成子类指针是不安全的。
  • 基本数据类型转换。enum, struct, int, char, float等。static_cast不能进行无关类型(如非基类和子类,如double *到int *)指针之间的转换。
  • 把空指针转换成目标类型的空指针。
  • 把任何类型的表达式转换成void类型。
  • static_cast不能去掉类型的const、volitale属性(用const_cast)。

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

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

1 避免返回handles(包括引用,指针,迭代器)指向对象内部,遵守这个条款可增加封装性,对于访问级别是pricate的数据或成员函数来说,用public级别的成员函数返回这个private级别的成员函数并以此修改数据的话,约等于private的函数也是public级别的。

2 如果某个类中的const成员函数传出一个引用,后者所指数据与对象有关联,但是存储在对象之外,那么这个函数的调用者可以修改那笔数据。如果在返回值上面加const可以使用户无法修改数据,但是仍可以传出handles,如果这个handles的生命期长于其所指对象的话,就会造成空悬指针的问题。

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

1 异常安全函数即使发生异常也不会泄露资源或者允许任何数据结构损坏。

2 强烈保证往往可以用先拷贝副本,然后对副本进行更改,再将原件和副本进行更换来保证,当更改出现异常时,不会对原件产生任何影响。

30 透彻了解inlining的里里外外

1 inline函数的观念是:对此函数的每一次调用,都用函数本体去替换。所以内联函数一般放在小型的,被频繁调用的函数身上收益比较大

2 inline函数越多,代码的大小就会越大,这样会造成代码运行时额外的系统换页行为,降低指令高速缓存装置的命中率,造成效率损失。

3 如果inline函数本体很小,编译器针对“函数本体”所产出的码可能比针对“函数调用”所产出的码体积更小。

4 将成员函数和友元函数的定义放在类内是隐含的说其为内联函数。

5 内联函数要在头文件中定义,因为函数的内联过程发生在编译期。模版函数一般也被放在头文件中(编译器具现化被使用的模版函数时,需要知道它长什么样子),但是模版函数不一定是内联函数。

6 虚函数不能是内联函数,因为虚函数要等到运行期间才确定调用什么函数,而内联函数在执行前要将调用动作替换为调用函数的本体。

7 函数指针进行函数调用一般不是内敛函数,因为要取函数的地址,而内联函数没有地址,所以编译器会为其生成一个非内联函数本体,这样从函数指针进行调用的时候,都不会被替换成函数本体。

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

将接口和实现分离的方法是将类中的实现细目(成员变量)和接口函数分开,在接口函数所在的类中声明指针(可以shared_ptr智能指针)指向实现的类,这就是接口与实现分离。

#include <string>   //标准程序库组建不该被前置声明    
class AImpl;    //前置声明

class A{
public:
    A(const string &_a, const string &_b, const string &_c);
    string aa() const;
    string bb() const;
    string cc() const;
    ...
private:
    shared_ptr<AImpl> pImpl;
};

这个分离的关键在于声明的依存性替换定义的依存性,这就是依存性最小化的本质,让头文件尽可能自我满足,若无法满足就让其和其他文件内部的声明式(而不是定义式)相依。

  • 如果使用引用或者指针可以完成任务,就不使用对象,因为仅仅声明式就可以定义指向该类型的引用和指针,但是定义某类型的对象的话,必须要有定义式(否则编译器怎么能知道分配多少空间给这个对象呢?)
  • 如果可以,尽量用类声明式替换类定义式,当声明函数而用到某个类作为参数的时候,按值/指针/引用传递都只需要这个类的声明式即可。(按值传递也可以的原因是,在调用这个函数之前,只要知道了类的定义式就可以了)
  • 为声明式和定义式提供不同的头文件,例如<iosfwd>包含iostream各组件的声明式,其对应定义则分布在不同的头文件内,包括<sstream><streambuf><fstream><iostream>

A这样使用pimpl的类被成分为handle类,这个类具体做些什么呢?往往就是定义和内部指针指向的类一模一样成员函数,这样所有的函数调用都可以完美转交给相应的实现类(就是指针指向的那个类的对象)。

A::A(const string &_a, const string &_b, const string &_c)
    :pImpl(new AImpl(_a, _b, _c)){ }
string A::name() const
{
    return A->aa();
}

还有一种实现handler类功能的方法,就是声明抽象类作为interface类。这种类的作用就是为了描述可能产生的派生类的接口,故类内只有纯虚函数做接口和一个虚析构函数。

interface类的客户必须可以为这种类创建新对象,通常可以调用一个工厂函数(也叫虚拟构造函数),返回的是抽象类的指针(可以是智能指针),但是这实际上是给派生类使用的,函数的内部是派生类的初始化,但是由基类指针指向它。

具体实现见下:

这个头文件就是用来声明接口的,纯粹是声明作用,被需要使用接口的用户所使用。

/**********testInterface.h**********/
#pragma once
#include <memory>
#include <string>
class Person
{
public:
    virtual ~Person(){ }   //虚dtor(必须要实现,否则无法链接)
    virtual std::string name() const = 0;   //纯虚函数做接口
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
    static std::shared_ptr<Person> create(  //工厂函数/虚构造函数
        const std::string &name,
        const std::string &birthday,
        const std::string &addr);
};

很明显抽象类是不能实例化的,所以我们要通过派生来实现实例化,即对外显示为接口的实例化,内部是派生类进行派生,再通过抽象类的工厂函数进行对象指针(智能指针)的生成。

/*********anotherDerived.h*********/
//如果不派生这个类的话,如何真正调用类的构造函数声称对象并且执行这些成员函数呢?
#pragma once
#include "testInterface.h"
class realPerson : public Person    //悄悄派生的类,和虚构造函数实现代码是在一个文件中的,这个代码的改变是不需要进行重新编译的(除非改变接口)
{
public:
    realPerson(const std::string &name,
        const std::string &birthday, const std::string &addr)
        :theName(name), theBirth(birthday), theAddr(addr){ }
    virtual ~realPerson(){ }
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::string theName;
    std::string theBirth;
    std::string theAddr;
};

std::string realPerson::name() const
{
    return theName;
}
std::string realPerson::birthDate() const
{
    return theBirth;
}
std::string realPerson::address() const
{
    return theAddr;
}

//虚构造函数
std::shared_ptr<Person> Person::create(const std::string &name,
        const std::string &birthday,
        const std::string &addr)
{
    return std::shared_ptr<Person>(new realPerson(name, birthday, addr));
}

最后让然用户自行编写的代码了,通过工厂函数和悄悄派生的类形成的真正的构造函数,用户是感觉不出来有什么变化的。

/*********testInterface.cpp*********/
#include "testInterface.h"
#include <iostream>
using namespace std;

int main()
{
    string name = "test";
    string dateOfbirth = "20180509";
    string address = "heu-isrc";
    shared_ptr<Person> pp(Person::create(name, dateOfbirth, address));

    cout << pp->name() << " was born on " 
         << pp->birthDate() << " and now lives at "
         << pp->address() << endl;

    return 0;
}

handle类和interface类都解除了接口和实现之间的耦合关系。但是handle类上是要通过指针访问的,增加了一层间接性;而interface类由于每个函数都是虚函数,所以函数调用的时候指针会在虚函数表之间进行跳跃,并且虚表指针也会增加存储空间。

猜你喜欢

转载自blog.csdn.net/u012630961/article/details/80253087