条款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. Squid 搭建正向代理服务器

    Squid 是一款缓存代理服务器软件,广泛用于网站的负载均衡架构中,常见的缓存服务器还有varnish.ATS等. 正向代理服务器可满足内网仅有一台服务器可以上网,而要供内网所有机器上网的需求,也可以 ...

  2. python imaplib无痕取信的主要

    typ, data = M.fetch(num, (UID BODY.PEEK[]))  

  3. System.Runtime.InteropServices.COMException: 检索 COM 类工厂中 CLSID 为 {0002E510-0000-0000-C000-000000000046} 的组件时失败,原因是出现以下错误: 80040154

    这个问题困恼我好几天了,今天终于解决. 开始我在网上左百度右google,都没搜到最终的解决方案,今天我把解决方案贴出来,以供大家分享! 网上有些是报80070005错误的,跟我这个80040154错 ...

  4. MySQL学习笔记-数据库文件

    数据库文件 MySQL主要文件类型有如下几种 参数文件:my.cnf--MySQL实例启动的时候在哪里可以找到数据库文件,并且指定某些初始化参数,这些参数定义了某种内存结构的大小等设置,还介绍了参数类 ...

  5. Java 使用jdk自带的wsimport命令生成webservice客户端代码

    wsimport -s E:\workspace\givemewords\src -p com.test.service -keep http://localhost:8085/Service/Fun ...

  6. 设计模式之生成者模式java源代码

    假设要组装一辆自行车,并且自行车就是车轮和车架组成. Builder对应于组装自行车所使用的车轮和车架 ConcreteBuiler对应于自行车的车轮和车架,同时可以返回一辆自行车. Product对 ...

  7. 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> ...

  8. PID控制算法的C语音实现

    http://wenku.baidu.com/link?url=_u7LmA1-gzG5H8DzFYsrbttaLdvhlHVn5L54pgxgUiyyJK_eWtX0LbS7d0SEbHtHzAoK ...

  9. linux下设置mysql表名不区分大小写

    原文:http://blog.csdn.net/johnsonvily/article/details/6703902 1.Linux下mysql安装完后是默认:区分表名的大小写,不区分列名的大小写: ...

  10. JS高级- OOP-ES5

    1. OOP 面向对象三大特点: 封装,继承,多态 封装: 问题: 构造函数可重用代码和结构定义,但无法节约内存 为什么: 放在构造函数内的方法定义,每new一次,都会反复创建副本——浪费内存 解决: ...