使用引用容易犯的错误

1、引用是对象的别名,那么假如该对象不存在了,那么我们使用这个对象的别名将会产生什么样的后果?请看下面的例子:

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){x = i;}
	int get() const {return x ;}
private:
	int x ;
};

A& fun()   //函数的返回类型是类A对象的引用(即别名)
{
	A a(23) ;
	return a ;//fun函数会返回在函数fun创建的对象a的别名
}
int main()
{
	A &ra = fun() ;//我们用一个别名ra来接受fun函数返回的别名
	cout << ra.get() << endl ;
	return 0 ;
}


程序运行的结果:

我们可以看到程序输出的结果是一个随机值,并不是对象a的值23。其实,在程序编译时会出现一条警告:

              warning C4172: 返回局部变量或临时变量的地址

上面的警告是说:函数fun返回了一个局部变量。

      让我们回到fun函数中,我们可以看到程序返回了局部对象a的别名。因为对象a是一个局部对象,因此当函数fun结束后,局部对象a也就被删除了,由于对象a消失了,所以fun函数返回的其实是一个不存在的对象的别名。这个别名赋给了ra,那么ra其实也同样是一个不存在的对象的别名,我们使用不存在的对象的别名ra去调用get函数,那么get函数将会返回一个并不存在的对象的成员x。因为该对象不存在,所以成员x也是不存在的。程序就输出了一个随机数。如果我们做以下修改:

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){x = i;}
	int get() const {return x ;}
private:
	int x ;
};

A fun()   //函数的返回类型是类A,不是引用了
{
	A a(23) ;
	return a ;//fun函数会返回在函数fun创建的对象a的别名
}
int main()
{
	A &ra = fun() ;//我们用一个别名ra来接受fun函数返回的别名
	cout << ra.get() << endl ;
	return 0 ;
}


程序运行的结果如下:

       我们可以看到程序输出正确的结果,对象a的成员x的值23。这里我们只是将fun函数的返回值的类型去掉引用,直接是返回类型A。为什么去掉引用运算符后,结果就正确呢?这时因为去掉引用运算符以后,fun函数的返回方式是按值返回。按值返回的方式会自动调用类A的复制构造函数复制对象a的一个副本,然后将这个副本赋给ra,这样这个ra就是这个副本的别名。下面我们举例说明这一点(按值返回,返回的是对象的副本,这和按值传递是一样的):

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){cout << "执行构造函数创建一个对象\n"; x = i ;}
	//加上复制构造函数的目的是为了说明按值返回一个对象时会默认自动调用
	//复制构造函数创建该对象的一个副本
	A(A &a){cout << "执行复制构造函数创建一个对象\n"; x = a.x ;}
	~A(){cout << "执行了析构函数!\n" ;}
	int get() const {return x ;}
private:
	int x ;
};

A fun()
{
	cout << "跳转到fun函数中\n" ;
	A a(23) ;
	cout << "对象A的地址:\t\t" << &a << endl ;
	return a ;
}

int main()
{
	A &ra = fun() ; //使用别名接受fun函数的返回值
	cout << "对象a的副本的地址:\t" << &ra << endl ;
	cout << ra.get() << endl ;
	return 0 ;
}


程序运行的结果如下:

我们对着上面的执行结果来看程序:

1、进入main函数中,执行A &ra = fun() ; 程序跳转到fun函数中,所以输出了“跳转到fun函数中”。

2、进入fun函数后,在fun函数中定义了一个局部对象a,所以调用了类A的构造函数,所以输出了“执行构造函数创建一个对象”。

3、创建完局部对象a后,输出局部对象a的地址:0021F920

4、输出完局部对象a的地址后,执行return语句,因为fun函数是按值返回的,所以会调用类A的复制构造函数返回了局部对象a的一个副本,然后fun函数将这个副本返回。所以程序输出了“执行复制构造函数创建一个对象” 。

5、完成了局部对象a的复制后,局部对象a的生命周期就结束了,这时会调用类A的析构函数,释放局部对象a所占用的内存空间。所以程序输出了“执行了析构函数!”。

6、fun函数返回,将局部对象a的副本用别名ra来接收,所以,我们输出局部对象a的副本的地址,也就是别名ra的地址:0021FA20

7、再后输出局部对象a的副本的成员x的值23

8、当指向main函数的右大括号时,局部对象a 的副本才会消失,即系统会调用析构函数来释放局部对象a的副本所占用的内存空间。所以输出了“执行了析构函数!”。

      上面的分析中,有一个问题:就是为什么局部对象a的副本的生命会一直持续到main函数结束呢?这是因为对于引用而言,如果引用的是一个临时变量,那么这个临时变量的生存期不少于这个引用的生存期。针对于上面的例子而言就是:直到main函数结束时,引用ra的生存期结束,ra所引用的临时变量(局部对象a的副本)的生存期才结束,由于ra所引用的是一个类对象,因此,系统会自动调用该类的析构函数来释放其所占用的内存空间。但是,指针却没有这样的特性,假如将局部对象a的副本赋给一个指针,那么fun函数在返回局部对象a的副本时,就可以析构局部对象a的副本。具体可以看下面的例子:

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){cout << "执行构造函数创建一个对象\n"; x = i ;}
	//加上复制构造函数的目的是为了说明按值返回一个对象时会默认自动调用
	//复制构造函数创建该对象的一个副本
	A(A &a){cout << "执行复制构造函数创建一个对象\n"; x = a.x ;}
	~A(){cout << "执行了析构函数!\n" ;}
	int get() const {return x ;}
private:
	int x ;
};

A fun()
{
	cout << "跳转到fun函数中\n" ;
	A a(23) ;
	cout << "对象A的地址:\t\t" << &a << endl ;
	return a ;
}

int main()
{
	A *ra = &fun() ; //使用指针接受fun函数的返回值,注意这里fun函数前面的取地址符
	cout << "对象a的副本的地址:\t" << ra << endl ;
	cout << ra->get() << endl ;
	return 0 ;
}


程序运行的结果如下:

       注意上面画红线处。在fun函数结束了调用了两次析构函数,即将局部对象a和局部对象a的副本两个都释放了。这就是使用引用接收fun函数的返回值和使用指针接收fun函数的返回值的区别。

       但是这里还有一个重大的问题:就是我们最后看到程序输出了局部对象a的副本的成员x的值23,前面我们明明将局部对象a的副本释放了,怎么还可以输出它的成员值呢?

       这是因为析构函数调用并析构某个对象后,只是告诉编译器,这一块内存空间不再为某个对象所独占了,你可以访问它,别的对象或者变量也可以访问它并使用该内存空间来存放他们之间的数据,但是在它们使用之前,存放在该内存区域的数据并没有被删除,因此,我们还是可以使用指针来访问该内存区域任然能够得到未被修改的x的值。

 

2、内存泄露

我们首先演示这个错误,请看下面的例子:

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){cout << "执行构造函数创建一个对象\n"; x = i ;}
	//加上复制构造函数的目的是为了说明按值返回一个对象时会默认自动调用
	//复制构造函数创建该对象的一个副本
	A(A &a){cout << "执行复制构造函数创建一个对象\n"; x = a.x ;}
	~A(){cout << "执行了析构函数!\n" ;}
	int get() const {return x ;}
private:
	int x ;
};

A fun()
{
	cout << "跳转到fun函数中\n" ;
	A *p = new A(99) ;  //在堆中创建一个新的类A的对象
	cout << "堆中对象的地址:\t\t" << p << endl ;
	return *p ;
}

int main()
{
	A &ra = fun() ; //使用引用接受fun函数的返回值
	cout << "堆中对象的副本的地址:\t" << &ra << endl ;
	cout << ra.get() << endl ;
	return 0 ;
}


程序运行的结果:

       我们可以看到执行return语句时,调用了类A的复制构造函数,因为我们是按值返回类型。执行完类A的复制构造函数后,在fun函数中定义的局部指针p将会没有用,因为它会被系统自动销毁掉。p指针指向的是一块堆中空间,由于在堆中创建的空间,必须使用delete运算符才能被删除,因此p指向的堆中的空间成了不可访问的空间,结果导致了内存泄露。换句话说就是:p指针被删除了(不是使用delete),它指向的内存空间还是存在,该空间的地址只有p保存着,而p找不到了,所以我们无法找到该空间,由于无法找到该空间,所以我们无法对其进行释放,这样就造成了内存泄露。所以fun函数的返回的不是p指向的在堆中创建的对象,而是返回的是由复制构造函数创建的堆中对象的副本,关于这一点,我们可以看输出结果中划红线的两个地址值。是不一样的,说明这是两个不同的对象。由于我们以引用来接受fun函数的返回值,且引用会延长临时变量的生存期,所以当main结束时,有复制构造函数创建的堆中对象的副本才被销毁,所以输出了“执行了析构函数!”,由于堆中对象我们无法找到,无法对其占用的内存空间进行释放,所以我们看到只执行了一次析构函数。这个析构函数是对堆中对象的副本进行释放。那么我们如何释放堆中对象占用的内存空间呢?请看下面的示例:

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){cout << "执行构造函数创建一个对象\n"; x = i ;}
	//加上复制构造函数的目的是为了说明按值返回一个对象时会默认自动调用
	//复制构造函数创建该对象的一个副本
	A(A &a){cout << "执行复制构造函数创建一个对象\n"; x = a.x ;}
	~A(){cout << "执行了析构函数!\n" ;}
	int get() const {return x ;}
private:
	int x ;
};

A fun()
{
	cout << "跳转到fun函数中\n" ;
	A *p = new A(99) ;  //在堆中创建一个新的类A的对象
	cout << "堆中对象的地址:\t\t" << p << endl ;
	return *p ;
}

int main()
{
	A &ra = fun() ; //使用引用接受fun函数的返回值
	cout << "堆中对象的副本的地址:\t" << &ra << endl ;
	cout << ra.get() << endl ;
	A *rp = &ra ; //很多人会在这里添加这样一句,其实是错误的!!
	delete rp ;   //程序会崩溃
	return 0 ;
}

 

       上面的程序会崩溃,原因前面我们已经讲 了。这是因为fun函数返回的不是堆中的对象,而是堆中对象的副本,这个堆中对象我们已经找不到了,由于返回的是堆中对象的副本,这个副本是有复制构造函数创建的,所以这个副本是保存在栈中,不是在堆中。(要用new创建的对象才保存在堆中)。我们知道存放在栈中的数据都是有系统自动释放,而delete运算符删除的堆中空间,而不是栈中空间,所以导致了错误。

       另外需要再次强调的是,指向堆中空间的指针p是个局部变量,因此在fun函数返回时,它被系统自动释放掉了,然而由于它指向的堆中空间必须使用delete才能被删除,因此该空间就成了不可访问的空间,结果导致了内存泄露。

       那么,如何来解决这个问题呢?我们要避免内存泄露,我们就不能用按值返回的方式来返回一个堆中的对象。而必须以按地址或别名的方式返回一个内存地址或别名,这样就不会调用复制构造函数来创建该对象的副本,而是直接将该对象的别名或地址返回。由于返回的对象的别名或地址初始化给了main函数中的一个引用或者指针,因此即使被调用函数中的局部指针超出作用域被系统释放,也可由main函数中的引用或指针找堆中空间,不会令该空间成为不可访问的区域,从而避免了内存泄露。请看下面的例子:

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){cout << "执行构造函数创建一个对象\n"; x = i ;}
	//加上复制构造函数的目的是为了说明按值返回一个对象时会默认自动调用
	//复制构造函数创建该对象的一个副本
	A(A &a){cout << "执行复制构造函数创建一个对象\n"; x = a.x ;}
	~A(){cout << "执行了析构函数!\n" ;}
	int get() const {return x ;}
private:
	int x ;
};

A& fun()  //按引用返回
{
	cout << "跳转到fun函数中\n" ;
	A *p = new A(99) ;  //在堆中创建一个新的类A的对象
	cout << "堆中对象的地址:\t\t" << p << endl ;
	return *p ;
}

int main()
{
	A &ra = fun() ; //使用引用接受fun函数的返回值
	cout << "堆中对象的副本的地址:\t" << &ra << endl ;
	cout << ra.get() << endl ;
	A *rp = &ra ;  //正确
	delete rp ;   
	return 0 ;
}


程序运行的结果如下:

       我们可以程序的运行的结果:fun函数里面没有调用复制构造函数,且程序没有崩溃。说明堆中的对象所占的内存空间被正常释放了。还有一点就是:堆中对象的地址和和fun函数返回对象的地址是一样的,都是006B78D8。由于他们的地址是一样的,即使局部指针p在fun函数结束时被系统自动销毁了,但是我们还是可以通过main函数中的别名ra来访问到堆中的对象。即也就是说,对别名ra的操作也就是对堆中对象的操作。由于无法对引用使用delete运算符,因此,我们只能定义一个指针来存储引用的地址,然后删除该指针指向的内存空间。所以呢,调用了析构函数,输出了“执行析构函数了!”。

      这一切的操作都非常的顺利,程序也输出了正确的结果,但是这里却隐藏了一个非常严重问题:由于p指向的堆中的对象被删除了,因此堆中对象的别名ra变成了一个空别名。它是一个不存在的对象的别名。因此,假如我们再用这个别名ra来调用get函数时,程序输出一个随机值。

       这个错误不会被编译器捕获到,也不会令编译器崩溃,所以要检查到这个错误是很困难的。那么。到底我们应该怎么做呢?我们遵循一个原则:在哪里创建,就在哪里释放

#include <iostream>
using namespace std;

class A
{
public:
	A(int i){cout << "执行构造函数创建一个对象\n"; x = i ;}
	//加上复制构造函数的目的是为了说明按值返回一个对象时会默认自动调用
	//复制构造函数创建该对象的一个副本
	A(A &a){cout << "执行复制构造函数创建一个对象\n"; x = a.x ;}
	~A(){cout << "执行了析构函数!\n" ;}
	int get() const {return x ;}
	void set(int i){x = i ;}
private:
	int x ;
};

A& fun(A &a)  //按引用返回
{
	cout << "跳转到fun函数中\n" ;
	a.set(66) ;
	return a ;
}

int main()
{
	A *p = new A(99) ;  //在堆中创建一个新的类A的对象
	fun(*p) ;
	cout << p->get() << endl ;
	delete p ;
	return 0 ;
}


程序运行的结果:

       我们在main函数中创建了对象a,然后在main函数中释放了对象a所占的内存空间。fun函数返回的类型是按引用返回。这样就不会出现上面所讲的出现的所有的问题。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值