实用经验 45 禁止函数返回局部变量的引用

局部变量和引用是每个C++程序员都不会陌生的两个概念。是使用最多同时也是出问题最多的两个概念。引用是被绑定变量或对象的别名。局部变量,顾名思义就是在局部范围内有效的变量。

引用是C++语言区别C语言一个新引入的重要的扩充,引用是被绑定变量或对象的别名,就像我们小时候的乳名和上学时的学名一样,无论母亲叫你乳名还是学名指的都是你。假设定义一个变量如下:

int nNumber = 5; 
int &refNumber = nNumber;

nNumber是5对应4Bytes内存的标识名称(对应你的乳名)。refNumber是nNumber的引用或别名(对应你的学名)。

局部变量由C语言继承而来。那些在某一范围内有效,超过此范围而无效的变量(即变量就不存在了),都称之为局部变量。假设有一语句段如下:

{
    int a = 3;
    a += 2;
}

在大括号内,笔者定义了一个变量a,在VC++2010 IDE编译器上。笔者做了这样的实验。在大括号外部使用变量a。结果编译器抛出一个编译错误:error C2065: “a”未声明的标识符。此处a即我们所讨论的局部变量。

然而,当局部变量和引用遭遇,会发生什么事情呢?让我们拭目以待。

首先看一下我们经常使用的复数四则运算C++实现和测试代码。复数CComplex类的实现和测试代码,笔者的实现如下:

class CComplex                        // 复数类实现
{
public:
    CComplex(double dReal = 0double dImagin = 0) : m_dReal(dReal)m_dImagin(dImagin){}
    virtual ~CComplex(void){}
    inline friend const CComplex& operator+ (const CComplex& lhs, const CComplex& rhs);
    inline friend const CComplex& operator- (const CComplex& lhs, const CComplex& rhs);
    inline friend const CComplex& operator/ (const CComplex& lhs, const CComplex& rhs);
    inline friend const CComplex& operator* (const CComplex& lhs, const CComplex& rhs);
private:
    double   m_dReal;   	       // 实部
    double   m_dImagin;  	       // 虚部
};

//  复数加法运算
inline  const CComplex& operator+ (const CComplex& lhs, const CComplex& rhs)
{
    CComplex result(lhs.m_dReal+rhs.m_dImagin, lhs.m_dImagin+rhs.m_dImagin);
    return result;
}

//  复数减法运算
inline  const CComplex& operator- (const CComplex& lhs, const CComplex& rhs)
{
    CComplex result(lhs.m_dReal-rhs.m_dImagin, lhs.m_dImagin-rhs.m_dImagin);
    return result;
}

// 复数除法运算
inline  const CComplex& operator/ (const CComplex& lhs, const CComplex& rhs)
{
    double dDenominator = lhs.m_dImagin*lhs.m_dImagin+rhs.m_dImagin*rhs.m_dImagin;
    CComplex result((lhs.m_dReal*rhs.m_dImagin+lhs.m_dImagin*rhs.m_dImagin)/dDenominator, (lhs.m_dImagin*rhs.m_dReal-lhs.m_dReal*rhs.m_dImagin)/dDenominator);
    return result;
}

// 复数的乘法运算
inline  const CComplex& operator* (const CComplex& lhs, const CComplex& rhs)
{
    CComplex result(lhs.m_dReal*rhs.m_dImagin-lhs.m_dImagin*rhs.m_dImagin, lhs.m_dReal*rhs.m_dImagin+lhs.m_dImagin*rhs.m_dReal);
    return result;
}

// 测试代码
CComplex complexA(12);
CComplex complexB(22);
CComplex complexC = complexA + complexB;
const CComplex& complexD = complexA + complexB;

我想你会说上述代码的运行结果是complexC== complexD,但事实是这样的吗?将上述代码在GCC或VC++上运行一下。也许会让你大失所料。笔者在VC++2010版本上运行的结果是complexA=(-9.2559631349317831e+061)– (9.2559631349317831e+061)i,complexD = 3 + 4i。如果你足够细心,你可能会发现VC++2010抛出了一个警告:warning C4172: 返回局部变量或临时量的地址。这是为什么呢?

我们分析一下函数operator+调用的过程到底发生了什么,函数首先构造了一个名称为result的局部变量,然后生成result的别名并作为函数返回值返回。最后局部变量result生命期结束被销毁。而此时result的别名还存在着。在C++标准中,临时变量消失后,临时变量的引用是没有定义的。所以编译器的告警就产生了。

至此问题根源貌似已经找到了,为了加深理解,我们看一下编译层次的东西。这样可以加深你对局部变量引用的理解。

注意:为了保证函数执行后程序可找到正确位置继续执行,需要对当前操作的上下文进行保护。为了保证此目标的实现。函数在调用时,函数的输入参数、返回值、局部变量都放到先进后出的堆栈上存储。

当发生函数调用时,编译器首先把函数的输入/出参数压入到堆栈,指令寄存器IP压入到堆栈(作为函数返回出口地址),然后是基址寄存器,接着是函数的局部变量。当函数返回时执行弹出操作,其顺序正好与压入到堆栈的顺序正好相反(首先释放堆栈中的局部存储变量,然后是基址寄存器,IP寄存器地址和函数的输入/出变量),同时把压入到堆栈的IP寄存器地址作为函数的出口地址并退出函数。

所以可看出函数所有局部变量都是分配到堆栈上,当函数退出时堆栈也就释放了,分配局部变量的内存被操作系统重新收回。而函数局部变量所在的内存段具体变成什么了,编程人员无法确定(这和编译器的实现有关,一般Windows系统下的VC++默认保持原样)。

分析到这儿,也许有同学会说,那还不简单:返回局部变量的引用不行,在函数中new生成一个对象,然后返回生成对象的引用,问题不就解决了。代码实现如下:

inline  const CComplex& operator+ (const CComplex& lhs, const CComplex& rhs)
{
	CComplex *presult = new CComplex(lhs.m_dReal+rhs.m_dImagin, lhs.m_dImagin+rhs.m_dImagin);
    return *presult;
}
inline  const CComplex& operator- (const CComplex& lhs, const CComplex& rhs)
{
    CComplex *presult = new CComplex(lhs.m_dReal-rhs.m_dImagin, lhs.m_dImagin-rhs.m_dImagin);
    return *presult;
}
inline  const CComplex& operator/ (const CComplex& lhs, const CComplex& rhs)
{
    double dDenominator = lhs.m_dImagin*lhs.m_dImagin+rhs.m_dImagin*rhs.m_dImagin;
    CComplex *presult = new CComplex(lhs.m_dReal*rhs.m_dImagin+lhs.m_dImagin*rhs.m_dImagin)/dDenominator,(lhs.m_dImagin*rhs.m_dReal-lhs.m_dReal*rhs.m_dImagin)/dDenominator);
    return *presult;
}
inline  const CComplex& operator* (const CComplex& lhs, const CComplex& rhs)
{
	CComplex *presult = new CComplex(lhs.m_dReal*rhs.m_dImagin-lhs.m_dImagin*rhs.m_dImagin, lhs.m_dReal*rhs.m_dImagin+lhs.m_dImagin*rhs.m_dReal);
    return * presult;
}

首先说明一下,这种实现引用已释放内存问题确实解决了,测试程序可以运行正常。但你别高兴的太早了(你脚下踩着雷呢,一个不小心就会粉身碎骨)。代码新引入的问题并不亚于返回一个局部变量的引用。返回new生成变量的引用,存在3个缺点:

  1. CComplex的operator系列函数,只申请内存不释放内存。容易造成内存泄露。增加了用户的使用负担。
  2. 内存申请和释放不在一个模块中。影响模块的完整性和单一性,破坏了函数的内聚性。
  3. 如果你编写的程序是以库的形式提供给别人调用,那问题就更多了。别人怎么知道你在函数中申请了内存了,而帮你释放你释放的内存呢?我想使用人员是不会帮你释放你申请的内存的。因为你代码实现违背了一个C++经典原则:谁创建,谁释放原则。

所以,返回new生成对象的引用,亦不是可取的方法。

请谨记

  • 函数返回时,保证返回数据超出函数范围后依然有效。像返回局部变量的引入就是不靠谱的事情。
  • 函数返回时,返回new生成的对象,同样不是一个可取的方法。因为这样的代码层次混乱,会让代码上层使用人员苦不堪言。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值