CPP学习之——引用一个按别名返回的堆中对象(9.18)

一 概述

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

二 上节错误的修正

2.1 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include<iostream>
using namespace std;
class A
{
private:
int x;
public:
A(int i)
{
cout << "执行构造函数创建一个对象\n";
x = i;
}
A(const A &a)
{
x = a.x;
cout << "执行复制构造函数创建对象\n";
}
~A() {cout << "执行析构函数!\n";}
int get() const {return x;}
};
A& func()
{
cout << "跳转到func函数中!\n";
A *p=new A(99);
cout << "对象a的地址:" << p << endl;
return *p;
}
int main()
{
const A&r=func();
cout << "对象a的副本的地址:" << &r << endl;
cout << r.get() << endl;
const A *p=&r;
delete p;
cout<<r.get()<<endl;
return 0;
}

2.2 输出结果

1
2
3
4
5
6
跳转到func函数中!
执行构造函数创建一个对象
对象a的地址:0x3378d0
对象a的副本的地址:0x3378d0
99
执行析构函数!

2.3 代码说明

  • 我们再来看一下这个堆中对象的地址,和从func函数返回的对象的地址,我们看到这两个地址都是相同的
  • 由于它们的地址相同,所以即使func函数中的局部指针p被系统自动销毁了,我们也能通过main函数中的别名r来访问到堆中对象
  • 因为r与堆中对象的地址是相同 ,r是堆中对象的别名,所以对r的操作就是对堆中对象的操作
  • 由于无法对引用使用delete运算符,因此我们只能定义一个指针来存储引用的地址,然后删除该指针指向的内存空间
  • "执行析构函数"证明析构函数被调用,同时证明堆中对象已经被销毁
  • 这一切操作都非常顺利,程序也输出了正确的结果。但是这个程序却隐藏着一个非常严重的问题:由于p所指向的堆中对象被删除了,因此堆中对象的别名r成了空别名。A *p=&r中,r是一个不存在的对象的别名,因此假如再使用这个空别名去访问不存在 的对象的成员函数get()时,由于r所引起的对象已经不存在,所以再次访问该对象的X成员时,输出一个随机数,这样就导致了一个不宜觉察的错误,这个错误不易被编译器捕获到,也不会令程序奔溃,所以要检测到这个错误很困难