文章目录

  • 一、string类的模拟实现
    • 1.成员变量
    • 2.构造函数
      • (1)无参构造函数
      • (2)有参构造函数
    • 3.c_str函数
    • 4.operator[]
    • 5.深浅拷贝问题
      • (1)浅拷贝
      • (2)深拷贝
    • 6.size函数和capacity函数
    • 7.reserve函数
    • 8.resize函数
    • 9.string的插入函数
      • (1)push_back函数
      • (2)append函数
      • (3)operator+=函数
      • (4)insert函数
    • 10.字符串比较函数
    • 11.迭代器
    • 12.erase函数
    • 13.find函数
    • 14.string类的流插入和流提取函数
    • 15.swap函数
    • 16.拷贝构造函数和赋值函数的现代写法
  • 二、代码

一、string类的模拟实现

1.成员变量

string类的成员变量分别是存储字符串的一段空间_str,表示字符串的有效字符个数_size和表示存储有效字符空间的_capacity。

private:char *_str;size_t _size;// 有效字符的个数size_t _capacity;// 存储有效字符的空间

2.构造函数

(1)无参构造函数

string类的无参构造函数非常简单,_size和_capacity都设置为0,但是_str不能设置为nullptr,因为根据标准库里的string设计,无参构造函数里的_str设置为空串。

string():_size(0),_capacity(0)
{// 按照标准库里的string设计无参构造函数// _str存放一个空串,而不是直接设置为nullptr_str = new char[1];_str[0] = '\0';
}

(2)有参构造函数

首先,string类的有参构造函数其实可以设计为全缺省函数,缺省值设置为空串,当没有传入参数时使用缺省值,将_str设置为空串,这样就可以不需要定义无参构造函数了。其次,如果传入了参数,就将_size和_capacity设置为形参的长度,然后开一段大小和形参相同的空间给_str,最后将值拷贝过去即可。

string(const char *str = ""):_size(strlen(str)),_capacity(_size)
{// strcpy函数会将\0也拷贝过去_str = new char[_capacity + 1];strcpy(_str, str);
}

3.c_str函数

string类的c_str函数是为了方便string字符串配合C语言的字符串函数接口使用而设计的,它可以返回string对象的char * 类型的指针,这个函数实现起来非常简单,直接返回我们成员变量_str即可,它就是一个char * 类型的指针。需要注意的是,c_str函数的返回类型是 const char*,要加上const的原因调用该函数只能获取指针,并不能对指针进行修改。

const char *c_str() const
{return _str;
}

4.operator[]

string类中对运算符[]的重载是很重要的,提供这个运算符重载可以方便我们对string对象进行下标访问,也可以修改该下标对应的值。函数的返回类型是char&,一般引用返回是为了减少拷贝,但这里我们只需要返回一个字符,拷贝成本并不大。这里使用引用返回的目的是允许修改,因为引用就是返回值的一个别名,我们就可以对返回值进行修改。

char &operator[](size_t pos)
{return _str[pos];
}// 再提供一个const版本,让const对象也能调用,就不会出现权限放大的问题
const char &operator[](size_t pos) const
{return _str[pos];
}

5.深浅拷贝问题

(1)浅拷贝

在C++的类设计中,如果我们没有写拷贝构造函数,那么会使用默认的拷贝构造函数,默认的拷贝构造函数是浅拷贝,也就是简单地将一个对象的值拷贝给另一个对象。如果对于内置类型的话浅拷贝没什么太大的影响,但如果是自定义类型,就比如我们的string类,将一个string对象的值拷贝给另一个string对象本质上是将指针内容进行拷贝,这样就会导致两个对象指向同一块空间。

这样浅拷贝的话会引发一些问题,比如析构函数的时候会被析构两次,或者一个对象改变自己字符串的值会影响另一个对象字符串的值。

(2)深拷贝

深拷贝和浅拷贝不同的是深拷贝是另外开一块同样的空间,然后将字符串的内容拷贝下来,这样就让两个对象指向不同的空间,但这两个不同空间的字符串值是相同的,这样就解决了自定义类型浅拷贝带来的问题。

要完成深拷贝的话就需要我们自己写拷贝构造函数了,不能使用默认的拷贝构造函数:

string(const string &s):_size(strlen(s._str)),_capacity(_size)
{_str = new char[_capacity + 1];strcpy(_str, s._str);
}

赋值运算符重载和拷贝构造一样,如果我们没有写赋值运算符重载,默认使用的是浅拷贝,所以我们也要自己写赋值运算符重载。
赋值运算符重载的写法和拷贝构造函数的写法不同,不能直接复制过来,因为赋值运算符重载面对的是两个都已经存在了的开好空间了的对象,有可能需要被赋值的对象的空间比较小,会存在越界访问的问题;也有可能需要被赋值的对象的空间比较大,虽然不会出现越界访问但会造成空间的浪费。所以简单粗暴的就是先释放原有的空间,再进行复制。

string& operator=(const string& s)
{// 防止出现自己给自己赋值导致的错误if (this != &s){delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}return *this;
}

6.size函数和capacity函数

这两个函数很简单,分别返回_size的值和_capacity的值即可。

size_t size() const
{return _size;
}size_t capacity() const
{return _capacity;
}

7.reserve函数

reserve函数是扩容函数,当容量不足时,会扩大到指定的容量。这个函数实现起来比较简单,当指定容量大于当前容量时说明要扩容,我们定义一个新的空间,把这块新空间的大小设置为指定的新容量,然后将原来字符串的值拷贝到新开的空间上,再将原来的空间释放,让_str指针指向新的空间,最后更新_capacity的值即可。

void reserve(size_t n)
{// 如果容量不够,就要扩容if (n > _capacity){char* tmp = new char[n + 1];// 保留一个位置给\0strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}

8.resize函数

resize函数也是扩容函数,但是它改变的是_size的值,这个函数的实现分三种情况讨论(假设新的size值为newsize):

  1. 当newsize > _capacity时:说明容量不够了,首先要进行扩容,可以服用reserve函数进行扩容操作,并将字符串填充满。
  2. 当_size < newsize <= capacity时:说明容量是够的,所以只需要增大_size的值,并且将字符串填满即可。
  3. 当newsize <= _size时:只需要缩小_size的值即可。
void resize(size_t n, char ch = '\0')
{// 如果容量不够,首先要扩容if (n > _capacity){reserve(n);}// 到这里代表容量一定足够for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';
}

9.string的插入函数

(1)push_back函数

push_back函数是在字符串末尾插入一个字符,首先需要考虑容量是否满了,如果容量满了就需要先扩容。扩容的时候还需要特别关注一下如果字符串是空串,那么_capacity的值是0,需要特殊处理一下。扩容完以后就是简单的尾插操作即可。

void push_back(char ch)
{// 说明容量满了,需要先扩容if (_size == _capacity){// 需要考虑到_capacity是不是等于0这个情况reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0';
}

(2)append函数

我们只实现append插入一个常量字符串的函数,首先是要计算新插入的字符串加上原来的字符串一共有多长,然后要判断这个长度是否大于_capacity,如果是的话就需要扩容。最后直接用strcpy函数拼接起来即可。

void append(const char* str)
{// 先计算插入字符串以后的长度int len = _size + strlen(str);// 如果容量不大需要扩容if (len > _capacity){reserve(len);}strcpy(_str + _size, str);_size = len;_str[_size] = '\0';
}

(3)operator+=函数

这个函数只需要复用push_back函数和append函数即可。

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

(4)insert函数

insert函数我们实现两个版本,一个是插入一个字符,一个是插入一个常量字符串。这个函数实现的逻辑也比较简单,就是将pos位置到_size位置的字符往后挪动,然后在空位处插入新的字符或者字符串。

string& insert(size_t pos, char ch)
{// 断言防止pos出现非法范围,当pos=_size时就是push_back// 所以push_back可以复用这个insertassert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;return *this;
}string& insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}size_t end = _size + len;while (end > pos + len -1){_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;return *this;
}

10.字符串比较函数

字符串比较函数需要实现各种比较符号的运算符重载函数,字符串比较的原理是根据字符的ASCII码值的大小进行比较,这可以利用C语言的strcmp函数进行比较。其实我们只需要实现其中个别几个函数,剩下的直接复用即可。

    bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator<=(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) <= 0;}bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator>(const string& s1, const string& s2){return !(s1 <= s2);}bool operator>=(const string& s1, const string& s2){return !(s1 < s2);}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);}

11.迭代器

string类的迭代器非常简单,其实就是 char* 类型的指针,与迭代器配合使用的begin函数和end函数实现起来也非常简单,begin函数只要返回字符串第一个字符的地址即可,end函数只要返回字符串最后一个字符的下一个位置的地址。同时,我们也要实现一个const的迭代器和const修饰的begin函数和end函数,让const对象也可以调用。

typedef char* iterator;
typedef const char* const_iterator;iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}const_iterator begin() const
{return _str;
}const_ iterator end() const
{return _str + _size;
}

string的遍历方式有三种,分别是通过下标遍历、通过迭代器遍历和通过范围for来遍历,通过下标遍历借助operator[]函数即可,通过迭代器遍历只要实现迭代器即可,范围for遍历其实底层也是通过迭代器来遍历的,例如下面的例子:ch是迭代器解引用的一份拷贝,假设迭代器变量为it,等价于 ch = *it;

void test_string1()
{JJP::string s("hello");for (auto ch : s){cout << ch;}cout << endl;
}

所以我们就能意识到一个问题,如果我们对ch的值进行改变的话,原来的字符串的值并不会发生改变,因为改变的是ch这个变量的值,并没有改变*it的值,因此,如果想要改变原来字符串的值,需要带上引用。

void test_string1()
{JJP::string s("hello");// 只改变ch的值,不能改变原字符串的值for (auto ch : s){ch -= 1;cout << ch;}cout << endl;cout << s.c_str() << endl;// 加上引用就可以改变原来字符串的值for (auto& ch : s){ch -= 1;cout << ch;}cout << endl;cout << s.c_str() << endl;
}

12.erase函数

string的删除函数与插入函数相反,挪动数据覆盖即可。

string& erase(size_t pos, size_t len = npos)
{assert(pos <= _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t begin = pos + len;while (begin <= _size){_str[begin - len] = _str[begin];begin++;}_size -= len;}
}

13.find函数

find函数实现起来也非常简单,只需要遍历查找即可。我们实现一个查找字符函数和一个查找字符串函数,查找字符函数挨着遍历去查找即可,查找字符串函数可以使用C语言的库函数strstr去查找子串。

size_t find(char ch, size_t pos = 0)
{for (; pos < _size; pos++){if (_str[pos] == ch){return pos;}}return npos;
}size_t find(const char* str, size_t pos = 0)
{const char* p = strstr(_str + pos, str);if (p == nullptr){return npos;}else{return p-_str;}
}

14.string类的流插入和流提取函数

我们还需要实现以下流插入和流提取函数,这样方便我们输入字符串和输出字符串。

    ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){// 先将字符串清空s.resize(0, '\0');// 这种方法可能存在多次扩容的情况,效率较低//char ch;in >> ch;//ch = in.get();//while (ch != ' ' && ch != '\n')//{// s += ch;//    //in >> ch;// ch = in.get();//}//return in;char ch;ch = in.get();char buff[128] = {'\0'};size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){s += buff;memset(buff, '\0', 128);i = 0;}ch = in.get();}s += buff;return in;}

15.swap函数

string类的swap函数是在底层直接交换两个对象的指针,所以实现起来非常简单。

void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}

16.拷贝构造函数和赋值函数的现代写法

我们上面写的拷贝构造函数方法太麻烦了,有一种更加方便的写法,就是定义一个局部对象tmp,让tmp去调用常量字符串构造函数从而完成深拷贝,最后将tmp与this交换即可。需要注意的是一开始的时候this的_str、_size和_capacity需要初始化一下,因为如果没有初始化最后交换给tmp的就是随机值,tmp在最后析构的时候可能会报错。

// 现代写法,让tmp去完成深拷贝,复用常量字符串构造函数
string(const string &s):_str(nullptr),_size(0),_capacity(0)
{string tmp(s._str);swap(tmp);// 这个调用的是string类的swap函数
}

除了拷贝构造函数有现代写法,赋值运算符重载函数也有现代写法。第一种现代写法和拷贝构造函数的写法相似,也是借助局部对象tmp完成深拷贝,最后交换即可。

string& operator=(const string& s)
{if (this != &s){string tmp(s._str);swap(tmp);}return *this;
}

第二种写法更加的简洁粗暴,我们可以直接将形参的对象与this交换即可。由于我们参数传递是传值传参,不是引用传参,而是实参的一份临时拷贝,所以交换以后并不会影响实参的值。

// 现代写法更简洁的版本
string& operator=(string s)
{swap(s);return *this;
}

二、代码

#pragma once
#include <iostream>
#include <string.h>
#include <assert.h>// 使用命名空间为了不让库的string和我们自己定义的string冲突
namespace JJP
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}// 可以不给无参构造函数,直接将有参构造函数设计成全缺省// 可以达到一样的效果// string()//     :_size(0)//     ,_capacity(0)// {//     // 按照标准库里的string设计无参构造函数//     // _str存放一个空串,而不是直接设置为nullptr//     _str = new char[1];//     _str[0] = '\0';// }string(const char *str = ""):_size(strlen(str)),_capacity(_size){// strcpy函数会将\0也拷贝过去_str = new char[_capacity + 1];strcpy(_str, str);}~string(){if (_str){delete[] _str;_str = nullptr;_size = _capacity = 0;}}const char *c_str() const{return _str;}char &operator[](size_t pos){return _str[pos];}const char &operator[](size_t pos) const{return _str[pos];}// // 原始写法,代码比较多// string(const string &s)//     :_size(strlen(s._str))//     ,_capacity(_size)// {//     _str = new char[_capacity + 1];//     strcpy(_str, s._str);// }void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}// 现代写法,让tmp去完成深拷贝,复用常量字符串构造函数string(const string &s):_str(nullptr),_size(0),_capacity(0){string tmp(s._str);swap(tmp);// 这个调用的是string类的swap函数}// 原始写法// string &operator=(const string &s)// {//     // 防止出现自己给自己赋值导致的错误//     if (this != &s)//     {//         delete[] _str;//         _str = new char[s._capacity + 1];//         strcpy(_str, s._str);//         _size = s._size;//         _capacity = s._capacity;//     }//     return *this;// }// 现代写法// string& operator=(const string& s)// {//    if (this != &s)//  {//         string tmp(s._str);//       swap(tmp);//    }//     return *this;// }// 现代写法更简洁的版本string& operator=(string s){swap(s);return *this;}size_t size() const{return _size;}size_t capacity() const{return _capacity;}void push_back(char ch){// 说明容量满了,需要先扩容if (_size == _capacity){// 需要考虑到_capacity是不是等于0这个情况reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;_size++;_str[_size] = '\0';}void append(const char* str){// 先计算插入字符串以后的长度int len = _size + strlen(str);// 如果容量不大需要扩容if (len > _capacity){reserve(len);}strcpy(_str + _size, str);_size = len;_str[_size] = '\0';}void reserve(size_t n){// 如果容量不够,就要扩容if (n > _capacity){char* tmp = new char[n + 1];// 保留一个位置给\0strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}string& operator+=(const char* str){append(str);return *this;}string& operator+=(char ch){push_back(ch);return *this;}void resize(size_t n, char ch = '\0'){// 如果容量不够,首先要扩容if (n > _capacity){reserve(n);}// 到这里代表容量一定足够for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}string& insert(size_t pos, char ch){// 断言防止pos出现非法范围,当pos=_size时就是push_back// 所以push_back可以复用这个insertassert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;return *this;}string& insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}size_t end = _size + len;while (end > pos + len -1){_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;return *this;}string& erase(size_t pos, size_t len = npos){assert(pos <= _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t begin = pos + len;while (begin <= _size){_str[begin - len] = _str[begin];begin++;}_size -= len;}return *this;}size_t find(char ch, size_t pos = 0){for (; pos < _size; pos++){if (_str[pos] == ch){return pos;}}return npos;}size_t find(const char* str, size_t pos = 0){const char* p = strstr(_str + pos, str);if (p == nullptr){return npos;}else{return p-_str;}}private:char *_str;size_t _size;// 有效字符的个数size_t _capacity;// 存储有效字符的空间const static size_t npos;};const size_t string::npos = -1;std::ostream& operator<<(std::ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}std::istream& operator>>(std::istream& in, string& s){// 先将字符串清空s.resize(0, '\0');// 这种方法可能存在多次扩容的情况,效率较低//char ch;in >> ch;//ch = in.get();//while (ch != ' ' && ch != '\n')//{// s += ch;//    //in >> ch;// ch = in.get();//}//return in;char ch;ch = in.get();char buff[128] = {'\0'};size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){s += buff;memset(buff, '\0', 128);i = 0;}ch = in.get();}s += buff;return in;}bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator<=(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) <= 0;}bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator>(const string& s1, const string& s2){return !(s1 <= s2);}bool operator>=(const string& s1, const string& s2){return !(s1 < s2);}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);}
}

C++关于string类的模拟实现相关推荐

  1. String类的模拟实现

    目录: 一.经典String类的问题 1,浅拷贝 2,深拷贝 二.现代写法版的string类 三.传统String类的模拟实现 1.迭代器 2.operator[] 3.size() 4.c_str( ...

  2. 【c++】string类的模拟实现(下)

    昨天学习了string类的基本实现,今天学习了完整的简单版实现,特来社区记录. 目录 前言 一.增 1.reserve扩容 2.push_back()插入 3.append()插入 4.+=重载 二. ...

  3. 内存分布malloc/calloc/realloc/free/new/delete、内存泄露、String模板、浅拷贝与深拷贝以及模拟string类的实现

    内存分布 一.C语言中的动态内存管理方式:malloc/calloc/realloc和free 1.malloc: 从堆上获得指定字节的内存空间,函数声明:void *malloc (int n); ...

  4. C++STL详解(一)string类的使用及其模拟实现

    文章目录 1. 为什么有string类 2. 什么是string类 3. string的常用接口 构造函数 赋值重载 遍历: 下标+[] operator[]和at对比 迭代器 范围for(C++11 ...

  5. 冰冰学习笔记:string类的简单模拟

    欢迎各位大佬光临本文章!!! 还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正. 本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬.帅哥.美女点点支 ...

  6. 『C++』string类模拟实现

    深拷贝与浅拷贝 首先来看一段代码 #include <iostream> #include <string.h> #include <assert.h>class ...

  7. C++---string类接口整理与深浅拷贝

    string:是表示字符串序列的类,不能操作多字节或者变长字符序列 在使用string类时,必须包含#include头文件以及using namespace std; 常见的接口整理 常见的strin ...

  8. 《C++中STL引入和string类常用接口+自我实现-》

    前言 在这篇博客里将详细说说C++中的STL,通过这篇我们可以学习到什么是STL,以及STL的六大组件,STL具有的缺陷,最后看看string类及面试会让模拟实现string类的操作. 文章目录 前言 ...

  9. Learning C++ No.11【string类实现】

    引言: 北京时间:2023/2/19/8:48,昨天更新了有关进程状态的博客,然后在休息的时候,打开了腾讯视屏,然后看到了了一个电视剧,导致上头,从晚上6点看到了10点,把我宝贵的博客时间给搞没了,伤 ...

最新文章

  1. 鹅厂AI科学家,偷偷把无人摩托写进了年终总结
  2. MySQL安装最后一步apply security settings错误
  3. jquery的实时触发事件(textarea实时获取中文个数)
  4. 表单验证的完美解决方案Validform
  5. mysql nosql 同步_使用canal和canal_mysql_nosql_sync同步mysql数据
  6. linux中进程的控制总结,Linux中的进程控制
  7. SQL服务器引擎组件概览
  8. vs2015打开慢的解决方法
  9. 不要在变量名的旁边加echo和.br;
  10. Multiavatar 多元文化头像生成器
  11. python库怎么绘画_python中的turtle库(图形绘画库)
  12. flex 3 学习小结2
  13. 【软考系统架构设计师】2013年下系统架构师案例分析历年真题
  14. SVN检出项目到本地
  15. 各种分类算法的优缺点
  16. EC11编码器原理以及驱动程序
  17. C-free5 安装教程
  18. 解决Ubuntu远程连接mysql连不上的问题
  19. 捷联惯导算法--体会与心得
  20. ubuntu 配置登录失败次数限制

热门文章

  1. java 实现仿word2007字数统计功能
  2. Word 中利用“多级列表“功能实现章节标题自动编号
  3. mtk插u盘如何休眠?_iOS13.3.1 U盘越狱卡代码问题,你也是吗?
  4. android+高德地图教程,Android高德地图开发(三)地图简单操作
  5. excel表格末尾添加一行_Excel这些办公必备小技巧,Excel表格轻松搞定
  6. ios(苹果)设备直播流媒体 服务搭建
  7. 一键代理浏览器_支持IPv6的防关联指纹浏览器 VMLogin 支持模拟电脑名称、支持修改MAC地址、支持模拟真人输入、支持自定义经纬度...
  8. (64)-- 爬取58同城网页信息
  9. 微信公众号支付宝授权
  10. 快速排序的5种优化方法