C++初阶之一篇文章让你掌握string类(模拟实现)

C++初阶之一篇文章让你掌握string类(模拟实现)_第1张图片

string类模拟实现

  • 1.为什么要模拟实现string
  • 2.string的模拟实现需要注意哪些问题
  • 3.经典的string类问题
  • 4.写时拷贝
  • 5.传统版写法的String类(参考)
  • 6.现代版写法的String类(参考)
  • 7.string类的模拟实现(讲解)
    • 7.1 命名空间string类的成员变量定义
    • 7.2 string类构造函数
    • 7.3 string类拷贝构造函数
    • 7.4 string类赋值运算符重载
    • 7.5 string类析构函数和易实现的成员函数
    • 7.6 string类reserve函数
    • 7.7 string类resize函数
    • 7.8 string类insert函数、append函数、push_back函数、+=重载
    • 7.9 string类erase函数
    • 7.10 string类erase函数
    • 7.11 string类substr 函数
    • 7.12 string类比较运算符重载
    • 7.13 string类流插入<<和流提取>>重载
  • 8.string类的模拟实现(完整代码)
  • 结语

1.为什么要模拟实现string

C++初阶之一篇文章让你掌握string类(模拟实现)_第2张图片
模拟实现 std::string 是一个有挑战性的练习,它可以带来多方面的收益,尤其对于学习 C++ 和深入了解字符串操作以及动态内存管理的机制。以下是模拟实现 std::string 的一些好处和重要意义:

  1. 学习 C++ 内存管理:std::string 是一个动态分配内存的容器,模拟实现需要手动处理内存的分配和释放。这可以让你更深入地理解动态内存管理的原理和机制,如何正确地使用 new 和 delete 运算符,以及如何避免内存泄漏和悬空指针。
  2. 字符串操作的练习:在模拟实现过程中,您需要实现字符串的拼接、插入、删除、查找等操作,以及其他与字符串处理相关的函数。这可以帮助您熟悉 C++ 中字符串的操作和处理方式。
  3. 深入理解类和对象:std::string 是一个类模板,模拟实现它需要深入理解类和对象的概念,包括构造函数、析构函数、成员函数、成员变量等。通过实现一个类似 std::string 的类,你可以更好地理解类的设计和使用。
  4. 提高编程技能:模拟实现 std::string 是一项挑战性的任务,它可以锻炼你的编程技能,让你更加熟练地使用 C++ 的语法和特性。
  5. 深入学习模板编程:std::string 是一个类模板,模拟实现它可以帮助你深入了解模板编程的机制和技巧。
  6. 实现自定义容器:std::string 是 C++ 标准库中的一个容器类,模拟实现它是实现自定义容器的练习。自定义容器可以帮助您更好地理解容器的设计和实现。

2.string的模拟实现需要注意哪些问题

模拟实现 std::string 类是一个有挑战性的任务,因为 std::string 是 C++ 标准库中的一个复杂数据类型,它有很多功能和特性,而其实现涉及到动态内存管理、字符串操作、复制语义等方面。在进行模拟实现时,需要注意以下一些关键问题:

  1. 内存管理:std::string 类是一个动态分配内存的容器,模拟实现需要正确地处理内存的分配和释放。你可以使用动态数组、指针或其他数据结构来模拟动态内存的管理。
  2. 字符串操作:模拟实现需要支持字符串的拼接、插入、删除、查找等操作,以及其他字符串处理的函数(如 size()、substr()、find() 等)。
  3. 异常处理:std::string 在一些情况下可能会引发异常,例如内存分配失败或访问越界等。模拟实现需要考虑如何正确处理异常情况,以确保程序的稳定性和安全性。
  4. 内存拷贝:std::string 采用了深拷贝(deep copy)语义,即在复制时会复制整个字符串的内容。模拟实现需要正确地处理内存的拷贝,以避免悬空指针和资源泄漏等问题。
  5. 迭代器支持:std::string 支持迭代器用于访问字符串的内容,模拟实现需要提供相应的迭代器支持。
  6. 性能优化:std::string 的标准实现通常会对性能进行优化,例如采用了扩容策略来减少频繁的内存分配。模拟实现可以考虑一些优化策略,提高性能和效率。
  7. 边界条件:在进行模拟实现时,需要特别注意边界条件和特殊情况,确保实现的正确性和鲁棒性。
  8. 完整性:std::string 类是一个非常复杂的数据类型,模拟实现时需要尽可能完整地实现其功能和接口。

虽然模拟实现 std::string 是一个复杂的任务,但它也是一个很好的学习练习,可以加深对 C++ 内存管理、字符串处理等方面的理解。

3.经典的string类问题

上一篇文章已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下string类的实现是否有问题?

// 为了和标准库区分,此处使用String
class String
{
public:
	/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
	String(const char* str = "")
	{
		// 构造String类对象时,如果传递nullptr指针,可以认为程序非法
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};
// 测试
void TestString()
{
	String s1("hello bit!!!");
	String s2(s1);
}

C++初阶之一篇文章让你掌握string类(模拟实现)_第3张图片
上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝

什么是浅拷贝

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

什么是深拷贝

深拷贝是指在进行对象拷贝时,不仅复制对象本身的成员变量,还复制对象所指向的动态分配的资源(例如堆内存)到新的对象中。这意味着拷贝后的对象和原对象拥有独立的资源副本,彼此之间不会相互影响。

当对象中含有动态分配的资源,如指针指向的内存块,或者其他动态分配的资源(文件句柄、网络连接等),进行深拷贝是非常重要的,以避免多个对象共享同一块资源导致释放重复、悬挂指针等问题。

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
C++初阶之一篇文章让你掌握string类(模拟实现)_第4张图片

4.写时拷贝

“写时拷贝”(Copy on Write,简称为 COW)是一种优化技术,通常应用于操作系统的内存管理或数据结构中,目的是节省内存和提高性能。在 COW 中,当多个对象共享同一份资源时,只有在某个对象试图修改资源内容时,才会进行实际的拷贝操作,否则所有对象共享相同的原始资源。这样可以避免在修改前对整个资源进行拷贝,节省了内存和执行时间。

COW 最常见的应用是在操作系统中的进程管理和内存分配。当一个进程 fork(复制)自身时,通常会采用 COW 机制。在 fork 时,子进程会与父进程共享相同的内存空间,即物理页框。只有当子进程或父进程中的一个试图修改其中的内容时,操作系统才会执行实际的拷贝,将要修改的页框内容复制到新的页框中,使得两个进程的内存空间独立开来。这样,父子进程可以共享大部分资源,而无需进行大规模的内存拷贝,从而提高了 fork 操作的效率。

在编程语言或数据结构中,写实拷贝也可以用于优化数据结构的复制操作。例如,在某些容器类(如字符串、数组、向量等)中,当多个对象共享相同的数据时,只有在其中一个对象试图修改数据时,才会进行实际的拷贝操作,确保各个对象之间的数据相互独立。

需要注意的是,COW 并不是适用于所有情况的通用优化技术,它的有效性取决于具体的应用场景。在某些情况下,COW 可以带来显著的性能优势,但在其他情况下,可能会增加复杂性和开销。因此,在实现时需要仔细权衡利弊,根据实际需求选择合适的优化策略。

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

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

一个常见的例子是字符串的写实拷贝。

在许多编程语言中,字符串通常是不可变的(immutable),即一旦创建后,就无法修改其内容。在这种情况下,当多个变量或对象引用同一个字符串时,如果其中一个变量试图修改字符串的内容,就需要创建一个新的字符串对象,而不是直接在原始字符串上进行修改。

假设有两个变量 str1 和 str2 都指向相同的字符串 “Hello”:

std::string str1 = "Hello";
std::string str2 = str1;

在这里,str2 是通过拷贝构造函数从 str1 创建的。在传统的拷贝情况下,这将导致整个字符串 “Hello” 的拷贝,即两个变量 str1 和 str2 都指向不同的内存地址,但其内容是相同的。

但是,写时拷贝可以优化这种情况。在写时拷贝中,当 str2 拷贝 str1 时,并不会立即创建一个新的字符串副本。而是让 str2 和 str1 共享同一个底层的字符串数据。只有当其中一个字符串试图修改其内容时,才会触发实际的拷贝操作

例如,如果现在对 str2 进行修改操作:

str2[0] = 'h'; // 修改第一个字符为小写 'h'

在写时拷贝机制下,会创建一个新的字符串 “hello”,然后 str2 的内容指向新的字符串,而 str1 的内容保持不变。这样,两个变量 str1 和 str2 仍然共享相同的底层数据,但它们的内容已经不再相同。

写时拷贝可以有效地节省内存,尤其在字符串长期共享的情况下,避免了不必要的内存复制。但在其他情况下,可能会增加复杂性和开销。因此,在实现时需要仔细权衡利弊,根据实际需求选择合适的优化策略。

5.传统版写法的String类(参考)

class String
{
public:
	String(const char* str = "")
	{
		// 构造String类对象时,如果传递nullptr指针,可以认为程序非法
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	String(const String& s)
		: _str(new char[strlen(s._str) + 1])
	{
		strcpy(_str, s._str);
	}
	String& operator=(const String& s)
	{
		if (this != &s)
		{
			char* pStr = new char[strlen(s._str) + 1];
			strcpy(pStr, s._str);
			delete[] _str;
			_str = pStr;
		}
		return *this;
	}
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

6.现代版写法的String类(参考)

class String
{
public:
	String(const char* str = "")
	{
		if (nullptr == str)
		{
			assert(false);
			return;
		}
		_str = new char[strlen(str) + 1];
		strcpy(_str, str);
	}
	String(const String& s)
		: _str(nullptr)
	{
		String strTmp(s._str);
		swap(_str, strTmp._str);
	}

	String& operator=(String s)
	{
		swap(_str, s._str);
		return *this;
	}
	/*
	String& operator=(const String& s)
	{
	if(this != &s)
	{
	String strTmp(s);
	swap(_str, strTmp._str);
	}
	return *this;
}
*/
	~String()
	{
		if (_str)
		{
			delete[] _str;
			_str = nullptr;
		}
	}
private:
	char* _str;
};

7.string类的模拟实现(讲解)

根据上面提到的内容和知识,我们可以来实现string类框架和大部分的接口函数,但在实际面试中,我们可能需要实现的功能并不多,所以我们这里只把最常见和常用的那些部分模拟实现。

7.1 命名空间string类的成员变量定义

namespace mystring
{
	class string
	{
	public:
		//...

	private:
		size_t _capacity;
		size_t _size;
		char* _str;
	public:
		// const static 语法特殊处理
		// 直接可以当成定义初始化
		const static size_t npos = -1;
}

首先我们重新定义一个命名空间,防止和库中的string类重定义,或者重新写一个别的名字的string类也可以,类成员包括capacity,size和字符串str,npos定义成公有并初始化。

7.2 string类构造函数

string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];

			strcpy(_str, str);
		}

const char* str = “” 是构造函数的默认参数。默认参数是在函数声明中为函数参数提供默认值的一种特性,它允许在调用函数时,如果没有提供相应的参数值,就会使用默认值作为参数的值,实际包含一个’\0’,分配足够存储字符串的内存空间(_size + 1,其中 _size 是输入字符串的长度),然后通过 strcpy 函数将输入的 C 风格字符串复制到 _str 成员变量中。

7.3 string类拷贝构造函数

传统写法:

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

现代写法:

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


string(const string& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	string tmp(s._str);
	swap(tmp); //this->swap(tmp);
}

第一段代码中,拷贝构造函数采用传统的深拷贝方式。它首先分配了与源对象(s)相同大小的内存空间(包括结尾的空字符),然后将源对象的内容复制到新分配的内存空间中

这种实现方式确保了新创建的对象和源对象具有独立的内存空间,即它们不共享资源。这样,当一个对象修改其内容时,不会影响到另一个对象,从而保证了对象之间的数据隔离。

而在第二段代码中,拷贝构造函数使用了 C++11 引入的移动语义。它先创建了一个名为 tmp 的临时对象,并使用 s._str 初始化了这个临时对象。接着,通过调用成员函数 swap(tmp) 将当前对象的成员和临时对象的成员进行交换。

swap 函数的实现会使当前对象的成员指向临时对象的内存空间,而临时对象的成员指向了当前对象之前的内存空间。这样一来,原来的资源被交换了,临时对象会在析构时释放当前对象原来的资源,而当前对象则拥有了 s 对象的资源

这种实现方式通过避免了不必要的内存拷贝,从而提高了拷贝构造函数的性能。在 tmp 作为临时对象被析构时,它会自动释放原来 s 对象的资源,因此没有内存泄漏。

两种实现方式都是有效的拷贝构造函数,但第二种实现利用了移动语义,可以在拷贝对象时避免不必要的内存复制,提高性能。在 C++11 及以上版本中,推荐使用第二种实现方式。

7.4 string类赋值运算符重载

传统写法:

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

现代写法:

string& operator=(string s)
{
	swap(s);
	return *this;
}

第一个函数中,赋值运算符采用了传统的深拷贝方式。它首先检查目标对象与源对象是否是同一个对象(地址比较),如果是同一个对象则不执行赋值操作,避免了自赋值的情况

然后,它创建一个临时的 string 对象 tmp,并将源对象 s 的内容复制到 tmp 中。接着,通过调用 swap(tmp),将当前对象的成员和临时对象的成员进行交换

这样,原来的资源被交换了,当前对象拥有了 s 对象的资源,而临时对象在析构时会自动释放当前对象原来的资源。这样实现了赋值操作,并在赋值时避免了不必要的内存拷贝。

第二个函数中,赋值运算符使用了 C++11 引入的移动语义。它的参数是一个 string 对象 s,这里采用了按值传递,即通过值拷贝的方式传递参数。

在函数内部,它直接通过 swap(s) 将当前对象的成员和参数 s 对象的成员进行交换。由于参数 s 是按值传递的,意味着在调用函数时会执行一次拷贝构造函数来创建 s 对象的副本,因此在 swap(s) 中,将 s 对象的资源交换给了当前对象,同时临时对象 s 会在函数结束时自动析构并释放当前对象原来的资源。

这样,通过移动语义实现了赋值操作,并在赋值时避免了不必要的内存复制

区别总结:

参数传递方式:第一个函数采用了常量引用传递,而第二个函数采用了按值传递。
拷贝控制技术:第一个函数使用了深拷贝和交换资源的方式,而第二个函数利用了移动语义和 swap 操作来避免拷贝。

两者都能正确实现赋值操作,并避免了不必要的内存拷贝。然而,第二个函数在 C++11 及以上版本中更推荐,因为它利用了移动语义,性能更高效。如果你的代码环境支持 C++11 或更高版本,建议优先考虑使用第二种实现方式。

7.5 string类析构函数和易实现的成员函数

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

这里的析构函数通过 delete[] 操作释放 _str 指向的动态分配的字符数组(字符串内存),然后将 _str 置为 nullptr,同时将 _size 和 _capacity 设置为 0。这样确保对象被销毁时内存得到正确的释放,防止内存泄漏。

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

c_str()函数用于返回指向存储字符串的字符数组的指针。

size_t size() const
{
	return _size;
}

size()函数用于返回字符串的大小,即字符串中实际存储的字符个数,返回类型为 size_t。这里的 _size 成员变量表示实际存储的字符个数,因此直接返回 _size 即可。

size_t capacity() const
{
	return _capacity;
}

capacity()函数用于返回字符串的容量,即字符串中当前分配的内存空间大小,返回类型为 size_t。这里的 _capacity 成员变量表示当前的容量,因此直接返回 _capacity 即可。

const char& operator[](size_t pos) const
{
	assert(pos < _size);

	return _str[pos];
}

operator[](size_t pos) const是 const 版本的下标操作符重载函数,用于访问字符串中指定位置 pos 处的字符。函数返回类型为 const char&,表示返回的是常量字符的引用,即不允许通过该引用修改字符内容。这是为了确保字符串的不可变性。

char& operator[](size_t pos)
{
	assert(pos < _size);

	return _str[pos];
}

operator[](size_t pos)是非 const 版本的下标操作符重载函数,功能与上面的 const 版本类似,但这个函数返回类型是 char&,表示返回的是可修改字符的引用,允许通过该引用修改字符内容。

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

clear 函数用于清空字符串,即将字符串内容全部置为空,并将实际大小 _size 设为 0,将字符数组的第一个字符(即字符串的起始位置)设置为空字符 ‘\0’,以将字符串内容清空。

7.6 string类reserve函数

void reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;

		_str = tmp;
		_capacity = n;
	}
}

`void reserve(size_t n):这是 std::string 类中的 reserve 函数的声明,表示该函数将预留 n 个字符的内存空间。n 是传入的参数,表示需要预留的字符个数。

if (n > _capacity):这里通过比较传入的 n 和当前字符串的容量 _capacity,来判断是否需要增加字符串的容量。只有当需要预留的字符个数 n 大于当前容量 _capacity 时,才需要进行内存扩展操作。
char* tmp = new char[n + 1];:如果需要增加容量,首先创建一个新的字符数组 tmp,长度为 n + 1,即预留的字符个数加上结尾的空字符。这里将字符串的容量设置为 n,是为了预留额外的一个位置来存储结尾的空字符。
strcpy(tmp, _str);:将原来的字符串内容复制到新创建的字符数组 tmp 中。
delete[] _str;:释放原来字符串 _str 指向的动态分配的字符数组,即释放原来的内存空间。
_str = tmp;:将原来的指针 _str 指向新的字符数组 tmp,这样字符串的内存空间得到了扩展。
_capacity = n;:将 _capacity 更新为新的容量 n。

这样,当需要预留更多的内存空间时,reserve 函数会创建一个新的字符数组,并将原来的字符串内容复制到新数组中,然后释放原来的内存空间,并将 _str 指向新的字符数组,更新容量 _capacity 为新的预留值 n。

7.7 string类resize函数

void resize(size_t n, char ch = '\0')
{
	if (n > _size)
	{
		// 插入数据
		reserve(n);
		for (size_t i = _size; i < n; ++i)
		{
			_str[i] = ch;
		}
		_str[n] = '\0';
		_size = n;
	}
	else
	{
		// 删除数据
		_str[n] = '\0';
		_size = n;
	}
}

resize 函数用于改变字符串的大小,即增加或减少字符串中的字符个数。这里简单解释一下这个函数的实现:

void resize(size_t n, char ch = '\0'):这是 std::string 类中的 resize 函数的声明,表示该函数将改变字符串的大小为 n。n 是传入的参数,表示新的字符串大小。参数 ch 是可选的,默认值为 ‘\0’,用于在扩展字符串大小时填充新增的字符。
if (n > _size):在这个条件分支中,判断需要增加字符串大小的情况。如果传入的新大小 n 大于当前字符串大小 _size,表示需要在字符串末尾添加新的字符。
reserve(n);:首先调用 reserve 函数来预留足够的内存空间,确保字符串有足够的容量来容纳新增的字符。
for (size_t i = _size; i < n; ++i):然后在字符串中新增的位置,从当前字符串的大小 _size 开始循环添加字符。这里将新增的字符都设置为 ch,即传入的第二个参数。
_str[n] = '\0';:在循环结束后,将字符串的新末尾字符设置为空字符 ‘\0’,保证新的字符串正确终止。
_size = n;:最后将字符串的大小 _size 更新为新的大小 n。
else:在这个条件分支中,处理需要减小字符串大小的情况。如果传入的新大小 n 小于当前字符串大小 _size,表示需要删除字符串中多余的字符。
_str[n] = '\0';:将字符串的新末尾字符设置为空字符 ‘\0’,保证新的字符串正确终止。
_size = n;:最后将字符串的大小 _size 更新为新的大小 n。

这样,resize 函数可以根据传入的大小 n,扩展或缩小字符串的大小,并在必要时添加或删除字符。

7.8 string类insert函数、append函数、push_back函数、+=重载

insert函数
insert的模拟实现主要实现字符和字符串插入两种
字符插入

string& insert(size_t pos, char ch)
{
	assert(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;
}

insert 函数在字符串中指定位置插入一个字符。这里简单解释一下这个函数的实现:

string& insert(size_t pos, char ch):这是 std::string 类中的 insert 函数的声明,表示该函数将在指定位置 pos 插入字符 ch。pos 是传入的参数,表示插入位置的索引;ch 是要插入的字符。
assert(pos <= _size);:使用 assert 断言来确保插入位置 pos 不超过字符串的实际大小 _size。如果断言失败(pos 大于 _size),则会触发断言失败错误,帮助调试找到错误的位置。
if (_size == _capacity):检查当前字符串是否已满(即 _size 等于 _capacity)。如果字符串已满,则需要扩容,以确保有足够的容量来插入新字符。这里使用 reserve 函数扩容,使字符串有足够的容量来容纳新字符。
size_t end = _size + 1;:在插入字符前,先将字符串的末尾位置(实际字符个数 _size 后面)向后移动一个位置,为新字符留出空间。这样做是为了将插入位置 pos 之后的字符后移。
while (end > pos):通过一个循环,将插入位置 pos 之后的字符依次向后移动一个位置。
_str[pos] = ch;:将字符 ch 插入到指定的插入位置 pos。
++_size;:插入字符后,将字符串的实际大小 _size 增加 1。
return *this;:返回当前 std::string 对象的引用,以支持链式调用。

字符串插入

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)
	{
		_str[end] = _str[end - len];
		--end;
	}

	strncpy(_str + pos, str, len);
	_size += len;

	return *this;
}

与上一个 insert 函数相比,这里的参数 str 是一个 C-style 字符串(const char*),而不是一个单个字符。函数的功能是在字符串中指定位置插入一个 C-style 字符串。现在来解释这个函数的实现:

string& insert(size_t pos, const char* str):这是 std::string 类中的 insert 函数的声明,表示该函数将在指定位置 pos 插入一个 C-style 字符串 str。pos 是传入的参数,表示插入位置的索引;str 是要插入的 C-style 字符串。
assert(pos <= _size);:使用 assert 断言来确保插入位置 pos 不超过字符串的实际大小 _size。如果断言失败(pos 大于 _size),则会触发断言失败错误,帮助调试找到错误的位置。
size_t len = strlen(str);:计算要插入的 C-style 字符串 str 的长度,即字符个数。
if (_size + len > _capacity):检查插入后的字符串大小是否超过当前的容量 _capacity,如果超过,则需要扩容,以确保有足够的容量来容纳插入的字符串。
reserve(_size + len);:调用 reserve 函数来扩容,保证有足够的容量来容纳插入的字符串。
size_t end = _size + len;:在插入字符串前,先将字符串的末尾位置(实际字符个数 _size 后面)向后移动 len 个位置,为新字符串留出空间。
while (end >= pos + len):通过一个循环,将插入位置 pos 之后的字符依次向后移动 len 个位置,为新字符串的插入留出空间。
strncpy(_str + pos, str, len);:使用 strncpy 函数将 C-style 字符串 str 复制到指定的插入位置 pos,并且只复制 len 个字符。
_size += len;:插入字符串后,将字符串的实际大小 _size 增加 len,以反映插入后的新大小。
return *this;:返回当前 std::string 对象的引用,以支持链式调用。

append函数

void append(const char* str)
{
	size_t len = strlen(str);

	// 满了就扩容
	if (_size + len > _capacity)
	{
		reserve(_size+len);
	}

	strcpy(_str + _size, str);
	//strcat(_str, str); 需要找\0,效率低
	_size += len;
}

append 函数用于在字符串末尾添加一个 C-style 字符串。现在来解释这个函数的实现:

void append(const char* str):这是 std::string 类中的 append 函数的声明,表示该函数将在字符串末尾添加一个 C-style 字符串 str。str 是传入的参数,表示要添加的 C-style 字符串。
size_t len = strlen(str);:计算要添加的 C-style 字符串 str 的长度,即字符个数。
if (_size + len > _capacity):检查添加后的字符串大小是否超过当前的容量 _capacity,如果超过,则需要扩容,以确保有足够的容量来容纳添加的字符串。
reserve(_size + len);:调用 reserve 函数来扩容,保证有足够的容量来容纳添加的字符串。
strcpy(_str + _size, str);:使用 strcpy 函数将 C-style 字符串 str 复制到字符串末尾,即从 _str 的实际字符个数 _size 处开始复制。
_size += len;:添加字符串后,将字符串的实际大小 _size 增加 len,以反映添加后的新大小。

这样,append 函数将 C-style 字符串 str 添加到字符串末尾,并且在必要时进行了内存扩容。
当然你可以对insert函数复用

void append(const char* str)
{
	insert(_size, str);
}

push_back函数

void push_back(char ch)
{
	// 满了就扩容
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}

	_str[_size] = ch;
	++_size;
	_str[_size] = '\0';
}

push_back 函数用于在字符串末尾添加一个字符。现在来解释这个函数的实现:

void push_back(char ch):这是 std::string 类中的 push_back 函数的声明,表示该函数将在字符串末尾添加一个字符 ch。ch 是传入的参数,表示要添加的字符。
if (_size == _capacity):检查当前字符串是否已满(即 _size 等于 _capacity)。如果字符串已满,则需要扩容,以确保有足够的容量来容纳新增的字符。这里使用 reserve 函数扩容,使字符串有足够的容量来容纳新字符。
_str[_size] = ch;:将字符 ch 添加到字符串末尾,即在 _str 的实际字符个数 _size 处添加字符。
++_size;:字符串的实际大小 _size 增加 1,以反映添加后的新大小。
_str[_size] = '\0';:在字符串末尾添加一个空字符 ‘\0’,以保证新的字符串正确终止。

这样,push_back 函数将字符 ch 添加到字符串末尾,并在必要时进行了内存扩容。
同样的,push_back 函数你也可以对insert函数复用

void push_back(char ch)
{
	insert(_size, ch);
}

+=重载

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

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

operator+= 运算符重载用于在现有字符串后追加字符或 C-style 字符串。现在来解释这个函数的实现:

string& operator+=(char ch):这是 operator+= 运算符重载的第一个版本,表示该运算符将在字符串末尾追加一个字符 ch。在这个版本中,直接调用了 push_back 函数,将字符 ch 添加到字符串末尾。
string& operator+=(const char* str):这是 operator+= 运算符重载的第二个版本,表示该运算符将在字符串末尾追加一个 C-style 字符串 str。在这个版本中,直接调用了 append 函数,将 C-style 字符串 str 添加到字符串末尾。

在两个版本的实现中,都返回当前 std::string 对象的引用,以支持链式调用。

7.9 string类erase函数

void erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);

	if (len == npos || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		strcpy(_str + pos, _str + pos + len);
		_size -= len;
	}
}

erase 函数用于从字符串中删除指定位置开始的一定长度的字符。现在来解释这个函数的实现:

void erase(size_t pos, size_t len = npos):这是 std::string 类中的 erase 函数的声明,表示该函数将从指定位置 pos 开始删除一定长度 len 的字符。pos 是传入的参数,表示删除的起始位置的索引;len 是要删除的字符个数,默认值为 npos,表示删除从起始位置开始的所有字符。
assert(pos < _size);:使用 assert 断言来确保删除的起始位置 pos 不超过字符串的实际大小 _size。如果断言失败(pos 大于等于 _size),则会触发断言失败错误,帮助调试找到错误的位置。
if (len == npos || pos + len >= _size):检查是否要删除从起始位置 pos 开始的所有字符(即 len 等于 npos),或者是否要删除的字符个数超过字符串末尾(即 pos + len 大于等于 _size)。如果是其中一种情况,表示要删除从 pos 开始的所有字符或从 pos 开始直到末尾的所有字符。
_str[pos] = '\0'; 和 _size = pos;:在上述情况下,将字符串从位置 pos 处截断,即将字符数组的第 pos 个字符设置为空字符 ‘\0’,并更新字符串的实际大小 _size 为 pos,以反映删除后的新大小。
else:如果要删除的字符个数小于字符串末尾的字符个数,则需要将后面的字符向前移动。
strcpy(_str + pos, _str + pos + len);:将从位置 pos + len 开始的字符复制到位置 pos,覆盖掉要删除的字符。
_size -= len;:删除字符后,将字符串的实际大小 _size 减去 len,以反映删除后的新大小。

7.10 string类erase函数

size_t find(char ch, size_t pos = 0) const
{
	assert(pos < _size);

	for (size_t i = pos; i < _size; ++i)
	{
		if (ch == _str[i])
		{
			return i;
		}
	}
	return npos;
}

find 函数用于在字符串中查找指定字符或子串,并返回其位置。现在来解释这个函数的实现:

size_t find(char ch, size_t pos = 0) const:这是 std::string 类中的 find 函数的第一个版本,表示该函数将在字符串中从位置 pos 开始查找字符 ch。pos 是传入的参数,表示查找的起始位置的索引,默认值为 0,表示从字符串的开头开始查找。
assert(pos < _size);:使用 assert 断言来确保查找的起始位置 pos 不超过字符串的实际大小 _size。如果断言失败(pos 大于等于 _size),则会触发断言失败错误,帮助调试找到错误的位置。
在这个版本中,使用了简单的循环遍历,从位置 pos 开始遍历字符串,查找是否存在字符 ch。如果找到了,就返回该字符的位置索引;如果未找到,则返回 npos。

size_t find(const char* sub, size_t pos = 0) const
{
	assert(sub);
	assert(pos < _size);

	const char* ptr = strstr(_str + pos, sub);
	if (ptr == nullptr)
	{
		return npos;
	}
	else
	{
		return ptr - _str;
	}
}

size_t find(const char* sub, size_t pos = 0) const:这是 std::string 类中的 find 函数的第二个版本,表示该函数将在字符串中从位置 pos 开始查找子串 sub。sub 是传入的参数,表示要查找的子串;pos 是传入的参数,表示查找的起始位置的索引,默认值为 0,表示从字符串的开头开始查找。
assert(sub);:使用 assert 断言来确保传入的子串 sub 不为空指针。如果断言失败(sub 为空指针),则会触发断言失败错误,帮助调试找到错误的位置。
assert(pos < _size);:同样,使用 assert 断言来确保查找的起始位置 pos 不超过字符串的实际大小 _size。
在这个版本中,使用 strstr 函数在字符串中查找子串 sub,如果找到了,就返回子串的位置索引;如果未找到,则返回 npos。

7.11 string类substr 函数

string substr(size_t pos, size_t len = npos) const
{
	assert(pos < _size);
	size_t realLen = len;
	if (len == npos || pos + len > _size)
	{
		realLen = _size - pos;
	}

	string sub;
	for (size_t i = 0; i < realLen; ++i)
	{
		sub += _str[pos + i];
	}

	return sub;
}

substr 函数用于从字符串中提取子串,从指定位置 pos 开始,并且可选地指定子串的长度 len。现在来解释这个函数的实现:

string substr(size_t pos, size_t len = npos) const:这是 std::string 类中的 substr 函数的声明,表示该函数将从指定位置 pos 开始提取子串,并且可选地指定子串的长度 len。pos 是传入的参数,表示提取子串的起始位置的索引;len 是传入的参数,表示要提取的子串的长度,默认值为 npos,表示提取从起始位置 pos 开始的所有字符。
assert(pos < _size);:使用 assert 断言来确保提取子串的起始位置 pos 不超过字符串的实际大小 _size。如果断言失败(pos 大于等于 _size),则会触发断言失败错误,帮助调试找到错误的位置。
size_t realLen = len;:定义一个变量 realLen,用于存储实际要提取的子串的长度。初始值为传入的参数 len。
if (len == npos || pos + len > _size):检查是否要提取从起始位置 pos 开始的所有字符(即 len 等于 npos),或者是否要提取的字符个数超过字符串末尾(即 pos + len 大于等于 _size)。如果是其中一种情况,表示要提取从 pos 开始的所有字符或从 pos 开始直到末尾的所有字符。此时,将 realLen 更新为从 pos 开始到末尾的字符个数。创建一个名为 sub 的新的 std::string 对象,用于存储提取的子串。使用循环从位置 pos 开始,逐个字符地将子串添加到 sub 中。返回提取的子串 sub。

7.12 string类比较运算符重载

bool operator>(const string& s) const
{
	return strcmp(_str, s._str) > 0;
}

这是大于运算符 > 的重载版本,表示该运算符用于比较当前字符串与另一个字符串 s 的大小关系。在这个版本中,使用 strcmp 函数比较两个字符串 _str 和 s._str 的字典序。如果 _str 大于 s._str,则返回 true,否则返回 false。

bool operator==(const string& s) const
{
	return strcmp(_str, s._str) == 0;
}

这是等于运算符 == 的重载版本,表示该运算符用于比较当前字符串与另一个字符串 s 是否相等。同样,使用 strcmp 函数比较两个字符串 _str 和 s._str 的内容是否相同。如果相同,返回 true,否则返回 false。

bool operator>=(const string& s) const
{
	return *this > s || *this == s;
}

这是大于等于运算符 >= 的重载版本,表示该运算符用于比较当前字符串是否大于或等于另一个字符串 s。在这个版本中,直接使用已经定义好的大于运算符 > 和等于运算符 == 进行组合,如果当前字符串大于 s 或者与 s 相等,则返回 true,否则返回 false。

bool operator<=(const string& s) const
{
	return !(*this > s);
}

这是小于等于运算符 <= 的重载版本,表示该运算符用于比较当前字符串是否小于或等于另一个字符串 s。同样,直接使用已经定义好的大于等于运算符 >= 进行取反,如果当前字符串小于 s,则返回 true,否则返回 false。

bool operator<(const string& s) const
{
	return !(*this >= s);
}

这是小于运算符 < 的重载版本,表示该运算符用于比较当前字符串是否小于另一个字符串 s。同样,直接使用已经定义好的大于等于运算符 >= 进行取反,如果当前字符串不大于等于 s,则说明当前字符串小于 s,返回 true,否则返回 false。

bool operator!=(const string& s) const
{
	return !(*this == s);
}

这是不等于运算符 != 的重载版本,表示该运算符用于比较当前字符串是否不等于另一个字符串 s。同样,直接使用已经定义好的等于运算符 == 进行取反,如果当前字符串与 s 不相等,则返回 true,否则返回 false。

其实和之前类和对象的文章中讲到的日期类比较运算符重载一样,先实现> ==< ==后面的都可以进行复用。

7.13 string类流插入<<和流提取>>重载

首先这里要注意的是,流插入和流提取在这里定义为全局函数,因此我们不要再类中定义,而是在类外,即全局定义。这样定义的运算符重载函数不属于类的成员,因此在其实现中不能直接访问类的私有成员,而需要通过类的公有接口进行访问。

运算符重载函数可以作为成员函数或全局非成员函数进行定义,具体取决于使用场景和设计需求。通常情况下,如果运算符的操作数为类对象本身或需要直接访问类的私有成员,可以考虑将其定义为成员函数。而如果运算符的操作数为类对象外的其他类型,或者运算符涉及的操作不仅限于类对象本身,可以考虑将其定义为全局非成员函数

流插入<<

ostream& operator<<(ostream& out, const string& s)
{
	for (size_t i = 0; i < s.size(); ++i)
	{
		out << s[i];
	}
	return out;
}

这是输出运算符 << 的重载版本,表示将 std::string 类对象 s 输出到输出流 out 中。

使用一个循环遍历 s 中的每个字符,并将每个字符依次输出到输出流 out 中。最后,将输出流 out 返回,以支持链式输出。

istream& operator>>(istream& in, string& s)
{
	s.clear();

	char ch;
	ch = in.get();

	const size_t N = 32;
	char buff[N];
	size_t i = 0;

	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		if (i == N - 1)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}

		ch = in.get();
	}

	buff[i] = '\0';
	s += buff;

	return in;
}

这是输入运算符 >> 的重载版本,表示将输入流 in 中的数据读取并存储到 std::string 类对象 s 中。

首先调用 s.clear() 函数,将 s 的内容清空,以便接收新的输入。然后,使用一个循环从输入流 in 中逐个读取字符 ch。如果字符 ch 不是空格或换行符,就将字符添加到一个临时字符数组 buff 中,并增加索引 i。一旦 buff 已满(i == N - 1),就将 buff 最后一个元素设为空字符 ‘\0’,然后将 buff 添加到 s 中,然后将索引 i 重置为 0,以继续接收后续字符。如果字符 ch 是空格或换行符,说明一个单词的输入结束,将 buff 最后一个元素设为空字符 ‘\0’,然后将 buff 添加到 s 中。最后,将输入流 in 返回,以支持链式输入。

8.string类的模拟实现(完整代码)

#include
#include
#include
using namespace std;
namespace mystring
{
	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(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];

			strcpy(_str, str);
		}

		// 传统写法
		//string(const string& s)
		//	:_str(new char[s._capacity+1])
		//	, _size(s._size)
		//	, _capacity(s._capacity)
		//{
		//	strcpy(_str, s._str);
		//}

		// 现代写法 
		void swap(string& tmp)
		{
			::swap(_str, tmp._str);
			::swap(_size, tmp._size);
			::swap(_capacity, tmp._capacity);
		}


		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			string tmp(s._str);
			swap(tmp);
		}

		//string& operator=(const string& s)
		//{
		//	if (this != &s)
		//	{
		//		//string tmp(s._str);
		//		string tmp(s);
		//		swap(tmp); // this->swap(tmp);
		//	}

		//	return *this;
		//}


		string& operator=(string s)
		{
			swap(s);
			return *this;
		}

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

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

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);

			return _str[pos];
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);

			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;

				_str = tmp;
				_capacity = n;
			}
		}

		void resize(size_t n, char ch = '\0')
		{
			if (n > _size)
			{
				// 插入数据
				reserve(n);
				for (size_t i = _size; i < n; ++i)
				{
					_str[i] = ch;
				}
				_str[n] = '\0';
				_size = n;
			}
			else
			{
				// 删除数据
				_str[n] = '\0';
				_size = n;
			}
		}

		void push_back(char ch)
		{
			// 满了就扩容
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : _capacity * 2);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
			//insert(_size, ch);
		}

		void append(const char* str)
		{
			size_t len = strlen(str);

			// 满了就扩容
			if (_size + len > _capacity)
			{
				reserve(_size+len);
			}

			strcpy(_str + _size, str);
			//strcat(_str, str); 需要找\0,效率低
			_size += len;
			//insert(_size, str);
		}


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

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

		string& insert(size_t pos, char ch)
		{
			assert(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)
			{
				_str[end] = _str[end - len];
				--end;
			}

			strncpy(_str + pos, str, len);
			_size += len;

			return *this;
		}

		void erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);

			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
		}

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

		size_t find(char ch, size_t pos = 0) const
		{
			assert(pos < _size);

			for (size_t i = pos; i < _size; ++i)
			{
				if (ch == _str[i])
				{
					return i;
				}
			}

			return npos;
		}

		size_t find(const char* sub, size_t pos = 0) const
		{
			assert(sub);
			assert(pos < _size);

			// kmp/bm
			const char* ptr = strstr(_str + pos, sub);
			if (ptr == nullptr)
			{
				return npos;
			}
			else
			{
				return ptr - _str;
			}
		}

		string substr(size_t pos, size_t len = npos) const
		{
			assert(pos < _size);
			size_t realLen = len;
			if (len == npos || pos + len > _size)
			{
				realLen = _size - pos;
			}

			string sub;
			for (size_t i = 0; i < realLen; ++i)
			{
				sub += _str[pos + i];
			}

			return sub;
		}

		bool operator>(const string& s) const
		{
			return strcmp(_str, s._str) > 0;
		}

		bool operator==(const string& s) const
		{
			return strcmp(_str, s._str) == 0;
		}

		bool operator>=(const string& s) const
		{
			return *this > s || *this == s;
		}

		bool operator<=(const string& s) const
		{
			return !(*this > s);
		}

		bool operator<(const string& s) const
		{
			return !(*this >= s);
		}

		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}
	private:
		size_t _capacity;
		size_t _size;
		char* _str;
	public:
		const static size_t npos = -1;
	};

	ostream& operator<<(ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i)
		{
			out << s[i];
		}

		return out;
	}


	istream& operator>>(istream& in, string& s)
	{
		s.clear();

		char ch;
		ch = in.get();

		const size_t N = 32;
		char buff[N];
		size_t i = 0;

		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();
		}

		buff[i] = '\0';
		s += buff;

		return in;
	}
}

结语

有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!
C++初阶之一篇文章让你掌握string类(模拟实现)_第5张图片

你可能感兴趣的:(C++初阶,c++,开发语言,vscode)