【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

相关文章
|
7天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
33 4
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
30 4
|
1月前
|
Java
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
本文深入探讨了Java中方法参数的传递机制,包括值传递和引用传递的区别,以及String类对象的不可变性。通过详细讲解和示例代码,帮助读者理解参数传递的内部原理,并掌握在实际编程中正确处理参数传递的方法。关键词:Java, 方法参数传递, 值传递, 引用传递, String不可变性。
55 1
【编程基础知识】(讲解+示例实战)方法参数的传递机制(值传递及地址传递)以及String类的对象的不可变性
|
28天前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
24 2
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
数据可视化 Java
让星星月亮告诉你,通过反射创建类的实例对象,并通过Unsafe theUnsafe来修改实例对象的私有的String类型的成员属性的值
本文介绍了如何使用 Unsafe 类通过反射机制修改对象的私有属性值。主要包括: 1. 获取 Unsafe 的 theUnsafe 属性:通过反射获取 Unsafe类的私有静态属性theUnsafe,并放开其访问权限,以便后续操作 2. 利用反射创建 User 类的实例对象:通过反射创建User类的实例对象,并定义预期值 3. 利用反射获取实例对象的name属性并修改:通过反射获取 User类实例对象的私有属性name,使用 Unsafe`的compareAndSwapObject方法直接在内存地址上修改属性值 核心代码展示了详细的步骤和逻辑,确保了对私有属性的修改不受 JVM 访问权限的限制
50 4
|
1月前
|
存储 安全 Java
【一步一步了解Java系列】:认识String类
【一步一步了解Java系列】:认识String类
25 2
|
1月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
16 0