拷贝构造函数、拷贝赋值函数之间的关系(如何规范设计带指针数据的类)—HJ-record03

目录

类声明部分

Big Three:三个特殊函数

拷贝构造函数

拷贝赋值函数

析构函数

类定义部分

ctor和dtor(构造函数和析构函数)

经验总结

包含指针的类中必须要有copy ctor(拷贝构造函数)和copy op=(拷贝赋值函数)

浅拷贝 

深拷贝(拷贝构造函数 copy ctor)

拷贝赋值函数(copy assignment operator)

一定要在拷贝赋值函数(operator=)中检查是否自我赋值(self assignment)

output函数

类定义部分代码


假设我现在定义了一个对象,其里面有一个对应的指针,然后,我现在再拷贝产生一个新的对象,就将是只把指针拷贝了过来,造成了两个指针其实都是指向的同一个地方,本质上只是把指针给拷贝了一份,并不是真正的拷贝,而且,风险性很大。

所以,只要类中带指针,就不能用编译器给的默认拷贝方式,一定要自己写。

下面来定义一个带指针的类,字符串的类(string.h):

想将设计的字符串类可以完成如下的调用的功能:

#include "string.h"
#include <iostream>

using namespace std;

int main()
{
  String s1("hello"); 
  String s2("world");
    
  String s3(s2);
  cout << s3 << endl;
  
  s3 = s1;
  cout << s3 << endl;     
  cout << s2 << endl;  
  cout << s1 << endl;      
}

 还是分成类的声明和类的实现两个部分来进行逐行的分析:

类声明部分

一般情况下对字符串类的设计都是这样的:让字符串类里面有一根指针,在需要内存的时候才创建另外一块空间来放字符,这是因为字符本身的大小是不确定的,所以,这种设计最好是动态的,根据字符的大小来调整开辟的内存空间。所以,在这个字符串中的数据定义成一个指针"char* m_data;",放在private区域内,同时,因为字符串类中的数据是通过指针进行操作的,所以,需要自己去定义一套拷贝函数放在public区域当中,具体如下:

class String
{
public:                                 
   String(const char* cstr=0);      //构造函数               
   String(const String& str);       //拷贝构造             
   String& operator=(const String& str);         
   ~String();                                    
   char* get_c_str() const { return m_data; }
private:
   char* m_data;
};

但是仔细发现,上面的public区域的函数,除了第一个是构造函数外,剩下的都是比较特殊的!下面逐个进行分析:

只要类带着指针,就一定要写出下面这三个函数!!!

Big Three:三个特殊函数

拷贝构造函数

这个函数和类的名称一样,应该也是一种构造函数,同时,它传递的就是一个函数本身定义的传入参数,就是一个对象"String&",这就显的很特殊,自己处理自己?,这是一个构造函数,但接收的是自己这种东西"String&",所以这种函数就称为拷贝构造函数。

   String(const String& str);       //拷贝构造  

拷贝赋值函数

可以发现有一个"operator=",这个是给等号操作符进行复制操作,将括号里的String& str,复制给函数名前的"String&"。

   String& operator=(const String& str);

析构函数

以这个类创建的对象,当其离开它的作用域或者其他的时候,对象死亡,析构函数会将对象所占用的内存给释放掉。

   ~String(); 

 析构函数是相对于构造函数而言的,

类定义部分

ctor和dtor(构造函数和析构函数)

在各种程序语言中,字符串如何处理就是比较经典的问题,大部分都把一个字符串定义为前面有一个指针指着,后面以“\0”做结尾,字符串的求长度,主流有两种方式来计算,

一种是不管字符串有多长,只要检测到最后的"\0",那么就可以算出来,比如C/C++

还有一种是,字符串后面没有"\0",这样的结束符号,但字符串的前面多了一个字符串长度信息的数值,直接提取这个,也可以获得字符串的长度(len),比如Python。

那么下面是通过第一种的方式来操作字符串:

先判断字符串是否指定初始值,为空的话(cstr=0),对应的外界调用情况为:"String s1()",就是创建的对象就没有设置初始值,就返回一个空字符串;如果非空,意味着外界的调用情况为:"String s2(”hello“)",就要分配一块足够的空间(意味着要计算字符串长度),用来存放字符串。

并且,在构造函数的时候用"new"动态的调用空间来存储字符串,最后,运行完了,也是需要用析构函数去动态的delete将这块内存空间释放的。

经验总结

  • 类里面包含指针,多半是要进行动态分配的,即使用new和delete.

包含指针的类中必须要有copy ctor(拷贝构造函数)和copy op=(拷贝赋值函数)

如果不这么做会怎样?比如,想完成下面的操作,将a中的数据拷贝一份到b中,让两个对象独立的拥有同一份内容的数据("Hello\0"):

 

浅拷贝 

如果按照编译器提供的默认的拷贝方式会是怎样的呢?如下:

可以发现,其实只是调用了指针而已,只是改变了一下指针的指向,b中的,内容"World\0"还是没改,而且,原来指向这块儿的指针指向了新的"Hello\0",造成了对象b的内存泄露。再看a中的数据"Hello\0",对象a和对象b的指针都指向它,通过任意一个指针对其作修改,都会影响另一个的调用。这种状况就叫作浅拷贝。

 

深拷贝(拷贝构造函数 copy ctor)

拷贝构造函数可以完成深拷贝(拷贝内容,而不是拷贝指针)的任务,这样当进行拷贝的时候,会调用拷贝构造函数,将”自己拷贝给自己“,

拷贝构造会创建出足够的空间,来放蓝本,比如:”String s2(s1)“意思是说,以s1为蓝本创建出来一个s2;"String s2 = s1;",创建一个新对象s2,将s1赋值给它,既然s2是新创建出来的,就需要去调用构造函数。

 

拷贝赋值函数(copy assignment operator)

接着按上面的例子来举例:

如果想把a中的值拷贝到b中去,应该分三步:

  • 第一步:把b中的数据清空;
  • 第二步:在b中开一块儿可以容纳a中要拷贝过去的数据的空间;
  • 第三步:将放在a中的数据拷贝到b中去。那么拷贝赋值函数的设计思路也是如此的:

如图上标蓝的部分:"s2 = s1;",就是在调用拷贝赋值函数,其中的1,2,3序号,对应的上面的第一、二、三步。这也是经典的写法。

一定要在拷贝赋值函数(operator=)中检查是否自我赋值(self assignment)

接着上面的代码,在三步开始前,有一个对自我赋值的检测,这是要防止因为指针名称变动或者继承等原因,先进行检测(这是高手的经验的写法)。如果不作这个判断,不止是影响效率,甚至会影响结果。

比如下面这种情况,两个对象的指针指向同一块地方:

两个变量的指针都指向同一块内存空间,如果不作是否存在自我赋值的检查的话,直接执行拷贝构造步骤,先清除掉内存存放的数据,再留对应的字节,最后,放新数据,会发现,中间清除完要拷贝到的数据也被清除了!

这就不是效率低的事儿了,是直接就报错了!

output函数

由于我们再使用字符串的时候,会需要对字符串作输出,需要将其打印到外界,就需要这么一个函数:

所以,这里也需要一个操作符"<<"的重载的函数,需要注意的话,这个函数不能变成一个成员函数,必须写成全局函数,否则cout的输出顺序会与习惯性相反。在进行设计的时候,我们只要把字符串对应的指针抛出来,那么可以顺着指针把指向的内容都给打印出来了!如何获取这个指针呢?在类的定义部分其实就已经定义出来了!

char* get_c_str() const { return m_data; }

那么,在output函数中,直接把get_c_str()函数的结果就可以将其给输出了!

类定义部分代码

#include <cstring>

inline
String::String(const char* cstr)
{
   if (cstr) {
      m_data = new char[strlen(cstr)+1];
      strcpy(m_data, cstr);
   }
   else {   
      m_data = new char[1];
      *m_data = '\0';
   }
}

inline
String::~String()
{
   delete[] m_data;
}

inline
String& String::operator=(const String& str)
{
   if (this == &str)
      return *this;

   delete[] m_data;
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
   return *this;
}

inline
String::String(const String& str)
{
   m_data = new char[ strlen(str.m_data) + 1 ];
   strcpy(m_data, str.m_data);
}

#include <iostream>
using namespace std;

ostream& operator<<(ostream& os, const String& str)
{
   os << str.get_c_str();
   return os;
}

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值