条款27-31

本文介绍了C++编程中的一些最佳实践,包括减少转型操作、避免返回对象内部引用、确保异常安全性、合理使用inline函数以及最小化文件间编译依赖等。这些实践有助于提高代码质量、维护性和性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条款27:尽量少做转型动作
class window{
public:
virtual void onResize(){...};
...
};
class specialWindow:public window{
public:
virtual void onResize()
{
static_cast<window>(*this).onResize();
}
};

        上述代码并非在当前对象身上调用window::onResize之后又在该对象身上执行specialwindow专属动作。它是在当前对象之base class成分的副本上调用window::onResize,然后在当前对象身上执行specialwindow专属动作。如果window::onResize修改了对象内容,当前对象其实没有改动,改动的是副本。

        dynamic_cast许多实现版本执行速度相当缓慢。之所以需要dynamic_cast,通常是因为你想在某个认定为derived class对象身上执行derived class操作函数,但手上只有一个指向base class的指针。通常这种情况可以通过在base class中提供virtual函数解决

请记住:

        如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。如果有个设计需要转型动作,试着发展无需转型的替代设计

        如果转型是必要的,试着将它们隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放入自己的代码中

        宁使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨别,而且也有着分门别类的执掌


条款28:避免返回handle指向对象内部成分
class Point{
public:
Point(int x,int y);
...
void setX(int newvalue);
void setY(int newvalue);
...
};
struct RectData{
Point ulhc;      //upper left-hand corner
Point lrhc;      //lower right-hand corner
};
class Rectangel{
...
private:
std::tr1::shared_ptr<RectData> pData;
};
class Rectangle{
public:
...
Point& upperLeft()const {return pData->ulhc;}
Point& lowerRight()const {return pData->lrhc;}
...
};

        以上设计可以通过编译,但却是错误的。一方面将函数声明为const成员函数,是为了返回相关坐标点而不改变它,另一方面却返回了reference指向private内部数据成员。我们可以通过其返回值改变数据。

        由此,我们知道:成员变量的封装性最多只等于返回其reference的函数的访问级别,其次,如果const成员函数传递出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据

        还有可能导致dangling(空悬)handles:这种handles所指东西不存在。

class GUIObject{...};
const Rectangle boundingBox(const GUIObject &obj);
GUIObject *pob;
const Point *pUpperleft=&(boundingBox(*pob).Uppertleft());

        对boundingBox的调用获得一个新的、临时的Rectangle对象。语句结束前,pUpperleft指向临时对象temp内部的point部分。但是在语句结束后,temp被销毁,最终pUpperleft指向一个不存在的对象。若其返回值不是reference而是by value,编译器不会通过编译

请记住:

        避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生dangling handle的可能性降到最低。


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

        当异常被抛出时,具有异常安全性的函数:不泄露任何资源、不允许数据败坏。对于资源泄露可以使用智能指针管理,我们就能专注于数据败坏问题。

        异常安全函数提供三个保证之一:

基本承诺:如果异常被抛出,程序内任何事物仍保持在有效状态下。没有任何数据结构或者对象会因此被破坏,所有对象都处于一种前后一致的状态。然而程序的现实状态却不可预料,只要那个状态合法。

强烈保证:如果异常被抛出,程序状态不改变。程序状态只有两种:如预期到达函数成功执行后的状态,或回到函数被调用之前的状态。

不抛出(throw)保证:承诺函数绝不抛出异常,总能完成预期功能。

        异常安全码必须能提供以上三种保证之一,否则它就不具备异常安全性。对大部分函数而言,抉择往往落在基本保证和强烈保证之间。

        有个一般化的方法往往会导出强烈保证,那就是copy and swap。原则很简单:为你打算修改的对象(原件)做出一个备份,然后在备份身上做出必要的改变,若修改动作抛出异常,原对象任保持未改变状态。待所有改变完成后再将修改的副本和原对象置换。

        copy and swap一般而言并不能保证整个函数具有异常安全性。例如someFunc使用copy and swap策略,但函数还包括另外两个函数f1和f2的调用:

void someFunc()
{
...      //对local状态做一个副本
f1();
f2();
...      //将修改之后的状态置换
}

        如果f1和f2都是强烈异常安全,情况并不会就此好转。毕竟如果f1圆满结束,程序状态在任何方面都可能有所改变,因此如果f2随后抛出异常,程序状态和someFunc被调用前并不相同。在强烈保证不切实际时,你就必须提供基本保证。

请记住:

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

        强烈保证往往能够以copy and swap实现出来,但强烈保证并非对所有函数都可实现或具备现实意义

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


条款30:彻底了解inline的里里外外

        inline函数会增加目标代码。inline函数只是对编译器的一个申请,不是强制命令。可以隐喻提出,也可以显示提出。隐喻的方式是将函数定义于class定义式内,friend函数也可以定义于class内,如果如此,它们也是被隐喻声明为inline。

        inline函数通常置于头文件内,因为编译过程中进行inllining,将函数调用替换为被调用函数的本体,因此编译器必须知道函数的样子。template通常也置于头文件中,因为一旦它被使用,编译器为了将它具现化,就需要知道它的具体形式。但是template的具现化和inlining无关。根据现实情况决定是否将template声明为inline。

        inline是个申请,编译器可以加易忽略:例如,所有对于virtual函数的inline声明都将被忽略。virtual意味着等待,直到运行期间才决定调用哪个函数,而inline意味着执行前现将调用函数动作替换为函数本体,这是自相矛盾的。

        如果函数要取某个inline函数的地址,编译器通常必须为此函数生成一个outline函数本体。inline函数的调用有可能被inlined也有,可能不被inlined,取决于调用方式:

inline void f(){...};
void (*pf)()=f;
...
f();                     //这个调用将被inlined
pf();                    //这给调用通过指针将不被inlined

        构造和析构函数通常是inlining函数糟糕的候选人,编译器会自动生成构造和析构函数的outline副本。

        inline函数不能随着程序库的升级而升级,即f是一个inline函数,将f函数本体编进程序中,一旦改变f,所有用到f的客户端程序都将重新编译。除此以外,大部分调试器,对饮里呢函数都束手无策,毕竟不能在一个并不存在的函数内设置断点。

请记住:

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

        不要因为function template出现在头文件中就将它们声明为inline


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

        如果我们修改某个class实现,而不是接口,然后当我们重新建置这个程序时,所有东西都被重新编译和连接了。这个问题主要是由于C++并没有把接口从实现中分离

        为什么C++坚持将class的实现细目至于class定义式中,为什么不能将实现细目分开叙述?这主要是由于编译器必须在编译期间直到对象的大小。编译器看到某个类型的定义式就必须知道它分配多少内存。

        解决办法之一就是将对象的实现细目隐藏于一个指针背后

class PersonImpl;      //Person实现类的前置声明
class Data;
class Address;
class Person{
public:
Person(const std::string &name,const Data &birthday,const Address &addr);
std::string name()const;
std::string birthData()const;
std::string address()const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl;
};

        这样的设计之下,Person的客户就与Data、Address以及Person的实现细目分开了。那些class的实现修改就不需要Person客户端重新编译。这才是真正的接口与实现分离,这个分离的关键在于声明的依存性替换定义的依存性,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其它文件声明的声明式(而非定义式)相依。下列每一件事都源于这个策略:

  • 如果使用object reference或object pointer可以完成任务,就不要使用object。object reference或object pointer的使用只需要声明,但object的使用需要定义式
  • 如果能够尽量以class声明式替换class定义式。当声明一个函数而用到一个class时,并不需要class的定义

class Data;
Data today();
void clearAppointments(Data d);

        当调用函数时,才会需要Data的定义式。你可能会觉得既然不调用为何要声明函数呢?并非是不调用,而是并非每个都调用:假如有一个函数库内含有数百个函数声明,可以将提供class定义的义务从函数声明的头文件转移到内含函数调用的客户文件中,便可以将非真正必要的类型定义与客户端之间的编译依存性去除掉。

  • 为声明式和定义式提供不同的头文件。两个头文件一个用于声明,一个用于定义。程序库客户因总是#include一个声明文件而非前置声明若干函数

        这种解决办法中Person被称为Handle class(句柄类)。

        另一种制作句柄类的方法是令Person成为一种特殊的abstract base class(虚基类),成为interface class。这种class的目的是详细描述derived 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来撰写程序,因为它不可能针对内含pure virtual函数的Person class具现出实体。就像Handle class的客户一样,除非interface class的接口被修改否则其客户不需重新编译

        interface class的客户必须有办法为这种class创建新对象。通常调用一个特殊函数,此函数扮演真正被具现化的那个derived class的构造函数角色。这样的函数常被称为factory(工厂)函数或virtual构造函数。它们返回指针,指向动态分配所得对象,而该对象支持interface class的接口。这样的函数往往在interface class内声明为static

class Person{
public:
...
static std::tr1::shared_ptr<Person> creat(const std::string &name,const Date &birtday,const Address &addr);
...
};
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;
};
std::tr1::shared_ptr<Person> Person::creat(const std::string &name,const Date &birthday,const Address &addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr));
}

        这是实现interface的常见机制之一:从interface class(Person)继承接口规则,然后实现出接口所覆盖函数。interface的第二个实现方法涉及多重继承,那是条款40所探讨的主题。

        handle class和interface class解除了接口和实现之间的耦合关系从而降低文件的编译依存性。

        handle class必须通过implementation pointer取得对象数据,每一次访问增加一层间接性,没一个对象消耗的内存必须增加一个implementation pointer的大小。最后implementation pointer必须初始化,指向一个动态分配得来的implementation object,所以将蒙受动态内存分配带来的额外开销。

        interface class,由于每个函数都是virtual,所以每次调用都付出一个间接跳跃成本,对象还必须含有一个vptr(见条款7),肯能会增加内存。

请记住:

        支持编译依存性最小化的一般构想是:相依与声明,不要相依与定义。基于此构想的两个手段是handle class和interface class

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值