【Effective C++】实现

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

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

1、变量定义时间点的问题

  在程序中定义一个变量,当控制流(control flow)到达这个变量定义时,程序就要承受变量的构造成本,当控制流离开这个作用域时,程序也要承受析构成本。无论这个变量是否使用,都要承受这些成本。应该尽量避免这种情形。或许你认为自己不会这样使用,但也未必。例如要写一个加密函数,但加密的密码要足够长。如果密码太短,会抛出一个异常logic_error。

std::string encryptPassword(const std::string& psaaword)
{
    using namespace std;
    string encrypted;
    if(password.length()<MinimumPasswordLength)
    {
        throw logic_error("Password is too short");
    }
    ……//加密密码,把加密结果放到encrypted内
    return encrypted;
}

  如果这个函数抛出异常,那么变量encrypted即便是未使用,也会执行构造函数和析构函数。

  那么循环时怎么做呢?把变量定义在循环外还是循环内?

Widget w;//定义在循环外
for(int i=0;i < n;++i)
    w=……;
    ……
}
for(int i=0;i<n;++i){
    Widget w(……);//定义并赋值
    ……
}
代码A:1个构造函数+1个析构函数+n个赋值操作
代码B:n个构造函数+n个析构函数

2、请记住

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

二、尽量少做转型动作

1、C++四种转型动作

  • static_cast(expression):用来强迫隐式转换,例如将non-const对象转为const对象,几乎旧式转型的操作都可以通过static_cast来替换实现。但它无法将const转为non-const——这个只有const_cast才办得到。

  • const_cast(expression):通常被用来将对象的常量性移除。它也是唯一有此能力的C+±style转型操作符。

  • dynamic_cast(expression):主要用来执行“安全向下转型”(基类向子类的向下转型(Down Cast)),(“安全向下转型”,一般而言,基类向子类转型为不安全转型,会引发程序异常,使用dynamic_cast,这类转型则会返回一个Null值,消除了异常,实现了“安全”)也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。表达式dynamic_cast<T*>(a) 将a值转换为类型为T的对象指针。如果类型T不是a的某个基类型,该操作将返回一个空指针。

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

为了兼容,旧式的转型仍然合法,但是更提倡用新式的形式。因为:

  • 新式转型很容易被辨识出来,可以很快找到代码中有哪些转型。

  • 新式转型动作的目标愈窄化,编译器愈可能诊断出错误的运用

2、请记住

  • 应该尽量少使用转型,尤其是在注重效率的代码中使用dynamic_cast。如果需要转型,试着设计无需转型代替。

  • 如果必须使用转型,把它隐藏在函数内,客户通过接口使用,而不是由客户来实现转型。

  • 使用新式的C++ style转型来代替旧的转型,因为新式的转型很容易辨识出来,而且它们有分类。


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

1、返回一个“代表对象内部数据”的handle存在的问题

class Point   // 表示点  
{
    public:
        point(int x, int y);
        ...
        void setX(int newVal);
        void setY(int newVal);
        ...
};
struct RectData      // 表示 矩阵
{
    Point ulhc;     // 左上角
    Point lrhc;     // 右下角
};
class Rectangle
{
    public:
        ...
        // 错误,,如果保证安全可以对它们的返回类型加上const
        Point& upperLeft() const { return pData->ulhc; } 
        Point& lowerRight() const { return pData->lrhc; }
        ...
    private:
        std::tr1::shared_ptr<RectData> pData; 
};
// 考虑以下调用
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);
// 下面调用,rec左上角从(0,0)变成(50,0)
rec.upperLeft().setX(50);

【问题1】:

  reference、指针和迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。同时,一如稍早所见,它也可能导致“虽然调用const成员函数却造成对象状态被更改”。任何调用者返一个指针(reference/迭代器)指向一个“访问级别较低”的内部成员,都会导致“降低对象封装性”的风险。

【问题2】:

  返回“代表对象内部”的handles有可能导致dangling handles(空悬的号码牌):handles所指东西(的所属对象)不复存在,有空悬的问题。关键在于:一旦有个handle被传出去,你就要承担“handle比其所指对象寿命更长”的风险。(对象只有语句内的生命周期;或者对象生命周期在语句块,离开语句块对象将被销毁;对诸如此情况的对象返回reference,等同于返回 局部对象的reference,将导致空悬指针。)

2、请记住

  • 避免返回handles(包括reference、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。

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

1、异常安全函数的三个保证

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。

  • 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到”调用函数之前“的状态。

  • 不抛掷(nothrow)保证:承诺绝不抛出异常,因为它们总能完全原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

  有个一般化的设计策略很典型地会导致”强烈保证“,这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)

【总结】:

  提供函数异常安全性,通常需要作如下考虑:以对象管理资源(RAII),那可阻止资源泄漏;挑选三个“异常安全保证”中的某一个实施于你所写的每一个函数身上。

2、请记住

  • 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型

  • “强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义(效率和复杂程度带来的成本)。

  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。


五、透彻了解inline函数的里里外外

1、inline函数的特点

  inline函数,比宏好得多,“免除函数调用成本”;编译器最优化机制能对它(函数本体)执行语境相关最优化。然而,过多热衷inlining会增加目标码大小,造成程序体积太大(对可用空间而言),这进而会导致换页行为,降低指令高速缓存装置的击中率,以及伴随而来的效率损失。

  切记,inline只是对编译器的一个申请(编译器不一定批准),不是强制命令。这项申请可用隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内,明确声明inline函数的做法则是在其定义式前加上关键字inline:

class Person
{
    public:
        ....
        int age() cosnt { return theAge; }      // 一个隐喻的inline申请
        ...
    private:
        int theAge;
};
// 明确声明inline函数的做法则是在其定义式前加上关键字inline。
template<typename T>
inline const T& std::max(const T& a, const T& b)     // 明确申请inline
{ return a < b ? b : a; }

2、inline函数为什么放到头文件

  • inline函数和templates通常都被定义于头文件。这不是巧合。大多数建置环境(build environment)在编译过程中进行inlining,将一个“函数调用”替换为“被调用函数的本体”,在替换时需要知道这个函数长什么样子。Inlining在大多数C++中是编译期行为。

  • Templates通常也放置在头文件,因为它一旦被使用,编译器为了将它具体化,也需要知道它长什么样子。(有些编译环境可以在链接期间才执行template具体化,但是编译期间完成的更常见)。

  大部分编译器拒绝太过复杂的inlining函数(例如有循环或递归)。virtual函数也不能是inline函数,因为virtual函数是直到运行时才确定调用哪个函数,而inline是执行前将调用动作替换为函数本体。

3、什么时候不应该用inline

  • 大部分编译器拒绝将太过复杂(例如带有循环或递归)的函数inlining

  • 所有对virtual函数的调用(除非是最平淡无奇的)也都会使inlining落空。因为virtual意味“等待,直到运行期才确定调用哪个函数”,而inline意味“执行前,先将调用动作替换为被调用函数的本体”。如果编译器不知道该调用哪个函数,则拒绝将函数本体inlining。

  • 有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。例如,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。毕竟编译器哪有能力提出一个指针指向并不存在的函数呢?于此并提的是,编译器通常不对“通过函数指针而进行的调用”实施inlining,这意味对inline函数的调用有可能被inlined,也可能不被inlined。

  • 构造函数和析构函数往往是inlining的糟糕候选人。

4、请记住

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

  • 不要只因为function templates出现在头文件,就将它们声明为inline。


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

1、降低文件编译依存关系

  假设你对C++程序的某个class实现文件做了些轻微修改。注意,修改的不是class接口,而是实现,而且只改private成分。而当你编译的时候,却编译了整个工程,这绝对不可被接受。问题出在C++并没有把“将接口从实现中分离”这事做得很好。Class的定义式不只详细叙述了class接口,还包括十足的实现细目。如:

// 如果没有加入一下头文件,Person无法通过编译
// 因为编译器没有取得其实现代码所用到的classes string, Date 和 Address的定义式
#include <string>
#include "date.h"
#include "address.h"
//
class Person
{
    public:
        Person(const std::string& name, const Date& birthDate, const Address& addr);
        std::string name() const;
        std::string birthDate() const;
        std::string address() const;
        ....
    private:
        std::string theName;      //实现细目
        Date thBirthDate;
        Address theAddress;
};

  #include头文件,这么一来却在Person定义文件和其含入文件之间形成了一种编译依存关系。如果这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入Person class的文件就得重新编译,任何使用Person class的文件也必须重新编译。(其实,现实中有很多的工程项目即是如此,改动头文件便会导致所有关联的文件重新编译,所以在实际中尽量不去改动到头文件)。

【解决方案1】:

  制造Handle class:“将对象实现细目隐藏于一个指针背后”(将对象实现细目抽离到另一个类里面,在原来类中保留一个指向实现细目所在类的指针,也即pimpl)。针对Person:把Person分割为两个classes,一个只提供接口,另一个负责实现该接口:

#include <string>    // 标准程序库组件不该被前置声明
#include <memory>    // 为了tr1::shared_ptr而含入

class PersonImpl;    // Person实现类的前置声明
class Date;
class Address;

class Person
{
    public:
        Person(const std::string& name, const Date& birthDate, const Address& addr);
        std::string name() const;
        std::string birthDate() const;
        std::string address() const;
        ....
    private:
    	 // 智能指针(条款13),指向实现物
        std::tr1::shared_ptr<PersonImpl> pImpl;     
};

  这个分离的关键在于以“声明的依存性”替换“定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。

【解决方案2】:

  令Person成为一种特殊的abstract base class(抽象基类),称为Interface class(将原来类里面的接口抽离到另一个类(抽象基类)里面,包含实现细目的类继承此抽象基类,同时实现其抽象接口.注意,抽象基类里面的接口为static并且virtual,迫使派生类必须自己重新实现这个继承而来的接口.).这种class的目的是详细一一描述derived classes的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口。

class Person
{
    public:
        .....
        virtual ~Person();
        virtual std::string name() const = 0;
        virtual std::string birthDate() const = 0;
        virtual std::string address() const = 0;
        ..... 
         // 返回一个tr1::shared_ptr,指向一个新的Person,并以给定参数初始化
        static std::tr1::shared_ptr<Person>    
        create(const std::string& name,        
               const Date& birthday,
               const Address& addr);
        .....
};

// 如下使用
std::string name;
Date dateOfBirth;
Address address;
...
//创建一个对象,支持Person接口
shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout << pp->name() // 提供Person的接口使用这个对象
              << " was born on " 
              << pp->birthDate() 
              << " and now lives at " 
              << pp->address();

  当然,支持Interface class接口的那个具象类必须被定义出来,而且真正的构造函数必须被调用。一切都在virtual构造函数实现码所在的文件内秘密发生,如下:

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;
        std::string birthDate() const;
        std::string address() 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));  // new 不同的派生类对象,但都用抽象类指向它
}

2、请记住

  • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes 和 Interface classes。

  • 程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。

猜你喜欢

转载自blog.csdn.net/daaikuaichuan/article/details/85253852