优化程序与设计——来源于《effective C++》

 

目录

一、让自己习惯C++

条款1:视C++为一个语言联邦

条款2:尽量以const、enum、inline替换#define

条款3:尽可能使用const

条款4:确定对象被使用前已先被初始化

二、构造、析构、赋值运算     

条款5:C++会默认生成构造函数、拷贝构造函数、拷贝操作符、析构函数

条款6:为多态类声明virtual析构函数

条款7:绝不在构造和析构函数过程中调用virtual函数

条款8:令operator= 返回一个reference to *this;在operator= 中处理“自我赋值”

条款9:复制对象时勿忘其每一个成分

三、资源管理

条款10:以对象管理资源

条款11:在资源管理中小心copying行为

条款12:在资源管理类中提供对原始资源的访问

条款13:成对使用new和delete时要采用相同形式

四、设计与声明

条款14:以pass-by-reference-to-const替换pass-by-value

条款15:将成员变量声明为private

条款16:宁愿用non-member、non-friend替换member函数

条款17:若所有参数都需类型转换,请采用non-member函数

五、实现

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

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

条款20:为异常安全而努力是值得的

条款21:了解inline函数

条款22:将文件间的编译依存关系降至最低

六、继承与面向对象设计

条款23:确定你的public继承是is-a关系

条款24:避免遮掩继承而来的名称

条款25:区分接口继承和实现继承

条款26:绝不重新定义继承而来的non-virtual函数

条款27:绝不重新定义继承而来的缺省参数值

条款28:明智使用private继承

条款29:明智使用多重继承


一、让自己习惯C++

条款1:视C++为一个语言联邦

       C++是个多重范型编程语言,同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式。它主要有四个次语言。

       1.C:C++仍以C为基础。区块、语句、预处理、内置数据类型、数组、指针都来自于C。

       2.object-oriented  C++:这部分是C with classes所追求的。构造与析构、封装、继承、多态、虚函数。

       3.template  C++:C++的泛型编程,带来了模板元编程(TMP)。

       4.STL:是个template程序库。

 

条款2:尽量以const、enum、inline替换#define

       因为#define在编译前就处理完,所以在获得错误信息时,编译器只告诉某个常量有错误而不告诉常量的变量名。这样导致调试bug时比较麻烦。以下几种方法都可以替换宏:

        1.用常量替换非指针内置类型:const   double   pi  =   3.14;

        2.用指向常量的常量指针替换char*类型:const   char*  const   authorName   =   "Scott    Meyers";

                  或者用string替换char*类型:const    std::string   authorName("Scott    Meyers");

        3.用静态成员常量替换class专属常量:

class  GamePlayer {
private :
          static  const  int  NumTurns  =  5;
          int  scores[NumTurns];     };

                或者用枚举类型:

class  GamePlayer {
private :
          enum  {  NumTurns  =  5; };
          int  scores[NumTurns];   };

        4.用inline函数替换形似函数的宏(macros)

 

条款3:尽可能使用const

       const允许指定一个语义约束,而编译器会强制实施这项语义约束。

       const写在类型前和写在类型后没区别:const   Widget*   pw和Widget  const*  pw意义相同。

       若是成员函数的功能不会改变成员变量,则在函数结尾加上const(bitwise   constness)。但若是想修改某个成员变量,则在该变量前加上mutable(logical  constness)。

 

条款4:确定对象被使用前已先被初始化

        C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前,所以在构造函数内部的动作叫做赋值而不是初始化。初始化效率比赋值高。但如果多个初始化函数中,某些成员变量动作相同,那么可以把它们放到某个private函数中,对它们进行赋值动作,从而减少重复操作。

 

二、构造、析构、赋值运算     

条款5:C++会默认生成构造函数、拷贝构造函数、拷贝操作符、析构函数

       这些函数都是public和inline的。如果不想使用默认生成的函数,可以在private里声明函数,但不定义它。这样外面成员无法调用,内部成员也无法使用。

 

条款6:为多态类声明virtual析构函数

 

条款7:绝不在构造和析构函数过程中调用virtual函数

        基类的构造和析构函数要早于继承类。

 

条款8:令operator= 返回一个reference to *this;在operator= 中处理“自我赋值”

 

条款9:复制对象时勿忘其每一个成分

 

三、资源管理

        一旦用了资源,将来必须还给系统。C++程序中最常见的资源是动态分配内存。其他还有:文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、网络sockets。

条款10:以对象管理资源

        管理对象运用析构函数确保资源被释放。

 

条款11:在资源管理中小心copying行为

        比如互斥锁,C API函数处理该对象有lock和unlock两个函数可用:

void  lock(Mutex*  pm);
void  unlock(Mutex*  pm);

 为确保不忘记一个被锁住的Mutex解锁,可以建立一个class来管理锁:

class  Lock
{
public:
   explicit  Lock(Mutex*  pm)
           : mutexPtr(pm)
           {lock(mutexPtr);}
   ~Lock(){unlock(mutexPtr);}
private:
   Mutex* mutexPtr;
}

       但若lock对象有可能被复制,所以要么禁止复制,要么计算资源的被引用次数。

 

条款12:在资源管理类中提供对原始资源的访问

       许多API函数的参数需要提供资源,比如条款11的互斥锁。函数可以提供显示转换和隐式转换。

显示转换:

//定义
class  Font
{
public:
    ...
    FontHandle  get()  const {return  f;}
    ...
private:
    FontHandle  f;
}

//调用
Font f;
FontHandle f1 = f.get();

隐式转换:

//定义
class  Font
{
public:
    ...
    operator FontHandle()  const {return  f;}
    ...
private:
    FontHandle  f;
}

//调用
Font f;
FontHandle f1 = f;

条款13:成对使用new和delete时要采用相同形式

std::string*  stringPtr1 = new std::string;
std::string*  stringPtr12 = new std::string[100];

delete stringPtr1;
delete[] stringPtr2;

 

四、设计与声明

条款14:以pass-by-reference-to-const替换pass-by-value

        缺省情况下,C++以by value方式传递对象至函数,这是以实际实参的复件为初值,调用对象的copy构造函数产出。by value方式传递一次的成本至少是一次构造函数和析构函数,若是继承类则可能是多次构造函数和继承函数。这使得by value方式是费时操作。

       但对于内置类型(如int)和STL,选择pass-by-value则比较好。

 

条款15:将成员变量声明为private

        private变量封装性更好。

 

条款16:宁愿用non-member、non-friend替换member函数

        如果某些东西被封装,它就不再可见。愈多东西被封装,愈少人看到它,就有愈大得到弹性改变它。如果一个member函数和non-member、non-friend提供相同机能,那么较大封装性的是后者。因为它并不增加“能够访问class内private变量”的函数数量。但为了它可以访问某类,可以将它放入命名空间中:

namespace WebBrowserStuff {
    class  WebBrowser { ... };
    void  clearBrowser{WebBrowser& wb};
    ...
}

        namepsace和class不同,前者可以跨越多个源码文件而后者不能。这些函数放在多个头文件但属于同一个namespace,意味着用户可以轻松扩展这些函数:通过添加更多non-member、non-friend函数到此命名空间中。这是class定义式对用户而言不能扩展的。

 

条款17:若所有参数都需类型转换,请采用non-member函数

        之前提到class应避免隐式转换,但也有例外,比如建立数值类型。比如原本有理数相乘的成员函数写法如下:

 const   Rational  operator*  (const  Rational&  rhs)   const;

那么最正确的写法应该是:

Rational  oneEighth(1, 8);
Rational  oneHalf(1, 2);
Rational  result = oneHalf * oneEighth;
result = result * oneEighth;

       但若是result = result * 2就会出现错误。因为2不是Rational类型,需要进行隐式转换:

const Rational temp(2);
result = oneHalf * temp;

所以这时构造函数应该是non-explicit类型的。

       但若是result = 2 * result又不行了。this对象不能进行隐式转换。这时还需要将operator操作符重载为non-member函数,就可以对两个参数都进行隐式转换:

const Rational  operator*  (const Ratioanl& lhs, const Rational& rhs)
{
    return Ratinal(lhs.numerator() * rhs.numerator(),
                   lshs.denominator() * rhs.denominator() );
}

 

五、实现

        太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢;返回对象handles可能会破坏封装并留给客户dangling handles;未考虑异常带来的冲击可能导致资源泄露和数据破坏。过度inlining可能引起代码膨胀,过度耦合可能导致冗长build tmes。

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

        

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

        

条款20:为异常安全而努力是值得的

        对于下面的代码,从“异常安全性”观点来看,这个函数很糟。

void change()
{
    lock(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
    unlock(&mutex);
}

        当异常抛出时,带有异常安全性的函数会:

         1.不泄露任何资源:一旦new Iamge导致异常,unlock的调用就不会执行。

         2.不允许数据败坏:一旦new Iamge导致异常,imageChanges累加是不应该的。

        异常安全函数提供以下三种保证之一:
        1.基本承诺:如果异常被抛出,程序内任何事物仍然保持有效状态;

        2.强烈保证:如果异常被抛出,程序状态不改变。即函数调用失败,程序会回复到调用函数之前的状态。

        3.不抛掷保证:承诺绝不抛出异常。

        “强烈保证”可使用copy-swap策略:先对修改的对象做出一份副本,然后在副本上做出修改。如果有异常则抛出,原对象仍保持原有状态。但这种策略耗费时间和空间成本,具体使用视情况而定。

 

条款21:了解inline函数

       调用inline无需承受函数调用所招致的额外开销,但过度使用会造成程序体积太大,导致额外的换页行为,降低指令高速缓存装置的击中率。

       inline只是个申请,不是强制命令。申请可以隐式提出,也可以显式提出。隐式方式是将函数定义在class定义式内:

class  Person {
public:
   ...
   int age() const { return theAge; }
   ...
}

显式是使用inline命令。

 

条款22:将文件间的编译依存关系降至最低

#include <string>
#include "date.h"
#include "address.h"

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::string theName;   //实现细目
   Date theBirthDate;   //实现细目
   Address theAddress;   //实现细目
}

      若是date和address头文件有任何改变,那么重新编译时person文件也需要重新编译。这时可以用class的声明替换class的定义:

#include <string>    //标准程序库组件不该被前置声明
#include <memory>    

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;   //指向实现类的指针,属于pimpl idiom设计
}

       这样Person的客户就完全与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();
}

如果可以使用对象引用或对象指针完成的操作,就不要直接使用对象。因为只靠一个类型声明就可以定义指向该类型的引用或指针,而定义某类型的对象需要该类型的定义式。

 

六、继承与面向对象设计

条款23:确定你的public继承是is-a关系

        public继承意味着“is-a”,适用于base classes身上的每一件事情一定适用于derived classes。因为每一个derived class对象也是一个base class对象。

 

条款24:避免遮掩继承而来的名称

       如果在derived class里重新声明了base class里的函数,那么会把base class里的函数覆盖掉(即base class里对应函数不可用)。但如果想让base class里的函数可以和derived class里名称相同的函数同时使用(即在derived class里进行函数重载),那么可以在derived class里使用using命令声明base class里的函数。

       若是derived class是private继承base class,并只想继承base class里的某个函数,可以使用转交函数:

class Derived class : private Base {
public :
   virtual void mf1 ()   //转交函数(forwarding function)
   { Base::mf1(); }      //暗自成为inline函数
   ...
};

 

条款25:区分接口继承和实现继承

       接口继承相当于(纯虚函数和虚函数),实现继承相当于非虚函数。

 

条款26:绝不重新定义继承而来的non-virtual函数

 

条款27:绝不重新定义继承而来的缺省参数值

 

条款28:明智使用private继承

       private继承后,base class中的所有成员在derived class中都会变成private类型。同时,private继承意味着只有实现部分被继承,接口部分应被忽略。

 

条款29:明智使用多重继承

       若是一个类继承多个类,使用基类函数时需要明确指出调用哪个函数。

class  BorrowableItem {
public:
   void checkOut();
};

class  ElectronicGadget {
public:
   void checkOut();
};

class MP3Player:
   public BorrowableItem,
   public ElectronicGadget
{};

MP3Player mp;
mp.BorrowableItem::checkOut();

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值