C++本质:类的赋值运算符=的重载,以及深拷贝和浅拷贝
关键词:构造函数,浅拷贝,深拷贝,堆栈(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 <iostream>
#include "stdio.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("\nUsername is %s\n", 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("\nA is destructed\n");
}
}

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("\ndeep 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\n",m_username,m_password);
}
~B()
{
delete m_password;
printf("B is destructed\n");
}
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("\nbegin 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 重载赋值运算符返回结果为类对象引用的运行结果
很明显,没有调用拷贝构造函数
转载来源:http://www.cnblogs.com/alexusli/archive/2008/08/27/1277683.html

赋值运算符重载和拷贝构造函数相关推荐

  1. c++操作符重载,拷贝构造函数和友…

    操作符重载 把c++的操作符在自己的新函数中重新实现就是操作符重载. 可以在类里面以成员函数的方式重载某个操作符,也可以在某个名字空间中重载操作符. ------------------------- ...

  2. 浅谈深浅拷贝问题(这里只针对拷贝构造函数和赋值运算符重载)和简易srting类模拟实现

    浅谈深浅拷贝问题(这里只针对拷贝构造函数和赋值运算符重载)和简易srting类模拟实现 文章目录 浅谈深浅拷贝问题(这里只针对拷贝构造函数和赋值运算符重载)和简易srting类模拟实现 一.什么是浅拷 ...

  3. 运算符重载——拷贝构造函数与赋值运算符的区别、如何实现赋值运算符的深拷贝

    目录: 运算符重载--算术运算符重载 运算符重载--递增运算符重载 运算符重载--左移运算符重载 运算符重载--赋值运算符重载 写在前面 1)C++的编译器会默认给一个类提供4个函数 默认构造函数(无 ...

  4. 类的6个默认成员函数:构造函数、析构函数、拷贝构造函数、重载运算符、三/五法则

    文章目录 6个默认成员函数 构造函数 概念 默认构造函数的类型 默认实参 概念 默认实参的使用 默认实参声明 全局变量作为默认实参 某些类不能依赖于编译器合成的默认构造函数 第一个原因 第二个原因 第 ...

  5. C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解

    目录 类的6个默认成员函数 一.构造函数 1.概念 2.特征如下: (1) 函数名与类名相同. (2)无返回值. (3)对象实例化时编译器自动调用对应的构造函数. (4)构造函数可以重载. (5)如果 ...

  6. C++ 拷贝构造函数和赋值运算符

    本文主要介绍了拷贝构造函数和赋值运算符的区别,以及在什么时候调用拷贝构造函数.什么情况下调用赋值运算符.最后,简单的分析了下深拷贝和浅拷贝的问题. 拷贝构造函数和赋值运算符 在默认情况下(用户没有定义 ...

  7. C++:拷贝构造函数与深/浅拷贝

    浅拷贝只是增加了一个指针指向已存在的内存地址, 深拷贝是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存, 拷贝后两者是同一个地址,则是浅拷贝,否则是深拷贝. 可以结合值传递和引 ...

  8. C++之继承探究(十二):子类的构造、析构和赋值运算符重载

    涉及到本文所讲知识点的博文: C++之析构函数探究 C++之常引用和浅拷贝探究 C++之一个函数链的简单例子(分文件实现) C++之Big Three:拷贝构造.拷贝赋值.析构函数探究 C++之操作符 ...

  9. C++ 拷贝构造函数和赋值构造函数

    在C++中复制控制是一个比较重要的话题,主要包括复制构造函数.重载赋值操作符.析构函数这三部分,这三个函数是一致的,如果类需要析构函数,则它也需要复制操作符 和 复制构造函数,这个规则被称为 C++的 ...

最新文章

  1. java错误代码1061_java.sql.SQLException
  2. SQLServer · 最佳实践 · RDS for SQL Server 2012 权限限制的提升与改善
  3. VS2017 ASP.NET MVC 5.0 开部署问题汇总
  4. 小学生python-用Python实现小学生四则运算
  5. Razor语法(一)
  6. 文巾解题 1418. 点菜展示表
  7. 微信小程序开发之scroll-view上拉加载数据实现
  8. linux copy_from/to_user原理
  9. SAP APF tile点击之后,都发生了哪些事情
  10. Spring JDBC数据库连接池设置
  11. 【CodeForces - 227B 】Effective Approach (STL,思维)
  12. 螺旋桨设计软件_第四届智能工业软件及设计技术研讨会暨2019天洑软件用户大会成功举办...
  13. 卸载docker后部署k8s后docker无法启动问题
  14. COCO数据集提取自己需要的类,转VOC
  15. Eclipse中Java编程时快速生成set和get方法
  16. redis简单运用,数据类型,适合入门
  17. 机器学习-样本集(包括训练集及测试集)的选取
  18. MSIL实用指南-一维数组的操作
  19. 【计算机基础】思维导图
  20. 零基础应该怎么学习商业插画?

热门文章

  1. Linux常用文本编辑器,及文本查看摘选的常用命令
  2. 2019年一月十日 Mixin Network 资产持有量快照
  3. 晶振、时钟周期、机器周期
  4. 推荐多款免费的开源建站系统和内容管理系统
  5. 在MATLAB中调用 Python
  6. 三、Camunda工作流的表和用途说明(实践是检验真理的唯一标准)
  7. 【UWB 定位】高精度定位
  8. android p支持4.1,版本过老 Android P将不再支持这类应用
  9. 2020年HIT行业企业营收排行榜(转载)
  10. PL/SQL Developer中用dbms_output.put_line输出