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

文章目录

  • [一. string的默认成员函数](#一. string的默认成员函数)
    • [1.1 构造函数](#1.1 构造函数)
    • [1.2 析构函数](#1.2 析构函数)
    • [1.3 拷贝构造函数](#1.3 拷贝构造函数)
    • [1.4 赋值运算符重载](#1.4 赋值运算符重载)
  • [二. 模拟实现string库](#二. 模拟实现string库)
    • [2.1 c_str() 的实现](#2.1 c_str() 的实现)
    • [2.2 size() 的实现](#2.2 size() 的实现)
    • [2.3 capacity() 的实现](#2.3 capacity() 的实现)
    • [2.4 empty() 的实现](#2.4 empty() 的实现)
    • [2.5 clear() 的实现](#2.5 clear() 的实现)
    • [2.6 operator[ ] 的实现](#2.6 operator[ ] 的实现)
    • [2.7 reserve() 的实现](#2.7 reserve() 的实现)
    • [2.8 resize() 的实现](#2.8 resize() 的实现)
  • [三. 实现迭代器(iterator)](#三. 实现迭代器(iterator))
    • [3.1 范围for](#3.1 范围for)
  • [四. string的增删改查](#四. string的增删改查)
    • [4.1 push_back() 的实现](#4.1 push_back() 的实现)
    • [4.2 append() 的实现](#4.2 append() 的实现)
    • [4.3 operator+= 的实现](#4.3 operator+= 的实现)
    • [4.4 insert() 的实现](#4.4 insert() 的实现)
    • [4.5 erase() 的实现](#4.5 erase() 的实现)
    • [4.6 find() 的实现](#4.6 find() 的实现)
  • [五. 传统写法和现代写法的对比](#五. 传统写法和现代写法的对比)
    • [5.1 拷贝构造的现代写法](#5.1 拷贝构造的现代写法)
    • [5.2 赋值运算符重载的现代写法](#5.2 赋值运算符重载的现代写法)
    • [5.3 整体代码改进](#5.3 整体代码改进)
  • [六. 其他运算符重载](#六. 其他运算符重载)
    • [6.1 operator+ 的实现](#6.1 operator+ 的实现)
    • [6.2 operator== 的实现](#6.2 operator== 的实现)
    • [6.3 operator> 的实现](#6.3 operator> 的实现)
    • [6.4 operator< 的实现](#6.4 operator< 的实现)
    • [6.5 剩下的直接复用](#6.5 剩下的直接复用)
  • [七. 流操作](#七. 流操作)
    • [7.1 流插入](#7.1 流插入)
    • [7.2 流提取](#7.2 流提取)
    • [7.3 getline() 的实现](#7.3 getline() 的实现)
  • [八. MyString::string源码](#八. MyString::string源码)
  • [九. 写时拷贝(了解)](#九. 写时拷贝(了解))
  • END

string类在我们日常写算法题和项目中经常用得到,有了string使我们对字符串的操作极其方便,那么string的底层原理到底是什么呢?接下来我们模拟一下实现string类,去了解它的本质到底是什么,只有自己尝试造一次轮子,心里才会更加清楚它和有利于加深对它的理解。

string的存储结构与顺序表很相似:

💬string.h文件

cpp 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;
namespace MyString
{
	class string
	{
	public:
		static const size_t npos = -1; //npos是size_t类型的最大值
	private:
		char* _str; //存储字符串的首地址
		size_t _size; //存储字符串的长度
		size_t _capacity; //存储字符串的容量
	};
}

使用命名空间MyString将string类进行封装,是为了防止与std标准库的string冲突。

  • _str:指向字符串首字符的指针
  • _size:代表所存储字符串的有效个数
  • _capacity:代表字符串的最大容量
  • _npos:是一个静态常量的无符号整型,此常量值为定为-1,因为size_t是无符号整型,所以它代表的是无符号整型的最大值。

关于npos被定义为static const size_t。我们知道类里的static变量只能在类中声明,在类外定义 ,因为如果一开始就给初始值,这个初始值是给构造函数的初始化列表的,但是static变量不走初始化列表,那么就会报错。如果该静态变量被const修饰,那它既可以在类中定义初始化也可以在类外定义初始化,因为它的初始化只有一次,就当作编译器对它的特殊处理。

关于npos的初始化,以下方式都是可以的:

cpp 复制代码
class string
{
public:
	static const size_t npos = -1;
};
cpp 复制代码
class string
{
public:
	static const size_t npos;
};
const size_t string::npos = -1;

一. string的默认成员函数

1.1 构造函数

我们来看这样的一个代码:

cpp 复制代码
string::string(char* str)
	:_str(str)
{
	_size = strlen(str);
	_capacity = _size + 1;
}

这个构造函数存在两个问题:

1.潜在的内存管理问题

这个构造函数直接将传入的char* str 指针赋值给类内部的_str成员变量。这意味着类实例的_str指针和外部传入的char* str指向同一块内存。如果外部代码修改或释放了这块内存,那么类内部的_str指针将变成悬空指针或指向无效内存,导致未定义行为。

2.缺乏对参数的保护

如果传入的char* strnullptr 或者指向的内存不是以'\0'结尾的字符串,strlen(str)将导致未定义行为,可能程序崩溃。

所以每次调用构造函数时都要进行动态内存分配并复制内存,确保了对资源的拥有和管理。

💬string.h文件

cpp 复制代码
string(const char* str = ""); //默认构造函数,缺省值为空字符串
string(size_t n, char c); //初始化为n个字符c

💬string.cpp文件

cpp 复制代码
string::string(const char* str)
	:_size(strlen(str))
{
	_capacity = _size;
	_str = new char[_size + 1]; //多开一个空间用来存放'\0'
	strcpy(_str, str);
}

string::string(size_t n, char c)
	:_size(n)
{
	_capacity = _size;
	_str = new char[_size + 1];
	for (size_t i = 0; i < n; i++)
	{
		_str[i] = c;
	}
	_str[n] = '\0';
}

这里解释一下为什么在string(const char* str = ""); 中字符串str给的缺省值是空字符串,不能是'\0'和nullptr吗?

  1. 不能是'\0'的原因

因为str是C语言风格的常量字符串,在字符串的末尾会自动添加'\0',无需我们手动写。如果写了'\0',那么该字符串str就有两个'\0'了。

  1. 不能是nullptr的原因

如果str指向nullptr,那么当调用函数strlen(str)时程序就会崩溃,因为strlen()函数是不会去检查空的,它是去找'\0'的。相当于直接对指针str解引用了,因为str指向空,所以引发了空指针问题。

所以我们这里给的是空字符串"",因为常量字符串默认就带有'\0',所以不会出现问题。

1.2 析构函数

注意:申请内存时用的是new[ ],那么释放内存时就要用delete[ ]。

💬string.h文件

cpp 复制代码
~string();

💬string.cpp文件

cpp 复制代码
string::~string()
{
	delete[] _str;
	_str = nullptr; //不要忘了将指针_str置为空,避免_str成为野指针
	_size = _capacity = 0;
}

1.3 拷贝构造函数

我们知道,当我们不写拷贝构造函数时,编译器会自动生成一个拷贝构造函数。

💬 Test.cpp文件

cpp 复制代码
#include "string.h"
void Test1()
{
	MyString::string b(10, 'c');
	MyString::string s1(b);
}
int main()
{
	Test1();
	return 0;
}

当执行上述程序时,程序就崩溃了:

上述string类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会生成默认的,当用b构造s1时,编译器会调用默认的拷贝构造。最终导致的问题是,b._str,s1._str共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝/值拷贝。

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

通过调试可以发现,b._str和s1._str确实指向了同一块内存空间。因为后定义的先析构,s1先调用析构函数将_str指向的内存空间释放掉了,b再想调用析构函数将_str指向的空间释放就会报错,因为一块空间不能被释放多次,只能释放一次。

那么如何解决这个问题呢❓

只需要将对象b和s1的_str指向两个不同的内存空间即可。

这种拷贝方式叫做深拷贝 :对于包含指针或动态分配内存的对象,深拷贝会分配新的内存并复制内容,确保两个对象不共享同一块内存。

使用场景:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。

浅拷贝与深拷贝的区别

特性 浅拷贝 深拷贝
内存分配 不分配内存,直接复制指针 分配新内存并复制内容
对象独立性 两个对象共享同一块内存 两个对象拥有独立的内存
安全性 可能导致双重释放或悬空指针等问题 更安全,避免了共享内存带来的问题
性能 更快,因为不涉及内存分配和数据复制 较慢,因为需要内存分配和数据复制
适用场景 对象不包含动态内存分配的资源或指针 对象包含动态内存分配的资源或指针,需要独立副本

💬string的拷贝构造函数

cpp 复制代码
string::string(const string& s)
{
	_str = new char[s._capacity + 1]; //多开一个空间用来存放'\0'
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}

通过调试可以看到b._str和s1._str指向不同的两个内存空间。

1.4 赋值运算符重载

cpp 复制代码
void Test1()
{
    MyString::string s1("hello world");
	MyString::string s2("hello");
	s1 = s2;
}

看如上代码,试图将s2赋值给s1,结果发生了如下报错。和之前拷贝构造一样,如果自己不写,编译器会自动生成一个浅拷贝方式的赋值运算符重载函数,s1._str和s2._str又指向了同一块内存空间,当调用析构函数时一块内存空间被析构多次而引发报错。

先看一下以下这段代码有没有什么问题

cpp 复制代码
string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
{
	delete[] _str; //先将当前的内存空间释放掉
	_str = new char[s._capacity + 1]; //重新开辟和s一样大的空间,多开一个空间是为了存储'\0'
	strcpy(_str, s._str); //复制数据
	_size = s._size;
	_capacity = s._capacity;
	return *this;
}

看似好像没问题,赋值后s1和s2都指向了两块不同的内存空间。

那我们再来看一下以下示例:

cpp 复制代码
void Test2()
{
	MyString::string s1("hello world");
	s1 = s1;
}

如果一个对象给自己赋值就会发生如上问题:先将_str指向的内存空间释放掉,然后再开辟一个新的空间,然后再尝试复制数据,但是_str之前的空间已经被释放了,现在的_str指向的是新的空间,新空间里面没有存储任何数据(包括 '\0' ),自己给自己拷贝数据时就会出现随机字符。

那么怎么解决这个问题呢?------>很简单,只要判断传入的对象s的地址是否和当前对象的地址相同即可,如果相同则无需任何操作,直接返回,否则再进行对应的操作。

💬string的赋值运算符重载

cpp 复制代码
string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
{
	if (this != &s) //防止自己给自己赋值
	{
		delete[] _str; //先将当前的内存空间释放掉
		_str = new char[s._capacity + 1]; //重新开辟和s一样大的空间,多开一个空间是为了存储'\0'
		strcpy(_str, s._str); //复制数据
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

如果担心开辟空间失败,又已经把_str指向的内存空间释放了,那么可以先用临时指针去指向新开辟的空间,如果开辟成功了再将_str指向的旧空间释放,再指向新空间即可。

cpp 复制代码
string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
{
	if (this != &s) //防止自己给自己赋值
	{
		char* tmp = new char[s._capacity + 1]; //先用临时指针tmp接受新开辟的空间
		strcpy(tmp, s._str); //如果开辟成功就将数据赋值到这个空间
		delete[] _str; //释放旧空间
		_str = tmp; //将_str指向新空间
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

再来看一下如下代码:

cpp 复制代码
void Test2()
{
	//构造+拷贝------>直接构造
	MyString::string s1 = "hello world";
}

看到这段代码是不是会想到,先用字符串"hello world"构造一个临时对象(隐式类型转换),再用这个临时对象拷贝构造出s1。但是编译器将这个过程优化了,优化成直接用字符串"hello world"构造s1,调用的是string(const char* str = ""); 这个函数。

注意 :字符串"hello world"隐式类型转换为string对象时,需要有用字符串构造对象的构造函数,比如string(const char* str = "");

转到反汇编验证一下:

发现它们调用的是一模一样的函数。

二. 模拟实现string库

2.1 c_str() 的实现

cpp 复制代码
const char* c_str() const;

这个函数返回的是C语言风格的字符串,不支持修改。因为不涉及对象成员的修改,所以可以用const修饰this,同时也兼并了const对象。

cpp 复制代码
const char* string::c_str() const
{
	return _str;
}
cpp 复制代码
void Test3()
{
	MyString::string s1("hello world");
	cout << s1.c_str() << endl;
}

2.2 size() 的实现

cpp 复制代码
size_t size() const;

这个函数的功能是返回字符串的长度,跟上面的类似,这里直接返回_size即可。

cpp 复制代码
size_t string::size() const
{
	return _size;
}

2.3 capacity() 的实现

cpp 复制代码
size_t capacity() const;

这个函数的功能返回字符串的容量,跟上面的类似,这里直接返回_capacity即可。

cpp 复制代码
size_t string::capacity() const
{
	return _capacity;
}

2.4 empty() 的实现

cpp 复制代码
bool empty() const;

这个函数的功能是判断字符串是否为空,如果为空返回true,否则返回false。

cpp 复制代码
bool string::empty() const
{
	return _size == 0;
}

2.5 clear() 的实现

cpp 复制代码
void clear();

该函数的功能是清空一个字符串,但是不释放空间,只是在下标位置为0的字符置为'\0',_size置为0即可。

cpp 复制代码
void string::clear()
{
	_str[0] = '\0';
	_size = 0;
}

2.6 operator[ ] 的实现

cpp 复制代码
char& operator[](size_t pos);
const char& operator[](size_t pos) const;

这里不仅要写普通对象的版本,还要写const对象的版本。因为普通对象指定位置的字符是可以通过该函数进行修改的,但是const对象不行,它的字符串是不能修改的。

cpp 复制代码
char& string::operator[](size_t pos)
{
	assert(pos < _size); //确保pos不能越界,越界了就断言
	return _str[pos];
}
const char& string::operator[](size_t pos) const
{
	assert(pos < _size); //确保pos不能越界,越界了就断言
	return _str[pos];
}
cpp 复制代码
void Test3()
{
	MyString::string s1("hello world");
	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
	}
	cout << endl;
	for (size_t i = 0; i < s1.size(); i++)
	{
		s1[i] += 1;
		cout << s1[i] << " ";
	}
}

2.7 reserve() 的实现

cpp 复制代码
void reserve(size_t n = 0);

该函数的功能是调整字符串容量大小为n,如果n大于字符串容量则将其扩容到n个字节,否则不会进行任何操作

cpp 复制代码
void string::reserve(size_t n)
{
	//检查n是否大于原有的容量,必须大于才能调整字符串的容量
	if (n > _capacity) {
		char* tmp = new char[n + 1]; //用指针tmp去接受新开的内存空间
		strcpy(tmp, _str); //如果开辟成功,将数据拷贝到新空间
		delete[] _str; //将旧空间释放掉
		_str = tmp; //将_str指向新空间
		_capacity = n; //更改容量值
	}
}

注意 :不要忘记多开一个空间存储 \0

接下来测试一下案例:

cpp 复制代码
void Test4()
{
	MyString::string s1("hello world");
	s1.reserve(20);
	s1.reserve(5);
}

当前字符串容量小于20,新开一个内存空间的大小为20,并将数据拷贝给这个新空间。 当前字符串容量大于5,则不会进行任何操作。

2.8 resize() 的实现

cpp 复制代码
void resize(size_t n);
void resize(size_t n, char c);

功能:将字符串的长度调整为n个字符。

解释:

  1. 如果n小于当前字符串的长度,则字符串缩短为前n个字符,并删除第n个字符以外的字符。
  2. 如果n大于当前字符串的长度,则通过在字符串末尾插入所需数量的字符来增加字符串的长度,以达到n的大小。如果函数参数指定了字符c,则在字符串末尾插入的字符为c的副本,否则,插入的字符为空字符。
  • 缩小长度:将_size赋值为n,并在下标为_size的字符设置为\0
  • 增加长度:1. 增加后的长度在容量范围内:如果指定了字符c,则在[_size,n-1]的位置插入字符c,再将_size赋值为n,并在n的位置插入\0,如果没有指定字符c,就执行后两步。2. 增加后的长度超过了容量范围:先对字符串扩容,再执行以上操作。
cpp 复制代码
void string::resize(size_t n)
{
	if (n > _capacity) {
		reserve(n); //扩容
	}
	_size = n;
	_str[_size] = '\0';
}
void string::resize(size_t n, char c)
{
	if (n > _capacity) {
		reserve(n); //扩容
	}
	for (size_t i = _size; i < n; i++)
	{
		//如果是增加长度就会进入循环,在字符串末尾插入所需数量的字符c
		_str[i] = c;
	}
	_size = n;
	_str[_size] = '\0';
}

测试:
创建string类型的对象为s1 当前字符串容量小于20,先扩容再执行操作,因为没有给定补全的字符,所以给空字符 调整字符串长度为5,_size赋值为5,并在位置为_size的字符赋为\0 创建string类型的对象为s2 当前字符串容量小于20,先扩容再执行操作,因为给定了补全的字符,所以在字符串末尾插入所需数量的字符('x') 调整字符串长度为5,_size赋值为5,并在位置为_size的字符赋为\0

三. 实现迭代器(iterator)

迭代器的本质

  • 指针或指针封装:迭代器的底层本质是一个指针,或者对指针进行了封装。例如,vector的迭代器就是一个原生态指针T*,而其他容器如list、map等的迭代器则是对指针进行了封装,以适应其内部复杂的数据结构。
  • 运算符重载:迭代器通过重载解引用运算符(operator*)、递增运算符(operator++)等,使得用户可以像操作指针一样操作迭代器,实现对容器元素的访问和遍历。

在C++中,std::string 的迭代器底层实现本质上是一个指针。std::string 内部通常使用连续的内存块来存储字符序列,因此其迭代器可以直接使用字符指针char*(对于非const字符串)或const char*(对于const字符串)来实现。

迭代器的声明:

cpp 复制代码
typedef char* iterator;
iterator begin();
iterator end();

typedef const char* const_iterator;
const_iterator begin() const;
const_iterator end() const;

迭代器的定义:

cpp 复制代码
string::iterator string::begin()
{
	return _str;
}
string::iterator string::end()
{
	return _str + _size;
}
string::const_iterator string::begin() const
{
	return _str;
}
string::const_iterator string::end() const
{
	return _str + _size;
}

测试:

cpp 复制代码
void Test5()
{
	MyString::string s1("hello world");
	MyString::string::iterator it1 = s1.begin();
	while (it1 != s1.end())
	{
		cout << *it1;
		it1++;
	}
	cout << endl;

	const MyString::string s2("hello hello");
	MyString::string::const_iterator it2 = s2.begin();
	while (it2 != s2.end())
	{
		cout << *it2;
		it2++;
	}
	cout << endl;
}

3.1 范围for

cpp 复制代码
MyString::string s1("hello world");
for (auto e : s1)
{
	cout << e;
}
cout << endl;

因为范围for的的底层实现本质是迭代器,编译时范围for会被替换成迭代器,所以只要把迭代器实现好了,范围for就可以直接使用。

转到反汇编就可以验证:

注意

如果将迭代器begin()替换成Begin(),范围for还能正常使用吗?

cpp 复制代码
string::iterator string::Begin()
{
	return _str;
}
string::const_iterator string::Begin() const
{
	return _str;
}

程序发生了以下报错:

这说明范围for是按照迭代器固定名称(begin和end)去查找并替换的,如果自己实现的迭代器没有按固定名称去命名,那范围for也不再支持。

四. string的增删改查

4.1 push_back() 的实现

该函数的功能是将字符c插入到字符串末尾,使字符串长度加一。

cpp 复制代码
void string::push_back(char c)
{
	//首先检查是否需要扩容
	if (_size == _capacity) {
		//扩容
		reserve(_size == 0 ? 4 : 2 * _size);
	}
	_str[_size] = c;
	_size++;
	_str[_size] = '\0'; //最后不要忘了在末尾字符的下一个位置放\0
}

测试:

cpp 复制代码
void Test6()
{
	MyString::string s1;
	s1.push_back('a');
	s1.push_back('b');
	s1.push_back('c');
}

字符串s1被初始化为空串,此时_size和_capacity都为0

开辟了4个大小的空间,将a,b,c都插入到了字符串的末尾

4.2 append() 的实现

append有以上这些函数,在这里实现前五个。

cpp 复制代码
//尾部追加一个string对象
string& append(const string& str);

//从对象str的subpos位置开始获取sublen个字符,如果sublen大于剩余字符串的最大长度则将其剩余字符串全部追加
string& append(const string& str, size_t subpos, size_t sublen = npos);

//尾部追加字符串s
string& append(const char* s);

//尾部追加字符串s的前n个字符
string& append(const char* s, size_t n);

//尾部追加n个字符c
string& append(size_t n, char c);
  1. 尾部追加一个string对象
cpp 复制代码
string& string::append(const string& str)
{
	size_t len = str._size;
	//首先检查是否需要扩容
	if (_size + len > _capacity) {
		size_t newCapacity = 2 * _capacity;
		if (newCapacity < _size + len) {
			newCapacity = _size + len;
		}
		reserve(newCapacity);
	}
	strcpy(_str + _size, str._str); //拷贝数据,同时\0也拷贝过去了,不用手动设置\0
	_size += len;
	return *this;
}
  1. 尾部追加字符串s
cpp 复制代码
string& string::append(const char* s)
{
	size_t len = strlen(s);
	if (_size + len > _capacity) {
		size_t newCapacity = 2 * _capacity;
		if (newCapacity < _size + len) {
			newCapacity = _size + len;
		}
		reserve(newCapacity);
	}
	strcpy(_str + _size, s);
	_size += len;
	return *this;
}
  1. 尾部追加字符串s的前n个字符
cpp 复制代码
string& string::append(const char* s, size_t n)
{
	size_t len = strlen(s);
	if (n > len) {
		//如果n大于待插入的字符串长度,则赋值为该字符串的长度值
		n = len;
	}
	//检查扩容
	if (_size + n > _capacity) {
		size_t newCapacity = 2 * _capacity;
		if (newCapacity < _size + n) {
			newCapacity = _size + n;
		}
		reserve(newCapacity);
	}
	for (size_t i = 0; i < n; i++)
	{
		_str[_size++] = s[i];
	}
	_str[_size] = '\0'; //需要手动设置\0
	return *this;
}
  1. 从对象str的subpos位置开始获取sublen个字符,如果sublen大于剩余字符串的最大长度则将其剩余字符串全部追加
cpp 复制代码
string& string::append(const string& str, size_t subpos, size_t sublen)
{
	size_t len = str._size;
	assert(subpos < len); //检查subpos是否合法
	if (subpos + sublen >= len) {
		append(str._str + subpos); //复用append(const char* s)
	}
	else {
		append(str._str + subpos, sublen); //复用append(const char* s, size_t n)
	}
	return *this;
}

测试:

cpp 复制代码
MyString::string s1("hello world");
cout << s1.c_str() << endl;
MyString::string s2("xxxxxxxx");
s1.append(s2, 0);
cout << s1.c_str() << endl;
MyString::string s3("abcdefg");
s1.append(s3, 0, 3);
cout << s1.c_str() << endl;

MyString::string s4;
s4.append(s3, 0);
cout << s3.c_str() << endl;
  1. 尾部追加n个字符c
cpp 复制代码
string& string::append(size_t n, char c)
{
	//检查扩容
	if (_size + n > _capacity) {
		size_t newCapacity = 2 * _capacity;
		if (newCapacity < _size + n) {
			newCapacity = _size + n;
		}
		reserve(newCapacity);
	}
	for (size_t i = 0; i < n; i++)
	{
		_str[_size + i] = c;
	}
	_size += n;
	_str[_size] = '\0';//手动设置\0
	return *this;
}

4.3 operator+= 的实现

cpp 复制代码
string& operator+=(const string& str);
string& operator+=(const char* s);
string& operator+=(char c);

因为我们之前实现了append和push_back,直接复用就可以了。

cpp 复制代码
string& string::operator+=(const string& str)
{
	append(str);
	return *this;
}
string& string::operator+=(const char* s)
{
	append(s);
	return *this;
}
string& string::operator+=(char c)
{
	push_back(c);
	return *this;
}

4.4 insert() 的实现

这里主要实现以下几个函数:

cpp 复制代码
//在指定位置pos插入字符串s,在pos以及之后的字符向后移动
string& insert(size_t pos, const char* s);

//在指定位置pos插入字符串对象str,在pos以及之后的字符向后移动
string& insert(size_t pos, const string& str);

//在指定位置pos插入字符c,在pos以及之后的字符向后移动
string& insert(size_t pos, char c);

//在指定位置pos插入n个字符c,在pos以及之后的字符向后移动
string& insert(size_t pos, size_t n, char c);
cpp 复制代码
string& string::insert(size_t pos, const char* s)
{
	assert(pos <= _size);
	size_t len = strlen(s);
	if (_size + len > _capacity) {
		size_t newCapacity = 2 * _capacity;
		if (newCapacity < _size + len) {
			newCapacity = _size + len;
		}
		reserve(newCapacity);
	}
	size_t end = _size + len;
	while (end >= pos + len)
	{
		_str[end] = _str[end - len];
		end--;
	}
	for (size_t i = 0; i < len; i++)
	{
		_str[pos + i] = s[i];
	}
	_size += len;
	return *this;
}
string& string::insert(size_t pos, const string& str)
{
	insert(pos, str._str); //复用
	return *this;
}
string& string::insert(size_t pos, char c)
{
	assert(pos <= _size);
	if (_size == _capacity) {
		reserve(_size == 0 ? 4 : 2 * _size);
	}
	size_t end = _size + 1;
	while (end >= pos + 1)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = c;
	_size++;
	return *this;
}
string& string::insert(size_t pos, size_t n, char c)
{
	assert(pos <= _size);
	if (_size + n > _capacity) {
		size_t newCapacity = 2 * _capacity;
		if (newCapacity < _size + n) {
			newCapacity = _size + n;
		}
		reserve(newCapacity);
	}
	size_t end = _size + n;
	while (end >= pos + n)
	{
		_str[end] = _str[end - n];
		end--;
	}
	for (size_t i = 0; i < n; i++)
	{
		_str[pos + i] = c;
	}
	_size += n;
	return *this;
}

测试:

cpp 复制代码
void Test7()
{
	MyString::string s1("hello world");
	MyString::string s2("xxx");
	s2.insert(0, s1);
	cout << s2.c_str() << endl;
	s2.insert(10, "zzz");
	cout << s2.c_str() << endl;
	s2.insert(15, 'o');
	cout << s2.c_str() << endl;
	s2.insert(0, 5, 'k');
	cout << s2.c_str() << endl;
}

4.5 erase() 的实现

cpp 复制代码
//在指定位置pos开始删除len个字符
string& erase(size_t pos = 0, size_t len = npos);

该函数的功能是在指定位置pos开始删除len个字符,pos不写时默认为0,len不写时默认npos,表示将第pos个以及之后的字符删除。

cpp 复制代码
string& string::erase(size_t pos, size_t len)
{
	assert(pos < _size);
	if (len >= _size - pos) {
		_size = pos;
		_str[_size] = '\0';
	}
	else {
		size_t end = pos + len;
		while (end <= _size)
		{
			_str[end - len] = _str[end];
			end++;
		}
		_size -= len;
	}
	return *this;
}

4.6 find() 的实现

主要实现以下两个函数:

cpp 复制代码
//从指定位置pos开始查找一个字符c,并返回字符c首次出现的位置,如果找不到就返回npos
size_t find(char c, size_t pos = 0) const;

//从指定位置pos开始查找字符串s,找到后返回字符串s第一个字符的下标,否则返回npos
size_t find(const char* s, size_t pos = 0) const;

注意:要用const修饰this指针指向的对象,目的是不能通过this指针去修改对象,同时还兼并了const对象。

  1. 查找字符
cpp 复制代码
size_t string::find(char c, size_t pos = 0) const
{
	assert(pos < _size);
	for (size_t i = pos; i < _size; i++)
	{
		if (_str[i] == c) {
			return i;
		}
	}
	return npos;
}

测试:

cpp 复制代码
MyString::string s1("hello world");
cout << s1.c_str() << endl;
size_t n = s1.find(' ');
s1.erase(n, 1);
cout << s1.c_str() << endl;
  1. 查找字符串

首先介绍一个函数:

c 复制代码
char *strstr(char *str1, const char *str2);

该函数的功能:在字符串 str1 中查找子串 str2 的首次出现位置,并返回指向该位置的指针。如果找不到子串,则返回空。

cpp 复制代码
size_t string::find(const char* s, size_t pos) const
{
	assert(pos < _size);
	const char* ptr = strstr(_str + pos, s);
	if (ptr == nullptr) {
		return npos;
	}
	return ptr - _str;
}

测试:

cpp 复制代码
MyString::string s1("hello world");
cout << s1.c_str() << endl;
size_t n = s1.find("hello");
s1.erase(n, 5);
cout << s1.c_str() << endl;
n = s1.find("rld");
s1.erase(n);
cout << s1.c_str() << endl;

五. 传统写法和现代写法的对比

5.1 拷贝构造的现代写法

💬传统写法

cpp 复制代码
string::string(const string& s)
{
	_str = new char[s._capacity + 1]; //多开一个空间用来存放'\0'
	strcpy(_str, s._str);
	_size = s._size;
	_capacity = s._capacity;
}

传统写法非常老实,先开特定大小的空间给_str再拷贝数据,最后更新_size和_capacity

💬现代写法

cpp 复制代码
string::string(const string& s)
{
	string tmp(s.c_str());
	swap(_str, tmp._str);
	swap(_size, tmp._size);
	swap(_capacity, tmp._capacity);
}

现代写法就是复用string(const char* str = ""); 这个默认构造函数,创建一个新对象tmp,再将tmp里面所有的成员变量与当前对象的成员变量进行交换,出了作用域tmp就会被析构,不会造成内存泄漏。

注意 :在类的成员函数中,可以直接访问该类的私有和保护成员,包括其他对象的私有和保护成员。

在C++中,类的成员函数在访问权限上具有以下特点:

  1. 访问本类的私有和保护成员:类的成员函数可以访问同一类中其他对象的私有和保护成员。这是因为成员函数属于类的内部实现细节,设计上允许它们操作类的内部状态,包括其他对象的内部状态。
  2. 友元函数:如果一个函数被声明为类的友元函数,那么它也可以访问该类的私有和保护成员。

5.2 赋值运算符重载的现代写法

💬传统写法

cpp 复制代码
string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
{
	if (this != &s) //防止自己给自己赋值
	{
		char* tmp = new char[s._capacity + 1]; //先用临时指针tmp接受新开辟的空间
		strcpy(tmp, s._str); //如果开辟成功就将数据赋值到这个空间
		delete[] _str; //释放旧空间
		_str = tmp; //将_str指向新空间
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

赋值运算符重载和拷贝构造是差不多的,传统写法也是类似,开新空间------>拷贝数据------>释放旧空间------>指向新空间------>更新其他的成员变量。

💬现代写法

cpp 复制代码
string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
{
	if (this != &s) //防止自己给自己赋值
	{
		string tmp(s);
		swap(_str, tmp._str);
		swap(_size, tmp._size);
		swap(_capacity, tmp._capacity);
	}
	return *this;
}

现代写法就是创建一个临时对象,再与其成员变量进行交换即可。

还有一个更简单的写法:

cpp 复制代码
string& string::operator=(string s) //返回当前对象的引用是为了能连线赋值
{
	swap(_str, s._str);
	swap(_size, s._size);
	swap(_capacity, s._capacity);
	return *this;
}

进入该函数之前会先调用拷贝构造函数(将s2传给s),构造对象s,再将s所有的成员变量与当前对象的成员变量进行交换。这样看起来就非常简洁,复用性很高。

5.3 整体代码改进

在以上函数中,我们可以发现我们大量调用了swap函数:

cpp 复制代码
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);

我们可以将这些内容封装成一个成员函数:

cpp 复制代码
void string::swap(string& s)
{
	std::swap(_str, s._str);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

注意:在类成员函数swap内部要调用std库里的swap函数时,要指定std,因为编译器会先去寻找当前域里面的swap函数。

进一步改善后的代码如下:

cpp 复制代码
string::string(const string& s)
{
	string tmp(s.c_str());
	swap(tmp);
}

string& string::operator=(string s) //返回当前对象的引用是为了能连线赋值
{
	swap(s);
	return *this;
}

非常的简洁!

如果想交换两个string对象(比如s1和s2),得这样写:s1.swap(s2)或s2.swap(s1),为了和std库里的swap相适应,我们可以这样写:swap(s1,s2),这就需要在类外实现一个swap函数,这样就不会通过函数模板再来创建一个swap函数了。

cpp 复制代码
void swap(string& s1, string& s2)
{
	s1.swap(s2);
}

很奇怪这里为什么调用的是MyString域里面的swap函数,而不是std库里的swap函数呢?
这是因为编译器识别string对象s1和s2是在MyString域里面的,会优先在MyString域里寻找swap函数,找到了就直接调用,找不到才会去调用std库里的swap函数(前提是在全局域中展开了std库)。

六. 其他运算符重载

6.1 operator+ 的实现

主要实现以下函数:

cpp 复制代码
string operator+(const string& s1, const string& s2);
string operator+(const string& s1, const char* str);
string operator+(const string& s1, char c);
cpp 复制代码
string operator+(const string& s1, const string& s2)
{
	string tmp(s1);
	tmp += s2;
	return tmp;
}
string operator+(const string& s1, const char* str)
{
	string tmp(s1);
	tmp += str;
	return tmp;
}
string operator+(const string& s1, char c)
{
	string tmp(s1);
	tmp.push_back(c);
	return tmp;
}
  1. 因为operator+不会改变对象里的成员变量,所以可以使用const修饰对象。
  2. 因为返回的是临时对象,所以不能使用引用返回。

6.2 operator== 的实现

先来介绍一下strcmp函数:

c 复制代码
int strcmp(const char *str1, const char *str2);

参数

  • str1:指向第一个 C 字符串的指针。
  • str2:指向第二个 C 字符串的指针。

返回值

  • 如果 str1 和 str2 相等,返回 0。
  • 如果 str1 小于 str2,返回一个负整数。
  • 如果 str1 大于 str2,返回一个正整数。

比较方式
strcmp 按字典顺序逐个字符比较两个字符串,直到遇到不同的字符或空字符 \0 为止。比较是基于字符的 ASCII 值进行的。

cpp 复制代码
bool operator==(const string& lhs, const string& rhs)
{
	return strcmp(lhs.c_str(), rhs.c_str()) == 0;
}

6.3 operator> 的实现

cpp 复制代码
bool operator>(const string& lhs, const string& rhs)
{
	return strcmp(lhs.c_str(), rhs.c_str()) > 0;
}

6.4 operator< 的实现

cpp 复制代码
bool operator<(const string& lhs, const string& rhs)
{
	return strcmp(lhs.c_str(), rhs.c_str()) < 0;
}

6.5 剩下的直接复用

cpp 复制代码
bool operator!=(const string& lhs, const string& rhs)
{
	return !(lhs == rhs);
}
bool operator>=(const string& lhs, const string& rhs)
{
	return !(lhs < rhs);
}
bool operator<=(const string& lhs, const string& rhs)
{
	return !(lhs > rhs);
}

测试:

cpp 复制代码
MyString::string s1("abc");
MyString::string s2("efg");
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;
cout << (s1 >= s2) << endl;
cout << (s1 <= s2) << endl;
cout << (s1 > s2) << endl;
cout << (s1 < s2) << endl;

七. 流操作

7.1 流插入

cpp 复制代码
ostream& operator<< (ostream& os, const string& str); //流插入

因为cout支持连续输出,所以要返回ostream对象的引用使其可以继续调用。

cpp 复制代码
ostream& operator<< (ostream& os, const string& str)
{
	for (auto& e : str)
	{
		os << e;
	}
	return os;
}

测试:

cpp 复制代码
void Test11()
{
	MyString::string s1("abcdefg");
	cout << s1 << endl;
	MyString::string s2("zzzllllll");
	cout << s2 << endl;
}

7.2 流提取

cpp 复制代码
istream& operator>> (istream& is, string& str); //流提取

为了支持连续输入,返回istream对象的引用使其可以继续调用。

因为我们无法确定输入字符串的长度,如果一个一个地将字符给str,必然会引起频繁地扩容,从而导致性能的下降。

所以我们可以定义一定长度的缓冲区buff,先将字符串输入到缓冲区中,如果缓冲区满了就将该缓冲区所有的字符写入到string字符串中,再刷新缓冲区,继续接收字符,这样就避免了频繁扩容的问题。

注意:在使用流提取前,先将string字符串清空(clear),不管该字符串有没有字符。

cpp 复制代码
istream& operator>> (istream& is, string& str)
{
	str.clear();//清空字符串
	char buff[256];//定义缓冲区
	char ch = is.get();//从输入流中获取一个字符
	int i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == 255) {
			buff[i] = '\0'; //不要忘了在字符串末尾给\0
			str += buff;
			i = 0;
		}
		ch = is.get();//从输入流中获取一个字符
	}
	if (i) {
		buff[i] = '\0';
		str += buff;
	}
	return is;
}

测试:

cpp 复制代码
void Test12()
{
	MyString::string s1;
	MyString::string s2;
	cin >> s1 >> s2;
	cout << s1 << endl << s2;
}

7.3 getline() 的实现

cpp 复制代码
istream& getline(istream& is, string& str, char delim = '\n'); //自定义获取字符串

与流提取不同的是,流提取是遇到空格(' ')或换行符('\n')就停止提取了,getline是根据传入的参数delim来判断什么时候停止输入,即遇到字符delim就停止输入。如果不传字符delim,就默认为'\n',即遇到换行符就停止输入。

cpp 复制代码
istream& getline(istream& is, string& str, char delim)
{
	str.clear();//清空字符串
	char buff[256];//定义缓冲区
	char ch = is.get();//从输入流中获取一个字符
	int i = 0;
	while (ch != delim)
	{
		buff[i++] = ch;
		if (i == 255) {
			buff[i] = '\0';//不要忘了在字符串末尾给\0
			str += buff;
			i = 0;
		}
		ch = is.get();//从输入流中获取一个字符
	}
	if (i) {
		buff[i] = '\0';
		str += buff;
	}
	return is;
}

测试:

cpp 复制代码
void Test11()
{
	MyString::string s1;
	MyString::string s2;
	getline(cin, s1);
	getline(cin, s2, '#');
	cout << s1 << endl;
	cout << s2;
}

八. MyString::string源码

string.h文件

cpp 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;
namespace MyString
{
	class string
	{
	public:
		static const size_t npos; //npos是size_t类型的最大值
		string(const char* str = ""); //默认构造函数,缺省值为空字符串
		string(size_t n, char c); //初始化为n个字符c
		string(const string& s); //拷贝构造函数
		string& operator=(string s);
		~string();
		void swap(string& s);

		const char* c_str() const;
		size_t size() const;
		size_t capacity() const;
		bool empty() const;
		void clear();
		char& operator[](size_t pos);
		const char& operator[](size_t pos) const;

		void reserve(size_t n = 0);
		void resize(size_t n);
		void resize(size_t n, char c);

		typedef char* iterator;
		iterator begin();
		iterator end();

		typedef const char* const_iterator;
		const_iterator begin() const;
		const_iterator end() const;

		void push_back(char c);

		//尾部追加一个string对象
		string& append(const string& str);

		//从对象str的subpos位置开始获取sublen个字符,如果sublen大于剩余字符串的最大长度则将其剩余字符串全部追加
		string& append(const string& str, size_t subpos, size_t sublen = npos);

		//尾部追加字符串s
		string& append(const char* s);

		//尾部追加字符串s的前n个字符
		string& append(const char* s, size_t n);

		//尾部追加n个字符c
		string& append(size_t n, char c);

		string& operator+=(const string& str);
		string& operator+=(const char* s);
		string& operator+=(char c);

		//在指定位置pos插入字符串s,在pos以及之后的字符向后移动
		string& insert(size_t pos, const char* s);

		//在指定位置pos插入字符串对象str,在pos以及之后的字符向后移动
		string& insert(size_t pos, const string& str);

		//在指定位置pos插入字符c,在pos以及之后的字符向后移动
		string& insert(size_t pos, char c);

		//在指定位置pos插入n个字符c,在pos以及之后的字符向后移动
		string& insert(size_t pos, size_t n, char c);

		//在指定位置pos开始删除len个字符
		string& erase(size_t pos = 0, size_t len = npos);

		//从指定位置pos开始查找一个字符c,并返回字符c首次出现的位置,如果找不到就返回npos
		size_t find(char c, size_t pos = 0) const;
		//从指定位置pos开始查找字符串s,找到后返回字符串s第一个字符的下标,否则返回npos
		size_t find(const char* s, size_t pos = 0) const;


	private:
		char* _str; //存储字符串的首地址
		size_t _size; //存储字符串的长度
		size_t _capacity; //存储字符串的容量
	};

	void swap(string& s1, string& s2);

	string operator+(const string& s1, const string& s2);
	string operator+(const string& s1, const char* str);
	string operator+(const string& s1, char c);

	bool operator==(const string& lhs, const string& rhs);
	bool operator!=(const string& lhs, const string& rhs);
	bool operator>=(const string& lhs, const string& rhs);
	bool operator<=(const string& lhs, const string& rhs);
	bool operator>(const string& lhs, const string& rhs);
	bool operator<(const string& lhs, const string& rhs);

	ostream& operator<< (ostream& os, const string& str); //流插入
	istream& operator>> (istream& is, string& str); //流提取
	istream& getline(istream& is, string& str, char delim = '\n'); //自定义获取字符串
}

string.cpp文件

cpp 复制代码
#include "string.h"
namespace MyString
{
	const size_t string::npos = -1;
	string::string(const char* str)
		:_size(strlen(str))
	{
		_capacity = _size;
		_str = new char[_size + 1]; //多开一个空间用来存放'\0'
		strcpy(_str, str);
	}
	string::string(size_t n, char c)
		:_size(n)
	{
		_capacity = _size;
		_str = new char[_size + 1];
		for (size_t i = 0; i < n; i++)
		{
			_str[i] = c;
		}
		_str[n] = '\0';
	}
	//string::string(const string& s)
	//{
	//	_str = new char[s._capacity + 1]; //多开一个空间用来存放'\0'
	//	strcpy(_str, s._str);
	//	_size = s._size;
	//	_capacity = s._capacity;
	//}
	string::string(const string& s)
	{
		string tmp(s.c_str());
		swap(tmp);
	}
	//string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
	//{
	//	if (this != &s) //防止自己给自己赋值
	//	{
	//		delete[] _str; //先将当前的内存空间释放掉
	//		_str = new char[s._capacity + 1]; //重新开辟和s一样大的空间,多开一个空间是为了存储'\0'
	//		strcpy(_str, s._str); //复制数据
	//		_size = s._size;
	//		_capacity = s._capacity;
	//	}
	//	return *this;
	//}
	//string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
	//{
	//	if (this != &s) //防止自己给自己赋值
	//	{
	//		char* tmp = new char[s._capacity + 1]; //先用临时指针tmp接受新开辟的空间
	//		strcpy(tmp, s._str); //如果开辟成功就将数据赋值到这个空间
	//		delete[] _str; //释放旧空间
	//		_str = tmp; //将_str指向新空间
	//		_size = s._size;
	//		_capacity = s._capacity;
	//	}
	//	return *this;
	//}
	//string& string::operator=(const string& s) //返回当前对象的引用是为了能连线赋值
	//{
	//	if (this != &s) //防止自己给自己赋值
	//	{
	//		string tmp(s);
	//		swap(_str, tmp._str);
	//		swap(_size, tmp._size);
	//		swap(_capacity, tmp._capacity);
	//	}
	//	return *this;
	//}
	string& string::operator=(string s) //返回当前对象的引用是为了能连线赋值
	{
		swap(s);
		return *this;
	}
	string::~string()
	{
		delete[] _str;
		_str = nullptr; //不要忘了将指针_str置为空,避免_str成为野指针
		_size = _capacity = 0;
	}
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}
	const char* string::c_str() const
	{
		return _str;
	}
	size_t string::size() const
	{
		return _size;
	}
	size_t string::capacity() const
	{
		return _capacity;
	}
	bool string::empty() const
	{
		return _size == 0;
	}
	void string::clear()
	{
		_str[0] = '\0';
		_size = 0;
	}
	char& string::operator[](size_t pos)
	{
		assert(pos < _size); //确保pos不能越界,越界了就断言
		return _str[pos];
	}
	const char& string::operator[](size_t pos) const
	{
		assert(pos < _size); //确保pos不能越界,越界了就断言
		return _str[pos];
	}
	void string::reserve(size_t n)
	{
		//检查n是否大于原有的容量,必须大于才能调整字符串的容量
		if (n > _capacity) {
			char* tmp = new char[n + 1]; //用指针tmp去接受新开的内存空间
			strcpy(tmp, _str); //如果开辟成功,将数据拷贝到新空间
			delete[] _str; //将旧空间释放掉
			_str = tmp; //将_str指向新空间
			_capacity = n; //更改容量值
		}
	}
	void string::resize(size_t n)
	{
		if (n > _capacity) {
			reserve(n); //扩容
		}
		_size = n;
		_str[_size] = '\0';
	}
	void string::resize(size_t n, char c)
	{
		if (n > _capacity) {
			reserve(n); //扩容
		}
		for (size_t i = _size; i < n; i++)
		{
			//如果是增加长度就会进入循环,在字符串末尾插入所需数量的字符c
			_str[i] = c;
		}
		_size = n;
		_str[_size] = '\0';
	}
	string::iterator string::begin()
	{
		return _str;
	}
	string::iterator string::end()
	{
		return _str + _size;
	}
	string::const_iterator string::begin() const
	{
		return _str;
	}
	string::const_iterator string::end() const
	{
		return _str + _size;
	}
	void string::push_back(char c)
	{
		//首先检查是否需要扩容
		if (_size == _capacity) {
			//扩容
			reserve(_size == 0 ? 4 : 2 * _size);
		}
		_str[_size] = c;
		_size++;
		_str[_size] = '\0'; //最后不要忘了在末尾字符的下一个位置放\0
	}
	string& string::append(const string& str)
	{
		size_t len = str._size;
		//首先检查是否需要扩容
		if (_size + len > _capacity) {
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + len) {
				newCapacity = _size + len;
			}
			reserve(newCapacity);
		}
		strcpy(_str + _size, str._str); //拷贝数据,同时\0也拷贝过去了,不用手动设置\0
		_size += len;
		return *this;
	}
	string& string::append(const string& str, size_t subpos, size_t sublen)
	{
		size_t len = str._size;
		assert(subpos < len); //检查subpos是否合法
		if (subpos + sublen >= len) {
			append(str._str + subpos); //复用append(const char* s)
		}
		else {
			append(str._str + subpos, sublen); //复用append(const char* s, size_t n)
		}
		return *this;
	}
	string& string::append(const char* s)
	{
		size_t len = strlen(s);
		if (_size + len > _capacity) {
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + len) {
				newCapacity = _size + len;
			}
			reserve(newCapacity);
		}
		strcpy(_str + _size, s);
		_size += len;
		return *this;
	}
	string& string::append(const char* s, size_t n)
	{
		size_t len = strlen(s);
		if (n > len) {
			//如果n大于待插入的字符串长度,则赋值为该字符串的长度值
			n = len;
		}
		//检查扩容
		if (_size + n > _capacity) {
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + n) {
				newCapacity = _size + n;
			}
			reserve(newCapacity);
		}
		for (size_t i = 0; i < n; i++)
		{
			_str[_size++] = s[i];
		}
		_str[_size] = '\0'; //需要手动设置\0
		return *this;
	}
	string& string::append(size_t n, char c)
	{
		//检查扩容
		if (_size + n > _capacity) {
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + n) {
				newCapacity = _size + n;
			}
			reserve(newCapacity);
		}
		for (size_t i = 0; i < n; i++)
		{
			_str[_size + i] = c;
		}
		_size += n;
		_str[_size] = '\0';//手动设置\0
		return *this;
	}
	string& string::operator+=(const string& str)
	{
		append(str);
		return *this;
	}
	string& string::operator+=(const char* s)
	{
		append(s);
		return *this;
	}
	string& string::operator+=(char c)
	{
		push_back(c);
		return *this;
	}
	string& string::insert(size_t pos, const char* s)
	{
		assert(pos <= _size);
		size_t len = strlen(s);
		if (_size + len > _capacity) {
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + len) {
				newCapacity = _size + len;
			}
			reserve(newCapacity);
		}
		size_t end = _size + len;
		while (end >= pos + len)
		{
			_str[end] = _str[end - len];
			end--;
		}
		for (size_t i = 0; i < len; i++)
		{
			_str[pos + i] = s[i];
		}
		_size += len;
		return *this;
	}
	string& string::insert(size_t pos, const string& str)
	{
		insert(pos, str._str); //复用
		return *this;
	}
	string& string::insert(size_t pos, char c)
	{
		assert(pos <= _size);
		if (_size == _capacity) {
			reserve(_size == 0 ? 4 : 2 * _size);
		}
		size_t end = _size + 1;
		while (end >= pos + 1)
		{
			_str[end] = _str[end - 1];
			end--;
		}
		_str[pos] = c;
		_size++;
		return *this;
	}
	string& string::insert(size_t pos, size_t n, char c)
	{
		assert(pos <= _size);
		if (_size + n > _capacity) {
			size_t newCapacity = 2 * _capacity;
			if (newCapacity < _size + n) {
				newCapacity = _size + n;
			}
			reserve(newCapacity);
		}
		size_t end = _size + n;
		while (end >= pos + n)
		{
			_str[end] = _str[end - n];
			end--;
		}
		for (size_t i = 0; i < n; i++)
		{
			_str[pos + i] = c;
		}
		_size += n;
		return *this;
	}
	string& string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len >= _size - pos) {
			_size = pos;
			_str[_size] = '\0';
		}
		else {
			size_t end = pos + len;
			while (end <= _size)
			{
				_str[end - len] = _str[end];
				end++;
			}
			_size -= len;
		}
		return *this;
	}
	size_t string::find(char c, size_t pos) const
	{
		assert(pos < _size);
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == c) {
				return i;
			}
		}
		return npos;
	}
	size_t string::find(const char* s, size_t pos) const
	{
		assert(pos < _size);
		const char* ptr = strstr(_str + pos, s);
		if (ptr == nullptr) {
			return npos;
		}
		return ptr - _str;
	}
	void swap(string& s1, string& s2)
	{
		s1.swap(s2);
	}
	string operator+(const string& s1, const string& s2)
	{
		string tmp(s1);
		tmp += s2;
		return tmp;
	}
	string operator+(const string& s1, const char* str)
	{
		string tmp(s1);
		tmp += str;
		return tmp;
	}
	string operator+(const string& s1, char c)
	{
		string tmp(s1);
		tmp.push_back(c);
		return tmp;
	}
	bool operator==(const string& lhs, const string& rhs)
	{
		return strcmp(lhs.c_str(), rhs.c_str()) == 0;
	}
	bool operator!=(const string& lhs, const string& rhs)
	{
		return !(lhs == rhs);
	}
	bool operator>=(const string& lhs, const string& rhs)
	{
		return !(lhs < rhs);
	}
	bool operator<=(const string& lhs, const string& rhs)
	{
		return !(lhs > rhs);
	}
	bool operator>(const string& lhs, const string& rhs)
	{
		return strcmp(lhs.c_str(), rhs.c_str()) > 0;
	}
	bool operator<(const string& lhs, const string& rhs)
	{
		return strcmp(lhs.c_str(), rhs.c_str()) < 0;
	}
	ostream& operator<< (ostream& os, const string& str)
	{
		for (auto& e : str)
		{
			os << e;
		}
		return os;
	}
	istream& operator>> (istream& is, string& str)
	{
		str.clear();//清空字符串
		char buff[256];//定义缓冲区
		char ch = is.get();//从输入流中获取一个字符
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 255) {
				buff[i] = '\0'; //不要忘了在字符串末尾给\0
				str += buff;
				i = 0;
			}
			ch = is.get();//从输入流中获取一个字符
		}
		if (i) {
			buff[i] = '\0';
			str += buff;
		}
		return is;
	}
	istream& getline(istream& is, string& str, char delim)
	{
		str.clear();//清空字符串
		char buff[256];//定义缓冲区
		char ch = is.get();//从输入流中获取一个字符
		int i = 0;
		while (ch != delim)
		{
			buff[i++] = ch;
			if (i == 255) {
				buff[i] = '\0';//不要忘了在字符串末尾给\0
				str += buff;
				i = 0;
			}
			ch = is.get();//从输入流中获取一个字符
		}
		if (i) {
			buff[i] = '\0';
			str += buff;
		}
		return is;
	}
}

九. 写时拷贝(了解)

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

可看一下以下两个文章:

写时拷贝
写时拷贝在读取时的缺陷

写时拷贝的实现代码(可参考):

cpp 复制代码
//构造函数(分存内存)
    string::string(const char* tmp)
{
    _Len = strlen(tmp);
    _Ptr = new char[_Len+1+1];
    strcpy( _Ptr, tmp );
    _Ptr[_Len+1]=0;  // 设置引用计数  
}
 
//拷贝构造(共享内存)
    string::string(const string& str)
    {
         if (*this != str){
              this->_Ptr = str.c_str();   //共享内存
              this->_Len = str.szie();
              this->_Ptr[_Len+1] ++;  //引用计数加一
         }
}
 
//写时才拷贝Copy-On-Write
char& string::operator[](unsigned int idx)
{
    if (idx > _Len || _Ptr == 0 ) {
         static char nullchar = 0;
return nullchar;
          }
   
_Ptr[_Len+1]--;   //引用计数减一
    char* tmp = new char[_Len+1+1];
    strncpy( tmp, _Ptr, _Len+1);
    _Ptr = tmp;
    _Ptr[_Len+1]=0; // 设置新的共享内存的引用计数
   
    return _Ptr[idx];
}

//析构函数的一些处理
~string()
{ 
_Ptr[_Len+1]--;   //引用计数减一
   
         // 引用计数为0时,释放内存 
    if (_Ptr[_Len+1]==0) {
        delete[] _Ptr;
         }
 
}

END

相关推荐
ITLiu_JH1 分钟前
scikit-surprise 智能推荐模块使用说明
开发语言·数据分析·智能推荐
User_芊芊君子9 分钟前
【Java】——数组深度解析(从内存原理到高效应用实践)
java·开发语言
珹洺1 小时前
C++从入门到实战(十)类和对象(最终部分)static成员,内部类,匿名对象与对象拷贝时的编译器优化详解
java·数据结构·c++·redis·后端·算法·链表
一 乐1 小时前
网红酒店|基于java+vue的网红酒店预定系统(源码+数据库+文档)
java·开发语言·数据库·毕业设计·论文·springboot·网红酒店预定系统
写bug的小屁孩1 小时前
移动零+复写零+快乐数+盛最多水的容器+有效三角形的个数
c++·算法·双指针
DARLING Zero two♡1 小时前
C++底层学习精进:模板进阶
开发语言·c++·模板
飞川撸码1 小时前
【LeetCode 热题100】208:实现 Trie (前缀树)(详细解析)(Go语言版)
算法·leetcode·golang·图搜索算法
这就是编程2 小时前
自回归模型的新浪潮?GPT-4o图像生成技术解析与未来展望
人工智能·算法·机器学习·数据挖掘·回归
勘察加熊人2 小时前
c++生成html文件helloworld
开发语言·c++·html
羑悻的小杀马特2 小时前
【狂热算法篇】探寻图论幽径:Bellman - Ford 算法的浪漫征程(通俗易懂版)
c++·算法·图论·bellman_ford算法