C++ string类详解及模拟实现

目录

【本节目标】

 1. 为什么学习string类?

1.1 C语言中的字符串

 1.2 面试题(暂不做讲解)

 2. 标准库中的string类

2.1 string类(了解)

 2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)

3. string类的模拟实现

3.1string类常用成员函数的模拟实现

3.2 浅拷贝和深拷贝 

 4.完整代码                                                            


【本节目标】

1. 为什么要学习string类

2. 标准库中的string类

3. string类的模拟实现

4. 扩展阅读


 1. 为什么学习string类?


1.1 C语言中的字符串

C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问


 1.2 面试题(暂不做讲解)

415. 字符串相加 - 力扣(LeetCode)

在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。


 2. 标准库中的string类


2.1 string类(了解)

string类的文档介绍:sscplusplus.com/reference/string/string/?kw=string

1. 字符串是表示字符序列的类

2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。

3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。

4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参basic_string)。

5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。

总结:

1. string是表示字符串的字符串类

2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。

3. string在底层实际是:basic_string模板类的别名,typedef basic_stringstring;

4. 不能操作多字节或者变长字符的序列
在使用string类时,必须包含#include头文件以及using namespace std;


 2.2 string类的常用接口说明(注意下面我只讲解最常用的接口)

1. string类对象的常见构造

(constructor)函数名称功能说明
string() (重点)构造空的string类对象,即空字符串
string(const char* s) (重点)用C-string来构造string类对象
string(size_t n, char c)string类对象中包含n个字符c
string(const string&s) (重点)拷贝构造函数

演示:

void Teststring()
{
    string s1; // 构造空的string类对象s1
    string s2("hello world"); // 用C格式字符串构造string类对象s2
    string s3(s2); // 拷贝构造s3
}

2.string类非成员函数

函数功能说明
operator+尽量少用,因为传值返回,导致深拷贝效率低
operator>> (重点)输入运算符重载
operator<< (重点)输出运算符重载
getline (重点)获取一行字符串
relational operators (重点)大小比较

上面的几个接口了解一下即可,下面的OJ题目中会有一些体现他们的使用。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。

演示:

void Teststring()
{
	string s1; // 构造空的string类对象s1
	string s2("hello world"); // 用C格式字符串构造string类对象s2
	string s3(s2); // 拷贝构造s3
	cin >> s1;
	cout << s1 << endl;
	cout << s2 << endl;
}

3. string类对象的容量操作

函数名称功能说明
size(重点)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty (重点)检测字符串释放为空串,是返回true,否则返回false
clear (重点)清空有效字符
reserve (重点)为字符串预留空间**
resize (重点)将有效字符的个数该成n个,多出的空间用字符c填充

演示:

void test1()
{
	// 注意:string类对象支持直接用cin和cout进行输入和输出
	string s("hello, world!!!");
	cout << s.size() << endl;
	cout << s.length() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
	// 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
	s.clear();
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}
void test2()
{
	string s;
	// 将s中有效字符个数增加到10个,多出位置用'a'进行填充
	// “aaaaaaaaaa”
	s.resize(10, 'a');
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充
	// "aaaaaaaaaa\0\0\0\0\0"
	// 注意此时s中有效字符个数已经增加到15个
	s.resize(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
	// 将s中有效字符个数缩小到5个
	s.resize(5);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;
}
void test3()
{
	string s;
	// 测试reserve是否会改变string中有效元素个数
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	// 测试reserve参数小于string的底层空间大小时,是否会将空间缩小
	s.reserve(50);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}

注意:

1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。

2. clear()只是将string中有效字符清空,不改变底层空间大小

3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。

4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。

4. string类对象的访问及遍历操作

函数名称功能说明
operator[] (重

点)

返回pos位置的字符,const string类对象调用
begin+ endbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭

代器

rbegin + rendbegin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭

代器

范围for

C++11支持更简洁的范围for的新遍历方式

 string的遍历
迭代器 begin()+end()   for+[]  范围for

 注意:string遍历时使用最多的还是for+下标 或者 范围for(c++11后才支持)

 begin()+end()大多数使用在需要使用stl提供的算法操作string时,比如:采用reverse逆置string

演示:

void test4()
{
	string s1("hello world");
	const string s2("Hello world");
	cout << s1 << " " << s2 << endl;
	cout << s1[0] << " " << s2[0] << endl;
	s1[0] = 'H';
	cout << s1 << endl;
	// s2[0] = 'h';   代码编译失败,因为const类型对象不能修改
}
void test5()
{
	string s("hello world");
	// 3种遍历方式:
	// 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符,
	// 另外以下三种方式对于string而言,第一种使用最多
	// 1. for+operator[]
	for (size_t i = 0; i < s.size(); ++i)
		cout << s[i] << endl;
	// 2.迭代器
	string::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << endl;
		++it;
	}
	// string::reverse_iterator rit = s.rbegin();
	// C++11之后,直接使用auto定义迭代器,让编译器推到迭代器的类型
	auto rit = s.rbegin();
	while (rit != s.rend())
		cout << *rit << endl;
	// 3.范围for(auto的底层还是迭代器)
	for (auto ch : s)
		cout << ch << endl;
}

C++中的迭代器是一种用于遍历容器(如数组、向量、链表等)中元素的对象。迭代器提供了一种统一的访问容器元素的方式,使得我们可以通过迭代器来访问容器中的元素,而不用关心容器的具体实现细节。

在C++标准库中,迭代器被设计为类似指针的对象,可以通过递增(++)和递减(--)操作符来访问容器中的元素。迭代器可以分为多种类型,包括输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器,每种类型的迭代器支持不同的操作。

通过使用迭代器,我们可以在容器中进行元素的遍历、查找、修改等操作,这使得C++中的容器类成为非常强大和灵活的工具。在编写C++程序时,熟练掌握迭代器的使用可以帮助我们更高效地处理容器中的数据。

注意:普通迭代器和const迭代器的区别就是可不可以修改,const 对象就要去调用const的迭代器。

void test7()
{
	string s1("hello");
	const string s2("hello");
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << endl;
		++it;
	}
	string::const_iterator it = s2.begin();
	while (it != s2.end())
	{
		cout << *it << endl;
	}
}

这是反向的迭代器,用法和正向的一样,只是反着遍历。

当然,如果你觉得反向迭代器太长,不方便,你可以直接用auto定义迭代器。

5.string类对象的修改操作

函数名称功能说明
push_back在字符串后尾插字符c
append在字符串后追加一个字符串
operator+= (重点)在字符串后追加字符串str
c_str(重点)返回C格式字符串
find + npos(重点)从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置
rfind从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置
substr在str中从pos位置开始,截取n个字符,然后将其返回

 测试string:

 1. 插入(拼接)方式:push_back  append  operator+= 

 2. 正向和反向查找:find() + rfind()

 3. 截取子串:substr()

 4. 删除:erase

演示:

void test6()
{
	string str;
	str.push_back(' ');   // 在str后插入空格
	str.append("hello");  // 在str后追加一个字符"hello"
	str += 'w';           // 在str后追加一个字符'w'   
	str += "orld";          // 在str后追加一个字符串"orld"
	cout << str << endl;
	cout << str.c_str() << endl;   // 以C语言的方式打印字符串
	// 获取file的后缀
	string file("string.cpp");
	size_t pos = file.rfind('.');
	string suffix(file.substr(pos, file.size() - pos));
	cout << suffix << endl;
	// npos是string里面的一个静态成员变量
	// static const size_t npos = -1;
	// 取出url中的域名
	string url("http://www.cplusplus.com/reference/string/string/find/");
	cout << url << endl;
	size_t start = url.find("://");
	if (start == string::npos)
	{
		cout << "invalid url" << endl;
		return;
	}
	start += 3;
	size_t finish = url.find('/', start);
	string address = url.substr(start, finish - start);
	cout << address << endl;
	// 删除url的协议前缀
	pos = url.find("://");
	url.erase(0, pos + 3);
	cout << url << endl;
}

6. vs和g++下string结构的说明

注意:下述结构是在32位平台下进行验证,32位平台下指针占4个字节

  • vs下string的结构

    string总共占28个字节,内部结构稍微复杂一点,先是有一个联合体,联合体用来定义string中字符串的存储空间:

            当字符串长度小于16时,使用内部固定的字符数组来存放

            当字符串长度大于等于16时,从堆上开辟空间

    union _Bxty
    { // storage for small buffer or pointer to larger one
    	value_type _Buf[_BUF_SIZE];
    	pointer _Ptr;
    	char _Alias[_BUF_SIZE]; // to permit aliasing
    } _Bx;

            这种设计也是有一定道理的,大多数情况下字符串的长度都小于16,那string对象创建好之后,内部已经有了16个字符数组的固定空间,不需要通过堆创建,效率高。

    其次:还有一个size_t字段保存字符串长度,一个size_t字段保存从堆上开辟空间总的容量

    最后:还有一个指针做一些其他事情。

    故总共占16+4+4+4=28个字节。

    • g++下string的结构

      G++下,string是通过写时拷贝实现的,string对象总共占4个字节,内部只包含了一个指针,该指针将来指向一块堆空间,内部包含了如下字段:

              空间总大小

              字符串有效长度

              引用计数

      struct _Rep_base
      {
      	size_type _M_length;
      	size_type _M_capacity;
      	_Atomic_word _M_refcount;
      };

      string增删查改函数及其它常用函数总结:

      1.增加数据

      void test1()
      {
      	string s("hello world");
      	//任意位置做插入
      	s.insert(5, "xxxxx");
      	cout << s << endl;
      	//头插(注意头插是以单个字符为单位)
      	s.insert(0, 2, 'x');
      	cout << s << endl;
      }

      2.删除数据

      void test2()
      {
      	//从第5个位置开始删除4个字符
      	string s("hello world");
      	s.erase(5, 4);
      	cout << s << endl;
      	//第二个数据不给,从起始位置开始删除全部数据
      	s.erase(0);
      	cout << s << endl;
      }

      3.查找

      void test3()
      {
      	string s("hello world");
      	//查找空格位置
      	size_t pos = s.find(' ');
      	cout << pos << endl;
      }

      4.修改

      修改有很多函数,但我们最常用的就是第一个。

      eg:将所有空格位置都修改成 '%'

      void test4()
      {
      	string s("h e l l o w o r l d");
      	size_t pos = s.find(' ');
      	while (pos!=string::npos)
      	{
      		//从pos位置开始的一个字符修改成'%'
      		s.replace(pos, 1, "%");
      		pos = s.find(' ');
      	}
      	cout << s << endl;
      }

      5.c_str 函数

      eg:用c的方式将string创建的字符串读取到文件中,直接读取肯定是不行的,c语言中fopen函数要求是const char* 类型,而string是自定义类型,这时候类型不一样不能进行直接操作,为了兼容c语言string引入了c_str函数,返回c语言形式下字符串的指针类型。

      void test5()
      {
      	string filename("FileName.cpp");
      	FILE* f = fopen(filename.c_str(), "r");
      	char ch = fgetc(f);
      	while (ch != EOF)
      	{
      		cout << ch;
      		ch = fgetc(f);
      	}
      }

3. string类的模拟实现


3.1string类常用成员函数的模拟实现

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

成员变量,指向字符串的指针,字符个数,容量大小:

class my_string
{
public:
private:
	char* _str;
	size_t _size;
	size_t _capacity;
};

接下来我们实现类的构造,拷贝构造,析构函数,赋值重载:

 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];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

        我们在写构造的时候并没有去使用初始化列表,我们发现初始化这三个变量的时候都是要调用strlen函数去计算字符的长度,为了提高效率,避免重复使用就用了赋值的操作,而这种赋值的操作在初始化列表中像这样写是不行的,因为初始化列表初始的先后是根据变量在声明时候的先后来确定的,所以这里是不能使用初始化列表的。我们发现给了构造函数缺省参数, 这是为了满足在使用srting实例化对象的时候可以不进行初始化,如果没有提供实际的字符串作为参数,就会使用空字符串作为默认值,确保对象可以被正确初始化,库中的string在这里是默认给一个字符的空间,来存放'\0‘。在构造函数中初始化_str的时候为什么不能直接将str给_str,因为str为const类型权限不够,且_str必须是一个可以修改的值,所以只能先给_str开足够的空间,再将str的内容拷贝给_str。

        这里的赋值重载也是十分简单的,使用一个临时的空间交换数据,返回结果就可以了,需要注意的是返回的引用类型。(它返回引用类型的原因是为了支持连续赋值操作,比如str1 =str2=str3。通过返回引用类型,可以实现将多个赋值操作连在一起,确保每次赋值操作后返回的是被赋值的对象本身,而不是一个临时副本。)


 模拟实现string类中的迭代器:

        为什么说是string中的迭代器而不是迭代器,string中的空间是连续的,我们简单的去调用指针就可以去到达迭代器的效果(代器的类型是与容器类型相关联的,不同容器类型对应不同的迭代器类型。每种容器类型都有其特定的迭代器类型)

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;
		}

这样就实现了const和非const两种string的迭代器了。

效果:

void test_string1()
	{
		string s1("hello world");
		cout << s1.c_str() << endl;
		string::iterator it = s1.begin();
		while (it != s1.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

其它成员函数

返回整个字符串,返回size,下标访问

 const char* c_str() const
		{
			return _str;
		}
		size_t size() const
		{
			return _size;
		}
		const char& operator[](size_t pos) const
		{
			assert(pos <= _size);
			return _str[pos];
		}
		char& operator[](size_t pos)
		{
			assert(pos <= _size);
			return _str[pos];
		}

在这段代码中,成员函数 c_str() 、 size() 、 operator[] 分别被声明为 const 成员函数和非 const 成员函数。这种设计的目的是为了实现常量对象和非常量对象的区分,以提供更灵活的使用方式。

1. const char* c_str() const : 这个成员函数返回字符串的 const char* 指针,因为它不修改对象的成员变量,所以被声明为 const 成员函数。这样可以确保在常量对象上也可以调用这个函数。

2. size_t size() const : 这个成员函数返回字符串的大小,同样不修改对象的成员变量,因此被声明为 const 成员函数。这样可以在常量对象上安全地调用。

3. const char& operator[](size_t pos) const 和 char& operator[](size_t pos) : 这两个 operator[] 函数分别用于常量对象和非常量对象,以实现对字符串的访问。 const 版本的 operator[] 用于常量对象,返回一个 const char& ,表示只能读取字符而不能修改;非 const 版本的 operator[] 用于非常量对象,返回一个 char& ,允许修改字符。这种设计使得可以根据对象的常量性选择合适的操作符重载版本。

用来打印的函数

打印不能修改,要加const,因此需要有const的成员函数可以被调用,在这里就需要成员函数const的版本了。

 void print_str(const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
		{
			//s[i]++;
			cout << s[i] << " ";
		}
		cout << endl;
		string::const_iterator it = s.begin();
		while (it != s.end())
		{
			// *it = 'x';
			cout << *it << " ";
			++it;
		}
		cout << endl;
	}

用来增加(尾插)数据的函数及扩容的函数

下面是这些成员函数的逻辑解释:

void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size == _capacity)
			{
				size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newCapacity);
			}
			_str[_size] = ch;
			_size++;
			_str[_size] = '\0';
		}
		void append(const char* str)
		{
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			strcpy(_str + _size, str);
			_size += len;
		}
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

1. void reserve(size_t n) :

- 如果传入的大小 n 大于当前容量 _capacity ,则进行扩容。

- 创建一个新的大小为 n + 1 的临时字符数组 tmp 。

- 将原字符串 _str 的内容复制到临时数组 tmp 中。

- 删除原字符串 _str 的内存空间。

- 将临时数组 tmp 赋值给字符串成员 _str ,更新容量 _capacity 为 n 。

2. void push_back(char ch) :

- 如果当前字符串大小 _size 等于容量 _capacity ,则需要扩容。

- 计算新的容量 newCapacity ,如果当前容量为0,则设置为4,否则为原容量的两倍。

- 调用 reserve(newCapacity) 进行扩容。

- 将字符 ch 添加到字符串末尾,更新字符串大小 _size ,并在末尾添加字符串结束符 \0 。

3. void append(const char* str) :

- 获取要追加的字符串 str 的长度 len 。

- 如果追加后的总长度(当前大小 _size 加上 str 的长度 len )超过容量 _capacity ,则进行扩容。

- 将字符串 str 复制到当前字符串的末尾,更新字符串大小 _size 。

4.对于+=只需要重新去调用实现好的函数就好了

试试效果:

void test()
	{
		string s1("hello ");
		
		s1 += "world";
		cout<< s1.c_str() << endl;
	}

头插

 void insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newCapacity);
			}
			size_t end = _size + 1;
			while (end > pos)
			{
				_str[end] = _str[end - 1];
				--end;
			}
			_str[pos] = ch;
			_size++;
		}
		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				reserve(_size + len);
			}
			int end = _size;
			while (end >= (int)pos)
			{
				_str[end + len] = _str[end];
				--end;
			}
			strncpy(_str + pos, str, len);
			_size += len;
		}

1. void insert(size_t pos, char ch) :

- 首先,该函数接受要插入的位置 pos 和要插入的字符 ch 。

- 确保插入位置在有效范围内(小于等于当前字符串长度 _size )。

- 如果当前字符串的大小达到容量上限 _capacity ,则扩展容量。

- 从插入位置开始,依次将原字符串中的字符向后移动一个位置,为新字符 ch 腾出空间。

- 在插入位置处设置新字符 ch ,并增加字符串的大小 _size 。

2. void insert(size_t pos, const char* str) :

- 该函数接受要插入的位置 pos 和要插入的字符串 str 。

- 确保插入位置在有效范围内。

- 计算要插入的字符串 str 的长度 len 。

- 如果插入字符串后的总长度将超出容量 _capacity ,则扩展容量。

- 从插入位置开始,将原字符串中的字符逐个向后移动 len 个位置,为插入字符串 str 腾出空间。

- 使用 strncpy 函数将字符串 str 插入到指定位置处,并更新字符串的大小 _size 。

这两个函数实现了在字符串的指定位置插入字符或字符串的功能,并且在需要时会动态调整字符串的容量。如果你有任何疑问或需要进一步解释,请随时告诉我!

对任意位置进行删除

class string
{
public:
	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;
		}
	}
private:
	char* _str;
	size_t _size;
	size_t _capacity;
	const static size_t npos = -1;
};

1. 首先,函数接受两个参数: pos 表示擦除的起始位置, len 表示要擦除的字符数,缺省值为 npos 。

2. 函数首先检查起始位置 pos 是否有效,即小于当前字符串的大小 _size 。

3. 如果 len 等于 npos (默认值)或者起始位置 pos 加上要擦除的长度 len 大于等于当前字符串的大小 _size ,则说明要擦除的范围包括了字符串末尾或超出字符串末尾:

- 在这种情况下,将起始位置 pos 处的字符设置为结束符 '\0' ,表示字符串的结束。

- 更新字符串的大小 _size 为 pos ,即擦除到指定位置 pos 处。

4. 如果擦除范围在字符串中间:

- 使用 strcpy 函数将从起始位置 pos + len 处开始的剩余字符串复制到起始位置 pos 处,覆盖要擦除的字符。

- 减少字符串的大小 _size 以反映已擦除的字符数 len 。

5. 函数完成擦除操作后,字符串中从位置 pos 开始的字符将被擦除,而其余字符将向前移动以填补擦除的空间。

注意:此处的nops变量,可以声明时定义是因为加了const,你可以认为这是一种标示,让编译器做了特殊处理,使之可以进行定义,但这这种特殊处理只能用于整型,不能是其它类型。

查找

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

1. find 函数用于从指定位置 pos 开始在字符串中查找给定的子字符串 str 的第一次出现。

2. 使用 strstr 函数在从位置 pos 开始的字符串 _str 中查找子字符串 str 。

3. 如果 strstr 函数返回 nullptr ,表示从指定位置开始未找到子字符串。此时,函数返回 npos 的值(通常定义为表示“未找到”的大值)。

4. 如果找到子字符串,函数通过将指向子字符串开头的指针 ptr 减去指向原始字符串开头的指针 _str ,计算子字符串在原始字符串中的索引位置。

5. 然后,函数返回找到的子字符串的索引位置,或者如果从指定位置开始未找到子字符串,则返回 npos 。

substr(从pos位置开始取len个字符)

string substr(size_t pos = 0, size_t len = npos)
		{
			assert(pos < _size);
			size_t end = pos + len;
			if (len == npos || pos + len >= _size)
			{
				end = _size;
			}
			string str;
			str.reserve(end - pos);
			for (size_t i = pos; i < end; i++)
			{
				str += _str[i];
			}
			return str;
		}

1. substr 函数用于从指定位置 pos 开始截取指定长度 len 的子字符串。

2. 首先,函数检查起始位置 pos 是否在有效范围内(小于字符串的大小 _size )。

3. 然后,计算截取的结束位置 end 为 pos + len 。如果指定的长度 len 为 npos 或者起始位置加上长度超过了字符串的大小,则将结束位置设置为字符串的末尾。

4. 创建一个新的字符串 str 来存储截取后的子字符串。
5. 使用 reserve 函数预留足够的空间以容纳截取后的子字符串,避免不必要的内存重新分配。

6. 遍历从起始位置 pos 到结束位置 end 的字符,并将它们添加到新的字符串 str 中。

7. 最后,返回包含截取后子字符串的新字符串 str 。

我们来测试一下这个函数:

void test()
	{
		string str("hello ");
		
		string sub1;
		int pos1 = str.find('o');
		sub1 = str.substr(0, pos1-0);
		cout<< sub1.c_str() << endl;
	}

我们发现出错了,这是为什么呢?

这是发生浅拷贝的缘故

传值返回并不能返回当前对象,而是返回当前对象的拷贝。str会调用拷贝构造创建临时对象,将临时对象返回给str,但是我们并没有写拷贝构造,这里的拷贝是浅拷贝,浅拷贝对于内置类型直接拷贝它的值,导致临时对象也指向了,函数中str指向的空间,出作用域,str会调用析构函数,str所指向的空间会被释放,导致临时对象返回野指针。

为了解决浅拷贝问题,我自己写拷贝构造和赋值操作

// s2(s1)
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}
		// s1 = s3
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}

此时我们再次运行上面程序就成功了。


模拟实现string流插入和流提取

流提取

void string::clear()
	{
		_size = 0;
		_str[0] = '\0';
	}
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[128];
		char ch = in.get();
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

 函数重载了 >> 运算符,接受一个输入流对象 in 和一个字符串对象 s 作为参数。函数的主要功能是从输入流中读取字符串,遇到空格或换行符时停止读取,并将读取的字符串存储在参数 s 中。

在函数中,使用一个缓冲区 buff 来临时存储读取的字符,然后逐个字符读取输入流,直到遇到空格或换行符为止。读取的字符存储在 buff 中,当 buff 达到一定长度时(127个字符),将其添加到字符串 s 中,并清空 buff ,继续读取。最后,将剩余的字符添加到字符串 s 中,然后返回输入流对象 in 。

在这段代码中,调用 in.get() (in和scanf一样是不能读取到‘空格’和‘回车’的,因此要调用get函数来读取)函数是为了逐个读取输入流中的字符,以便构建字符串。每次调用 in.get() 可以读取输入流中的下一个字符,并将其存储在变量 ch 中。通过这种方式,可以逐个字符地读取输入流,直到遇到空格或换行符为止,从而构建出完整的字符串。

至于为什么要编写 clear 函数,这是因为在字符串类中,可能需要在不同的情况下清空字符串内容。在这段代码中, clear 函数的作用是将字符串对象的大小设置为0,同时将字符串内容的第一个字符设为 \0 ,以实现清空字符串的功能。这样做可以确保在需要清空字符串内容时,可以方便地调用 clear 函数来实现清空操作,使字符串对象重新变为空字符串,而不需要重复编写清空操作的代码。

流插入

 ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

这段代码是为自定义字符串类重载了输出流运算符 << 。函数接受一个输出流对象 out 和一个常量引用的字符串对象 s 作为参数。它的功能是将字符串 s 中的字符输出到输出流 out 中。

在函数中,使用了一个范围 for 循环来遍历字符串 s 中的每个字符。对于字符串中的每个字符 ch ,使用 << 运算符将其输出到输出流 out 中。这个循环实际上将字符串的每个字符输出到输出流中。

最后,函数返回输出流对象 out 。通过这种方式重载了 << 运算符,可以很方便地使用标准的输出流语法将自定义字符串类输出到输出流中。


3.2 浅拷贝和深拷贝 

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

 

深拷贝:如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供

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

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

拷贝构造和赋值操作不一样的写法:

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

首先是拷贝构造函数  ,它接受一个 string 对象 s 的引用作为参数。在函数内部,首先创建了一个临时的 string 对象 tmp ,并将参数 s 的内部字符串 _str 拷贝到了 tmp 对象中。然后通过调用 swap 函数,将 tmp 对象和当前对象进行交换,达到拷贝构造的目的。

接下来是赋值运算符重载函数 s。它接受一个 string 对象 s 作为参数,并返回一个 string 对象的引用。在函数内部,通过调用 swap 函数,将参数 s 和当前对象进行交换,实现了赋值操作。最后返回当前对象的引用。

这两个函数都使用了 swap 函数来交换对象的内部数据,这是一种常见的优化技巧,可以避免不必要的内存拷贝,提高程序的效率。

在上面的拷贝构造函数中,参数是一个 const string& s ,这意味着传递的是一个 string 对象的引用,并且在函数内部不会修改传入的对象。因为拷贝构造函数的目的是创建一个新的对象并初始化为另一个对象的副本,所以通常会使用引用来避免不必要的复制开销。

而在赋值运算符重载函数中,参数是 string s ,表示传递的是一个 string 对象的副本。因为在赋值操作中,我们通常会对传入的对象进行修改,所以直接传递一个副本是可以接受的。在这种情况下,函数内部对传入的对象进行修改,不会影响原始对象,因此不需要使用引用。

总的来说,拷贝构造函数通常使用引用来避免不必要的复制,而赋值运算符重载函数可以选择传递对象的副本来方便修改。


 4.完整代码                                                            

#include
#includeusing namespace std;
namespace bit
{
	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;
		}
		const char* c_str() const
		{
			return _str;
		}
		size_t size() const
		{
			return _size;
		}
		string(const char* str = "");
		// ִд
		string(const string& s);
		string& operator=(string s);
		~string();
		const char& operator[](size_t pos) const;
		char& operator[](size_t pos);
		void reserve(size_t n);
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);
		void swap(string& s);
		size_t find(char ch, size_t pos = 0);
		//21:10
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);
		void clear();
	private:
		size_t _capacity = 0;
		size_t _size = 0;
		char* _str = nullptr;
		const static size_t npos = -1;
	};
	istream& operator>>(istream& in, string& s);
	ostream& operator<<(ostream& out, const string& s);
}
namespace bit
{
	string::string(const char* str)
	{
		_size = strlen(str);
		_capacity = _size;
		_str = new char[_capacity + 1];
		strcpy(_str, str);
	}
	// ִд
	string::string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}
	string& string::operator=(string s)
	{
		swap(s);
		return *this;
	}
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = 0;
		_capacity = 0;
	}
	const char& string::operator[](size_t pos) const
	{
		assert(pos <= _size);
		return _str[pos];
	}
	char& string::operator[](size_t pos)
	{
		assert(pos <= _size);
		return _str[pos];
	}
	void string::reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			strcpy(tmp, _str);
			delete[] _str;
			_str = tmp;
			_capacity = n;
		}
	}
	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newCapacity);
		}
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';
	}
	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}
		strcpy(_str + _size, str);
		_size += len;
	}
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	void string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newCapacity);
		}
		size_t end = _size + 1;
		while (end > pos)
		{
			_str[end] = _str[end - 1];
			--end;
		}
		_str[pos] = ch;
		_size++;
	}
	void string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			reserve(_size + len);
		}
		int end = _size;
		while (end >= (int)pos)
		{
			_str[end + len] = _str[end];
			--end;
		}
		strncpy(_str + pos, str, len);
		_size += len;
	}
	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);
		if (len == npos || pos + len >= _size)
		{
			_str[pos] = '\0';
			_size = pos;
		}
		else
		{
			strcpy(_str + pos, _str + pos + len);
			_size -= len;
		}
	}
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}
	size_t string::find(char ch, size_t pos)
	{
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}
		return npos;
	}
	size_t string::find(const char* str, size_t pos)
	{
		const char* ptr = strstr(_str + pos, str);
		if (ptr == nullptr)
		{
			return npos;
		}
		else
		{
			return ptr - _str;
		}
	}
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);
		size_t end = pos + len;
		if (len == npos || pos + len >= _size)
		{
			end = _size;
		}
		string str;
		str.reserve(end - pos);
		for (size_t i = pos; i < end; i++)
		{
			str += _str[i];
		}
		return str;
	}
	void string::clear()
	{
		_size = 0;
		_str[0] = '\0';
	}
	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}
	istream& operator>>(istream& in, string& s)
	{
		s.clear();
		char buff[128];
		char ch = in.get();
		int i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
}