【C++】string类的模拟实现

简介: 主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数等资源管理功能。

一. 简单string类设计

主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数等资源管理功能。


1. private成员

就是一个C语言中的字符串指针


class string
{
public:
private:
  char* _str;
};


2. 构造函数

我们设计一个全缺省的默认构造函数,如果不传参时就默认存储\0,就是空串


class string
{
public:
  //构造函数(错误写法)
  string(char* str = '\0')
  {
  _str = str;
  }
private:
  char* _str;
};


上面的那个写法其实是错误的,当我们显示地给string对象初始值(字符串)时,这个字符串是存储在代码段的常量,只读不可写。不能进行修改操作的话这个string对象也就没意义了。


20210501175435780.png


既然不能传存在代码段的常量字符串,那么我们传存储在栈上的字符串行不行?也不行,栈上的字符串是存储在字符数组里的,我们虽然可以修改但是不能扩容,因为数组定义出来时空间就是定死的,这样不方便我们对字符串进行资源管理。


20210501181833128.png


既想要修改字符串内容又想随时扩容,那么把string对象的值放在堆上是最合适的。此时存储在堆空间上的字符串既可以像栈上的字符串一样修改,又可以随时通过new[ ]来开辟你想要大小的空间。


class string
  {
  public:
  //构造函数(正确写法)
  string(const char* str = '\0')//const char* str=""  两种写法一样
  {
    // 1.让_str指向我们在堆上开辟的空间(多开一个为了存储\0)
    _str = new char [strlen(str) + 1];
    // 2.把 str 内容拷贝到 _str(也就是把代码段的内容(str)拷贝到堆空间上(_str))
    strcpy(_str, str);
  }
  private:
  char* _str;
  };


3. 析构函数

使用delete[ ]释放我们开辟的空间,再把_str置为nullptr(防止野指针)


class string
  {
  public:
     //析构函数
  ~string()
  {
    delete[] _str;
    _str = nullptr;
  }
  private:
  char* _str;
  };


4. 拷贝构造和赋值重载

这里一定要显示定义拷贝构造和赋值重载,如果用默认的会造成浅拷贝问题(多次释放同一块空间)


4.1 什么是浅拷贝?

默认的拷贝构造和赋值重载都是通过浅拷贝实现的。浅拷贝就是一个字节一个字节的拷,浅拷贝也叫值拷贝


20210501182444443.png


4.2 浅拷贝带来的问题


20210501182734903.png

4.3 深拷贝完成拷贝构造和赋值重载

既然是指向同一块空间带来的问题,那我们就重新开辟一块同样大小的空间,利用strcpy把另一块空间的内容拷贝到新开辟空间上,这就是深拷贝。


浅拷贝:空间相同,内容相同

深拷贝:空间不同,内容相同

拷贝构造


传统写法:


c

lass string
  {
  public:
  //拷贝构造
  string(const string& s)
  {
    // 1.在_str指向一块新开辟的同样大小的空间(加一个是为了存储\0)
    _str = new char[strlen(_str) + 1];
    // 2.拷贝str空间的内容到_str指向的空间里
    strcpy(_str, s._str);
  }
  private:
  char* _str;
  };


现代写法(更加简洁):


class string
  {
  public:
  string(const string& s)
  {
    string tmp(s._str);
    //这里的swap是c++提供的
    //交换_str和tmp.str指向的空间
    //出了函数tmp生命周期结束,自动调用析构函数释放tmp的空间(也就是原来s的空间)
    swap(_str, tmp._str);
  }
  private:
  char* _str;
  };


赋值重载

赋值重载的两个对象都已经初始化过了,所以在把右值拷贝给左值前要先把左值的旧空间释放,在让它指向新空间


传统写法:


class string
  {
  public:
  //赋值重载
  string& operator=(const string& s)
  {
      if(this!=&s)
      {
      // 1.在_str指向一块新开辟的同样大小的空间(加一个是为了存储\0)
      char* newstr = new char[strlen(s._str) + 1];
      // 2.拷贝str空间的内容到newstr指向的空间里
      strcpy(newstr, s._str);
      // 3.释放旧的空间
      delete[] _str;
      // 4.让_str指向新开辟并且已经拷贝了值的空间
      _str = newstr;
      //返回
      return *this
    }
  }
  private:
  char* _str;
  };


现代写法:


class string
  {
  public:
  string& operator=(const string& s)
  {
    if (this != &s)
    {
    string tmp(s);//拷贝构造s
    swap(_str, tmp._str);
    }
    return *this;
  }
  private:
  char* _str;
  };


赋值重载的几点说明:


返回值:为了支持连等,返回值为string又因为出了这个函数之后*this(也就是左值)依然存在所以返回左值的引用(少一次拷贝构造)。

参数值:对于右值我们只是读它的值用来拷贝给左值,并不修改它的内容,所以加上const修饰

二. string类的模拟实现

private成员

除了字符数组(_str)外还加了 _size(记录当前有效字符个数),_capacity(记录可以存储多少个有效字符)和static常量npos(npos就是size_t类型的-1)


class string
{
public:
private:
  char* _str;
  size_t _size;
  size_t _capacity;
  static const size_t npos;
};


接下来我们介绍几个较复杂的接口


1. string类对象容量操作接口

1.1 reserve

原型:void reserve (size_t n = 0);

作用:给字符串对象扩容,若n小于等于当前容量(_capacity)啥事没有;n大于当前容量就扩容


void reserve(size_t n=0)
  {
    if (n > _capacity)
    {
    char* newstr = new char[n+1];// 1.开新空间
    strcpy(newstr, _str);        // 2.拷贝旧空间
    delete[] _str;               // 3.释放旧空间
    _str = newstr;               // 4.指向新空间
    _capacity = n;               // 5.更新容量
    }
  }


1.2 resize

原型:void resize (size_t n, char c=’\0’);

作用:将有效字符的个数改成n个,如果大于原size多出的有效空间用字符c填充,如果没有传字符c,那就默认是字符’ \0 ';如果小于原size就会截断多出来的有效字符。


void resize(size_t n, char c = '\0')
  {
    if (n > _size)
    {
    //检查是否需要扩容
    if (n > _capacity)
    {
      reserve(n);
    }
    memset(_str + _size, c, n - _size);
    _size = n;
    _str[_size] = '\0';
    }
    else if(n<_size)
    {
    _size = n;
    _str[_size] = '\0';
    }
  }
  //简化后可以这样写
  void resize(size_t n, char c = '\0')
  {
    if (n>_size)
    {
    if (n > _capacity)
    {
      reserve(n);
    }
    memset(_str + _size, c, n - _size);
    }
    _size = n;
    _str[_size] = '\0';
  }


如果要求的有效字符个数大于原来的size那么我们用memset来设置后面多出的有效空间,要注意的是memset是一个字节一个字节地拷贝,一般只在设置字符的时候才会用这个。


下面举个例子来说明这个问题:我们要把10容量的整形数组arr内容用memset设置为3


20210504112814862.png

通过计算器也可以佐证我们的结果


20210504112903216.png

按照一个字节一个字节来设置的就只适用于给字符数组来设置字符,因为一个字符的大小就是一个字节

20210504113248909.png


2. string类对象字符串操作接口

2.1 c_str

原型:const char* c_str() const;

作用:返回C格式字符串,只可读不可写


就是直接返回成员变量_str,它的类型是char*


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


C格式字符串和string对象还是不同的,C格式字符串看’ \0 ‘,用cout输出时遇到’ \0 ‘就结束了;而string对象看的是它的有效字符个数(也就是_size),不管中间有没有’ \0 ’


2.2 substr

原型:string substr (size_t pos = 0, size_t len = npos) const;

作用:在str中从pos下标开始,截取n个字符,然后将其返回


string substr(size_t pos = 0, size_t len = npos) const
  {
    //既然是子串,那下标必须合法
    assert(pos < _size);
    if (len > _size)
    {
    len = _size-pos;
    }
    char* tmp = new char[len + 1];// 1.开新空间(多开一个为了存储\0)
    strncpy(tmp, _str + pos, len);// 2.拷贝子串到新空间
    tmp[len] = '\0';              // 3.处理末尾的\0
    string s_tmp(tmp);            // 4.利用前面开的子串空间拷贝构造一个string对象
    delete[] tmp;                 // 5.释放前面开的新空间
    return s_tmp;                 // 6.返回拷贝构造的string类对象
  }


3. string类对象修改操作接口

3.1 insert

原型:string& insert (size_t pos, const char* s);

作用:在pos位置插入一个字符串


string& insert(size_t pos, const char* str)
  {
    assert(pos <= _size);
    // 1.判断容量是否足够,不够的话需要增容
    int len = strlen(str);
    if (_size + len > _capacity)
    {
    reserve(_size + len);
    }
    // 2.保证空间足够了,就开始挪动数据(一个字符一个字符的挪)
    size_t end = _size;
    while ((int)pos <= (int)end)
    {
    _str[end + len] = _str[end];
    end--;
    }
    // 3.挪好之后,开始放数据
    strncpy(_str + pos, str, len);
    _size += len;
    return *this;
  }


关于insert的几点说明



20210504143723141.png

3.2 erase

原型:string& erase (size_t pos = 0, size_t len = npos);

作用:删除 pos 下标后的 len 长度字符串


string& erase(size_t pos = 0, size_t len = npos)
  {
    assert(pos < _size);
    // 1.如果要求的长度大于等于后面的有效字符的长度,就是删除pos后面的所有有效字符
    if (len >= _size - pos)
    {
    len = _size - pos;
    resize(pos);
    }
    else// 2.删除的是中间一段的话,那就直接把前后拼接起来
    {
    strncpy(_str + pos, _str + pos + len, _size - pos - len + 1);
    _size -= len;
    }
    return *this;
  }


关于erase的几点说明

20210504145334962.png


4. string类的非成员函数

4.1 operator<<

原型:ostream& operator<< (ostream& out, const string& s);

作用:string类的<<运算符重载


o

stream& operator<<(ostream& out, const string& s)
  {
  int len = s.size();
  // 把字符串的字符一个一个的输出,共输出size个
  for (size_t i = 0; i < s.size(); i++)
  {
    out << s[i]; 
  }
  // 最后还要返回out,为了支持连续的<<操作
  return out;
  }


20210504152330637.png

4.2 operator>>

原型:istream& operator>> (istream& , string& s);

作用:重载string类的<<运算符


该运算符读取和C语言里的scanf一样,在读取字符串时不能读取到空格和回车,都是输入回车时算输入完毕。


istream& operator>>(istream& in, string& s)
  {
  while (1)
  {
    char c = in.get();// 从缓冲区接收数据,一个字符一个字符的接收
    //如果遇到空格或者回车算接收完毕
    if (c == ' ' || c == '\n')
    {
    break;
    }
    else// 否则把字符尾插到对象
    {
    s += c;
    }
  }
  return in;
  }


4.3 getline

原型:istream& getline (istream& is, string& str);

作用:string类对象接收一行的数据(空格也可以接收)


相当于C语言的gets(),可以接收一行的数据


istream& getline(istream& in, string& s)
  {
  while (1)
  {
    char c = in.get();
    if (c == '\n')// 从缓冲区接收数据,遇到回车才停止
    {
    break;
    }
    else
    {
    s += c;
    }
  }
  return in;
  }


5. string类的iterator

对于string类而言,它的typedef就是char*(iterator要在类内的pubulic内声明),既然是char*那么就可以像C语言里面的指针一样使用了。


class string
{
public:
    //string类的iterator
  typedef char* iterator;
private:
  char* _str;
  size_t _size;
  size_t _capacity;
  static const size_t npos;
};


我们可以用iterator来遍历string类对象

void test_string()
  {
  string s("hello");
  my_string::string::iterator it = s.begin();
  while (it != s.end())
  {
    cout << *it << " ";
    it++;
  }
  cout << endl;
  }


其实auto也是的底层也是迭代器,auto最终会被编译器转化成迭代器


20210504160606335.png

相关文章
|
1月前
|
编解码 Java 开发者
Java String类的关键方法总结
以上总结了Java `String` 类最常见和重要功能性方法。每种操作都对应着日常编程任务,并且理解每种操作如何影响及处理 `Strings` 对于任何使用 Java 的开发者来说都至关重要。
257 5
|
9月前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
对象存储 C++ 容器
c++的string一键介绍
这篇文章旨在帮助读者回忆如何使用string,并提醒注意事项。它不是一篇详细的功能介绍,而是一篇润色文章。先展示重载函数,如果该函数一笔不可带过,就先展示英文原档(附带翻译),最后展示代码实现与举例可以直接去看英文文档,也可以看本篇文章,但是更建议去看英文原档。那么废话少说直接开始进行挨个介绍。
119 3
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
156 0
|
5月前
|
存储 编译器 C语言
关于string的‘\0‘与string,vector构造特点,反迭代器与迭代器类等的讨论
你真的了解string的'\0'么?你知道创建一个string a("abcddddddddddddddddddddddddd", 16);这样的string对象要创建多少个对象么?你知道string与vector进行扩容时进行了怎么的操作么?你知道怎么求Vector 最大 最小值 索引 位置么?
142 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
239 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
281 12
|
8月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
171 16
|
8月前
|
缓存 安全 Java
《从头开始学java,一天一个知识点》之:字符串处理:String类的核心API
🌱 **《字符串处理:String类的核心API》一分钟速通!** 本文快速介绍Java中String类的3个高频API:`substring`、`indexOf`和`split`,并通过代码示例展示其用法。重点提示:`substring`的结束索引不包含该位置,`split`支持正则表达式。进一步探讨了String不可变性的高效设计原理及企业级编码规范,如避免使用`new String()`、拼接时使用`StringBuilder`等。最后通过互动解密游戏帮助读者巩固知识。 (上一篇:《多维数组与常见操作》 | 下一篇预告:《输入与输出:Scanner与System类》)
220 11