C++入门篇(9)string的模拟实现

文章目录

  • 前言
  • 深浅拷贝
  • string的实现
    • 结构定义
    • 构造函数
    • 拷贝构造
    • 析构函数
    • 赋值重载
    • []访问
    • 改变容量
    • 字符和字符串拼接
    • 查询大小
    • 迭代器
    • 插入和删除
    • clear()

前言

本篇文章讲解的内容主要是深浅拷贝string类的实现.


深浅拷贝

概念:

浅拷贝只将对象的值拷贝过来,存在一定的隐患.

深拷贝给每个对象单独分配资源,一般对象涉及资源管理都会用深拷贝.

什么意思呢?

我们假设自己定义一个类,如下:

 class String
{
public:
	String(const char* s = "123456")
	:_size(strlen(_s)),_capacity(_size)
     {
        _s = new char[strlen(s)+1];
        _s[_size] = '\0';
        strcpy(_s,s);
	}
	~String()
	{
		delete[] _s;
		_size = _capacity = 0;
	}
private:
	char* _s;
	int _size;
	int _capacity;
};

如果我们使用该类创建对象,然后再释放资源,看看会怎样?

int main()
{
    String s1;
	String s2 = s1;
    return 0;
} 

C++入门篇(9)string的模拟实现_第1张图片


编译器会提示异常,触发断点,而这个原因就是因为浅拷贝.我们一开始创建了对象s1并给其赋值"123456",然后创建了对象s2并对其拷贝构造,在这个过程中,我们把s1的各个成员的值全对应的给了s2的成员.也就是说s1的_s和s2的_s指向的是同一块空间,当main函数结束时,s2先调用析构函数,对其_s进行释放,然后s1又调用析构函数,在对其_s进行释放,而同一块空间是不能释放两次的,因此产生错误.

那我们怎么解决呢? 答:深拷贝,也就是每个对象都有单独的属于自己的一份空间.

因此该类的拷贝构造函数应该如下:

String(const String& t)
    :_size(strlen(_s)),_capacity(_size);   //确定一下大小
{
        _s = new char[strlen(t._s)+1];    //然后给_s开辟一块空间,注意哦~,我这里多开了一块空间,是为了储存\0
        strcpy(_s,t._s);             //然后把值拷贝过去.
        _s[_size] = '\0';
}

该类的赋值运算符=重载如下:

String& operator=(String& t)
{
    if(this != &t){          //如果是同一个对象,自己赋值给自己就没有什么意义
        char* tmp = new char[strlen(t._s)+1];   //先开辟空间,然后用一个临时变量接收.
        delete[] _s;         //直接删除this原来的资源,然后重建一个和t._s一样大的空间
        _s = tmp;            //_s再接收
        strcpy(_s,t._s);
        _size = _capacity = strlen(t._s);
        _s[_size] = '\0';
    }
    return *this;
}

上面的两个例子都是为了解决浅拷贝问题而进行设计的,其写法是没有问题的(传统写法),但是我们更喜欢一种现代写法,

String(const String& t):_s(nullptr)   //必须有这一置空操作,因为_s开始是个随机数,交换给tmp._s后,被释放会引起大问题
{
    String tmp(t._s);                 //直接利用构造函数,给tmp对象开辟了一块空间,并把值穿进去.
    swap(_s,tmp._s);                  //然后交换_s和tmp._s,就相当于给this->_s开辟了一块空间,当拷贝函数结束,tmp就会被自动释放.
}

String& operator=(String t)      //注意一下哦~~~,我的形参写的是String t,本质上是利用的拷贝构造
{
    swap(_s,t._s);       //交换资源
    return *this;
}

咦~?,为啥线代写法的operator=操作不需要把_s初始化为nullptr?因为这是赋值操作,即对象都互相存在,也就是说_s本身是有资源的,交换后并不会造成对象t释放时候,其内部会释放一个随机的地址.

讲解完深浅拷贝的原理和应用以后,我们就开始string类的实现吧.


string的实现

结构定义

我们在上一节讲解了string的使用,其中涉及到长度,容量操作时候,博主说明了string的底层实现其实是顺序表.那么其结构大致如下:

namespace mystring  //因为string系统已经有了,为了防止重名,博主便重新定义了一块命名空间
{
	class string
	{
	public:

	private:
		char* _str;
		int _size;
		int _capacity;
	};
}

构造函数

在讲解string的使用时候,博主为大家演示过几个比较常用的构造函数,我们在这里实现的话,也就实现几个比较常用的吧:

  • string (const char* s); / /接收c格式的字符串

  • string (size_t n, char c); / /接收n个字符c

  • string() / /空字符串.

接收c格式的字符串,代码如下:

string(const char* s)
    :_size(strlen(s))   //给_size有效长度
{
    _str = new char[_size+1];      //给_str一个strlen(s)+1长度的空间
    strcpy(_str,s);               //拷贝字符串
    _capacity =  _size;           //更新容量
    _str[_size] = '\0';
}

接收n个字符c,代码如下

string(size_t n,char c)
    :_size(n)     //更新有效长度
{
        _str = new char[_size+1];  //给_str一个n+1长度的空间
        for(int i= 0;i<n;i++) _str[i] = c;
        _size = _capacity = n;   //更新capacity
}

空字符串,代码如下:

string()
    :_str(new char[1])
{
	*_str = '\0';
    _size = _capacity = 1;
}

拷贝构造

博主在最上面讲解深浅拷贝时候,已经说明了该函数,因此,博主这里便直接贴代码了

string(const string& t):_str(nullptr)  //必须有这一置空操作,因为_str开始是个随机数,交换给tmp._str后,被释放会引起大问题
{
    string tmp(t._str);                //直接利用构造函数,给tmp对象开辟了一块空间,并把值穿进去.
    swap(_str,tmp._str);   //然后交换_s和tmp._s,就相当于给this->_s开辟了一块空间,当拷贝函数结束,tmp就会被自动释放.
    swap(_size,tmp._size);
    swap(_capacity,tmp._capacity);
}

我们已经知道,拷贝构造需要深拷贝,而赋值也需要深拷贝,但是现代写法需要交换,因此博主便把交换操作封装一下,然后拷贝构造代码如下:

void Swap(string& a,string& b)
{
    swap(a._str,b._str);
    swap(a._size,b._size);
    swap(a._capacity,b._capacity);
}

string(const string& t):_str(nullptr)  //必须有这一置空操作,因为_str开始是个随机数,交换给tmp._str后,被释放会引起大问题
{
    string tmp(t._str);                //直接利用构造函数,给tmp对象开辟了一块空间,并把值传进去.
	Swap(*this,tmp);
}

析构函数

直接释放资源即可

~string()
{
    delete[] _str;
    _str = nullptr;
    _size = _capacity = 0;
}

赋值重载

深浅拷贝章节,博主已经讲解了,便直接贴代码:

string& operator=(string t)
{
    Swap(*this,t);  //注意哦,博主调用的是Swap,而不是swap.
    return *this;
}

[]访问

在c++中,string的operator[]有两个重载,分别是:

  • char& operator[] (size_t pos); //支持读和写
  • const char& operator[] (size_t pos) const; //只支持读

所以支持读和写的重载如下:

char& operator[] (size_t pos){
    return _str[pos];
}

只支持读如下:

const char& operator[] (size_t pos) const{
    return _str[pos];
}

改变容量

  • 大家还记得reserve吗?当给他传入一个n,且n大于该对象的capacity时候,才会增容.
void reserve(size_t n)
{
    if(n > _capacity){
        char* tmp = new char[n+1];        //注意哦~,这里必须要多开一个空间存储\0
        strcpy(tmp,_str);
        delete[] _str;
        _str = tmp;
        _capacity = n;
        _str[n] = '\0';                  //多开的那个空间存储位置
    }
}
  • 而相比于reserve来说,resize的功能就比较多了,且其分为三种情况
    • 当n小于size,则size等于n.
    • 当n>size但是小于capacity时,size仍等于n,但是这个时候即使你传入另一个参数ch,也没有用
    • 当n大于size时候,会增容,然后多出的空间会用ch初始化,ch如果不传,就是\0,最后size等于n,
void resize(size_t n, char ch = '\0')
{
    if (n > _capacity) reserve(n);                               //大于容量时,需要增容
    for (size_t i = _size; i < n; i++) _str[i] = ch;             //只有当n大于size时才起作用
    _size = n;                     //最后size等于n.
    _str[_size] = '\0';            //给字符串加一个\0
}

有人可能会问了,不说说分三种情况吗? 其实这里博主做了一个精简的处理,因为无论什么情况,都会有size等于n的操作,所以博主把这一步直接放到了外面.


字符和字符串拼接

对于字符和字符串拼接,用的做多有以下几个:

  • +=
  • push_back
  • append
  • void push_back(char c); //拼接字符

对于拼接字符操作来说,本质是上给数组中第size位置赋值,那么一定会遇到容量是否充足问题,所以是需要考虑此方面问题的:

void push_back(char c)
{
    if(_size >= _capacity){      
		int newcapacity =  _capacity == 0? 4 : _capacity*2;       //这一步是为了防止_capacity如果为0,开不了空间
		reserve(newcapacity);
    }
    _str[_size] = c;
    _size++;
    _str[_size]= '\0';      //给字符串末尾加\0
}

而拼接字符串则有比较多的重载,博主这里直接写一个比较常用的(拼接字符串或者拼接string对象)

  • void append(const char* s)

  • void append(const string s)

代码如下:

void append(const char* s){
    int len = strlen(s);
    if(_size + len > _capacity )
    {
        reserve((_size + len)*2);  //开大点
    }
    strcpy(_str+_size,s);
    _size += len;
    _str[_size] = '\0';
}

//同理,另一个重载只需要把char* 改成string就行

有了上面写好的push_backappend函数,那么我们便可以直接复用他们进行实现operator+=

string& operator+=(char c)
{
    push_back(c);
    return *this;
}

string& operator+=(const char* s)
{
    append(s);
    return *this;
}

查询大小

这里的大小有两个接口,分别是size和capacity.

size_t size() const 
{
    return _size;
}

size_t capacity() const
{
    return _capacity;
}

迭代器

我们在上一章节string的使用时候,就讲解了迭代器目前阶段是可以看作为指针的,而实际上在内部实现string时候,也差不多就是指针.

typedef char* iterator;
typedef const char* const_iterator;

iterator begin() {return _str;}
iterator end() {return _str+_size;}
iterator begin() const {return _str;}
iterator end() const {return _str+_size;}

现在我们再看看,之前是怎么使用迭代器的:

mystring::string s("123456");
mystring::string::iterator itb = s.begin();
mystring::string::iterator ite = s.end();
while(itb!=ite)
{
    cout<<*itb<<" ";
    itb++;
}

对照一下迭代器的使用方式和实现方式,是不是现在能够理解了呢?


插入和删除

  • insert(),有两个重载,分别是在某位置插入字符和字符串
void insert(size_t pos, const char c)
{
    if (_size == _capacity) {        //检查容量
        int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
        reserve(newcapacity);
    }
    
    for (int i = _size; i > pos; i--) _str[i] = _str[i - 1];   //把pos位置后的数据集体移动一位
    _str[pos] = c;   //插入数据
    _size++;
    _str[_size] = '\0';   //给末尾加上\0
}

void insert(size_t pos, const char* s)     
{
    size_t len = strlen(s);
    if (len + _size > _capacity) {
        reserve(len + _size);
    }
    for (int i = _size + len - 1; i > pos + len - 1; i--) {    //把pos到pos+len-1位置的数据集体后移strlen(s)位
        _str[i] = _str[i - len];
    }

    for (int i = pos + len - 1, j = len - 1; j >= 0; j--, i--) {   //放入数据
        _str[i] = s[j];
    }
    _size += len;
    _str[_size] = '\0';
}
  • erase(),从pos位置开始删除,len个,如果len不写,末尾删除pos后的全部
void erase(size_t pos, size_t len = npos)     //npos的定义为 size_t npos = -1;
{
    if (len == pos || pos + len >= _size) {
        _size = pos, _str[pos] = '\0';
        return;
    }

    for (int i = pos; i < _size; i++) {
        _str[i] = _str[i + len];
    }
    _size -= len;
    _str[_size] = '\0';
}

clear()

void clear()
{
    _str[0] = '\0';
    _size = 0;
}

你可能感兴趣的:(c/c++,c++,开发语言,后端)