条款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 推迟变量定义可以提高程序效率, 增强程序条理性, 减少对变量含义的注释;

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

  1. Effective Java 第二版 Enum

    /** * Effective Java 第二版 * 第30条:用enum代替int常量 */ import java.util.HashMap;import java.util.Map; publi ...

  2. 《python基础教程(第二版)》学习笔记 函数(第6章)

    <python基础教程(第二版)>学习笔记 函数(第6章) 创建函数:def function_name(params):  block  return values 记录函数:def f ...

  3. Effective C++ 第二版 1)const和inline 2)iostream

    条款1 尽量用const和inline而不用#define >"尽量用编译器而不用预处理" Ex. #define ASPECT_R 1.653    编译器永远不会看到AS ...

  4. 《Effective Java第二版》总结

    第1条:考虑用静态工厂方法代替构造器 通常我们会使用 构造方法 来实例化一个对象,例如: // 对象定义 public class Student{ // 姓名 private String name ...

  5. 《Effective Java 第二版》读书笔记

    想成为更优秀,更高效程序员,请阅读此书.总计78个条目,每个对应一个规则. 第二章 创建和销毁对象 一,考虑用静态工厂方法代替构造器 二, 遇到多个构造器参数时要考虑用builder模式 /** * ...

  6. Effective C++ 第二版 17)operator=检查自己 18)接口完整 19)成员和友元函数

    条款17 在operator=中检查给自己赋值的情况 1 2 3 class  X { ... }; X a; a = a;  // a 赋值给自己 >赋值给自己make no sense, 但 ...

  7. Effective C++ 第二版 40)分层 41)继承和模板 42)私有继承

    条款40 通过分层来体现"有一个"或"用...来实现" 使某个类的对象成为另一个类的数据成员, 实现将一个类构筑在另一个类之上, 这个过程称为 分层Layeri ...

  8. Effective C++ 第二版 5)new和delete形式 6) 析构函数里的delete

    内存管理 1)正确得到: 正确调用内存分配和释放程序; 2)有效使用: 写特定版本的内存分配和释放程序; C中用mallco分配的内存没有用free返回, 就会产生内存泄漏, C++中则是new和de ...

  9. Effective C++ 第二版 10) 写operator delete

    条款10 写了operator new就要同时写operator delete 写operator new和operator delete是为了提高效率; default的operator new和o ...

随机推荐

  1. 句子相似度_tf/idf

    import mathfrom math import isnanimport pandas as pd#结巴分词,切开之后,有分隔符def jieba_function(sent): import ...

  2. Sliding Window Median LT480

    Median is the middle value in an ordered integer list. If the size of the list is even, there is no ...

  3. 模板 Template

    package ${enclosing_package}; import java.io.IOException;import javax.servlet.ServletException;impor ...

  4. java itext 报错 com.itextpdf.text.DocumentException: Font 'STSong-Light' with 'UniGB-UCS2-H'

    com.itextpdf.text.DocumentException: Font 'STSong-Light' with 'UniGB-UCS2-H' 解决方案 <dependency> ...

  5. 使用EventLog Analyzer进行VMware日志管理

  6. 22.上传app一些相关问题

    1.截取上传的各个屏幕尺寸 1.按最大尺寸截取,快捷键 command+s 2.在模拟器上截取 3. 截图 iphone4 : 640x960 或者 960x640 phone5    640 x 1 ...

  7. 谷歌开源OCR,tesseract-ocr使用笔记

    官方教程地址:https://github.com/tesseract-ocr/tesseract/wiki/Compiling 测试版本为 root@9a2a063f9534:/tesseract/ ...

  8. php mongodb driver

    yum install -y PHP-devel php-pear httpd-devel pecl install mongo 执行以上命令后,你需要修改php.ini文件,在php.ini文件中添 ...

  9. atcoder题目合集(持续更新中)

    Choosing Points 数学 Integers on a Tree 构造 Leftmost Ball 计数dp+组合数学 Painting Graphs with AtCoDeer tarja ...

  10. 2018.11.24 poj3415Common Substrings(后缀数组+单调栈)

    传送门 常数实在压不下来(蒟蒻开O(3)都过不了). 但有正确性233. 首先肯定得把两个字符串接在一起. 相当于heightheightheight数组被height<kheight<k ...