深度解析C++中的容器 —— string

1 为什么要学习 string 类(为什么)?

** C语言中的字符串**

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

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

2 标准库中的 string 类(是什么)?

2.1 string 类

在这里插入图片描述

  1. string是表示字符串的字符串类
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>
    string;
  4. 不能操作多字节或者变长字符的序列。

在使用string类时,必须包含#include头文件以及using namespace std;

2.2 string 类的常用接口

2.2.1 string 类对象的常用构造函数

在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;
int main()
{
    
    
	string s1;//默认构造 空字符串
	string s2("czh");//用C字符串构造
	string s3(s2);// 拷贝构造函数

	return 0;
}

2.2.2 string 类对象的容量操作

在这里插入图片描述

string s1("chenzhihao");
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << s1.capacity() << endl;

在这里插入图片描述

//将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小
	s1.clear();
	cout << s1.size() << endl;
	cout << s1.capacity() << endl;

在这里插入图片描述

//将s1中有效字符个数增加到15个,用'a'进行填充
	s1.resize(15, 'a');
	cout << s1 << endl;
	//将s1中有效字符个数增加到20个,多出位置用缺省值'\0'进行填充
	s1.resize(20);
	cout << s1 << endl;

	s1.resize(5);
	cout << s1 << endl;

在这里插入图片描述

//reserve并不会改变string中的有效元素个数
	string s;
	cout << s.size() << endl;
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;

在这里插入图片描述

//reserve参数小于string的底层空间大小时,并不会将空间缩小
	s.reserve(50);
	cout << s.capacity() << endl;

在这里插入图片描述
利用 reserve 可以提高插入数据的效率,避免增容带来的开销,前提是你知道自己要多大的空间

void testPushBack1()
{
    
    
	string s;
	size_t cap = s.capacity();
	cout << "cap increasing:" << endl;
	for (int i = 0; i < 100; i++) {
    
    
		s.push_back('c');
		if (cap != s.capacity()) cout << "cap inc" << endl; 
	}
}

上面的代码,持续的增容会带来系统开销的增大。

在这里插入图片描述

void testPushBackReserve()
{
    
    
	string s;
	s.reserve(100);
	size_t cap = s.capacity();
	
	for (int i = 0; i < 100; i++) {
    
    
		s.push_back('c');
		if (cap != s.capacity()) cout << "cap inc" << 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不会改变容量大小。

2.2.3 string 类对象的访问和遍历操作

在这里插入图片描述
第一种遍历方式 :operator[]

void TestString1() {
    
    
	string s1("chenzhihao");
	const string s2("hello czh");
	for (size_t i = 0, j = 0; i < s1.size(),j < s2.size(); i++,j++) {
    
    
		cout << s1[i] << endl;
		cout << s2[j] << endl;
	}
}

第二种遍历方式 :迭代器

void TestString2() {
    
    
	string s1("chenzhihao");
	// 正向迭代器
	string::iterator it = s1.begin();
	while (it != s1.end())
	{
    
    
		cout << *it << " ";
		++it;
	}
	cout << endl;
	// 反向迭代器
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
    
    
		cout << *rit << " ";
		rit++;
	}
	cout << endl;
}

**第三种遍历方式 :范围 for **

void TestString3() {
    
    
	string s1("chenzhihao");
	for (auto e : s1)
	{
    
    
		cout << e << " ";
	}
	cout << endl;
}

2.2.4 string 类对象的修改操作

在这里插入图片描述

void TestString4()
{
    
    
	string str;
	str.push_back('c');
	str.append("henzhihao");
	str += 'a';
	str += "hahaha";
	cout << str << endl;
	cout << str.c_str() << endl;

	//获取file的后缀
	string file("string.cpp");
	size_t pos = file.rfind('.');
	string suffix(file.substr(pos, file.size() - pos));
	cout << file << endl;
	cout << suffix << endl;
}
void TestString5()
{
    
    
	// 取出url中的域名
	string url("http://www.cplusplus.com/reference/string/string/find/");
	size_t pos = url.find(':');
	size_t start;
	if (pos != string::npos) {
    
    
		start = pos + 3;
	}
	size_t end = url.find('/',start);
	string domainName = url.substr(start, end - start);
	cout << "domainName : " << domainName << endl;
	cout << url << endl;
	
	// 删除url的协议前缀
	string urlcopy(url);
	urlcopy.erase(0, start);
	cout << urlcopy << endl;

}

3 string类的模拟实现(怎么办)?

3.1 经典的 string 类问题

我们自己动手实现了一个 string 类,

namespace czh{
    
    
	class string{
    
    
	public:
		/*string()
		:_pStr(new char[1])
		{
		*_pStr = '\0';
		}*/
		string(const char* str = "")
		{
    
    
			if (str == nullptr)
			{
    
    
				assert(false);
				return;
			}
			_pStr = new char[strlen(str) + 1];
			strcpy(_pStr, str);
		}
		~string()
		{
    
    
			if (_pStr)
			{
    
    
				delete[] _pStr;
				_pStr = nullptr;
			}
		}
	private:
		char *_pStr;
	};
}
int main()
{
    
    
	czh::string s1("chenzhihao");
	czh::string s2(s1);
	return 0;
}

编译没有问题,当运行可执行程序的时候,结果程序崩溃,那么这是为什么呢?
我们观察上述的代码,这个string类没有显式定义其拷贝构造函数和赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造,最终导致的问题是s1、s2公用一块内存空间,当函数结束时,需要将s1和s2销毁掉,先析构s2,s2将其_pStr所指向的空间释放掉,s2对象成功销毁,但是s1中的_pStr称为野指针,当析构s1时候重复释放空间就引起了程序的崩溃,这种拷贝方式称为浅拷贝。

3.2 浅拷贝

上面的问题引入了浅拷贝的概念,那么再详细介绍下:
浅拷贝也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中涉及管理资源,最后会导致多个对象共享同一份资源,当一个对象被销毁的时就会将该资源释放掉,而此时另一些对象不知道资源已经被释放掉,以为还有效,所以当继续对该资源进行释放的时,就发生了违规访问。为了解决浅拷贝的问题,C++中引入了深拷贝。

3.3 深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出,一般都是按照深拷贝方式提供。每个string类对象都要用空间类存放字符串,而s2要s1拷贝构造出来,因此给每个对象独立分配资源,保证多个对象不会因共享资源而造成多次释放造成程序崩溃问题。

namespace czh{
    
    
	class string{
    
    
	public:
		/*string()
		:_pStr(new char[1])
		{
		*_pStr = '\0';
		}*/
		string(const char* str = "")
		{
    
    
			if (str == nullptr)
			{
    
    
				assert(false);
				return;
			}
			_pStr = new char[strlen(str) + 1];
			strcpy(_pStr, str);
		}
		//加上了显式定义的拷贝构造程序可以正常运行
		string(const string& s)
		{
    
    
			_pStr = new char[strlen(s._pStr) + 1];
			strcpy(_pStr, s._pStr);
		}
		~string()
		{
    
    
			if (_pStr)
			{
    
    
				delete[] _pStr;
				_pStr = nullptr;
			}
		}
	private:
		char *_pStr;
	};
}
int main()
{
    
    
	czh::string s1("chenzhihao");
	czh::string s2(s1);
	return 0;
}

加上赋值运算符重载的代码

namespace czh{
    
    
	class string{
    
    
	public:
		/*string()
		:_pStr(new char[1])
		{
		*_pStr = '\0';
		}*/
		string(const char* str = "")
		{
    
    
			if (str == nullptr)
			{
    
    
				assert(false);
				return;
			}
			_pStr = new char[strlen(str) + 1];
			strcpy(_pStr, str);
		}
		//加上了显式定义的拷贝构造程序可以正常运行
		string(const string& s)
		{
    
    
			_pStr = new char[strlen(s._pStr) + 1];
			strcpy(_pStr, s._pStr);
		}
		//赋值运算符重载
		string& operator=(const string& s)
		{
    
    
			if (this != &s)
			{
    
    
				char * temp = new char[strlen(s._pStr) + 1];
				strcpy(temp, s._pStr);
				delete[] _pStr;
				this->_pStr = temp;
			}
			return *this;
		}
		~string()
		{
    
    
			if (_pStr)
			{
    
    
				delete[] _pStr;
				_pStr = nullptr;
			}
		}
	private:
		char *_pStr;
	};
}
int main()
{
    
    
	czh::string s1("chenzhihao");
	czh::string s2(s1);
	czh::string s3;
	s3 = s1;
	return 0;
}

3.3.1 现代版写法的 string 类

这里涉及到赋值运算符重载的一个异常安全性问题,要想在赋值运算符函数中实现异常安全性,我们有两种方法。一种简单的方法是先用 new 分配空间,再用 strcpy 分配内容,再用 delete 释放已有的内容,再让 _str 指向的新分配的内容。这样就保证了只有分配空间和内容成功之后再释放原来的内容,也就是当分配内存失败的时候我们可以确保 czh::string 的实例不会被修改我们还有一种更好的办法即先创建一个临时实例,再交换临时实例和原有实例的资源指向。下面是这种思路的string类实现

namespace czh{
    
    
	class string{
    
    
	public:
		/*string()
		:_pStr(new char[1])
		{
		*_pStr = '\0';
		}*/
		string(const char* str = "")
		{
    
    
			if (str == nullptr)
			{
    
    
				assert(false);
				return;
			}
			_pStr = new char[strlen(str) + 1];
			strcpy(_pStr, str);
		}
		//加上了显式定义的拷贝构造程序可以正常运行
		/*string(const string& s)
		{
			_pStr = new char[strlen(s._pStr) + 1];
			strcpy(_pStr, s._pStr);
		}*/
		//现代版本的拷贝构造函数
		string(const string& s)
			:_pStr(nullptr)
		{
    
    
			string strTmp(s._pStr);
			swap(_pStr, strTmp._pStr);
		}
		//赋值运算符重载
		/*string& operator=(const string& s)
		{
			if (this != &s)
			{
				char * temp = new char[strlen(s._pStr) + 1];
				strcpy(temp, s._pStr);
				delete[] _pStr;
				this->_pStr = temp;
			}
			return *this;
		}*/
		//现代版本的赋值运算符重载
		string& operator=(const string& s)
		{
    
    
			if (this != &s)
			{
    
    
				string strTmp(s);
				swap(_pStr, strTmp._pStr);
			}
			return *this;
		}

		~string()
		{
    
    
			if (_pStr)
			{
    
    
				delete[] _pStr;
				_pStr = nullptr;
			}
		}
	private:
		char *_pStr;
	};
}
int main()
{
    
    
	czh::string s1("chenzhihao");
	czh::string s2(s1);
	czh::string s3;
	s3 = s1;
	return 0;
}

现代版本的赋值运算符重载函数中,我们先创建了一个临时实例 strTmp,接着把 strTmp._pStr 和实例自身的 _pStr 进行交换。由于 strTmp 是一个局部变量当程序运行到 if 外面的时候也就出该对象(变量)的作用域,就会自动调用 strTmp 的析构函数,把 strTmp._pStr 所指向的内存释放掉,又由于strTmp._pStr 所指向的内存就是实例之前的 _pStr 的内存,这就相当于自己调用析构函数释放(this)实例的内存。这两个方法都考虑到了代码的异常安全性问题。

4 自己实现 string类

string.h

#pragma once
#include <string.h>
#include <assert.h>
#include <utility>
namespace czh{
    
    
	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 = "")
		{
    
    
			if (str == nullptr)
			{
    
    
				assert(false);
				return;
			}
			_size = strlen(str);
			_capacity = _size;
			_str = new char[strlen(str) + 1];
			strcpy(_str, str);
		}
		//拷贝构造
		string(const string& s)
		{
    
    
			_size = s._size;
			_capacity = s._capacity;
			/*_str = new char[strlen(s._str) + 1];
			strcpy(_str, s._str);*/
			string strTmp(s._str);
			std::swap(_str, strTmp._str);
		}
		//析构
		~string()
		{
    
    
			if (_str != nullptr)
			{
    
    
				delete[] _str;
				_str = nullptr;
			}
			_size = _capacity = 0;
		}
		//[]运算符重载
		char& operator[](size_t i)
		{
    
    
			assert(i < _size);
			return _str[i];
		}
		//赋值运算符重载
		string& operator=(const string& s)
		{
    
    
			//保证不给自己赋值
			if (this != &s)
			{
    
    
				/*char* temp = new char[strlen(s._str) + 1];
				strcpy(temp, s._str);
				delete[] _str;
				_str = temp;*/
				string strTmp(s);
				std::swap(_str, strTmp._str);
			}
			return *this;
		}
		void reserve(size_t n)
		{
    
    
			if (n > _capacity)
			{
    
    
				char *temp = new char[n];
				strcpy(temp, _str);
				delete[] _str;
				_str = temp;
			}
		}
		void push_back(char c)
		{
    
    
			if (_size == _capacity)
			{
    
    
				reserve(2 * _capacity);
			}
			_str[_size] = c;
			_size++;
		}
		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 c)
		{
    
    
			push_back(c);
			return *this;
		}
		string& operator+=(const char* str)
		{
    
    
			append(str);
			return *this;
		}
		string& operator+=(const string& s)
		{
    
    
			append(s._str);
			return *this;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

测试

#include "string.h"
#include <iostream>
using namespace std;
void PrintString(const czh::string& str)
{
    
    
	czh::string::const_iterator begin = str.begin();
	czh::string::const_iterator end = str.end();
	while (begin != end)
	{
    
    
		cout << *begin << " ";
		++begin;
	}
	cout << endl;

}
int main()
{
    
    
	czh::string s1 = "CHENZHIHAO";
	czh::string s2(s1);
	czh::string s3("hello");
	s3 = s1;
	//PrintString(s1);
	for (auto& e : s1)
	{
    
    
		e += 32;
		cout << e << " ";
	}
	return 0;
}

猜你喜欢

转载自blog.csdn.net/CZHLNN/article/details/115010103