Spiga

c++对象成员函数返回自身引用时出现的诡异问题及解决办法

2010-10-14 14:05 by BAsil, 1319 visits, 收藏, 编辑
list.h (实际上是数据结构顺序表的一个例子,为了展现问题,我简化了部分代码,只留下插入和打印)
#include<iostream>
using namespace std;
class List
{
public:
    List(int max_list_size)
    {
      max_size=max_list_size;
      data=new int[max_size];
      n=0;
    }
    ~List()
    {
        delete[]data;
    }
    bool empty()//const
    {
        return n==0;
    }
    int size()
    {
        return n;
    }
    List insert(int k,int &x)
    {
        for(int i=n-1;i>=k;i--)
            data[i+1]=data[i];
        data[k]=x;
        //此处有值
        cout<<"data inserted phase"<<endl;
        cout<<"data[0]="<<data[0]<<endl;
        n++;
        return *this;
    }
    void print_list() //const;
    {
      //此处没值
      cout<<"print phase"<<endl;
      for(int i=0;i<n;i++)
      {
        cout<<"data["<<i<<"]="<<data[i]<<endl;
      }
    }

private:
    int n;
    int max_size;
    int *data;
};

main函数

#include<iostream>
using namespace std;
#include"List.h"
int main()
{

    List list(5);
    int t;
    cout<<"please enter a integer and press enter"<<endl;
    cin>>t;
    cout<<t<<endl;
    list.insert(0,t);
    list.print_list();
    system("pause");
    return 0;
}

运行时输入2 回车 结果如图

result

你能看出问题在哪里吗?

当我把成员函数insert的返回值类型 由 List 改为List & 结果就正确了

result1

下面我们分析下出现该问题的原因,以及应对方法

首先大家要了解的是函数的返回值的传递方式,这里大家应该牢记的是函数返回值的传递和函数传递参数的方式是基本一样的,关于函数传递参数的方式请参见C++指针存储结构

函数返回值的传递也分为按值传递和按引用传递两种

示例代码中成员函数insert的函数原型为List insert(int k,int &x) 为按值传递,所以在返回值实际上复制了一个新的对象x,那么这个新的对象和原有的调用该函数的对象(假设为l)有什么区别和联系?

要回答这个问题就要了解一下拷贝构造函数,拷贝构造函数会在赋值(=)以及函数按值传递参数或者按值传递返回值的时候调用;当类中没有拷贝构造函数时,会调用默认的拷贝构造函数。一般情况下,默认构造函数会对源对象的每个数据成员一一赋值给目标对象的同一数据成员,但如果数据成员包含指针,可能导致严重问题。

List list(5);
int t;
cin>>t;    
list.insert(0,t);

当调用list.insert方法时,由于返回值是按值传递的,所以会调用默认拷贝构造函数复制一个新的对象(假设为x),但由于List类数据成员包含指针,会出现如下情况

ref1

目前好像看不出什么问题,接下来有问题了,由于该成员函数的返回对象x(实际上是临时对象)并没有赋给任何变量,所以会立即调用析构函数析构掉,析构函数的代码

~List()
    {
        delete[]data;
    }

析构以后如图

ref2

此时对象l的data指针却还指向那块被释放掉了的区域,形成了“虚悬引用”,也就是俗称的“野指针”。所以也当查看对象l的data[0]是变成一个奇怪的值。

问题的解决是加上自己的拷贝构造函数

List(const List& right):n(right.n),max_size(right.max_size)
{
        data = new int [max_size];
        for (int i=0; i<n;i++)
        {
            data[i]=right.data[i];
        }
}

此时调用 l.insert时,内存存储示意图如下

ref3

由于x是临时对象会接着调用析构函数

ref4 

增加了拷贝构造函数只是能够让结果正确的显示,但是在实际开发中,很少采用函数按值返回的形式返回一个对象(相当于赋值一份),浪费了空间不说,还容易造成上述的问题,解决的办法是按引用返回,做如下修改

List&  insert(int k,int &x)
    {
        for(int i=n-1;i>=k;i--)
            data[i+1]=data[i];
        data[k]=x;
        cout<<"data inserted phase"<<endl;
        cout<<"data[0]="<<data[0]<<endl;
        n++;
        return *this;
    }

把List改为List& ,引用就相当于一个别名(我的理解是隔山打牛,参见C++指针存储结构),至此问题解决。

Add your comment

10 条回复

  1. #1楼 嗷嗷      2010-10-14 16:25
    List list1(5);
    List list2(list1); //调用拷贝构造
    List list3 = list1; //调用拷贝构造
    List list4(1);
    list4 = list1;//这叫赋值,不调用拷贝构造,调用operator =
     回复 引用 查看   
  2. #2楼 南柯之石      2010-10-14 16:55
    @嗷嗷
    你是想用这几行来做个总结么?
     回复 引用 查看   
  3. #3楼 陈梓瀚(vczh)      2010-10-14 17:38
    - -b一眼就看出来insert应该返回引用了……
     回复 引用 查看   
  4. #4楼[楼主] BAsil      2010-10-14 18:32
    @嗷嗷
    说的没错啊
     回复 引用 查看   
  5. #5楼[楼主] BAsil      2010-10-14 18:33
    @陈梓瀚(vczh)
    @南柯之石
    这些都是比较基础的,我是想通过这些说明为什么应该返回引用而不是返回值。通过一些小例子可以引导大家知其所以然
     回复 引用 查看   
  6. #6楼 嗷嗷      2010-10-14 20:34
    引用BAsil:
    @嗷嗷
    说的没错啊

    "拷贝构造函数会在赋值(=)以及函数按值传递参数或者按值传递返回值的时候调用"
    这句话不准确,赋值的时候不会调用拷贝构造函数,调用的是operator =
    既然写了拷贝构造的深拷贝的问题,那么operator =自然也该一起总结了
     回复 引用 查看   
  7. #7楼[楼主] BAsil      2010-10-14 22:54
    @嗷嗷
    谢谢啊,我这方面还是学的不精,回头还得仔细再看看operator =。
     回复 引用 查看   
  8. #8楼[楼主] BAsil      2011-11-30 11:18
    引用嗷嗷:
    List list1(5);
    List list2(list1); //调用拷贝构造
    List list3 = list1; //调用拷贝构造
    List list4(1);
    list4 = list1;//这叫赋值,不调用拷贝构造,调用operator =

    呵呵,现在才明白了,多谢啊
     回复 引用 查看   
  9. #9楼 嗷嗷      2011-11-30 12:20
    @BAsil
    我倒,这都一年多了
     回复 引用 查看   
  10. #10楼[楼主] BAsil      2011-11-30 17:18
    引用嗷嗷:
    @BAsil
    我倒,这都一年多了

    翻以前的内容,刚看到,呵呵
     回复 引用 查看