C++ string类介绍以及模拟实现

string介绍

何为string?

string是一个类,准确的说string是一个类模板的实例化在这里插入图片描述
类模板是basic_string,所以string是用char作为模板参数实例化的一个类对象在这里插入图片描述
除了实例string,还有其他三个实例

wstring:针对宽字符的类(国标)
u16string:针对字符大小为2字节的类(utf-16)
u32string:针对字符大小为4字节的类(utf-32)

总之根据编码的标准不同,实例化的类也随之不同,string类的字符大小是1字节,是针对ASCII码的一个类。

string的构造函数

在这里插入图片描述
上面是string的所用构造函数,下面列出几个常用的

void stringTest1()
{
	string str1;// 构造一个空的string对象

	string str2("hello world"); // 用const char *s构造一个string对象
	cout << str2 << endl;

	string str3(str2); // 用str2拷贝构造str3
	cout << str3 << endl;

	string str4 = str3; // 本质上不是赋值,而是拷贝构造
	cout << str4 << endl;     

	// 不太常用
	string str5("hello world", 3); // 用字符串的第3个到结束的字符构造
	cout << str5 << endl;

	string str6(10, 'x'); //  用十个x字符构造
	cout << str6 << endl;

	string str7(str2, 3); // 用str2的第3个到结束的字符构造
	cout << str7 << endl;

	string str8(str2, 3, 7); // 用str2的第3个到第7个的字符构造
	cout << str8 << endl;
}

这里解释一下npos是什么
在这里插入图片描述
在这里插入图片描述
npos是一个无符号整型,但用-1初始化无符号整形得到的值是整形中的最大值(-1的补码是全1,用无符号的方式看待全1的二进制序列,这时-1就是整形中最大的数)。它意味着直到字符串结束,所以使用第3个构造函数,但不传第3个参数,默认会构造从pos位置到字符串结束的string对象。

string的赋值

库中对=进行了重载,总共有三种形式
在这里插入图片描述

void stringTest2()
{
	string str1 = "hello world";
	string str2; // 构造一个空的string对象
	str2 = str1; // 将str1赋值给str2

	cout << str1 << endl;
	cout << str2 << endl;

	str2 = "hello c++"; // 用const char*类型的字符串赋值给str2
	cout << str2 << endl;

	str2 = '!';         // 用字符赋值给str2
	cout << str2 << endl;
}

string的遍历

void stringTest3()
{
	// 遍历string的方式
	string str = "hello";
	// 第一种方式用下标遍历
	for (size_t i = 0; i < str.size(); i++)
	{
		cout << str[i];
	}
	cout << endl;

	// 第二种方式用迭代器
	string::iterator it = str.begin();
	while (it != str.end())
	{
		cout << *it;
		it++;
	}
	cout << endl;

	// 第三种方式范围for
	for (auto& e : str)
	{
		cout << e;
	}
	cout << endl;
}

解释一下string的迭代器的两个接口,begin()返回的是string的第一个字符的位置的地址,end()返回的是最后一个字符位置的下一个位置的地址
在这里插入图片描述
所以当迭代器走到end()指向的地址时不能继续访问,否则会出现非法访问,这也是循环的结束条件

可以通过下标也能通过at接口遍历string

void stringTest4()
{
	string str1 = "hello";
	string str2 = "hello";

	str1[7];
	str2.at(7);
}

通过下标访问如果越界,程序会有越界提示
在这里插入图片描述
用at接口程序报的错就让人拿不准了
在这里插入图片描述
虽然用下标和at接口都能访问string的数据但使用下标访问能在出错时更快找到错误,所以推荐使用下标访问。

在使用反向迭代器时,需要输入较长的类型名,string::reverse_iterator,而我们可以使用auto来让编译器根据函数返回类型自动推导类型名,这样也提高了编程效率。

void stringTest5()
{
	string str = "hello";
	//string::reverse_iterator it = str.rbegin();
	auto it = str.rbegin(); // 与上面的写法等价
	while (it != str.rend())
	{
		cout << *it;
		it++;
	}
}

string的插入与删除

在这里插入图片描述
插入函数有很多重载版本

void stringTest6()
{
	string str = "hello";
	str.insert(0, 3, 'x'); // 向下标为0处插入3个x
	cout << str << endl;

	str.insert(0, "   "); // 向下标为0处插入3个空格
	cout << str << endl;

	str.insert(str.begin() + 3, 3, 'y'); // 向下标为3处插入三个y
	cout << str << endl;

	str.insert(3, 3, 'z'); // 向下标为3处插入三个z
	cout << str << endl;
}

在这里插入图片描述

在这里插入图片描述

void stringTest7()
{
	string str = "hello";
	str.erase(); // 不传参默认删除所有数据
	cout << str << endl;

	str = "hello";
	str.erase(str.begin() + 2); // 删除下标为2处的字符
	cout << str << endl;

	str = "hello world";
	cout << str << endl;
	str.erase(2, 5);   // 从下标为2向后删除5个字符
	cout << str << endl;

	str = "hello world";
	cout << str << endl;
	str.erase(str.begin() + 2, str.begin() + 5); // 删除从下标为2下标为5的字串
	cout << str << endl;
}

在这里插入图片描述

string的交换

void stringTest8()
{
	string str1 = "hello";
	string str2 = "world";

	cout << str1 << endl;
	cout << str2 << endl;

	str1.swap(str2);
	//swap(str1, str2); // 两种交换效率不同

	cout << str1 << endl;
	cout << str2 << endl;
}

使用string的提供的交换接口与标准库中自带的交换函数两者有区别吗?使用string提供的交换只是交换两个指向字符串的指针与两个空间大小,而标准库中的交换则是创建一个中间变量,需要调用拷贝构造,但拷贝是深拷贝需要开辟空间,这样一比较显然string提供的交换效率更高。

string的查找

在这里插入图片描述
函数参数基本就是“要查找的字符/字符串”和“要开始查找的位置”,而开始查找的位置默认为0,就是从头开始查找。下面的代码是查找函数的使用

void stringTest9()
{
	string file = "test.cpp.txt";

	size_t pos = file.rfind('.');

	if (pos != string::npos)
	{
		cout << file << "后缀:" << file.substr(pos) << endl;
	}

	string url = "https://cplusplus.com/reference/string/string/?kw=string";
	size_t pos1 = url.find("://");
	// 输出协议
	if (pos1 != string::npos)
		cout << url.substr(0, pos1 + 3) << endl;
	else
		cout << "非法url" << endl;

	size_t pos2 = url.find('/', pos1 + 3);
	// 输出域名
	if (pos2 != string::npos)
		cout << url.substr(pos1 + 3, pos2 - (pos1 + 3)) << endl;
	else
		cout << "非法url" << endl;

	// 输出资源
	cout << url.substr(pos2 + 1) << endl;
}

关于string的容量

在这里插入图片描述
pushback向string中尾插一个字符。string像一个动态顺序表,有capacity和size保存顺序表的容量和当前的大小,由于string本身存储了一个\0,所以即使是空string也会向内存申请空间以保存\0。

在这里插入图片描述
在vs下string的初始容量是15,写一段代码验证capacity的增长

void stringTest10()
{
	string str;
	unsigned int size = str.capacity(); //先记录初始的容量
	cout << "capacity:" << str.capacity() << endl;

	for (size_t i = 0; i < 1000; i++)
	{
		str.push_back('c');
		if (size != str.capacity()) // 当扩容时打印扩容后的容量
		{
			size = str.capacity();
			cout << "capacity:" << str.capacity() << endl;
		}
	}
}

在这里插入图片描述
(在vs下)可以看到除了第一次的扩容其他扩容基本都是1.5倍扩,string有一个接口能改变string的容量,当直到string要存储字符串的长度时可以先改变它的容量,以节省扩容开辟空间的时间。
在这里插入图片描述
reserve有保留的意思,与reverse要注意区别在这里插入图片描述
将string的初始容量改为1000后,与刚刚的程序相比,减少了多次扩容
在这里插入图片描述
类似的函数还有一个resize,重置string的size,如果只传要重置的大小,这些空间默认会初始化为\0在这里插入图片描述
如果再传一个字符,函数会用该字符初始化空间

string的模拟实现

string类的声明

namespace myString
{
	class string
	{
	public:
		// 构造和析构
		string(const char* str = "");			 // string的构造,空串也开辟空间,只存储\0
		~string();								 // string的析构
		string(const string& str);               // string的拷贝构造
		string& operator=(const string& str);	 // string的赋值
		void swap(string& str);

		// 修改
		string& operator+=(const string& str);	 // string的追加
		string& operator+=(char c);	             // string的追加
		string& append(const char* str);		 // string的追加
		string& append(char c);				     // string的追加
		void push_back(char c);				     // string的尾插
		string& insert(size_t pos, char c);
		string& insert(size_t pos, const char* str);
		string& erase(size_t pos, size_t n);     // 从pos位置删除n个字符
		const char* c_str() const { return _str; } // 返回c类型的字符串

		// 容量
		void resize(size_t n, char c = '\0');	 // 修改string的大小
		void reserve(size_t n);					 // 修改string的容量
		size_t size() const { return _size; }					
		size_t capacity() const { return _capacity; }			
		
		// 迭代器
		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; }

		// 下标访问
		char& operator[](size_t pos);             // 通过下标访问string
		const char& operator[](size_t pos) const; // 通过下标访问string

	
	private:
		char* _str;
		size_t _size;
		size_t _capacity;

		const static size_t npos;
	};
	const size_t string::npos = -1; // 静态变量在类中声明但没有定义,需要在类外定义
}

首先说明capacity表示的是string最大能存储的有效字符个数(不包括’\0’),size表示当前存储的有效字符个数,当然也不包括’\0’,str就是指向存储字符串的指针。

构造和析构

在这里插入图片描述

首先实现的是string的构造和析构,构造函数呢,实现成无参的,这样不仅可以构造空串还能传字符串进行构造。先strlen求传入字符串的长度,将长度赋值给_size和_capacity,如果是空串长度就是0,然后为_str分配空间,大小是长度+1,这个1用来存储’\0’,最后再拷贝传入字符串到_str中。

myString::string::string(const char* str)// 说明写了默认参数,定义不用也不能写
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];
	strcpy(_str, str);
}

析构就是将_str释放,_size和_capacity重置为0。

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

拷贝构造有两种写法一种是利用构造函数,一种是不利用构造函数,但实现的代码与构造函数重复度高,因此复用构造函数实现拷贝构造更方便。

myString::string::string(const string& str) // 传统写法
{
	_size = str._size;
	_capacity = str._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, str._str);
}
myString::string::string(const string& str) // 现代写法
{
	string tmp(str._str); // 先用str的字符串构造tmp对象
	// 此时的tmp就是str的复制,只要把tmp与this交换
	swap(tmp);
}

当然还要实现swap函数,利用std库中的交换将两个指针交换,还有_capacity和_size也要交换

void myString::string::swap(string& str)
{
	std::swap(_str, str._str);
	std::swap(_capacity, str._capacity);
	std::swap(_size, str._size);
}

=的重载同样也是两种写法,传统的繁琐,现代的简洁。不过对于任何=的重载都要注意连续复制的情况,因此函数需要返回赋值完成的对象。

对于传统赋值,先判断容量是否足够存储字符串,若不够需要释放之前的空间再开辟一块足够的空间存储。

myString::string& myString::string::operator=(const string& str)
{
	if (&str != this) // 防止自己赋值给自己
	{
		if (str._size > _capacity)
			_str = new char[str._size]; // 空间不够的扩容
	
		_size = str._size;
		_capacity = str._capacity;
		strcpy(_str, str._str);
	}
	return *this;
}

// 现代写法
myString::string& myString::string::operator=(string str)
{
	swap(str); // 形参不是引用,所有调用了拷贝构造,构造了str,所以str是一个复制
	return *this;
}

显而易见,这样的复用构造函数让代码更简单也更简洁了

关于修改

在这里插入图片描述

修改无非就是增加与删除,实现了在任意位置的插入与删除,其他的接口也就能复用这两个接口。

说白了,插入就是检查容量,移动数据,插入数据,三个步骤

myString::string& myString::string::insert(size_t pos, char c)
{
	if (_size == _capacity)
		reserve(_capacity == 0 ? 4 : _capacity * 2); 
	size_t end = _size + 1; // _str[_size]是'\0',一起移动
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = c;
	_size++;
	return *this;	
}

(这里需要补充size_t的一个注意点,如果上面end先指向_size - 1的位置,然后循环写为end >= pos,移动的代码写为 _str[end + 1] = _str[end],这就很有问题,当pos为0,end为0时再走一遍循环,而end-1为-1,对吗?

myString::string& myString::string::insert(size_t pos, char c)
{
	assert(pos <= _size);
	if (_size == _capacity)
		reserve(_capacity + 1); 
	size_t end = _size - 1; // 错误示范
	while (end >= pos)
	{
		_str[end + 1] = _str[end];
		end--;
	}
	_str[pos] = c;
	_size++;
	return *this;	
}

end是无符号数size_t,所以-1存储到end中是一个很大的数,这样使得循环继续,但越界访问。因此不能这样写,总结:使用无符号数比较要特别注意“负数”问题)

插入的另一个重载:插入一串字符到string中,和插入一个字符类似,只是移动字符的距离变长了

myString::string& myString::string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	int len = strlen(str);
	if (len + _size >= _capacity) // 检查扩容
	{
		reserve(len + _size);
	}
	
	size_t end = _size + len;
	while (end - len + 1 > pos)
	{
		_str[end] = _str[end - len];
		end--;
	}
	memcpy(_str + pos, str, len);
	_size += len;
	return *this;
}

删除字符:函数第一个参数是要删除字符的位置,后一个参数是要删除字符的个数。如果位置加上字符个数大于字符串长度,就是把该位置后面的字符全删除,直接在该位置上放个’\0’。但如果不是全删除就需要移动后面的字符覆盖前面的字符。

myString::string& myString::string::erase(size_t pos, size_t n)
{
	assert(pos < _size);
	if (n + pos >= _size)
	{
		_str[pos] = '\0';
		_size = pos; // n的值可以是npos,不能减去npos因为npos可能是-1,最大的数
		return *this;
	}
	else
	{
		size_t end = pos + n; // 用end移动数据
		while (end <= _size) // 把'\0'也移过去
		{
			_str[end - n] = _str[end];
			end++;
		}
		size -= n;
		return *this;
	}
}

在这里插入图片描述
npos是一个最大的数,无符号的-1,作为string的静态成员。

剩下的接口就是复用insert和erase函数了

myString::string& myString::string::operator+=(const string& str)
{
	return insert(_size, str._str);
}

myString::string& myString::string::operator+=(char c)
{
	return insert(_size, c);
}

myString::string&  myString::string::append(const char* str)
{
	return insert(_size, str);
}


myString::string&  myString::string::append(char c)
{
	return insert(_size, c);
}

void myString::string::push_back(char c)
{
	insert(_size, c);
}

(范围for底层是迭代器,不实现迭代器就不能用范围for,并且迭代器的命名必须按约定走。假设我把模拟实现的迭代器屏蔽,程序报错)
在这里插入图片描述

关于容量

在这里插入图片描述
reserve为string扩容,先检查n是否大于当前容量,如果大于则扩容,小于不缩容。先开辟新的空间,将原来空间的数据拷贝到新空间,再释放原来的空间。

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

resize就是reserve加初始化了,没有给初始化的值就用’\0’初始化。

void myString::string::reserve(size_t n, char c)
{
	if (n > _capacity)
		reserve(n);
	
	while (_size < n)
		_str[_size++] = c;
	
	_size = n; // 如果n小于_size,上面的while不会进去,但长度要减小	
	_str[n] = '\0';  // 最后再结束的地方放'\0'
	}
}

下标访问

在这里插入图片描述
重载[],通过下标访问字符串,但要注意如果string被const修饰则不能写入字符串,所以需要重载两个版本来支持const对象

char& myString::string::operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

const char& myString::string::operator[](size_t pos) const
{
	assert(pos < _size);
	return _str[pos];
}

cin和cout的重载

cout的重载不能直接输出字符串,而是应该一个字符一个字符的输出,考虑到极端情况,string中存储了’\0’,直接输出的话字符串也就不完整

ostream& operator<<(ostream& out, const myString::string& str)
{
	for (int i = 0; i < _size; i++)
	{
		out << _str[i];
	}
	return out; //  为了支持连续输出
}

cin的重载也是一个一个字符的读,直到读入的字符为空格或者换行,但是直接使用>>遇到空格和换行也是停止的,所以>>永远无法读入空格和换行,要用istream对象的get函数,每次读一个字符,但不会停止

istream& operator>>(istream& in, myString::string& str)
{
	char ch;
	ch = in.get();
	while (ch != ' ' && ch != '\n')
	{
		push_back(ch);
		ch = in.get();
	}
	return in;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值