对象的生灭——构造函数、拷贝构造函数、赋值构造函数和析构函数

一、背景知识

1. 类机制中提供了默认的拷贝构造函数,赋值操作符,和析构函数,但是这些都是针对浅拷贝的。 当对象实体和本体不一致的时候,需要自定义拷贝构造函数、自定义赋值操作符。
2.自己定义类一旦重载了构造函数,那默认的构造函数就消失了,所以一旦重载构造函数就要定义一个默认的构造函数
3.Person p2=p1;其实调用的是自定义的拷贝构造函函数
   p1=p2调用的是赋值操作符
4.在自定义操作符时需要注意的是
A:肯能会出现p1=p1
B:P1=p2的时候原来p1可能有指向,所以需要释放内存
5.最值得注意的是:
重载拷贝构造函数的形参:常引用
重载赋值操作符的形参:常引用
注意:为什么上面的形参是引用而不是值呢?
对于拷贝构造函来说,如果是值的话就就会在传入参数的时候仍然调用拷贝构造函数,陷入死循环
重载赋值操作符的函数类型:类的引用:这样最的原因是,=之后还会有后续的例如++等  

二、深拷贝和浅拷贝——拷贝构造函数

对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a=88;
int b=a; 
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。 
#include <iostream>
using namespace std;
class CExample {
private:
    int a;
public:
    CExample(int b)
    { a=b;}  
    CExample(const CExample& C)
    {
        a=C.a;
    }
    void Show ()
    {
        cout<<a<<endl;
    }
};
int main()
{
    CExample A(100);
    CExample B=A;
    B.Show ();
    return 0;
}

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象B分配了内存并完成了与对象A的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的.

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体 
(2)一个对象以值传递的方式从函数返回 
(3)一个对象需要通过另外一个对象进行初始化

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率

浅拷贝和深拷贝

  在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

  深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。若是浅拷贝,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。下面举个深拷贝的例子。

#include <iostream>
using namespace std;
class CA
{
 public:
  CA(int b,char* cstr)
  {
   a=b;
   str=new char[b];
   strcpy(str,cstr);
  }
  CA(const CA& C)
  {
   a=C.a;
   str=new char[a]; //深拷贝
   if(str!=0)
    strcpy(str,C.str);
  }
  void Show()
  {
   cout<<str<<endl;
  }
  ~CA()
  {
   delete str;
str=NULL;
  }
 private:
  int a;
  char *str;
}; 
int main()
{
 CA A(10,"Hello!");
 CA B=A;
 B.Show();
 return 0;
}

三、深拷贝和浅拷贝——赋值运算符

 赋值运算符函数应该注意下面几点:
1 赋值运算符函数的返回值的类型声明必须为该类型的引用,并在函数结束前返回实例的自身的引用(即*this)。因为只有返回一个引用才会允许连续赋值的行为
2 传入的参数必须声明为常量引用
  A:是引用的原因是避免传入对象,调用拷贝构造函数引起的消耗,提高代码效率
  B:赋值运算符不会改变传入参数的值,因此加上const
3 一定要释放实例自身已有的内存,然后在分配新的内存
4首先判断传入的参数和当前的实例(*this)是否是同一个实例,如果是,就不要再释放了,直接返回(*this)
  否则会释放掉本身的内存
例子:
Cmystring &Cmystring ::operator=(const Cmysting &str)
{
        if(this==&str)
           return *this;
       delete[]m_pData;
      m_pata=NULL;
     m_pdata=new char[strlrn(str)+1];
     strcpy(m_Pdata,str.m_pdata);
     return *this;
)
或者借助临时变量解决赋值给本身的问题
Cmystring &Cmystring ::operator=(const Cmysting &str)
{
       char*ptemp=new char[strlrn(str)+1](*(str.p));
       delete p;
       p=ptemp;
       return *this;
}

2 拷贝构造函数的传入参数一定是引用的原因
   拷贝构造函数的传入参数一定是引用而不能是实例,如果传入的参数是实例,那么就会调用复制构造函数,这样会无休止的递归调用构造函数,导致栈溢出。——编译出错

四、虚析构函数


对比两个程序
左边输出的结果是:ABCdeA
右边输出的答案是ABC deC deB DEA
其实主要考察了下面几点
1)A a 这样定义类的对象会自动指向构造函数,在函数结束时自动执行析构函数
2)但是 new会执行构造函数只有delete才会执行析构函数
左边图片中不是虚析构函数,就只能调用A的析构函数,而右边由于是虚析构函数和内存存储的问题,指针访问的函数其实是C的析构函数,而C的析构函数自动隐含了AB的析构函数,调用玩C的会依次调用BA的析构函数
而且要注意的是,上面例子应该使用虚析构函数,否则会有一部分没有释放出现问题



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值