zoukankan      html  css  js  c++  java
  • C++ 中赋值运算符重载以及深拷贝浅拷贝解析

    转载自:http://blog.csdn.net/business122/article/details/21242857

    关键词:构造函数,浅拷贝,深拷贝,堆栈(stack),堆heap,赋值运算符
    摘要:
        在面向对象程序设计中,对象间的相互拷贝和赋值是经常进行的操作。
        如果对象在申明的同时马上进行的初始化操作,则称之为拷贝运算。例如:
            class1 A("af"); class1 B=A;
         此时其实际调用的是B(A)这样的浅拷贝操作。
        如果对象在申明之后,在进行的赋值运算,我们称之为赋值运算。例如:
            class1 A("af"); class1 B;
            B=A;
            此时实际调用的类的缺省赋值函数B.operator=(A);
            不管是浅拷贝还是赋值运算,其都有缺省的定义。也就是说,即使我们不overload这两种operation,仍然可以运行。
    那么,我们到底需不需要overload这两种operation 呢?
            答案就是:一般,我们我们需要手动编写析构函数的类,都需要overload 拷贝函数和赋值运算符。

            
     下面介绍类的赋值运算符
    1.C++中对象的内存分配方式
            在C++中,对象的实例在编译的时候,就需要为其分配内存大小,因此,系统都是在stack上为其分配内存的。这一点和C#完全不同!千万记住:在C#中,所有类都是reference type,要创建类的实体,必须通过new在heap上为其分配空间,同时返回在stack上指向其地址的reference.
            因此,在C++中,只要申明该实例,在程序编译后,就要为其分配相应的内存空间,至于实体内的各个域的值,就由其构造函数决定了。
        例如:
        

    class A
    {
    public:
        A()
        {
        }
        A(int id,char *t_name)
        {
        _id=id;
        name=new char[strlen(t_name)+1];
        strcpy(name,t_name);
        }
        private:
            char *username;
            int _id;
    }

    int main()
    {
    A a(1,"herengang");
    A b;
    }


    在程序编译之后,a和b在stack上都被分配相应的内存大小。只不过对象a的域都被初始化,而b则都为随机值。
    其内存分配如下:


    2. 缺省情况下的赋值运算符
        如果我们执行以下:
        b=a;
            则其执行的是缺省定义的缺省的赋值运算。所谓缺省的赋值运算,是指对象中的所有位于stack中的域,进行相应的复制。但是,如果对象有位于heap上的域的话,其不会为拷贝对象分配heap上的空间,而只是指向相同的heap上的同一个地址。
            执行b=a这样的缺省的赋值运算后,其内存分配如下:

            因此,对于缺省的赋值运算,如果对象域内没有heap上的空间,其不会产生任何问题。但是,如果对象域内需要申请heap上的空间,那么在析构对象的时候,就会连续两次释放heap上的同一块内存区域,从而导致异常。

        ~A()
        {        
            delete name;
        }


    3.解决办法--重载(overload)赋值运算符
            因此,对于对象的域在heap上分配内存的情况,我们必须重载赋值运算符。当对象间进行拷贝的时候,我们必须让不同对象的成员域指向其不同的heap地址--如果成员域属于heap的话。    
    因此,重载赋值运算符后的代码如下:

    class A
    {
    public:

        A()
        {
        }
        A(int id,char *t_name)
        {
            _id=id;
            name=new char[strlen(t_name)+1];
            strcpy(name,t_name);
        }
        
        A& operator =(A& a)
    //注意:此处一定要返回对象的引用,否则返回后其值立即消失!
        {
                if(name!=NULL)
                    delete name;
            this->_id=a._id;
            int len=strlen(a.name);
            name=new char[len+1];
            strcpy(name,a.name);
            return *this;
        }

        ~A()
        {
            cout<<"~destructor"<<endl;
            delete name;
        }

        int _id;
        char *name;
    };

    int main()
    {
     A a(1,"herengang");
     A b;
     b=a;
    }

    其内存分配如下:

    这样,在对象a,b退出相应的作用域,其调用相应的析构函数,然后释放分别属于不同heap空间的内存,程序正常结束。


    references:
    类的深拷贝函数的重载
        public class A
    {
        public:
            ...
            A(A &a);//重载拷贝函数
            A& operator=(A &b);//重载赋值函数
            //或者 我们也可以这样重载赋值运算符 void operator=(A &a);即不返回任何值。如果这样的话,他将不支持客户代买中的链式赋值 ,例如a=b=c will be prohibited!
        private:
            int _id;
            char *username;
    }

    A::A(A &a)
    {
        _id=a._id;
        username=new char[strlen(a.username)+1];
        if(username!=NULL)
            strcpy(username,a.usernam);
    }

    A& A::operaton=(A &a)
    {
            if(this==&a)//  问:什么需要判断这个条件?(不是必须,只是优化而已)。答案:提示:考虑a=a这样的操作。
                return *this;
            if(username!=NULL)
                delete username;
            _id=a._id;
            username=new char[strlen(a.username)+1];
            if(username!=NULL)
                strcpy(username,a.usernam);
            return *this;    
    }
    //另外一种写法:
    void A::operation=(A &a)
    {
            if(username!=NULL)
                delete username;
            _id=a._id;
            username=new char[strlen(a.username)+1];
            if(username!=NULL)
                strcpy(username,a.usernam);
    }

    其实,从上可以看出,赋值运算符和拷贝函数很相似。只不过赋值函数最好有返回值(进行链式赋值),返回也最好是对象的引用(为什么不是对象本身呢?note2有讲解), 而拷贝函数不需要返回任何。同时,赋值函数首先要释放掉对象自身的堆空间(如果需要的话),然后进行其他的operation.而拷贝函数不需要如此,因为对象此时还没有分配堆空间。 

    note1:
        不要按值向函数传递对象。如果对象有内部指针指向动态分配的堆内存,丝毫不要考虑把对象按值传递给函数,要按引用传递。并记住:若函数不能改变参数对象的状态和目标对象的状态,则要使用const修饰符 

    note2:问题:
        对于类的成员需要动态申请堆空间的类的对象,大家都知道,我们都最好要overload其赋值函数和拷贝函数。拷贝构造函数是没有任何返回类型的,这点毋庸置疑。 而赋值函数可以返回多种类型,例如以上讲的void,类本身class1,以及类的引用 class &? 问,这几种赋值函数的返回各有什么异同?
        答:1 如果赋值函数返回的是void ,我们知道,其唯一一点需要注意的是,其不支持链式赋值运算,即a=b=c这样是不允许的!
              2 对于返回的是类对象本身,还是类对象的引用,其有着本质的区别!
                  第一:如果其返回的是类对象本身。
       A operator =(A& a)
        {
                if(name!=NULL)
                    delete name;
            this->_id=a._id;
            int len=strlen(a.name);
           name=new char[len+1];
            strcpy(name,a.name);
            return *this;
        }
              其过程是这样的:
                           class1 A("herengnag");
                            class1 B;   
                            B=A;
                        看似简单的赋值操作,其所有的过程如下:
                           1 释放对象原来的堆资源
                           2 重新申请堆空间
                           3 拷贝源的值到对象的堆空间的值
                           4 创建临时对象(调用临时对象拷贝构造函数),将临时对象返回
                           5. 临时对象结束,调用临时对象析构函数,释放临时对象堆内存
    my god,还真复杂!!
                但是,在这些步骤里面,如果第4步,我们没有overload 拷贝函数,也就是没有进行深拷贝。那么在进行第5步释放临时对象的heap 空间时,将释放掉的是和目标对象同一块的heap空间。这样当目标对象B作用域结束调用析构函数时,就会产生错误!!
                因此,如果赋值运算符返回的是类对象本身,那么一定要overload 类的拷贝函数(进行深拷贝)!
                第二:如果赋值运算符返回的是对象的引用,
       A& operator =(A& a)
        {
                if(name!=NULL)
                    delete name;
            this->_id=a._id;
            int len=strlen(a.name);
           name=new char[len+1];
            strcpy(name,a.name);
            return *this;
        }
            那么其过程如下:
                       1 释放掉原来对象所占有的堆空间
                       1.申请一块新的堆内存
                       2 将源对象的堆内存的值copy给新的堆内存
                       3 返回源对象的引用
                        4 结束。
        因此,如果赋值运算符返回的是对象引用,那么其不会调用类的拷贝构造函数,这是问题的关键所在!!
     
    完整代码如下:

    复制代码
    // virtual.cpp : Defines the entry point for the console application.
    //

    #include "stdafx.h"
    #include "string.h"
    #include "stdlib.h"
    #include "assert.h"

    class complex
    {
    public:
            int real;
            int virt;
    public:
        complex(){real=virt=0;}
        complex(int treal,int tvirt){real=treal;virt=tvirt;}
        complex operator+(const complex &x)
        {
            real+=x.real;
            virt+=x.virt;
            return *this;
        }
        complex operator=(const complex &x)
        {
            return complex(x.real,x.virt);
        }
    };


    class A
    {
    public:
        A(){m_username=NULL;printf("null constructor");}
        A(char *username)
        {
            int len;
            len=strlen(username);
            m_username=new char[len+1];//(char*)malloc(sizeof(len+1));
            strcpy(m_username,username);
            printf(" Username is %s ",m_username);
        }
        
        A(A &a);
        A operator=(A &b);
        int test(const int &x)
        {
            return x;
        }

        virtual ~A()
        {
        //    if(m_username)
            {
            delete m_username;
            printf(" A is destructed ");
            }
        }



    protected:
        char *m_username;

    };



    A::A(A &a)
    {

        int len=strlen(a.m_username);
        this->m_username=new char[len+2];
        strcpy(m_username,a.m_username);
        strcat(m_username,"f");
        printf(" deep copy function");
    }


    A A::operator=(A &b)
    {
        if(m_username)
            delete m_username;

        int len=strlen(b.m_username);
        this->m_username=new char[len+1];
        strcpy(m_username,b.m_username);
    //    printf("copied successfully!");
         return *this;
    }

     

    class B:public A
    {
    public:
        B(char *username,char *password):A(username)
        {
            int len=strlen(password)+1;
            m_password=new char[len];//(char *)malloc(sizeof(len));
            strcpy(m_password,password);
            printf("username:%s,password:%s ",m_username,m_password);
        }
        ~B()
        {
            delete m_password;
            printf("B is destructed ");
        }
    protected:
        char *m_password;
    };

    int main(int argc, char* argv[])
    {
    //    B b("herengang","982135");
    //    A *a=&b;
    //    delete a;
        A a("haha");
        A b;

        printf(" begin to invoke copy function");
        b=a;

    //    printf("%d",b.test(2));
        //complex x(1,3),y(1,4);
        //x=(x+y);
        //printf("%d,%d",x.real,x.virt);
        return 0;


    }

    复制代码

    1 重载赋值运算符返回结果为类对象的运行结果


    明显, 运算符最后调用了拷贝构造函数

    2 重载赋值运算符返回结果为类对象引用的运行结果

    很明显,没有调用拷贝构造函数

  • 相关阅读:
    ffmpeg rtmp推流 视频转码
    java日志发展史 log4j slf4j log4j2 jul jcl 日志和各种桥接包的关系
    nginx stream 流转发,可以转发rtmp、mysql访问流,转发rtmp、jdbc请求
    java web http 转https 通过nginx代理访问
    linux 服务器磁盘挂载
    novnc 通过websockify代理 配置多点访问
    linux 文件服务 minio 安装部署配置
    AOP实现原理,手写aop
    java 泛型
    JAVA反射getGenericSuperclass()用法
  • 原文地址:https://www.cnblogs.com/shilinnpu/p/8119046.html
Copyright © 2011-2022 走看看