• Effective C++ 第二版 31)局部对象引用和函数内new的指针 32)推迟变量定义


    条款31 千万不要返回局部对象的引用, 不要返回函数内部用new初始化的指针的引用

    第一种情况: 返回局部对象的引用; 

    局部对象--仅仅是局部的, 在定义时创建, 在离开生命空间时被销毁; 所谓生命空间, 指它们所在的函数体; 当函数返回时, 程序的控制离开这个空间, 函数内部所有的局部对象被自动销毁; 因此, 如果返回局部对象的引用, 那个局部对象其实已经在函数调用者使用它之前被销毁了;

    当想提高程序的效率而使得函数的结果通过引用而不是值返回时, 就会遇到这个问题; 下例和条款23的一样, 目的在于说明什么时候该返回引用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class  Rational {  // 一个有理数类
    public :
         Rational( int  numerator = 0,  int  denominator = 1);
         ~Rational();
    ...
    private :
         int  n, d;  // 分子和分母
    // 注意operator* (不正确地)返回了一个引用
         friend  const  Rational& operator*( const  Rational& lhs,  const  Rational& rhs);
    };
    // operator*不正确的实现
    inline  const  Rational& operator*( const  Rational& lhs,  const  Rational& rhs)
    {
         Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
         return  result;
    }

    >局部对象result在刚进入operator*函数体的时候就被创建; 所有的局部对象在离开所在的空间时都会自动销毁; result是在执行return语句后离开所在的空间的; 所以:

    1
    2
    Rational two = 2;
    Rational four = two * two;  // 同operator*(two, two)

    >函数调用时发生的事件: 1) 局部对象result创建; 2) 初始化一个引用, 成为result的别名, 作为operator*的返回值; 3) 局部对象result被销毁, 在堆栈所占的空间可被程序其他部分或其他程序使用; 4) 用2)中的引用初始化对象four;

    程序运行到4)产生了一个错误; 2)中被初始化的引用在3)结束时指向的不再是有效对象, 所以four的初始化结果是不可确定的;

    Note 别返回一个局部对象引用;


    如果用new来解决对象离开空间太早的问题...

    1
    2
    3
    4
    5
    6
    7
    // operator*的另一个不正确的实现
    inline  const  Rational& operator*( const  Rational& lhs,  const  Rational& rhs)
    {
    // create a new object on the heap
         Rational *result =  new  Rational(lhs.n * rhs.n, lhs.d * rhs.d);
         return  *result;
    }

    >这个方法避免了局部变量离开生命空间的问题, 却引发了内存泄露的难题;

    为了避免内存泄露, 必须对每个用new产生的指针调用delete; 但是对于这个函数中的new, 应该由谁来delete?

    显然, 应该由operator*的调用者负责delete; 但是基于两条理由, 这还是会出问题:

    1) 马虎的程序员, [任何领域都有马虎的人]

    1
    2
    3
    const  Rational& four = two * two;  // 得到废弃的指针; 将它存在一个引用中
    //...
    delete  &four;  // 得到指针并删除

    >想让所有人记住无论何时调用operator*得到结果的指针后, 必须调用delete, 几乎不可能不出差池, 只要一个调用者忘了, 就会出现内存泄露;

    2) 返回废弃的指针还有一个更严重的问题: 当operator*的结果只是临时用于中间值; 它的存在是为了计算一个更大的表达式:

    1
    2
    3
    Rational one(1), two(2), three(3), four(4);
    Rational product;
    product = one * two * three * four;

    >product的计算表达式需要三个单独的operator*调用; 

    相应的函数形式: product = operator*(operator*(operator*(one, two), three), four); 每个operator*调用所返回的对象都要被删除, 但在这里无法调用delete, 因为没有一个返回对象被保存下来; [中间值问题]

    Solution: 让用户写麻烦的代码; [估计用户都会觉得这个接口很NC]

    1
    2
    3
    4
    5
    6
    const  Rational& temp1 = one * two;
    const  Rational& temp2 = temp1 * three;
    const  Rational& temp3 = temp2 * four;
    delete  &temp1;
    delete  &temp2;
    delete  &temp3;

    Note 写一个返回废弃指针的函数等于坐等内存泄露的来临;

    假如你认为想出了办法(static)避免"返回局部对象的引用"带来的不确定行为, 以及"返回堆heap上分配的对象的引用"所带来的内存泄露, 请见条款23, 返回局部静态static对象的引用也会工作失常; [比较操作符...]


    条款32 尽可能地推迟变量的定义

    我们同意C语言中变量放在模块头部定义的规定; 但在C++中没必要, 而且昂贵[消耗大];

    如果定义了一个有构造和析构函数的类型的变量, 当程序运行到变量定义处, 必然面临构造的开销; 当变量离开生命空间, 又要承担析构的开销; 这意味着定义无用的变量必然伴随不必要的开销; 所以只要可能, 就要避免这种情况;

    e.g. 函数: 当口令够长时, 返回口令的加密版本; 当口令太短, 函数抛出logic_error类型的异常(logic_error类型在C++标准库; 见条款49):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 此函数太早定义了变量"encrypted"
    string encryptPassword( const  string& password)
    {
         string encrypted;
         if  (password.length() < MINIMUM_PASSWORD_LENGTH) {
             throw  logic_error( "Password is too short" );
         }
    //进行必要的操作,将口令的加密版本放进 encrypted 之中;
         return  encrypted;
    }

    >对象encrypted在函数中并非完全没用, 但如果有异常抛出, 它就是无用的; 

    即使encryptPassword抛出异常, 程序也会承担encrypted构造和析构的开销; 所以最好将encrypted推迟到确实需要时才定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 这个函数推迟了encrypted 的定义,直到真正需要时才定义
    string encryptPassword( const  string& password)
    {
         if  (password.length() < MINIMUM_PASSWORD_LENGTH) {
             throw  logic_error( "Password is too short" );
         }
         string encrypted;
    //进行必要的操作,将口令的加密版本放进 encrypted 之中;
         return  encrypted;
    }

    >这段代码还是不够严谨, 因为encrypted定义时没有带任何初始化参数, 这会导致缺省构造函数被调用; 大多数情况下, 对一个对象首先做的事是赋值; 条款12说明了"缺省构造一个对象然后对它赋值"比"用真正的值来初始化对象"效率要低;

    假设encrptPassword中最难处理的部分在这个函数中进行: 

    1
    void  encrypt(string& s);  // s 在此加密
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 这个函数推迟了encrypted 的定义,直到需要时才定义,但还是很低效
    string encryptPassword( const  string& password)
    {
    ...  // 同上,检查长度
         string encrypted;  // 缺省构造encrypted
         encrypted = password;  // 给encrypted 赋值
         encrypt(encrypted);
         return  encrypted;
    }

    >不是最好的实现方式;

    更好的方法是用password来初始化encrypted, 绕过对缺省构造函数不必要的调用;

    1
    2
    3
    4
    5
    6
    7
    8
    // 定义和初始化encrypted 的最好方式
    string encryptPassword( const  string& password)
    {
    ...  // 检查长度
         string encrypted(password);  // 通过拷贝构造函数定义并初始化
         encrypt(encrypted);
         return  encrypted;
    }

    >这段代码表现了"尽可能"的含义 [ - -!]; 不仅要将变量的定义推迟到必须使用它的时候, 还有尽量推迟到可以为它提供一个初始化参数为止; 这样, 不仅可以避免对不必要的对象进行构造和析构, 还可以避免无意义的对缺省构造函数的调用; 在对变量进行初始化的场合下, 在推迟的地方定义变量有益于表明变量真正含义; 

    C语言的做法是, 每个变量的定义旁边最好有条短注释, 以标明这个变量做什么用; 现在, 取个合适的名字(条款28), 结合有意义的初始化参数, 通过变量本身就表明了含义, 去除不必要的注释;

    Note 推迟变量定义可以提高程序效率, 增强程序条理性, 减少对变量含义的注释;

  • 相关阅读:
    javascript Date format(js日期格式化)
    hcharts 教程
    UVA 10594 Data Flow
    UVA 10746 Crime Wave
    UVA 753 A Plug for UNIX
    UVA 11045 My T-shirt suits me
    UVA 10273 Eat or not to Eat?
    UVA 10806 Dijkstra, Dijkstra.
    UVA 10330 Power Transmission
    UVA 10803 Thunder Mountain
  • 原文地址:https://www.cnblogs.com/riskyer/p/3402635.html
Copyright © 2020-2023  润新知