string类的简单实现(深拷贝,浅拷贝)

时间:2021-04-06 19:51:53

一、前言

▶在c语言中我们来描述一个字符串是用const char* p=”abcdef”;然后我们也学习了许多关于字符串的操作函数。
▶那么,在学习了面向对象的c++语言之后,我们可以定义一个字符(string),这个类可以定义一个字符串对象;并且可以对该对象内存储的字符串进行各种操作;
▶接下来,我们模拟实现一个简单的string类;只需要实现★构造,★拷贝构造和★赋值运算符重载★析构函数即可;

二、各部分函数精讲

(1)成员变量

因为只需要保存字符串,所以写一个字符类指针指向字符串即可保存:

char* _str;

(2)构造函数

①常见错误写法

String(const char* str)//构造函数
        :_str(str)
    {

    }

分析:
这样写构造函数不对,因为传进来的str指针的值是一个常量字符串的首地址,然后把这个首地址给类中的_str这样会导致类的_str指针也指向常量字符串的首地址,那么如果用这个常量字符串再次构造一个string类对象时,就会让两个类中的指针同时指向一块空间,如果一个类先释放了空间,第二个类再次是释放时,就会释放NULL;程序出错;
▏见图(1)▏
string类的简单实现(深拷贝,浅拷贝)

②正确写法

String(const char* str="")//构造函数
        :_str(new char[strlen(str)+1])
    {
        strcpy(_str,str);
    }

分析:
先给_str开辟一段和常量字符串长度+1相等的空间,然后把常量字符串的内容(包含\n)拷贝到_str指向的新开辟的空间,这样,每个对象的数据成员都有属于自己的空间存放字符串,释放的时候自己释放自己空间;不会出现一块内存被多次释放造成的错误;
(▌特别说明▌,当构造对象的时候没有传参时,此时的str默认为只有一个字符的字符串”\0”空字符串;因为字符串的最后一个字符”\0”不写出来,系统默认加在后面,所以就写成”“,特别注意,不能写成默认为空,如果默认我空NULL;那么要是用这个对象来拷贝构造另一个对象时,在开辟空的时候使用strlen函数的时候,会出错,因为对空指针解引用是错误的)
▏见图(2)▏
string类的简单实现(深拷贝,浅拷贝)

(3)拷贝构造函数

①浅拷贝(值拷贝)

String(const String& s)//拷贝构造函数---浅拷贝(值拷贝)
    {
        _str=s._str;
    }

分析:
▌在没有引用计数时这样写拷贝构造函数不对▌▶因为这样是把一个对象的_str的地址赋给的另一个对象的_str;这样会出现两个类的_str指向同一块常量的内存空间;这样会出现两个错误
★当一个类对自己的字符串进行操作时,就算另一个类什么也没做,他的字符串也会被改变;
★当释放一个对象的_str所指向的空间之后,根据析构函数的写法,释放另一个类的对象的_str的时候,此时这个类的_str不为空,值为常量字符串的地址,但是进去释放的时候,这块空间已经被前一个类的析构函数释放了,这样就会一块内存被两次释放,出现错误;
▏见图(3)▏
string类的简单实现(深拷贝,浅拷贝)

②深拷贝

String(const String& s)//拷贝构造函数---深拷贝
        :_str(new char[strlen(s._str)+1])
    {
        strcpy(_str,s._str);
    }

分析:
为了解决浅拷贝的不足,那我们就用身拷贝;所谓深拷贝,就是说,每次用对象str1构造对象str2的时候;我们不是只把str1的_str给str2的_str,而是给str2重新开辟一段大小相等的空间;然后,把str1的_str里面的内容拷贝到str2的_str中去,这样每个类的对象的数据成员都有自己空间存放字符串;自己释放自己的空间;不争不抢;
▏见图(4)▏
string类的简单实现(深拷贝,浅拷贝)

(4)赋值运算符重载

①常规写法

String&  operator=(const String& s)//赋值运算符重载
    {
        if (this!=&s)
        {
            delete[] _str;
            _str=new char[strlen(s._str)+1];
            strcpy(_str,s._str);
        }

    }

分析:
赋值运算符的重载,是拿一个对象赋值给另一个对象;比如说str3=str2;就是将str2赋值给str3;
首先;我们先要判断是不是对象自己给自己赋值,如果是自己给自己赋值,直接返回对象自己;为什么要判断呢?因为如果不判断遇到这种情况str3=str3就会出错;如果说自己给自己赋值,那么delete[] _str后,对象的数据成员所对应的空间已经被释放而且_str=NULL;这时候在进行下一步开辟空间时_str=new char[strlen(s._str)+1];因为此时 s._str=NULL;用空指针左strlen的参数时,会报错;
然后,如果不是自己给自己赋值,那么就是两个不同的对象赋值;然后开辟空间,拷贝字符串,返回对象;
▏见图(5)▏
string类的简单实现(深拷贝,浅拷贝)

②现代写法:

String& operator=(String s)//赋值运算符重载---现代写法
    {
        std::swap(_str,s._str);
        return *this;
    }

分析:
首先,现代写法的赋值运算符重载函数形参不能带引用;因为引用的含义是一个变量的别名,这个别名和原对象指的同一块内存;如果将对象以引用的方式接收,那么在函数内部进行std::swap(_str,str._str)操作时;虽然这个时候,要赋值的对象的_str确实指向了字符串的空间,但是这个时候,用来赋值的对象的内容却改变了;变为了要赋值的对象里面的内容;这样做没有达到赋值操作的目的;

当我们用值传递的方式传进来参数时;这时,这个用来赋值的对象会调用类的拷贝构造函数,重新构造一个临时对象s;这个对象因为是调用拷贝构造函数得来的;所以里面也会有一个_str,这个_str也会有自己的空间,存放字符串;当进std::swap(_str,str._str)操作时,swap会将两个指针的值进行交换;这两个指针的值就是各自指向的字符串内存的首地址;当赋值函数调用完成的时候,因为s是一个临时变量;所以在出函数的时候,临时变量String类的对象s会调用String类的析构函数将自己的数据成员指向的内存释放(其实就是被赋值的对象原始的空间),并将自己的_str置空(避免成为野指针);

★个人觉得现代写法的赋值运算符重载函数就想一个强盗,把别人的东西拿来自己用,还把自己不好的东西让别人丢掉;最后在让这个人消失★
▏见图(6)▏
string类的简单实现(深拷贝,浅拷贝)

(5)析构函数

    ~String()
    {
       if (NULL!=_str)
       {
           delete[] _str;
           _str=NULL;
       }
    }

分析:略

三、代码实现

#pragma once
#include<iostream>
using namespace std;
#include<cstring>

class String
{
    friend ostream& operator<<(ostream& os,String& s);
public:
    String(const char* str="")//构造函数
        :_str(new char[strlen(str)+1])//strlen统计字符串字符个数(不包括"\0")
    {
        strcpy(_str,str);//strcpy从字符串第一个字符开始,一直到"\0"结束(包括"\0")
    }
    String(const String& s)//拷贝构造---深拷贝
        :_str(new char[strlen(s._str)+1])
    {
        strcpy(_str,s._str);
    }
    String& operator=(const String& s)//赋值操作符重载---常规写法
    {
        if (this!=&s)
        {
            delete[] _str;
            _str=new char[strlen(s._str)+1];
            strcpy(_str,s._str);
        }

        return *this;
    }

    /* String& operator=(String s)//赋值操作符重载---现代写法 { std::swap(_str,s._str); return *this; }*/

    ~String()
    {
        if (NULL!=_str)
        {
            delete[] _str;
            _str=NULL;
        }
    }
public:
    char* _str;
};
//因为模拟实现的string不是内置类型,所以要重载输出运算符,才能输出String类对象的内容
ostream& operator<<(ostream& os,String& s)
{
      os<<s._str;
      return os;
}

测试代码:

void Test()
{ 
     String s1("abcdef");
     String s2("ABCDEF");
     String s3(s1);
     String s4;
     s4=s1;
     cout<<"s1->"<<s1<<endl;
     cout<<"s2->"<<s2<<endl;
     cout<<"s3->"<<s3<<endl;
     cout<<"s4->"<<s4<<endl;

} 

四、运行结果
string类的简单实现(深拷贝,浅拷贝)

后面会写到引用计数器的写时拷贝的实现;

O(∩_∩)O