复制构造函数的用法及出现迷途指针问题

Posted 泡面小王子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了复制构造函数的用法及出现迷途指针问题相关的知识,希望对你有一定的参考价值。

 复制构造函数利用下面这行语句来复制一个对象

  A (A &a)

 从上面这句话可以看出,所有的复制构造函数均只有一个参数,及对同一个类的对象的引用

 

比如说我们有一个类A,定义如下:

 

1
2
3
4
5
6
7
8
9
10
class A
{
public:
A(int i,int j){n=i;m=j;} //构造函数带两个参数,并将参数的值分别赋给两个私有成员
A(A &t); //我们自己定义一个默认构造函数
void print(){cout<<n<<m;}//输出两个成员的值
private:
int n;  //数据成员n
int m; //数据成员m
};

 

      在上面这个类的定义中我们定义了一个默认的构造函数(虽然默认构造函数一般是通过编译器自动定义的,但是这里我们模拟一下它的工作过程)。默认构造函数的工作方法应该如下面所示:

 

1
2
3
4
A(A &t)
{
        n=t.n;m=t.m;
}

 

       在这里我们模拟了一个默认复制构造函数是如何运行的。它通过别名t访问一个对象,并将该对象的成员赋给新对象的成员。这样就完成了复制工作。这样一来,我们如果再程序中定义了:

 

A a(2,4);

 

       这个对象。这就表示,利用构造函数,对象a的数据成员a.n=2;a.m=4,如果我们利用下面这句话调用一个默认的复制构造函数:

 

A b(a)

 

       那么它就会调用复制构造函数,即上面我们模拟的复制构造函数中所定义的“n=t.n;m=t.m”。这时对象b的数据成员b.n=2;b.m=4,这与对象a中的数据成员的值应该是一模一样的。

 

迷途指针产生的原因:“浅层复制构造函数”

      一般地,编译器提供的默认复制构造函数的功能只是把传递进来的对象的每一个成员变量的值复制给了新对象的成员变量。但是,如果这个老对象是一个指针,那么新对象也是一个指针,且它指向的内存地址和老对象是一样的!这样就会产生2个显而易见的问题:

  1. 我们可以随意地对一个指针所指向的内存空间进行赋值操作。那么另外一个指针所指向的内存空间由于和前面那个指针式一模一样的,那么它就不可避免地被修改;
  2. 如果我们在程序中无意将老对象所指向的内存地址释放掉了,那么新对象的指针自然就变成了一个迷途指针。举一个形象的例子来说就是:如果B对象持用一个指针指向对象A, 现有一个B对象的拷贝C,那么它也持有一个指针p2指向A. 若某时, B释放了对象A, 但C是无法知晓的, C认为它持有的指针指向的A有效, 之后若C再调用A的方法就报错。
#include <iostream>

using namespace std;

class A 

{

public:
    A(){x=new int;*x=5;}  //创建一个对象的同时将成员指针指向的变量保存到 新空间中

    ~A(){delete x;x = NULL;}//析构对象的同时删除成员指针指向的内存空间并将指针赋为空
    A(A &a)

    {
        cout << "复制构造函数执行...\\n" <<endl;
        x = a.x;   //将旧对象的成员指针x指向的空间处的数据赋给新对象的成员指针x

    }

    void print(){cout<<*x<<endl;}

    void set(int i){*x=i;}

private:

    int *x;

};

int main() 
{
    A *a = new A(); //利用指针在堆中创建一个对象
    cout<<"a:";
    a->print();
    cout<<endl;

    A b=(*a); //这里初始化的对象为指针a所指向的堆中的对象
    //调用复制构造函数之后,将对象b变成对象a的一个拷贝,那么对象a和对象b所输出的值应该是一样的
    cout<<"b:";
    b.print();
    cout<<endl;
    //利用对象a中的成员函数set()将指针x指向的内存区域中的值改成32
    a->set(32);
    cout<<"a:";
    a->print();
    cout<<"b:";
    b.print();

    cout<<endl;

    //利用对象b中的成员函数set()将指针x指向的内存区域中的值改成99

    b.set(99);
    cout<<"a:";
    a->print();
    cout<<"b:";
    b.print();
    cout<<endl;
    delete a;
    return 0;

}

输出结果:

 

      上面红色框就代表了用a.set(32)来改变a中指针x所指向的内存空间的值,可以看出b的指针x所指向的内存空间的值也跟着变成了32。绿色框代表了用b.set(99)来改变b中指针x所指向的内存空间的值,可以看出a的指针x所指向的内存空间的值也跟着变成了99。而在程序崩溃对话框(白底黑字那个)中指明了程序的第52行引发了一个错误,大意就是那个内存区域是非法的(如上图中蓝色框所示)。出现这个错误的原因其实就是由迷途指针造成的:当main函数结束(右大括号)并析构对象b的时候,由于指针成员x所指向的内存区域已经在第52行被释放了,这样一来,对象b中的指针x就变成了迷途指针了,我们再次释放这块内存空间的时候肯定就会导致程序的崩溃。

上篇文章末尾谈到了指针悬挂的问题,这主要是由于浅层复制构造函数的原因。为了解决这个指针悬挂的问题,这时候我们就需要引进一个新的概念:深层复制构造函数。

      下面,我们来介绍一下浅层复制构造函数深层复制构造函数之间的区别与联系......

  • 浅层复制构造函数:浅层构造赋值函数主要是将传递进来的对象的成员变量的所有值赋值给新对象的成员变量。
     
    x=a.x;//把对象a中的指针成员变量x的值复制给了对象b中的指针成员变量x
          上面这个语句就会产生一个问题,即对象b的指针成员变量b.x和对象a的指针成员变量a.x所保存的值是同一块内存空间的地址。如果我们析构了对象a,那么编译器会自动释放该内存地址,而b并不知道,这样就产生了指针悬挂的问题。这就是由于浅层复制构造函数的运作机理产生的,它只是将旧对象的数据复制给新对象的数据,而如果是指针对象,它复制的就是指针所保存的地址。上面这句话是不是有点绕啊,我们用下图来解释一下:       从上面这个图就可以非常清楚地看到,当我 们析构掉对象a的时候,编译器会自动释放堆中所创建的内存空间。而对于对象b而言,它压根就不知道有编译器释放堆中内存这么一回事,所以自然b.x就变成迷途指针了。
  • 深层复制构造函数:浅层构造赋值函数主要功能虽然也是是将传递进来的对象的成员变量的所有值赋值给新对象的成员变量,但是有一点不同之处在于,先看程序:
    1
    2
    x=new int;//创建一块新空间
    *x=*(a.x);//把对象a中指针成员x所指向的值赋值给了利用深层赋值构造函数所创建的对象b的指针成员x
    由上面的程序可以看出,其实深层复制构造函数中我们只添加了为成员指针指向的数据成员分配内存,同时在赋值的时候,我们是把旧对象中指针成员所指向的值复制给了新对象的指针成员,而不是地址,这样就可以避免指针悬挂的问题了。你可能会问,上面这么长一串话是啥意思哦?不解释,我们直接上图!

      从上面这个图,我们就显而易见地看出深层复制构造函数的好处了!注意看如果我们析构了对象a,那么编译器只是会回收内存地址为A处得内存,而不会管内存地址为D处的值。这样就避免了利用浅层复制函数所产生的对象b的指针悬挂问题了。

以上是关于复制构造函数的用法及出现迷途指针问题的主要内容,如果未能解决你的问题,请参考以下文章

指针向量的复制构造函数

strcpy函数如何使用

Use-After-Free

如何获得指向类的复制构造函数的成员函数指针?

复制构造函数 C++ 没有正确复制指针

复制构造函数中的指针赋值