Effective C++ 5 实现 条款26-条款31

    科技2022-08-06  103

    Effective C++系列文章:

    Effective C++ 1自己习惯C++ 条款01-条款04 Effective C++ 2 构造/析构/赋值运算 条款5-条款12 Effective C++ 3 资源管理 条款13-条款17 Effective C++ 4 设计与声明 条款18-条款25 Effective C++ 5 实现 条款26-条款31 Effective C++ 6 继承与面向对象设计 条款32-条款40 Effective C++ 7 模板与泛型编程 条款41-条款52 Effective C++ 8 杂项讨论 条款53-条款55

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

    条款27 尽量减少转型动作

    转型动作:

    旧式转型 (T)expression T(expression)新式转型 static_cast class Window { public: Window(const int size) : _size(size) {} virtual ~Window() {} virtual void onResize(const int size) {_size = size;} int GetSize() const {return _size;} private: int _size; }; class GlassWinsow : public Window { public: GlassWinsow(const int size, const int gSize) : Window(size), _gSize(gSize) {} void onResize(const int size, const int gSize) { static_cast<Window>(*this).onResize(size); _gSize = gSize; } int GetGlassSize() const {return _gSize;} private: int _gSize; };

    调用

    GlassWinsow gw(2, 3); cout<<"Window size:"<<gw.GetSize()<<" GlassWindow size:"<<gw.GetGlassSize()<<endl; gw.onResize(4, 5); cout<<"Window size:"<<gw.GetSize()<<" GlassWindow size:"<<gw.GetGlassSize()<<endl;

    结果 Window size:2 GlassWindow size:3 Window size:2 GlassWindow size:5 dynamic_cast dynamic_cast的许多实现版本执行速度相当慢。假如至少有一个很普通的实现版本基于“class名称之字符串比较”,如果你在四层深的单继承体系内的某个对象身上执行dynamic_cast,可能会耗用多达四次的strcmp调用,用以比较class名称。深度继承或多重继承的成本更高!某些实现版本这样做有其原因(它们必须支持动态链接)。在对注重效率的代码中更应该对dynamic_cast保持机敏猜疑。 之所以需要用dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上只有一个“指向base”的pointer或者reference,你只能靠他们来处理对象。两个一般性做法可以避免这个问题: ①使用容器,并在其中存储直接指向derived class对象的指针(通常是智能指针),如此便消除了“通过base class接口处理对象”的需要。假设先前的Window/GlassWindow 继承体系中有GlassWindow 才支持闪烁效果,试着不要这样做: 在类GlassWindow 中添加闪烁效果函数

    void blink() {cout<<"GlassWindows Link\n";}

    这样调用

    typedef std::vector<std::tr1::shared_ptr<Window>> VPW; VPW winPtrs; winPtrs.push_back(std::tr1::shared_ptr<Window>(new GlassWinsow(2, 3))); for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) { if(GlassWinsow* psw = dynamic_cast<GlassWinsow*>(iter->get())) psw->blink(); }

    我们应该改成下面的调用方式

    typedef std::vector<std::tr1::shared_ptr<GlassWinsow>> VPSW; VPSW winPtrs; winPtrs.push_back(std::tr1::shared_ptr<GlassWinsow>(new GlassWinsow(2, 3))); for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) { (*iter)->blink(); } 这种做法无法在同一个容器内存储指针“指向所有可能之各种Window派生类”。在Window上继承,那么就要定义多个类型的容器

    ②通过base class接口处理“所有可能之各种window派生类”,那就是在base class 里提供virtual函数做你想对各个Window派生类做的事。 修改后代码

    class Window { public: Window(const int size) : _size(size) {} virtual ~Window() {} virtual void onResize(const int size) {_size = size;} int GetSize() const {return _size;} virtual void blink() {} private: int _size; }; class GlassWinsow : public Window { public: GlassWinsow(const int size, const int gSize) : Window(size), _gSize(gSize) {} void onResize(const int size, const int gSize) { Window::onResize(size); _gSize = gSize; } int GetGlassSize() const {return _gSize;} void blink() {cout<<"GlassWindows Link\n";} private: int _gSize; }; class WoodWindow : public Window { public: WoodWindow(const int size, const int gSize) : Window(size), _gSize(gSize) {} void onResize(const int size, const int gSize) { Window::onResize(size); _gSize = gSize; } int GetGlassSize() const {return _gSize;} void blink() {cout<<"WoodWindow Link\n";} private: int _gSize; };

    调用

    typedef std::vector<std::tr1::shared_ptr<Window>> VPSW; VPSW winPtrs; winPtrs.push_back(std::tr1::shared_ptr<Window>(new GlassWinsow(2, 3))); winPtrs.push_back(std::tr1::shared_ptr<Window>(new WoodWindow(4, 5))); for (VPSW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) { (*iter)->blink(); }

    输出结果: GlassWindows Link WoodWindow Link

    绝对必须拒绝的是所谓的“连串(cascading)dynamic_casts,这样导致代码又大又慢

    感觉说得不清楚,下边的内容来自Primer C++

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

    handles(号牌,用来取得某个对象):包括引用、指针、迭代器

    class Point{ public: Point(int x,int y); void setX(int newVal); void setY(int newVal); }; struct RectData{ Point ulhc;//矩形左上角 Point lrhc;//矩形右下角 }; class Rectangle{ private: std::shared_ptr<RectData> pData;//使用智能指针管理资源 public: Point & upperLeft()const{ //这里可以通过编译,但是却是错误的 //ulhc设置为private,这里却返回的是其引用,private了个寂寞 return pData->ulhc; } Point & lowerRight()const{ return pData->lrhc; } };

    返回一个代表对象内部数据的handle,随之而来的便是降低对象封装性的风险,同时,它也可能导致”虽然调用const成员函数却造成对象状态被更改”。 解决办法:返回类型加个const即可。

    const Point & upperLeft()const{ return pData->ulhc; } const Point & lowerRight()const{ return pData->lrhc; }

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

    当异常抛出时,带有异常安全性的函数会: 不泄露任何资源、不允许数据败坏 没有异常安全性的函数:

    class PrettyMenu{ public: void changeBackground(std::istream & imagSrc);//改变北京图像 private: Mutex mutex;//互斥器 Image * bgImage;//目前的背景图像 int imageChanges;//北京图像被改变的次数 }; void PrettyMenu::changeBackground(std::istream & imagSrc){ lock(&mutx); delete bgImage; ++imageChanges; baImage = new Image(imagSrc); unlock(&mutex); }

    更改保证不会泄露资源:

    void PrettyMenu::changeBackground(std::istream & imagSrc){ Lock ml(&mutex); //来自条款14 delete bgImage; ++imageChanges; baImage = new Image(imagSrc); }

    条款14内容:由一个类专门管理资源构造与释放。

    void lock(Mutex * pm);//锁定pm所指的互斥器 void unlock(Mutex * pm);//将互斥器接触锁定 class Lock{ public: explicit Lock(Mutex * pm):mutexPtr(pm){ lock(mutexPtr);//获取资源 } ~Lock(){ unLock(mutexPtr);//释放资源 } private: Mutex * mutexPtr; }

    保证不允许数据损坏,将资源管理交给智能指针处理

    class PrettyMenu{ ... std::shared_ptr<Image> bgImage; ... }; void PrettyMenu::changeBackground(std::istream & imagSrc){ Lock ml(&mutex); bgImage.reset(new Image(imagSrc));//以“new Image"的执行结果设定bgImage内部指针 ++imageChanges;//新图片更换之后再对数量进行修改 }

    典型做法(推荐):copy and swap 为打算修改的对象(原件)做一个副本,然后再那副本上做一切必要修改,若有任何修改动作抛出异常,原对象仍未改变状态。待所有改变都成功之后,再将修改过的那个副本和原对象再一个不跑出异常的操作中置换。 实现上通常是将所有“隶属于对象的数据”从原对象放进另一个对象内,然后赋予原对象一个指针,指向那个所谓的实现对象(即副本)。对于PrettyMenu而言,典型写法如下:

    struct PMImpl{ std::shared_ptr<Image> bgImage; int imageChanges; }; class PrettyMenu{ public: void changeBackground(std::istream & imagSrc);//改变北京图像 private: Mutex mutex;//互斥器 std::shared_ptr<PMImpl> pImpl; }; void PrettyMenu::changeBackground(std::istream & imagSrc){ using std::swap;//见条款25 Lock ml(&mutex); //先构建一个pNew去踩坑,踩完坑之后交换 std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));//拷贝构造一个 pNew->bgImage.reset(new Image(imagSrc));//对temp对象进行修改 ++pNew->imageChanges; swap(pImpl,pNew);//交换之 }

    条款30 透彻了解inlining的里里外外

    inline内联函数的整体观念是:“对此函数的每一个调用”都以函数样本替换之,这样做可能会增加你的目标码大小,即使有虚内存,inline造成的代码膨胀可能导致额外的换页行为,降低指令高速缓存装置的命中率,随之伴来效率的损失; 另一方面说,如果Inline函数䣌本体很小,编译器对函数本体做出的码可能比针对函数调用产生的码更小,这样可能导致较小的目标码和较高的指令高速缓存命中率。 一、 inline只是对编译器的一个申请,不是强制命令,编译器可以忽略 编译器拒绝将太复杂的函数(如带循环或递归)内联,所有对virtual函数的调用也会使得inlining落空,因为virtual意味着“等待,知道运行期才能确定调用哪个函数”,而inline意味着“执行前,先将调用动作替换为被调用函数的本体”。 成员函数都是内联函数

    隐式内联 class Person{ public: int age() const {return theAge;}//一个隐式inline申请 private: int theAge; }; 显式内联(需要再定义式前加inline关键字) template inline const T & std::max(const T& a, const T& b){//显式申请,std::max之前有inline关键字 return a < b ? b : a; }

    inline函数通常被置于头文件中,大多编译环境在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器需要知道那个函数长什么样子。某些编译器在连接器完成inlning,某些竟在运行期完成。 总结:一个表面是inline的函数是否真是inline,取决于你的编译器环境

    二、 将构造、析构函数设为inline是个坏主意 我们看到的:

    class Base{ public: ... private: std::string bm1,bm2; }; class Derrived:public Base{ private: std::string dm1,dm2,dm3; public: Derrived(){};//构造函数看似是空的 ... };

    实际上: 三、谨慎评估inline函数带来的冲击 1. inline函数无法随着程序库的升级而升级 2. 对inline函数调试困难 无法对并不存在的函数设立断点

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

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

    上面代码中用到的classes string,Data,Address的定义式通常由#include知识符提供,所以Person定义文件最上方肯能存在下边的内容:

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

    这造成了编译依存关系,头文件中一个改变,Person class的我呢见得重新编译,因为需要知道Person对象有多大,这三个头文件改变,三个成员变量得大小就可能改变,Person class需要重新编译。 解决方法:将三个成员变量定义一个指针指向之

    #include <string> #include <memory> 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 birthDate() const; std::string address() const; ... private: std::shared_ptr<PersonImpl> pImpl;//指针,指向实现物 };

    这样的设计之下,Person的客户就完全与Dates,Addresses,Persons的实现细目分离了,那些classes的任何实现修改都不需要Person客户端重新编译。 这个分离的关键在于以“声明的依存性”替换“定义的依存性”,这正是编译依存性最小化的本质:让头文件尽可能自我满足,万一不满足,就让它与其他文件内的声明式(而非定义式)相依。 设计策略:

    如果使用 类应用或类指针可以完成任务,就不要使用类:可以只靠一个类型声明式就定义出指向该类型的指针或者引用。如果可以,尽量以class声明式替换class定义式:声明一个函数而要用到某个类时,你并不要该类的定义,纵使函数以值传参也没问题: class Date;//class 声明式 Date today();//没问题,这里并不需要Date的定义式 void clearAppointments(Date d);//同上

    一旦任何人调用这些函数(today()和clearAppointments()),调用之前Date定义式一定要先曝光才行。 3. 为声明式和定义式提供不同的头文件

    #include "datefwd.h"//这个头文件内声明(但未定义)class Date Date today();//同前 void clearAppointments(Date d); #include "Person.h"//包含Person的定义式头文件 #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(); }

    这个还是不懂 唉 还是得继续看看

    Processed: 0.018, SQL: 8