【C++】STL--String

这一节主要总结string类的常见接口,以及完成了string类的模拟实现。

目录

标准库的String类

string类常见接口

string类对象的常见构造

string析构函数:~string

string类对象的容量操作

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

string类对象的修改操作

string类非成员函数

string类的模拟实现

经典的string类问题

浅拷贝

深拷贝

传统版写法的String类

现代版写法的String类

string类的模拟实现


标准库的String类

为了符合C++面向对象的特性,引入了string类,string是表示字符串的字符串类。下面说明一下string类的特点:

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

2.string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数

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

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

string类常见接口

string类对象的常见构造

(重点)1.string()

默认构造,构造空的string类对象,即空字符串

(重点)2.string(const string& str)

拷贝构造,使用str拷贝构造string类对象

(重点)3.string(const char* s)

使用C-string来构造string类对象

4.string(const string& str, size_t pos, size_t len=npos)

从str的第pos个位置,取len个字符构造,如果len超过字符串长度,那么只取到字符串末尾,如果len未给出,那么采用默认参数npos(0xFFFFFFFF)

5.string(const char* s, size_t n)

使用C-string的前n个字符来构造string类对象

6.string(size_t n, char c)

string类对象包含n个字符c

void test_string1()
{
	string s0;
	string s1("hello world");
	string s2(s1);
	string s3(s1, 5, 3);
	string s4(s1, 5, 10);
	string s5(s1, 5);
	string s6(10, '$');

	cout << s0 << endl;
	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
	cout << s5 << endl;
	cout << s6 << endl;
}

string析构函数:~string

在string类对象生命周期结束时,会被自动调用。

string类对象的容量操作

1.size()和length()

这两个均可以返回字符串有效字符长度,功能一样。可以通过size遍历字符串。有两个功能一样的函数是由于历史原因造成的!

2.max_size()

返回string最大的大小

3.capacity()

返回string类对象的容量大小。

//查看扩容机制
void test_string4()
{
	string s;
	size_t sz = s.capacity();
	cout << "capacity change:" << sz << endl;
	cout << "making s growing" << endl;
	for (int i = 0; i < 100; i++)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed" << sz << endl;
		}

	}
}

虽然是15、31...,但是没有包括'\0',所以实际大小应该是16、32、48...,第一次是2倍扩容,以后每次是1.5倍扩容(VS平台下)。但是在Linux平台下,扩容机制不太一样:

这就说明,STL是一个标准,具体怎么实现由编译器决定!

4.clear()

清除对象的数据,但是不会清空间。

5.shrink_to_fit()

在clear()后,为了释放一些已开辟的空间,可以缩容

6.reserve()

如果提前知道要开辟空间的大小,可以用reserve()为字符串提前预留空间,防止频繁扩容,因为扩容一般是异地扩容,效率低下。

避免了频繁扩容!

还有一个问题,reserve会不会缩容呢?不会的!当reserve()比capacity大,才会扩容!

  1. resize()

调整字符串大小。

如果采用第一种,那么默认用'\0'插入,第二种用指定的char插入。

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

1.operator[],返回pos位置的字符

string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
	cout << s1[i] << " ";
}

2.迭代器,begin+end,可以**通过迭代器的方式遍历字符串,**begin获取第一个字符的迭代器,end获取最后一个字符下一个位置的迭代器

string::iterator it1 = s1.begin();
	while (it1 != s1.end())
	{
		cout << *it1 << " ";
		++it1;
	}

我们感觉到迭代器很像指针,但是并不是指针!

3.rbegin+rend,支持倒着遍历,循环中用++rit,而不是--rit,对于反向迭代器而言,++就是倒着走

string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
	cout << *rit << " ";
	++rit;
}

除了正向和反向的正常iterator外,还有正向和反向的const_iterator ,是用来为const string构造迭代器,其特点是只能读不能写

//只读
const string s3("hello world");
string::const_iterator it3 = s3.begin();
while (it3 != s3.end())
{
	//*it3 += 3;//报错,因为只读
	cout << *it3 << " ";
}
cout << endl;
//string::const_reverse_iterator

4.用范围for遍历字符串

for (auto s : s1)
	{
		cout << s << " ";
	}

但是范围for并没有什么特别之处,其底层是迭代器!

string类对象的修改操作

1.pushback()

在字符串末尾尾插字符

2.append()

在字符串后追加字符串

3.operator+=

在字符串后追加字符串str

4.assign()

用新内容覆盖原字符串

5.insert()

在某一个位置插入字符串

注意:insert()尽量不要使用,因为在使用过程中会挪动数据,效率低。

6.erase()

删除字符

同样,erase()函数能不用就不用。

7.replace()

字符的替换

8.find()

查找某个字符的位置

小练习:将一个字符串中的空格替换成"%20"。

总结一下,insert、erase、replace能少用就少用,因为基本都要挪动数据,效率不高。

9.c_str

返回C格式字符串

在C++程序中,我们有可能会调用C语言的函数,比如fopen函数,但是在程序中文件名可能是string对象,需要转换成C字符串才行。

void test_string()
{
	string filename("test.cpp");
	FILE* fout = fopen(filename.c_str(), "r");
}

10.find

找到某一个字符或字符串第一次出现的位置

例如:获取后缀

void test_string()
{
	string s1("test.cpp");
	size_t pos = s1.find('.');
	if (pos != string::npos)
	{
		string suffix = s1.substr(pos);
		cout << suffix << endl;
	}
	else
	{
		cout << "没有后缀" << endl;
	}
}

11.rfind

从后往前找某一个字符或字符串第一次出现的位置

例如:获取后缀"file.c.tar.zip"的zip

void test_string()
{
	string s1("test.c.tar.zip");
	size_t pos = s1.rfind('.');
	if (pos != string::npos)
	{
		string suffix = s1.substr(pos);
		cout << suffix << endl;
	}
	else
	{
		cout << "没有后缀" << endl;
	}
}

string类非成员函数

1.operator+

尽量少用,因为传值返回,导致深拷贝效率低

2.operator>>&&operator<<

输入和输出运算符重载

3.getline

获取一行字符串。弥补了cin>>的缺点,cin遇到了空格或者换行就提取结束。

4.== != > >= < <=

大小比较

string类的模拟实现

经典的string类问题

首先来看一个经典关于浅拷贝的问题:

class String
{
public:
/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范,使用strlen时会涉及解引用
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);
}

说明 :在上面的String类中没有显式定义拷贝构造和赋值运算符重载,因此,当使用s1去构造时s2时,编译器会调用默认构造函数。这样导致的问题是,**s1和s2指向同一块内存空间,在释放空间时,同一块空间会被释放两次而引起程序崩溃,**这种拷贝方式,称为浅拷贝。

浅拷贝

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

可以用深拷贝解决浅拷贝 问题,也就是:每个对象都有一份独立的资源,不要和其他对象共享。

深拷贝

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

传统版写法的String类

namespace ghs
{
	class string
	{
	public:
		string(const char* str="")
			:_size(strlen(str))
		{
			_str = new char[_size + 1];
			strcpy(_str, str);
			_capacity = _size;
		}
		string(const string& str)
		{
		   _str = new char[str._capacity + 1];
		   strcpy(_str, str._str);
		   _size = str._size;
		   _capacity = str._capacity;
		}
		//s1 = s2
		string& operator=(const string& str)
		{
			char* tmp = new char[str._capacity + 1];
			strcpy(tmp, str._str);
			delete[] _str;
			_str = tmp;
			_size = str._size;
			_capacity = str._capacity;

			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0; 
		size_t _capacity = 0;
	public:
		static const int npos;
	};
	const int string::npos = -1;
}

在传统版的写法中,拷贝构造和赋值构造都是通过自己手动开空间,然后使用strcpy函数拷贝完成的,下面的现代写法在实现上更为简洁:

现代版写法的String类

namespace ghs
{
	class string
	{
	public:
		string(const char* str="")
			:_size(strlen(str))
		{
			_str = new char[_size + 1];
			strcpy(_str, str);
			_capacity = _size;
		}
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
		}
		//s1 = s2
		//string& operator=(const string& str)
		//{
		//	string ss(str);
		//	swap(ss);
			
		//	return *this;
		//}
		//上面的再优化,这种更推荐
		string& operator=(string ss)//传值传参,调用拷贝构造
		{
			swap(ss);

			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0; 
		size_t _capacity = 0;
	public:
		static const int npos;
	};
	const int string::npos = -1;
}

在现代写法中,拷贝构造是通过调用string(const char* str="")构造函数来生成临时的string对象(tmp),再将*this和tmp交换,完成拷贝。在赋值构造中,使用传值传参,其实调用拷贝构造,生成临时对象ss,再将ss和*this交换,完成拷贝。

总结:传统版的现代版的效率一样,只是现代版调用了之前已经实现好的函数,所以看起来更加简洁!

string类的模拟实现

namespace ghs
{
	class string
	{
	public:
		/*string()
			:_str(nullptr)
			,_size(0)
			,_capacity(0)
		{}*/
		/*string()
			:_str(new char[1])
			,_size(0)
			,_capacity(0)
		{
			_str[0] = '\0';
		}*/
		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))
		{
			_str = new char[_size + 1];
			strcpy(_str, str);
			_capacity = _size;
		}
		//string(const string& str)
		//{
		//	_str = new char[str._capacity + 1];
		//	strcpy(_str, str._str);
		//	_size = str._size;
		//	_capacity = str._capacity;
		//}
		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
		}
		//s1 = s2
		//string& operator=(const string& str)
		//{
		//	char* tmp = new char[str._capacity + 1];
		//	strcpy(tmp, str._str);
		//	delete[] _str;
		//	_str = tmp;
		//	_size = str._size;
		//	_capacity = str._capacity;

		//	return *this;
		//}
		string& operator=(const string& str)
		{
			string ss(str);
			swap(ss);
			
			return *this;
		}
		//上面的再优化,这种更推荐
		//string& operator=(string ss)//传值传参,调用拷贝构造
		//{
		//	swap(ss);

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

		size_t size()const
		{
			return _size;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t sz)
		{
			if (sz > _capacity)
			{
				char* p = new char[sz + 1];
				strcpy(p, _str);
				delete[] _str;
				_str = p;

				_capacity = sz;
			}
		}
		void push_back(char ch)
		{
			//扩容2倍
			/*if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			_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);
			///*for (size_t i = 0; i < len; i++)
			//{
			//	push_back(str[i]);
			//}*/
			//_size += len;
			insert(_size, str);
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}
		const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}
		size_t capacity()const
		{
			return _capacity;
		}
		void insert(size_t pos, char ch)
		{
			assert(pos <= _size);
			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			for (size_t end = _size+1; end > pos; end--)
			{
				_str[end] = _str[end - 1];
			}
			_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(len + _size);
			}
			for (size_t end = _size+len; end > pos+len-1; end--)
			{
				_str[end] = _str[end-len];
			}
			/*for (size_t i = 0; i < len; i++)
			{
				_str[pos + i] = str[i];
			}*/
			strncpy(_str + pos, str, len);
			_size += len;
		}
		void erase(size_t pos, size_t len = npos)
		{
			assert(pos < _size);
			if (len == npos ||  len>=_size-pos)
			{
				_size = pos;
				_str[_size] = '\0';
			}
			else
			{
				strcpy(_str + pos, _str + pos + len);
				_size -= len;
			}
		}
		void resize(size_t n, char ch = '\0')
		{
			if (n <= _size)
			{
				_str[n] = '\0';
				_size = n;
			}
			else
			{
				reserve(n);
				for (size_t i = _size; i < n; i++)
				{
					_str[i] = ch;
				}
				_str[n] = '\0';
				_size = n;
			}
		}
		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::swap(_capacity, s._capacity);
		}
		size_t find(char ch, size_t pos = 0)const
		{
			assert(pos < _size);
			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
		{
			assert(pos < _size);
			char* tmp = strstr(_str+pos, str);
			if (tmp != nullptr)
			{
				return tmp - _str;
			}
			else
			{
				return npos;
			}

		}

		string substr(size_t pos = 0, size_t len = npos)
		{
			string s;
			if (len == npos || len >= _size-pos)
			{
				for (size_t i = pos; i < _size; i++)
				{
					s += _str[i];
				}
			}
			else
			{
				for (size_t i = pos; i < pos + len; i++)
				{
					s += _str[i];
				}
			}
			return s;
		}
		
		void clear()
		{
			_size = 0;
			_str[_size] = '\0';
		}
		/*bool operator==(const string& str)
		{
			int ret = strcmp(_str, str._str);
			return ret == 0;
		}*/
		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0; 
		size_t _capacity = 0;
	public:
		static const int npos;
	};
	const int string::npos = -1;

	void swap(string& x, string& y)
	{
		x.swap(y);
	}
	bool operator==(const string& s1,const string& s2)
	{
		int ret = strcmp(s1.c_str(), s2.c_str());
		return ret == 0;
	}
	bool operator<(const string& s1, const string& s2)
	{
		int ret = strcmp(s1.c_str(), s2.c_str());
		return ret < 0;
	}
	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;
	}
	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}
	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);
	}

	ostream& operator<<(ostream& out, const string& str)
	{
		for (auto ch : str)
		{
			out << ch; 
		}
		return out;
	}
	istream& operator>>(istream& in, string& str)
	{
		char ch;
		str.clear();
		//in >> ch;
		ch = in.get();
		char buff[100];
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == 99)
			{
				buff[99] = '\0';
				str += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			str += buff;
		}
		return in; 
	}
	istream& getline(istream& in, string& str)
	{
		char ch;
		str.clear();
		char buff[100];
		//in >> ch;
		/*ch = in.get();
		while (ch != '\n')
		{
			str += ch;
			ch = in.get();
		}*/
		ch = in.get();
		size_t i = 0;
		while (ch != '\n')
		{
			buff[i++] = ch;
			if (i == 99)
			{
				buff[99] = '\0';
				str += buff;
				i = 0;
			}
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			str += buff;
		}
		return in;
	}
}
相关推荐
一只爱打拳的程序猿6 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
杨荧7 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
minDuck9 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
白子寰14 分钟前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_0118 分钟前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj24 分钟前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT26 分钟前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。28 分钟前
c++多线程
java·开发语言
-Even-28 分钟前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
小政爱学习!30 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript