本篇文章旨在阐述C++类的构造,拷贝构造,析构机制,以及指针成员变量指针悬空问题的解决。需要读者有较好的C++基础,熟悉引用,const的相关知识。

引言:

类作为C++语言的一种数据类型,是对C语言结构体的一种扩展。由于C++是面向过程与面向对象的混合语言,因此在使用面向对象思想解决现实问题模型时,设计好类是很重要的(跑题了)。关于类,这篇blog中有很好的介绍(链接http://blog.csdn.net/zqixiao_09/article/details/51474556)。我要介绍的是,关于创建一个空类,类体内都包含哪些成员函数呢?看下面例子 。

class MyClass { //创建一个空类MyClass

};
void main()
{
MyClass c; //创建该类的对象c,此处会自动调用默认构造函数
MyClass d(c); //创建一个对象d,并且用已经存在的同类对象c去初始化d,此处调用了默认拷贝构造函数
MyClass e; //创建一个对象e
e = c; //此处是对象赋值,调用了默认赋值运算符成员函数
}

那么我们来运行一下

可以看到是成功的。

以上实例说明,对于用户定义的空类,该类会自动包含六个成员函数,分别是:

l  默认构造函数 A(){//空函数体}

l  默认拷贝构造函数(本次讲解重点)A(const A & ){//简单的对象成员变量赋值操作}

l  默认析构函数 ~A(){//空函数体}

l  赋值运算符重载成员函数(本次讲解重点) A & operator =(const A &){//也是简单的对象成员变量赋值操作}

l  取地址操作符重载成员函数

l  Const修饰的取地址操作符重载成员函数

前四个是本次讲解的内容,重点放在拷贝构造,赋值运算符重载这两个成员函数

拷贝构造函数:

拷贝构造函数是一种特殊的构造函数,具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显示使用拷贝构造函数。归结来说。有三个场合要用到拷贝构造函数:

l  对象作为函数的参数,以值传递的方式传给函数

l  对象作为函数的返回值,以值传递的方式从函数返回调用处

l  使用一个对象去初始化一个新建的对象

有拷贝构造函数的调用一定会有新对象生成

还有一点需要注意的是,拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。

例子:

#include<iostream.h>
#include<string.h>
class Person{
public :
Person(); //无参构造函数
Person(int age,char na[]); //重载一般构造函数
Person(const Person & p);//拷贝构造函数
~Person(); // 析构函数
void disp();
private :
int age;
char *name;
};
Person::Person(){
age=0;
name=new char[2];
strcpy(name,"\0");
cout<<"default constructor\n";}
Person::Person(int age,char na[])
{
this->age=age;
name=new char[strlen(na)+1]; //为指针变量动态分配空间
strcpy(name,na); //赋值
cout<<"constructor\n";
}
Person::Person(const Person & p)
{
this->age=p.age;
this->name=new char[strlen(p.name)+1];
strcpy(name,p.name);
cout<<"copy constructor\n";
}
Person::~Person()
{
delete [] name;
cout<<"destroy\n";
}
void Person::disp()
{
cout<<"age "<<age<<" name "<<name<<endl;
}
void f(Person p)
{
cout<<"enter f \n";
p.disp();
return ;
}
Person f1()
{
cout<<"enter f \n";
Person p;
cout<<"next is return object of Person\n";
return p;
}
void main()
{
Person p1(21,"xiaowang");//调用一般构造函数
p1.disp();
Person p2(p1);//调用拷贝构造函数
p2.disp();
Person p3=p1;//调用拷贝构造函数
p3.disp();
cout<<"true\n";
cout<<"拷贝构造函数调用在函数形参是对象且值传递\n";
f(p1); //①
cout<<"拷贝构造函数调用在函数返回值是对象且值传递\n";
f1(); //②
cout<<"主函数结束,调用三次析构函数销毁对象\n";
}

运行结果

我们来分析一下源程序①②处以及运行结果的画线处

①  处是函数形参是对象,且是值传递的情况下调用了拷贝构造函数,我们可以看到该形参对象的生存期是只在函数f里面,当函数调用结束后,就自动被析构函数清理了。但是不会引起指针悬空问题,因为如下图所示。

其中p对象是f的形参,它由主函数调用f开始存在,由函数f调用结束而撤销,但是析构p时不会将p1的name所指空间析构,因此最终主函数main救赎后析构p1时不会引起指针悬空问题

②  函数返回值是对象且值传递返回方式时会调用靠宝贝构造函数。

分析结果会看到有两次对象创建,在子函数f1里面先创建默认对象p,然后返回对象p到调用处,会自动调用拷贝构造,创建一个匿名的对象(记为pi),调用结束后会先析构p,在析构pi

赋值运算符重载成员函数

拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。

实例:

#include<iostream.h>
const int MAX=;
class Array{
double * data;
public:
Array();
Array(const Array &a);
~Array();
double & operator [](int i); //下标重载运算符
Array & operator =(Array & a); //=重载赋值运算符
Array & operator +(Array& a); //+运算符重载成员函数
Array & operator -(Array & a); //-运算符重载成员函数
void disp(); //输出一个数组
};
Array::Array()
{
int i;
data=new double[MAX];
for(i=;i<MAX;i++)
data[i]=;
cout<<"construct"<<endl;
}
Array::Array(const Array &a)
{
data=a.data;
cout<<"copy construct \n";
}
Array::~Array()
{
delete [] data;
cout<<"destroy"<<endl;
}
double& Array::operator [](int i) //返回引用类型,可以是左值
{
return *(data+i);
}
Array& Array::operator =(Array &a) //=重载赋值运算符
{
int i;
for(i=;i<MAX;i++)
data[i]=a.data[i];
cout<<"对象赋值,调用赋值运算符重载函数\n";
return *this;
}
Array & Array::operator +(Array& a)
{
int i;
static Array tmp;
for(i=;i<MAX;i++)
tmp.data[i]=data[i]+a.data[i];
return tmp;
}
Array & Array::operator -(Array & a)
{
for(int i=;i<MAX;i++)
data[i]-=a.data[i];
return *this;
}
void Array::disp()
{
for(int i=;i<MAX;i++)
cout<<data[i]<<" ";
cout<<endl;
} void main()
{
Array a,b,c,d;
cout<<"创建四个数组对象\n";
cout<<"给数组a赋部分值\n";
a[]=;
a[]=;
a[]=;
a[]=;
cout<<"a=";a.disp();
cout<<"执行b=a\n";
b=a;
cout<<"b=";b.disp();
cout<<"执行c=a+b\n";
c=a+b;
cout<<"c=";c.disp();
cout<<"执行c=a+b之后a,b结果:\n";
cout<<"a=";a.disp();
cout<<"b=";b.disp();
cout<<"执行d=a-b\n";
d=a-b;
cout<<"d=";d.disp();
cout<<"执行d=a-b之后a,b结果:\n";
cout<<"a=";a.disp();
cout<<"b=";b.disp();
cout<<"主函数执行完毕,销毁四个对象和静态成员对象\n";
}

运行结果

分析:

从结果可以看出,如果函数的形参是对象,或者返回值是对象,但是是以引用传递的方式,那么靠诶构造函数就不会被调用,这也是引用的作用,即对同一个对象起别名。,但要注意在赋值运算符重载成员函数中,对象的定义为静态变量,这是为了防止子函数调用已结束就将析构该对象导致指针悬空问题。

深拷贝与浅拷贝

深拷贝和浅拷贝主要是针对类中的指针动态分配的空间来说的,因为对于指针只是简单的值复制并不能分割开两个对象的关联,任何一个对象对该指针的操作都会影响到另一个对象。这时候就需要提供自定义的深拷贝的拷贝构造函数,消除这种影响。通常的原则是:

  • 含有指针类型的成员或者有动态分配内存的成员都应该提供自定义的拷贝构造函数
  • 在提供拷贝构造函数的同时,还应该考虑实现自定义的赋值运算符

对于拷贝构造函数的实现要确保以下几点:

  • 对于值类型的成员进行值复制
  • 对于指针和动态分配的空间,在拷贝中应重新分配分配空间
  • 对于基类,要调用基类合适的拷贝方法,完成基类的拷贝
  • 拷贝构造函数和赋值运算符的行为比较相似,却产生不同的结果;拷贝构造函数使用已有的对象创建一个新的对象,赋值运算符是将一个对象的值复制给另一个已存在的对象。区分是调用拷贝构造函数还是赋值运算符,主要是否有新的对象产生。
  • 关于深拷贝和浅拷贝。当类有指针成员或有动态分配空间,都应实现自定义的拷贝构造函数。提供了拷贝构造函数,最后也实现赋值运算符。

总结:

  • 拷贝构造函数和赋值运算符的行为比较相似,却产生不同的结果;拷贝构造函数使用已有的对象创建一个新的对象,赋值运算符是将一个对象的值复制给另一个已存在的对象。区分是调用拷贝构造函数还是赋值运算符,主要是否有新的对象产生。
  • 关于深拷贝和浅拷贝。当类有指针成员或有动态分配空间,都应实现自定义的拷贝构造函数。提供了拷贝构造函数,最后也实现赋值运算符。

C++类指针类型的成员变量的浅复制与深复制的更多相关文章

  1. 福利->KVC+Runtime获取类/对象的属性/成员变量/方法/协议并实现字典转模型

    我们知道,KVC+Runtime可以做非常多的事情.有了这个,我们可以实现很多的效果. 这里来个福利,利用KVC+Runtime获取类/对象的所有成员变量.属性.方法及协议: 并利用它来实现字典转模型 ...

  2. C++ 类中特殊的成员变量(常变量、引用、静态)的初始化方法

    有些成员变量的数据类型比较特别,它们的初始化方式也和普通数据类型的成员变量有所不同.这些特殊的类型的成员变量包括: a.引用 b.常量 c.静态 d.静态常量(整型) e.静态常量(非整型) 常量和引 ...

  3. 张超超OC基础回顾03_结构体类型作为成员变量的特殊用法

    直接上例子: 要求: 合理的设计一个”学生“类 学生有* 姓名* 生日两个属性和说出自己姓名生日方法  要求利用设计的学生类创建学生对象,并说出自己的姓名和年龄 描述学生类 事物名称: 学生(Stud ...

  4. final 关键字:用来修饰类,方法,成员变量,局部变量

    final 关键字:用来修饰类,方法,成员变量,局部变量 表示最终的不可变的 1.final修饰一个类 表示当前的类不能有子类,也就是不能将一个类作为父类 格式: public final class ...

  5. 带有public static void main方法的类,其中的成员变量必须是static的,否则main方法没法调用。除非是main里的局部变量。因为main方法就是static的啊。

    带有public static void main方法的类,其中的成员变量必须是static的,否则main方法没法调用.除非是main里的局部变量.因为main方法就是static的啊.

  6. OC语法2——OC的类,方法,成员变量的创建

    类的创建: 与Java不同的是,OC创建一个类需要两个文件(.h和.m文件) 1> xxx.h:声明文件.用于声明成员变量和方法.关键字@interface和@end成对使用. 声明文件只是声明 ...

  7. java中的反射机制,以及如何通过反射获取一个类的构造方法 ,成员变量,方法,详细。。

    首先先说一下类的加载,流程.只有明确了类这个对象的存在才可以更好的理解反射的原因,以及反射的机制. 一.  类的加载 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三 ...

  8. 解析C++普通局部变量与指针类型的对象变量的应用区别

    首先我们先来了解一下MFC中的CPtrArray类,他可以说是CObject类型指针对象的集合.通过intAdd( CObject* newElement );注意参数是一个指针类型)可以向集合中添加 ...

  9. Java学习日记基础篇(四)——类,对象之成员变量,成员方法,构造方法

    面向对象(Object Oriented) 一.面向对象杂谈 面向对象(Object Oriented),我的翻译是以物体为目标的,就是说编程的时候是建立一个物体,然后对这个物体进行操作. Java语 ...

随机推荐

  1. bzoj2441【中山市选】小W的问题

    题目描述 有一天,小W找了一个笛卡尔坐标系,并在上面选取了N个整点.他发现通过这些整点能够画出很多个“W”出来.具体来说,对于五个不同的点(x1, y1), (x2, y2), (x3, y3), ( ...

  2. 【线段树】【CF1083C】 Max Mex

    Description 给定一棵有 \(n\) 个点的树,每个节点有点权.所有的点权构成了一个 \(0~\sim~n - 1\) 的排列.有 \(q\) 次操作,每次操作 \(1\) 为交换两个点的点 ...

  3. Zabbix利用msmtp+mutt发送邮件报警

    操作系统:CentOS 7 Web环境:Nginx+MySQL+PHP zabbix版本:zabbix-2.4.8.tar.gz 邮件服务:msmtp-1.4.32.tar.bz2     #http ...

  4. Mat中两种像素遍历方法比较

    小白,入门中,不足其指正.刚刚接触opencv,从一个Matlab风格的编程环境突然跳转到C++,实在有些不适.单就pixels scanning花了好长时间研究.opencv-tutorials给出 ...

  5. 布谷鸟搜索算法CS

    0 引言 布谷鸟搜索(Cuckoo Search,CS)是由 Xin-She Yang 和 Suash Deb 于 2009 年开发的自然启发式算法.CS 基于布谷鸟的寄生性育雏(brood para ...

  6. 【Asp.net入门04】第一个ASP.NET 应用程序-如何添加Web窗体到网站中

    添加Web窗体 本部分内容: 什么是web form 怎样添加web form 1.添加Web窗体到项目中 Web 窗体是一项 ASP.NET 功能,您可以使用它为 Web 应用程序创建用户界面.We ...

  7. Java入门:创建多个对象

    当使用一个类实例化多个对象时,多个对象之间是什么关系?他们各自的数据会不会发生混淆?这次课跟大家讲解一下这个问题.学完本次课,大家应该对对象在内存中的表示方式有一个初步的了解,为理解更深入的面向对象概 ...

  8. Xpath语法与lxml库的用法

    BeautifulSoup 已经是非常强大的库了,不过还有一些比较流行的解析库,例如 lxml,使用的是 Xpath 语法,同样是效率比较高的解析方法. 1.安装 pip install lxml 2 ...

  9. Lvs+Keepalived实现MySQL高可用

    LVS+Keepalived+MySQL高可用配置 本文所有配置前提是已实现MySQL双主备份(MySQL双主) 安装前的准备: VIP:192.168.0.201 Keepalived: Keepa ...

  10. Hadoop部署方式-伪分布式(Pseudo-Distributed Mode)

    Hadoop部署方式-伪分布式(Pseudo-Distributed Mode) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.下载相应的jdk和Hadoop安装包 JDK:h ...