《Effective C++》第5章 实现-读书笔记

章节回顾:

《Effective C++》第1章 让自己习惯C++-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(1)-读书笔记

《Effective C++》第2章 构造/析构/赋值运算(2)-读书笔记

《Effective C++》第3章 资源管理(1)-读书笔记

《Effective C++》第3章 资源管理(2)-读书笔记

《Effective C++》第4章 设计与声明(1)-读书笔记

《Effective C++》第4章 设计与声明(2)-读书笔记

《Effective C++》第5章 实现-读书笔记

《Effective C++》第8章 定制new和delete-读书笔记



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

你定义了一个类类型的变量,那么就要耗费一个构造函数和析构函数。如果你最终不使用这个变量,就应该避免这些耗费。

你可能会怀疑:怎么可能定义一个变量而不去使用呢?考虑下面的代码:

std::string encryptPassword(const std::string& password)
{
    using namespace std;
    string encrypted;

    if (password.length() < MinimumPasswordLength)
    {
        throw logic_error("Password is too short")
    }

    ...
    return encrypted;
}

先不去考虑代码具体含义。如果if语句为true,就会抛出异常,这个encrypted对象仍然需要耗费一个构造函数和一个析构函数。所以最好延后encrypted的定义式,直到确实需要它。

std::string encryptPassword(const std::string& password)
{
    using namespace std;

    if (password.length() < MinimumPasswordLength)
    {
        throw logic_error("Password is too short")
    }

    string encrypted;            //放在了后面
    ...
    return encrypted;
}

这段代码不够秾纤合度(不懂这个词)。因为encrypted对象调用的是默认构造函数,后面几乎一定会对它重新赋值。举例如下:

void encrypt(std::string& s);
std::string encryptPassword(const std::string& password)
{
    ...
    string encrypted;            //放在了后面,考虑到使用时才定义

    encrypted = password;        //重新赋值

    encrypt(encrypted);

    return encrypted;
}

更好的做法是跳过无意义的default构造函数:

std::string encryptPassword(const std::string& password)
{
    ...
    string encrypted(password);            //拷贝构造函数

    encrypt(encrypted);

    return encrypted;
}

所以,“尽可能延后”的真正意义是:你不仅要尽可能延后变量的定义直到要使用它,还应该延后这个变量的定义直到给它初值。这样可以避免没有必要的构造和析构对象以及没有意义的default构造函数。

还有一种情形出现在循环里面,分下下面两种做法A、B哪个更好:

//做法A
Widget w;
for (int i = 0; i < n; ++i)
{
    w = 取决于i的某个值;
}

//做法B
for (int i = 0; i < n; ++i)
{
    Widget w = 取决于i的某个值;
}

做法A的成本:1个构造函数、1个析构函数和n个赋值;做法B的成本:n个构造函数和n个析构函数。

如果赋值成本低于1个构造+1个析构,则做法A效率高一点,否则B的做法好。另外做法A造成Widget对象作用域扩大。所以,给出的建议是:除非你明确知道几个操作的成本,否则做法B是比较好的。

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



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

C风格的转型(旧式转型)如下:

(T) expression;        //两者含义相同
T(expression)

C++提供的4种新式转型如下:

const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)

一般来说新式转型比较好。可能旧式转型比较常用的地方是调用explicit构造函数传递一个对象给函数时。举例如下:

class Widget
{
public:
    explicit Widget(int size);
};
void doSomeWork(const Widget& w);

doSomeWork(Widget(15));                    //C的函数风格

doSomeWork(static_cast<Widget>(15));    //C++新风格

任何一个类型转换,无论是通过转型操作进行的显式转换或通过编译器进行的隐式转换,往往会导致编译器产生运行期执行的代码。

下面有个转型代码比较有迷惑:

class Window
{
public:
    Window(int n = 0) : m(n) {}

    virtual void onResize()
    {
        m = 10;
    }

    int m;
};

class SpecialWindow : public Window
{
public:
    virtual void onResize()
    {
        static_cast<Window>(*this).onResize();
    }
};

int main()
{
    SpecialWindow w1;
    cout << w1.m << endl;        //输出0

    w1.onResize();
    cout << w1.m << endl;        //输出0

    return 0;
}

两份输出都是0。不要怀疑,static_cast<Window>(*this).onResize();确实调用了class Window的onResize()函数,但关键是转型的结果是(*this)的一个副本,而不是对象本身。

如果你仍然需要调用class Window版本的onResize()函数,就要拿掉转型。

class SpecialWindow : public Window
{
public:
    virtual void onResize()
    {
        Window::onResize();
    }
};

dynamic_cast的成本很高,之所以需要它的一个原因是:在一个你认定为derived class对象身上执行derived class函数,但你只有一个指向base的指针或引用。

请记住:

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

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

(3)优先使用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
{
public:
    Point& upperLeft() const { return pData->ulhc; }            //返回左上角坐标
    Point& lowerRight() const { return pData->lrhc; }            //返回右下角坐标
};

Rectangle类设计两个成员函数upperLeft(),lowerRight()返回左上角和右下角坐标是必要的。但这两个函数都是const的,说明它的目的只是给用户查看,并不是让用户去修改这些坐标。但是客户这样做:

Point coord1(0, 0);
Point coord2(100, 100);

const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50);                //左上角坐标变为(50,0)

确实改变了坐标值,尽管point还是private数据。这给我们的启示是:成员变量的封装性最多只等于返回其reference函数的访问级别。虽然point是private的,但实际效果却是public的。

修改版本也很简单:

class Rectangle
{
public:
    const Point& upperLeft() const { return pData->ulhc; }            //返回左上角坐标的const
    const Point& lowerRight() const { return pData->lrhc; }            //返回右下角坐标的const
};

另外一点handles指向的东西返回后可能不再存在。举例说明:

class GUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj);

//客户如下这样调用
GUIObject *pgo;
const Point *pUpperLeft = &(boundingBox(*pgo).upperLeft());

boundingBox(*pgo)的调用将产生一个临时的Rectangle对象,在此对象上调用upperLeft()返回的是临时对象的左上角坐标,然后这个临时对象析构,这样pUpperLeft就是指向一个不存在的东西。

请记住:避免返回handles(包括引用、指针和迭代器)指向内部对象。遵守这个条款可增加封装性、帮助const成员函数的行为像个const,并降低发生handles指向不存在东西的可能性。

时间: 2024-11-07 20:50:55

《Effective C++》第5章 实现-读书笔记的相关文章

《C++primer》v5 第5章 语句 读书笔记 习题答案

5.1 空语句只有一个";".如果什么也不想做可以使用空语句. 5.2 用花括号{}括起来的叫块,也叫复合语句.有多条语句作用在同一个作用域时,需要用花括号括起来. 5.3 降低了. 5.4 (a)每次迭代时候会初始化iter,但是iter缺少初值,所以这段代码根本不会通过编译.另外这里的括号需要一个bool类型的,而定义迭代器根本不会返回一个bool类型.假如上面那些问题都可以通过,每次迭代都会初始化这个iter,会导致死循环. (b)我试了一下编译未通过是因为没找到适合的find函

《C++primer》v5 第1章 开始 读书笔记 习题答案

从今天开始在博客里写C++primer的文字.主要以后面的习题作业为主,会有必要的知识点补充. 本人也是菜鸟,可能有不对之处,还望指出. 前期内容可能会比较水. 1.1略 1.2略 1.3 cin和cout分别是istream和ostream的对象. #include<iostream> using namespace std; int main() { cout<<"Hello,world"<<endl; return 0; } 1.4 #incl

《C++primer》v5 第4章 表达式 读书笔记 习题答案

4.1 105 4.2 *vec.begin()=*(vec.begin())//先调用点运算符,再解引用 *vec.begin()+1=(*vec.begin())+1//先解引用,再加一 4.3略? 4.4 (12/3*4)+(5*15)+(24%4/2)=91 4.5 (a)-86(b)-16 (c)0 (d)0 4.6 n%2 4.7 溢出:计算结果超出该数据类型所能表示的范围 2147483647+1 1U-2 ... 4.8 比较低.. 4.9 首先判断cp是否为空指针,若非空指针则

《操作系统概论》第一章引论读书笔记

计算机系统分为硬件和软件. 硬件主要由:CPU.存储器.输入输出控制系统.各种输入输出设备组成. 软件主要分为:系统软件.支撑软件.以及应用软件. 操作系统定义:一般认为,操作系统是管理计算机系统资源.控制程序执行.改善人机界面和为应用软件提供支持的一种系统软件. 操作系统的作用: 管理计算机系统的资源. 为用户提供方便的使用接口. 具有扩充硬件的功能. 操作系统的功能: 从资源管理的观点看,操作系统的功能可以分为:处理器管理.存储管理.文件管理和设备管理. 处理器管理的主要工作是进行处理器的分

《Effective Java中文版第二版》读书笔记

说明 这里是阅读<Effective Java中文版第二版>的读书笔记,这里会记录一些个人感觉稍微有些重要的内容,方便以后查阅,可能会因为个人实力原因导致理解有误,若有发现欢迎指出.一些个人还不理解的会用斜线标注. 第一章是引言,所以跳过. 第二章 创建和销毁对象 第1条:考虑用静态工厂方法代替构造器 含义 静态工厂方法是指一个返回类的实例的静态方法,例如: public static Boolean valueOf(boolean b) { return b ? Boolean.TRUE :

《java编程思想-第2章》读书笔记

第2章:一切都是对象 若类的某个成员是基本数据类型,即使没有进行初始化,java也会确保它获得一个默认值.而对于"局部"变量(即非某各类的字段),在使用前,必须赋予一个值,如果你忘记了这么做,java会在编译时返回一个错误,告诉你此变量没有进行初始化. a是一个对象,int x=a.f(); 返回值的必须与x的类型兼容.这种调用方法的行为通常被称为发送消息给对象.面向对象的程序设计通常简单地归纳为向对象发送消息. 若返回类型为void,return关键字的作用则是用来退出方法的.因此,

Effective MySQL之SQL语句最优化——读书笔记之一

第一章,DBA5分钟速成 本章知识点如下: 寻找运行慢SQL的语句: show full processlist查看所有正在执行的进程及执行的语句耗时: 命令后面\G可以让命令按行显示(默认是按列). 手动用ad hoc方式执行该sql,大于10ms的查询通常会有问题(有可能是索引问题,也可能是结果集太大使得结果无意义). 对于低效的Delete和Update语句,可以通过将其改为Select大概判断其效率. 生成执行计划QEP(query excution plan),注意QEP结果不一定是真

《深入理解计算机系统》第七章 链接 读书笔记

第七章链接 链接:将各种代码和数据部分收集起来并组合成为一个单一文件的过程,这个文件可被加载.连接可执行于编译时.加载时.运行时.由叫链接器的程序执行. 链接器使得分离编译成为可能.使得可以把 大程序分解成小模块,利于管理. 理解链接器将帮助你构筑大程型序:避免一些危险的编程错误:帮助你理解语言的作用域规则是如何实现的:帮助你理解其他重要的系统概念:使你能够利用共享库. 7.1 编译器驱动程序 驱动程序的工作:1.运行C预处理器,将C源程序(.c)翻译成一个ASCⅡ码中间文件(.i):2.运行C

快学scala 第十一章 操作符 读书笔记及习题答案代码

chapter 11 操作符 标签:快学scala 一.笔记 scala种可以在反引号中包含几乎任何字符序列, val 'val' = 42 所有的操作符都是左结合的,除了以冒号(:)结尾的操作符,和赋值操作符.用于构造列表的::操作符是又结合的.1::2::Ni1的意思是1::(2::Ni1),先创建出包含2的列表,这个列表又被作为尾巴拼接到以1作为头部的列表中. 2. 函数调用语法:f(arg1, arg2,...)扩展到可以应用于函数之外的值,如果f不是函数或方法,那么这个表达式等于f.a