C++--6.string类

为什么学习 string 类?
标准库中的 string
string 类的模拟实现

为什么学习string类?

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

标准库中的string

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_string<char, char_traits, allocator>
string;
4. 不能操作多字节或者变长字符的序列

string类的常用接口说明

string 类对象的常见构造

 下来我们对其进行实现展示

#include<iostream>
#include<string>//含string的包
#include<Windows.h>
using namespace std;
//4个默认的成员函数
void test_string1()
{
	string s1;//构造空的string类对象,即空字符串,s1
	string s2("hello");//用C-string来构造string类对象s2
	string s3("hello", 2);//拷贝前两个字符赋给s3
	string s4(s2);//拷贝构造函数
	string s5(s2, 1, 8);//提取s2从1到8位置的字符,字符小于8则输出剩下全不自负
	string s6(s2, 1, string::npos);//提取从1到字符末尾
	string s7(10,'a');//构造10个a
	
	cout << "s1: " << s1 << endl;
	cout << "s2: " << s2 << endl;
	cout << "s3: " << s3 << endl;
	cout << "s4: " << s4 << endl;
	cout << "s5: " << s5 << endl;
	cout << "s6: " << s6 << endl;
	s1 = s7;
	cout << "s7: " << s7 << endl;
	cout << "s1: " << s1 << endl;
}
int main()
{
	test_string1();
	system("pause");
	return 0;
}

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

string类对象的方式问方式大体可以分为3种operator[]+下标,我们使用最多的访问方式,迭代器访问方式,范围for()访问方式,下面我们来一一介绍

operator[]+下标

void test_string2()//[]+下标的遍历方式,推荐用法
{
	string s1("hello");
	s1 += ' ';
	s1 += "world";
	cout << s1 << endl;
	//写
	for (size_t i = 0; i < s1.size(); i++)//对字符+1后移1个字符
	{
		s1[i] += 1;
	}
	//读
	for (size_t i = 0; i < s1.size(); i++)
	{
		cout << s1[i] << " ";
	}
	cout << endl;
}

这种方式基本就是我们在数组操作字符串时进行遍历读写的操作,这个操作输出了一个hello world,而后我们对于字符遍历进行了+1操作,再次遍历输出,得到的结果是i f m m p  x p s m e对所有的字符进行了+1,在进行遍历输出

 []+下标的方式也是我们最为熟悉的

迭代器

字符迭代器是一种相对模板化的一种字符在操作方式格式为 string::iterator it = s1.begin();类似与这样,当然,如果我们想调用其他的迭代器比如vector,只需要将string换为vector,而后后面再<类型>即可

	//迭代器
	//写
	string::iterator it = s1.begin();
	//auto it = s1.begin();也可以套用这种方式,之前学习的auto自动判断类型
	while (it != s1.end())
	{
		*it -= 1;
		++it;
	}
	//读
	it = s1.begin();
	while (it != s1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

因为我们刚在上面改变了hello world的字符,所以我们在这里用迭代器先将其改回来--对*it-=1,对每个字符-1,而后++it遍历,完成整个字符的-1,而后遍历字符串,进行输出

我们可以看到迭代器中的操作,很像指针,暂时我们可以将其当做指针

不过这只是最普通最常用的一种迭代器,还有三种迭代器

void test_string3()
{
	string s1("hello world");
	//倒着遍历
	string::reverse_iterator rit = s1.rbegin();
	while (rit != s1.rend())
	{
		cout << *rit << " ";
		++rit;
	}
	string num("12345");
	cout << string2int(num) << endl;
}

这种是reverse_iterator倒序迭代器,不过我们操作还是++进行操作的,内部自动转化为倒序

 这段代码利用倒序迭代器完成了将hello world,与12345倒序输出的功能

还有const修饰的迭代器,无法对原来的数据进行改变

int string2int(const string& str)
{
	int val = 0;
	//只能读,不能写
	string::const_iterator it = str.begin();
	while (it != str.end())
	{
		val *= 10;
		val += (*it - '0');
		++it;
	}
	//val = 0;
	/*string::const_reverse_iterator rit = str.rbegin();
	while (rit != str.rend())
	{
	val *= 10;
	val += (*rit - '0');
	++rit;
	}*/
	return val;
int main()
{
	string num("12345");
	//test_string2();
	//test_string3();
	cout << string2int(num) << endl;;
	system("pause");
	return 0;
}

这段代码完成了字符串对于int型的转化,正着转化和倒着转化

从顺序看迭代器分为两种,正序和倒序,从属性来看,迭代器分为const和普通,自由组合一共4种

范围for

范围for
C++11->原理其实是迭代器
for (auto ch : s1)
{
	cout << ch << " ";
}
cout << endl;
}

范围for其实就是用我们从前的知识,对s1进行依次遍历,不过这种方式底层原理也是迭代器,这便是三种遍历string的方式

string类对象的容量操作

 这些是基本的一些字符串操作函数,下面我们对capacity()进行剖析

 我们可以看到,当我们不断地去加入字符时,容量的大小变化,从0开始,差不多是每次变化1.5倍左右进行扩大得到,我们还可以推出,到了后面增容会越来越慢,随着基数的增大每次扩大的容量也会增加,而且增容是有代价的,不能频繁去增加

 下面我们再来了解一个关键字,reserve,这个关键字的含义是直接开辟定量的空间

 此时我们会发现,就没有扩容了,直接将容量增到了100,其实我们在开辟时,显示的是开辟了100个空间,实际上是101个,因为还会腾出一个空间去给\0

在了解reserve后,我们有个与之相对应的resize()

 这种是仅有一个int参数的情况,起到的作用就是开辟了100个字符串这种是reserve重载的方式,开辟了100个字符串并赋值

 那么我们再来看下这种情况

 当我们reserve了5个大小的空间,但是字符串不够放进去,所以就只有前五个放进去了,而后我们进行开辟20个,放入x,也不会再将world放入,全为x

 这是我们的字符串插入操作,两种都可以完成操作,不过推荐第二种,方便可读

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 不会改变容量大小。

 这是我们的插入删除

 还有一个小小的接口,c_str(),获取字符数组首地址,用C形式遍历,其实最主要的差距就是中间有无\0,对于C++的遍历方式而言,遇到\0并不会停止遍历,而对于C语言,碰到\0就停止了

 还有一个substr(pos)函数,访问从pos位到结束的所有子串,以及我们的find('s')函数,找到第一个与s相同的字符

1. string 尾部追加字符时, s.push_back(c) / s.append(1, c) / s += 'c' 三种的实现方式差不多,一般情况下string 类的 += 操作用的比较多, += 操作不仅可以连接单个字符,还可以连接字符串。
2. string 操作时,如果能够大概预估到放多少字符,可以先通过 reserve 把空间预留好。
void Teststring()
{
	string str;
	str.push_back(' '); // 在str后插入空格
	str.append("hello"); // 在str后追加一个字符"hello"
	str += 'b'; // 在str后追加一个字符'b' 
	str += "it"; // 在str后追加一个字符串"it"
	cout << str << endl;
	cout << str.c_str() << endl; // 以C语言的方式打印字符串

	// 获取file的后缀
	string file1("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中的域名
	sring 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);
	// 删除url的协议前缀
	pos = url.find("://");
	url.erase(0, pos + 3);
	cout << url << endl;
}

这是string一些接口的常用用法

还有一个需要我们注意的点是cin与getline,cin遇到空格或者换行都会停下来,而我们的getline只遇到换行会停下,空格不会,这在需要输入一些带空格的字符串时很有用

 String的模拟实现

在这里我们对String类进行模拟实现,加深我们对其的理解

构造函数

我们之前在其它类中定义构造函数的时候是怎么定义的呢?我们在这里试一下

 我们会发现,一般的写法对于读取字符串是可行的,但当我们想利用[ ]去改变它时,就会出现报错,原因是我们的s1存在栈中,去调用这个构造函数时会指向存在代码区的“hello”,而我们的operator[ ]返回的是字符串本身第i个位置的字符,因为存在代码段的字符串无法被修改,所以在我们想去修改字符串时会报错

所以我们就不能把字符串存储在常量区中,在这里我们选择堆,因为堆可以自行开辟大小

 而我们若想建立一个空对象,也不能直接取用nullptr,因为strlen时出现了空指针,找不到\0,所以我们在建立空对象时需要在堆上开辟一个大小的空间,附上\0

下面我们展示全缺省合并写法

 析构函数

析构函数就正常的资源清理与指针指控即可

 拷贝构造

我们先来试试如果我们不写拷贝构造会发生什么呢?

 我们先对c_str进行了重载,随后拷贝构造了s2,发现也可以实现拷贝构造,但是程序给崩溃了,这是什么原因呢,其实当我们没有重载拷贝构造时,系统自带的拷贝构造就会完成浅拷贝,也就是在栈中将s1中存的字符串地址按字节将值拷贝到同在栈中的s2,此时s1,s2指向堆中的同一个字符串,这里没有什么问题,但是最后在析构的时候,会对s1,s2分别进行析构,又因为他们指向同一地址空间,所以同一块空间被析构了两次,引起报错

接下来看看正确做法

 正确的做法就是在堆中在开辟一块一模一样大的空间,再将值拷进去,进行深拷贝

赋值

我们用一般的方法进行赋值

 发现同样的,会引起崩溃,这与上面的拷贝构造是一样的,系统自动浅拷贝,会在最后析构的时候出现问题,所以也需要我们对其进行重载

 具体做法就是在堆中开辟一块与s3相同的tmp空间,将s3的拷贝给这个tmp,再将原来的s1空间释放掉,再将s1指向tmp,就完成了赋值

下面我们展示代码

namespace wxy
{
	class string
	{
	public:
		/*string()
			:_str(nullptr)//而我们在调用无参时也不能这样调,会有空指针的问题,无法找到\0结束
			{}*/
		/*	string(char* str)
				:_str(str)//不能这么写,因为string对象中存的是指针,指针指向的数组中存储字符,字符无法修改
				{}*/

		//string()//可行方案

		//	:_str(new char[1])
		//{
		//	_str[0] = '0';
		//}

		//string(char* str)//将空间开辟在堆上,str+1是为了给\0空间
		//	:_str(new char[strlen(str + 1)])//开辟一个str+1大小的新空间,而后用strcpy函数将在常量区的str拷到堆区的_str中
		//{
		//	strcpy(_str, str);
		//}

		//将他们两个合并
		string(char* str = " ")
			:_str(new char[strlen(str) + 1])
		{
			strcpy(_str, str);
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		//string s2(s1)
		string(const string& s)
			:_str(new char[strlen(s._str)+1])
		{
			strcpy(_str, s._str);
		}
		size_t size()
		{
			return strlen(_str);
		}
		char& operator[](size_t i)
		{
			return _str[i];
		}
		const char* c_str()
		{
			return _str;
		}
		//s1=s3
		//s1=s1
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				char* tmp = new char[strlen(s._str) + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;

				return *this;
			}
		}
	private:
		char* _str;
	};
	void test_string2()
	{
		string s1("hello");
		string s2(s1);
		/*cout << s1.c_str() << endl;
		cout << s2.c_str() << endl;*/
		string s3("world");
		s1 = s3;
		cout << s1.c_str() << endl;
		cout << s3.c_str() << endl;
	}
}
	void test_string1()
	{
		string s1("hello");
		string s2;

		for (size_t i = 0; i < s1.size(); ++i)
		{
			s1[i] += 1;//
			cout << s1[i] << " ";
		}
		cout << endl;

		for (size_t i = 0; i < s2.size(); ++i)
		{
			s1[i] += 1;//
			cout << s2[i] << " ";
		}
		cout << endl;
	}

我们的深拷贝其实可以有一种现代写法

//深拷贝-现代写法
string(const string& s)
    :_str(nullptr)
{
	string tmp(s._str);
	swap(_str, tmp._str);
}
//s1=s2
string& operator=(const string& s)
{
	if (this != &s)
	{
		string  tmp(s);
		swap(_str, tmp._str);
	}
	return *this;
}
//最简单版本
string& operator=(const string& s)
{
	swap(_str, s._str);
	return *this;
}

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

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

接下来我们想实现一个更加复杂的string类

namespace wxy
{
	class string
	{
	public:
		typedef char* iterator;//迭代器
		iterator begin()//迭代器起点
		{
			return _str;
		}
		iterator end()//迭代器终点
		{
			return _str + _size;
		}
		string(const char* str = " ")//构造函数,堆上开辟空间将字符串拷进去
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		~string()//析构函数
		{
			delete[] _str;
			_str = nullptr;
			_size = _capacity = 0;
		}
		size_t size()const//计算有效字符个数
		{
			return _size;
		}
		size_t capacity()const//计算字符串容量,\0不算
		{
			return _capacity;
		}
	char& operator[](size_t i)//[]重载
		{
			assert(i < _size);
			return _str[i];
		}
	const char& operator[](size_t i)const
	{
		assert(i < _size);
		return _str[i];
	}
	const char* c_str()//c语言形式输出字符
	{
		return _str;
	}
	void reserve(size_t n)//开空间函数
	{
		if (n > _capacity)//当要开的空间大于当前总容量时
		{
			char* newstr = new char[n + 1];//堆上开辟n+1
			strcpy(newstr, _str);//原字符串拷进堆
			delete[] _str;//析构原字符串
			_str = newstr;//堆上开的新空间赋回字符串
			_capacity = n;//容量改为n
		}
	}
	void resize(size_t n, char ch = '\0')//改有效字符个数
	{
		if (n < _size)//当要改的有效字符个数小于当前有效字符个数
		{
			_str[n] = '\0';//将第n号下标元素改为\0,系统会自动识别到\0,剩下的就不识别了
			_size = n;//有效字符个数改为n
		}
		else
		{
			if (n > _capacity)//当有效字符个数大于整个容量
			{
				reserve(n);//开空间到n
			}
			for (size_t i = _size; i < n; ++i)
			{
				_str[i] = ch;//将剩下空出来的位置赋成字符ch,缺省时为\0
			}
			_size = n;//有效字符个数改为n
			_str[_size] = "\0";//最后一个位置上改为\0,好让系统识别结束
		}
	}
	void push_back(char ch)//尾插字符
	{
		//空间满,进行增容
		if (_size == _capacity)
		{
			size_t newcapacity = _capacity == 0 ? 2 : _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;//有效字符个数变为len
	}
	//s1+='a'
	string& operator+=(char ch)//+=重载
	{
		this->push_back(ch);
		return *this;
	}
	//s1+="aaaa"
	string& operator+=(const char* str)
	{
		this->append(str);
		return *this;
	}
	string& intsert(size_t pos, char ch)//插入字符
	{ 
		assert(pos < _size);//当插入位置小于有效字符个数断言
		if (_size == _capacity)//容量不够,扩容
		{
			size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
			reserve(newcapacity);
		}
		int end = _size;//size开始移动
		while (end >= pos)//从后往前循环全部后移1
		{
			_str[end + 1] = _str[end];
			--end;
		}
		_str[pos] = ch;//腾出来的位置插入ch
		++_size;//有效字符个数+1
	}
	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 >= pos)
		{
			_str[end + len] = _str[end];//全部后移len位
			--end;
		}
		/*for (size_t i = 0; i < len; ++i)
		{
			_str[pos] = str[i++];
		}*/
		strncpy(_str + pos, str, len);//拷贝len位
		_size += len;
	}
	void erase(size_t pos, size_t len = npos)//删除pos开始之后长为len的字符串
	{
		assert(pos < _size);
		if (len >= _size - pos)//字符串长度大于等于有效字符长度-删除的位置
		{
			_str[pos] = '\0';//删除的位置变为\0
			_size = pos;//有效长度变为前面没删的部分
		}
		else//当有效字符长度大于字符串长度+pos时
		{
			size_t i = pos + len;//删除位置+字符串长度=删后字符串长度i,i后数据要挪到前面
			while (i <= _size)
			{
				_str[i-len] = _str[i];//将i后数据挪到前len位置
				++i;
			}
			_size -= len;//最终长度
		}
	}
	size_t find(char ch, size_t pos = 0)//从pos开始寻找ch
	{
		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)
	{
		char* p = strstr(_str, str);//在_str中寻找str
		if (p == nullptr)//未找到
		{
			return npos;
		}
		else
		{
			return p-_str;//返回
		}
	}

	bool operator<(const string& s)
	{
		int ret = strcmp(_str, s._str);//s与_str比较,_str大返回>0,相等=0,小<0
		return ret < 0;
	}

	bool operator==(const string& s)
	{
		int ret = strcmp(_str, s._str);
		return ret == 0;
	}
	bool operator<=(const string& s)
	{
		return *this<s || *this == s;
	}
	bool operator>(const string& s)
	{
		return !(*this <= s);
	}
	bool operator>=(const string& s)
	{
		return !(*this < s);
	}
	bool operator!=(const string& s)
	{
		return !(*this == s);
	}
	private:
		char* _str;
		size_t _size;//已有多少个有效字符
		size_t _capacity;//可以存储多少个有效字符 \0不是有效字符
		static size_t npos;
	};
	size_t string::npos = -1;
	//getlin遇到空格不结束
	istream& operator>>(istream& in, string& s)
	{
		while (1)
		{
			char ch;
			//in >> ch;
			ch = in.get();
			if (ch == ' ' || ch == '\n')
			{
				break;
			}
			else{
				s += ch;
			}
		}
	}
	ostream& operator<<(ostream& out,const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i)
		{
			cout << s[i];
		}
		return out;
	}
	void test_string1()
	{
		string s1;
		string s2("hello");
		string::iterator it2 = s2.begin();
		while (it2 != s2.end())
		{
			cout << *it2 << " ";
			++it2;
		}
		cout << endl;
		//范围for,本质迭代器
		for (auto e : s2)
		{
			cout << e << " ";
		}
		cout << endl;
	}
}

 对于string的学习还需要在不断地练习中精进

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值