Effective C++笔记05:实现

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

博客地址:http://blog.csdn.net/cv_ronny 转载请注明出处!

有些对象,你可能过早的定义它,而在代码执行的过程中发生了导常,造成了开始定义的对象并没有被使用,而付出了构造成本来析构成本。

所以我们应该在定义对象时,尽可能的延后,甚至直到非得使用该变量前一刻为止,应该尝试延后这份定义直到能够给它初值实参为止。

这样做的好处是:不仅可以避免构造(析构)非必要对象,还可以避免无意义的default构造行为。

遇到循环怎么办?此时往往我们会有两个选择:

做法A:1个构造函数+1个析构函数+n个赋值操作 // 在循环外面定变量,在循环内赋值

做法B:n个构造函数+n个析构函数   // 在循环内定义并初始化变量

这时候要估计赋值的成本低还是构造+析构的成本低,另外值得考虑的是对象作用域的问题。

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

转型语法通常有三种不同的形式:

1,C风格的转型动作:(T)expression

2,函数风格的转型动作:T(expression)

3,上面的第二种被称为“旧式转型”,C++提供了四种新式转型:

const_cast<T>(expression)  //通常用来将对象的常量性转除

dynamic_cast<T>(expression)  // 转换为子类,用来决定某对象是否归属继承体系中的某个类型,但是耗费重大运行成本

reinterpret_cast<T>(expression) // 执行低级转型,实际动作取决于编译器,这也就表示它不可移植。如将一个point to int 转型为一个int

static_cast<T>(expression)  // 强迫隐式转换

在很多派生类的设计中,派生类的virtual函数需要去调用基类的virtual函数,下面是一个例子,window是一个基类,它定义了一个虚函数onResize,而Special Window是一个派生类。

// 博客地址:http://blog.csdn.net/cv_ronny/ 转载请注明出处!
class Window
{
public:
    virtual void onResize();
};

class SpecialWindow :public Window
{
public:
    virtual void onResize()
    {
        static_cast<Window>(*this).onResize(); // 将*this转型为Window,然后调用其onResize
        // ... SpecialWindow专属动作
    }
};

但是代码中的相通过转型来调用基类的virtual函数,事实上static_cast<Window>(*this).onResize()调用的是一个*this的基类成份的一份拷贝的onResize函数,所以onResize操作所能影响到的成员只属于一个临时对象。解决的办法是把转型拿掉即可,替换成Window::onResize()。

请记住:

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

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

宁可使用C++style(新式)转型,不要使用旧式转型,前者很容易被辨识,而且也比较角着分门别类的职掌。

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

class Point
{
public:
    Point(int x, int y);
    void SetX(int newVal);
    void SetY(int newVal);
private:
    int x_cor;
    int y_cor;
};

struct RectData
{
    Point ulhc;    // 矩形左上角的点
    Point lrhc;    // 矩形右上角的点
};
class Rectangle
{
private:
    shared_ptr<RectData> pData;
public:
    Point& upperLeft()const{ return pData->lrhc; }
    Point& lowerRight()const{ return pData->ulhc; }
};

上面的代吗中Point是表示坐标系中点的类,RectData表示一个矩形的左上角与右下角点的点坐标。Rectangle是一个矩形的类,包含了一个指向RectData的指针。

我们可以看到了uppLeft和lowerRight是两个const成员函数,它们的功能只是想向客户提供两个Rectangle相关的坐标点,而不是让客户修改Rectangle。但是两个函数却都返回了references指向了private内部数据,调用者于是可以通过references更改内部数据。

这给了我们一些警示:成员变量的封装性只等于“返回其reference”的函数的访问级别;如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。

handles(号码牌,用于取得某个对象)指reference、指针和迭代器,它们返回一个“代表对象内部数据”的handle。

我们可以对上面的成员函数返回类型上加上const来解决问题:

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

但是函数返回一个handle代表对象内部成分还总是危险的,因为可能会造成dangling handles(空悬的号牌)。比如某个函数返回GUI对象的外框(bounding box)。

class GUIObject{
    //..
};
const Rectangle boundingBox(const GUIObject&obj);
GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft());

对boundingBox的调用返回的是一个临时对象,这个对象没有名称,随后我们调用了这个对象的upperLeft,返回了一个指向临时对象内部数据的reference。但是在语句结束后,这个临时对象会被销毁,pUpperLeft会变成空悬的、虚吊的(dangling)!

请记住

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

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

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

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

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

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

关于inline:

1. inline函数的调用,是对函数本体的调用,是函数的展开,使用不当会造成代码膨胀。

2. 大多数C++程序的inline函数都放在头文件,inlining发生在编译期。

3. inline函数只代表“函数本体”,并没有“函数实质”,是没有函数地址的。

值得注意的是:

1. 构造函数与析构函数往往不适合inline。因为这两个函数都包含了很多隐式的调用,而这些调用付出的代价是值得考虑的。可能会有代码膨胀的情况。

2. inline函数无法随着程序库升级而升级。因为大多数都发生在编译期,升级意味着重新编译。

3. 大部分调试器是不能在inline函数设断点的。因为inline函数没有地址。

请记住

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

2. 另外,对function templates的inline也要慎重,保证其所有实现的函数都应该inlined后再加inline。

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

这个问题产生是源于希望编译时影响的范围尽量小,编译效率更高,维护成本更低,这一需求。

实现这个目标首先第一个想到的就是,声明与定义的分离,用户的使用只依赖声明,而不依赖定义(也就是具体实现)。

但C++的Class的定义式却不仅仅只有接口,还有实现细目(这里指实现接口需要的私有成员)。而有时候我们需要修改的通常是接口的实现方法,而这一修改可能需要添加私有变量,但这个私有变量对用户是不应该可见的。但这一修改却放在了定义式的头文件中,从而造成了,使用这一头文件的所有代码的重新编译。

于是就有了pimpl(pointer to implementation)的方法。用pimpl把实现细节隐藏起来,在头文件中只需要一个声明就可以,而这个poniter则作为private成员变量供调用。

这里会有个有意思的地方,为什么用的是指针,而不是具体对象呢?这就要问编译器了,因为编译器在定义变量时是需要预先知道变量的空间大小的,而如果只给一个声明而没有定义的话是不知道大小的,而指针的大小是固定的,所以可以定义指针(即使只提供了一个声明)。

这样把实现细节隐藏了,那么实现方法的改变就不会引起别的部分代码的重新编译了。而且头文件中只提供了impl类的声明,而基本的实现都不会让用户看见,也增加了封装性。

结构应该如下:

class AImpl;
class A {
public:
    ...
private:
    std::tr1::shared_ptr<AImpl> pImpl;
};

这一种类也叫handle class

另一种实现方法就是用带factory函数的interface class。就是把接口都写成纯虚的,实现都在子类中,通过factory函数或者是virtual构造函数来产生实例。

声明文件时这么写:

class Person
{
public:
    static shared_ptr<Person> create(const string&,
        const Data&,
        const Adress&);
};

定义实现的文件这么写

class RealPerson :public Person
{
public:
    RealPerson(...);
    virtual ~RealPerson(){}
    //...
private:
    // ...
};

以上说的为了去耦合而使用的方法不可避免地会带上一些性能上的牺牲,但作者建议是发展过程中使用以上方法,当以上方法在速度与/或大小上的影响比耦合更大时,再写成具体对象来替换以上方法。

请记住:

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

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

Effective C++笔记05:实现

时间: 2024-10-25 02:13:01

Effective C++笔记05:实现的相关文章

Effective C++笔记:构造/析构/赋值运算

条款05:了解C++默默编写并调用哪些函数 默认构造函数.拷贝构造函数.拷贝赋值函数.析构函数构成了一个类的脊梁,只有良好的处理这些函数的定义才能保证类的设计良好性. 当我们没有人为的定义上面的几个函数时,编译器会给我们构造默认的. 当成员变量里有const对象或引用类型时,编译器会不能合成默认的拷贝赋值函数:当一个基类把它的拷贝赋值函数定义为private时,它的派生类也不无生成默认的拷贝赋值函数,因为它无法完成基类成份的赋值. 条款06:若不想使用编译器自动生成的函数,就该明确拒绝 将拷贝构

SWIFT学习笔记05

1.Swift 无需写break,所以不会发生这种贯穿(fallthrough)的情况.2.//用不到变量名,可用"_"替换 for _ in 1...power { answer *= base } 3.case 可以匹配更多的类型模式,包括区间匹配(range matching),元组(tuple)和特定类型的描述. 可以这样用case case 1...3: naturalCount = "a few" 4.如果存在多个匹配,那么只会执行第一个被匹配到的 ca

Effective C++笔记06:继承与面向对象设计

关于OOP 博客地址:http://blog.csdn.net/cv_ronny 转载请注明出处! 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private,也可以是virtual或non-virtual. 2,成员函数的各个选项:virtual或non-virtual或pure-virtual. 3,成员函数和其他语言特性的交互影响:缺省参数值与virtual函数有什么交互影响?继承如何影响C++的名称查找规则?设计选项有如些?如果class的行为

《掌握需求过程》阅读笔记05

<掌握需求过程>阅读笔记05 我们的产品因为其功能而为用户使用而有意义,我们的产品才会有价值.而产品的需求又分为功能性需求和非功能性需求. 功能性需求就是因为产品存在的根本原因而存在的需求.功能性需求指明了产品必须做的事情,是产品功能的规格说明书,源于产品的基本目标.为了实现存在的根本理由,产品必须执行的一些动作,这些动作就与功能性需求有关. 我们可以通过用例描述.用户场景描述等地方挖掘出我们产品的功能性需求.那些我们产品必不可少的功能.目标就是我们产品的功能性需求.但是在分析功能性需求的时候

Effective c++(笔记)之继承关系与面向对象设计

1.公有继承(public inheritance) 意味着"是一种"(isa)的关系 解析:一定要深刻理解这句话的含义,不要认为这大家都知道,本来我也这样认为,当我看完这章后,就不这样认为了. 公有继承可以这样理解,如果令class D以public 的形式继承了class B ,那么可以这样认为,每一个类型为D的对象同时也可以认为是类型为B的对象,但反过来是不成立的,对象D是更特殊化更具体的的概念,而B是更一般化的概念,每一件事情只要能够施行于基类对象身上,就一定可以应用于派生类对

C++ GUI Qt4学习笔记05

C++ GUI Qt4学习笔记05 qtc++正则表达式 QIntValidator           --  只让用户输入整数 QDoubleValidator     --  只让用户输入浮点数 QRegExpValidator    --  只让用户按照正则表达式定义好的样式进行输入 本章讲解如何使用Qt开发自定义窗口部件. 通过对一个已经存在的Qt窗口部件进行子类化或者直接对QWidget进行子类化,就可以创建自定义窗口部件. 集成自定义窗口到Qt设计师中,这样就可以像使用内置的Qt窗

Effective c++(笔记) 之 类与函数的设计声明中常遇到的问题

1.当我们开始去敲代码的时候,想过这个问题么?怎么去设计一个类? 或者对于程序员来说,写代码真的就如同搬砖一样,每天都干的事情,但是我们是否曾想过,在c++的代码中怎么样去设计一个类?我觉得这个问题可比我们"搬砖"重要的多,大家说不是么? 这个答案在本博客中会细细道来,当我们设计一个类时,其实会出现很多问题,例如:我们是否应该在类中编写copy constructor 和assignment运算符(这个上篇博客中已说明),另外,我们是让编写的函数成为类的成员函数还是友元还是非成员函数,

[Effective JavaScript 笔记]第28条:不要信赖函数对象的toString方法

js函数有一个非凡的特性,即将其源代码重现为字符串的能力. (function(x){ return x+1 }).toString();//"function (x){ return x+1}" 反射获取函数源代码的功能很强大,使用函数对象的toString方法有严重的局限性.toString方法的局限性ECMAScript标准对函数对象的toString方法的返回结果(即该字符串)并没有任何要求.这意味着不同的js引擎将产生不同的字符串,甚至产生的字符串与该函数并不相关. 如果函数

Effective c++(笔记)----类与函数之实现

上篇博客中集中说明了在设计一个类的时候常遇到的问题,当然博客中还夹杂着我随时想到的一些知识,发现自己写博客没很多人写的好,可能是自己语言不会组织,要么就是写的东西大家不愿意看,反正是有这方面的专业问题或者博客中有什么明显的错误和问题,大家提出来,我也好改进哈! 回归正题,这篇博客就大概的把Effective c++中类与函数这节看到的知识点做个笔记. 设计好一个类后,自己就要去实现这个类(实现类中的成员函数.友元.非成员函数等) 可能大家会遇到以下问题 1.在类的成员函数中,尽量避免返回内部数据