探究C++中的对象赋值与拷贝

文章目录

    • 1、赋值运算符与复制构造器
      • 1.1、赋值运算符
      • 1.2、复制构造器
      • 2、浅拷贝与深拷贝
      • 2.1、重载赋值运算符
      • 2.1、编写复制构造器
    • 3、程序中的常见拷贝

由C++的 复制构造器=运算符重载入手,总结默认的 成员赋值(浅拷贝)深拷贝,最后探究程序中常见的对象赋值和拷贝。如有指正或补充,请不吝指出。

还是以一个例子辅以说明,这里使用一个自定义带编号字符串类MyString,每一个字符串对象都由其编号和字符串内容唯一确定。

1、赋值运算符与复制构造器

定义MyString类后,看如下语句:

MyString str1("C++");  //使用构造器
//先声明,再赋值
MyString str2;
str2=str1;
//声明并初始化
MyString str3=str1;

上面str2str3的两种赋值方式是否有区别?首先,从结果来看,它们是等效的,都是创建了一个字符串,并把str赋给它。但二者实现过程却具有本质的不同。前者是先使用无参构造器创建对象,再通过赋值运算把str1赋给该对象;后者是直接从复制构造器创建一个对象。因此,C++中的"初始化"特指声明的同时赋值。

1.1、赋值运算符

编译器会在类没有重载=运算符的时候提供默认的赋值运算操作,默认赋值只进行简单的成员赋值。如,在MyString中,类这样:

/*MyString对象具有属性成员: id,len,str(char*)*/
MyString& MyString::operator=(const MyString& s){
    id=s.id;
    len=s.len;
    str=s.str;    //直接把指针赋值
}

所以,即使没有重载=,类对象也能进行赋值,但这种赋值不一定安全。

1.2、复制构造器

复制构造器(copy constructor)是编译器默认为类添加的构造器。是的,编译器除了在没有构造器的时候提供默认无参构造器,还会在你没有提供复制构造器的时候提供默认的复制构造器。它的签名为:

Class Class(const Class&);//参数为const的对象引用

你也可以使用显式调用复制构造器

MyString str1;
MyString str2(str1);     //使用复制构造器
MyString str3=MyString(str1);  //同上

默认的复制构造器与默认的操作一样,只进行简单的成员赋值。

2、浅拷贝与深拷贝

无论是默认的赋值操作还是复制构造器,都是进行对象之间的成员赋值。在只有基本类型的时候,这种赋值往往是可行的,但是如果具有指针成员的时候,就不安全了。赋值:str1=str2,只是把str2.str地址赋给str1.str,导致两个指针指向同一个地址,这样一来,str1字符串的修改就会影响到str2的字符串,最后还会对同一个地址释放两次。总之,指针成员的复制会使两个对象共享一个指向目标,而不是独立的相同目标,因此,这种复制被称为浅拷贝

为了避免这个问题,我们需要编写赋值运算函数与复制构造器来实现深拷贝

2.1、重载赋值运算符

重载具有指针成员的类的赋值运算符时,需要记住几点:

  1. 如果需要重新分配空间,必须先释放原指针指向的空间。
  2. 防止自身赋值。自身赋值会导致自己的指针指向空间先被释放从而丢失内容,可以检查如果赋值对象是自身就直接返回。

重载的一个好处是可以自定义操作内容,比如MyString希望复制的时候不要改变原有的id,所以没有对id进行赋值,而是保留了原有。

根据以上两点,以及MyString的设计逻辑,可以编写如下运算符函数:

MyString& MyString::operator=(const MyString& s){
    if(this==&s)     //防止自身赋值
        return *this;
    len=s.len;
    delete[] str;    //释放原有空间
    str=new char[len+1];	//重新分配空间
    strcpy(str,s.str);      //复制字符串
}

2.1、编写复制构造器

复制构造器需要完成构造器的初始化任务,比赋值函数简单一些:

MyString::MyString(const MyString& s){
    id=++count;
    len=s.len;
    str=new char[len+1];
    strcpy(str,s.str);
}

3、程序中的常见拷贝

这里使用一个例子演示程序中有一些常见的拷贝(包括赋值和构造器拷贝)。看看具体都使用了什么函数,同时也复习一下构造器与析构函数。为了捕捉哪些函数在什么时候被调用了,在各个函数中都使用了打印消息。

//mystring.h
#ifndef MYSTRING_H
#define MYSTRING_H
#include 
class MyString{
private:
    static int count;  //静态成员记录对象数量
    char * str;
    int id;
    int len;
public:
    friend std::ostream& operator<<(std::ostream&, const MyString&);
    MyString(const char* ="");
    MyString(const MyString&);
    ~MyString();
    MyString& operator=(const MyString&);
};
#endif
//mystring.cpp
#include 
#include "mystring.h"
#include 

int MyString::count=0;
MyString::MyString(const char* s){
    id=++count;
    len=strlen(s);
    str=new char[len+1];
    strcpy(str,s);
    std::cout<<"(Create "<<*this<<")"<
//main.cpp
#include 
#include "mystring.h"
using namespace std;
void fun(MyString);
MyString createString();
int main(){
    cout<<"(Enter main)"<

输出如下:

探究C++中的对象赋值与拷贝_第1张图片

你可以试着自己去用调用逻辑去解释一下输出。前面初始化与赋值已经讲过。

直接看把对象按值传递的时候。输出Copy 3 to 4说明按值传递是使用复制构造器进行副本拷贝。

对象作为返回值的时候,没有发生拷贝行为(看输出)。说明被返回对象并不是传递了副本之后在函数本地被摧毁,而是实实在在传递了在本地声明的那个对象。它的声明周期与调用动作有关,如果返回后没有使用它,就被立刻摧毁。

(完。有一些是我基于测试之后的合理推断,如果有错误,望指正)

你可能感兴趣的:(C++)