【C++初阶】第九篇——string类(string类中一些常见接口的用法与介绍+string类的模拟实现)

⭐️从今天开始,我就要给大家介绍STL的内容了,今天我先为大家介绍一下第一号人物——string类,我会先介绍它的一些歌常见接口以及用法,然后再模拟实现它。
⭐️博客代码已上传至gitee:https://gitee.com/byte-binxin/cpp-class-code


🌏了解string类

string类 就是在C语言的字符串做了一些处理,C语言中有一些处理字符串的库函数,但是字符串和库函数是分离的,而string类提供了一些对字符串处理的接口,把这些接口和字符串封装成一个类,更加地方便我们使用和操作。

总结地说:

  1. string是表示字符串的字符串类
  2. string类的接口和一些常规容器的接口基本相同,而且添加了一些专门用了操作string的常规操作
  3. string的底层:typedef basic_string<char, char_traits, allocator> string;
  4. 不能操作多字节或者变长字符的序列
  5. 使用时包含sting的头文件,而且要展开std

🌏string类常见的接口

🌲string的几个构造函数

在这里插入图片描述

  1. 无参默认构造函数: string() 构造类的空对象,即空字符串
  2. 有参构造函数: string(size_t n, char c) 用n个字符c来构造string类对象
  3. 有参构造函数: string(const char* s) 用C字符串来构造string类对象
  4. 拷贝构造函数 string(const string& s)

实例演示:

void TestString1()
{
	// 构造函数
	string s1;// 重点
	string s2("hello world");// 用C string来构造string类对象 重点
	string s3(s2);// 拷贝构造函数
	string s4(10, 'a'); // 用n个字符'a'构造一个类对象 重点

	cout << s1 << endl;
	cout << s2 << endl;
	cout << s3 << endl;
	cout << s4 << endl;
}

代码运行结果如下:
在这里插入图片描述

🌲string类的三种遍历方式

  1. for+operator[]访问
  2. 迭代器遍历
  3. 范围for(会被编译器替换成迭代器来遍历)

实例演示:

void TestString2()
{
	string s("hello world");
	// 3种遍历方式
	// 1.for+operator[]
	for (size_t i = 0; i < s.size(); ++i)
	{
		cout << s[i];
	}
	cout << endl;

	// 2.迭代器 iterator  不一定是指针,但是像
	string::iterator it = s.begin(); // auto it = s.begin();
	while (it < s.end())
	{
		cout << *it;
		++it;
	}
	cout << endl;

	// 3.范围for  原理被迭代器替换
	for (auto e : s)
	{
		cout << e;
	}
	cout << endl;
}

代码运行结果如下:
在这里插入图片描述

其中迭代器是属于string类中的一个成员,它可能是指针,但不一定是指针,string类中它其实就是一个指针,但是其他容器就不一定了,后面会介绍。
在这里插入图片描述

🌲string类的四种迭代器

按方向分: 有正向迭代器和反向迭代器(iterator和reverse_iterator)分别配合being()、end()和rbegin()、rend()使用
按属性分: 有普通迭代器和const迭代器 (iterator const_iterator | reverse_iterator const_reverse_iterator)

实例演示:

void TestString3()
{
	string s("hello world");
	string::reverse_iterator rit = s.rbegin();
	while (rit < s.rend())
	{
		cout << *rit;
		++rit;
	}
	cout << endl;	
}

代码运行结果如下:
在这里插入图片描述
注意: 对于const的类对象,要是有const的迭代器,否则会报错。

🌲string类的大小操作

在这里插入图片描述

  1. size和length 返回字符串的有效长度
  2. capacity 返回有效空间的大小
  3. clear 清空有效字符,不改变底层空间的大小
  4. empty 返回字符串是否为空的状态
  5. reserve reserve(size_t n = 0); 为字符串预留空间
  6. resize resize(size_t n, char c = ‘\0’); 改变字符串有效长度,多出的空间用字符c补充

实例演示:

void TestString4()
{
	string s("hello world");

	cout << s.size() << endl;   // 为了与其他容器的接口保持一致
	cout << s.length() << endl;
	cout << s.capacity() << endl;
	cout << "-----------------------" << endl;

	s.clear();// 不改变底层空间的大小
	cout << s.size() << endl;
	cout << s.capacity() << endl;// 不变
	cout << "-----------------------" << endl;

	// 将有效字符个数增加为10个,多余的用'x'补充
	s.resize(10, 'x');
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << "-----------------------" << endl
	// 将有效字符个数增加为15个,多余的用'\0'补充
	s.resize(15);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << "-----------------------" << endl;

	// 将有效字符个数缩小为5个 
	s.resize(5);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
	cout << s << endl;

}

代码运行结果如下:
在这里插入图片描述
总结: 对于resize,当字符增多时,多出的空间用使用者给的字符补充,没给就使用缺省参数’\0’赋值,元素增多可能会扩大底层空间大小,元素减小不会堆底层空间大小有影响。

🌲string类的增删查改操作

在这里插入图片描述

  1. push_back 在字符串后尾插字符‘c’
  2. append 在字符串后尾插一段字符串
  3. operator+= 在字符串后追加字符串str
  4. insert 在pos位置插入一个字符或者一段字符串
  5. erase 从pos位置开始,删去n个字符
    实例演示:
void TestString8()
{
	string s;
	s.push_back('h');
	s.push_back('e');
	s.push_back('l');
	s.push_back('l');
	s += 'o';

	cout << s << endl;

	s.append(" wo");
	s += "rld";
	cout << s << endl;

}

代码运行结果如下:
在这里插入图片描述

  1. c_str 返回C格式的字符串

  2. find 没找到就返回npos的结果(string::npos = -1)
    在这里插入图片描述

  3. rfind 反向从pos的位置开始寻找
    在这里插入图片描述

  4. substr string substr (size_t pos = 0, size_t len = npos) const 从pos位置开始,截取n个字符

实例演示:

// 分割网址  http(s)+网站名+资源
void SplitUrl(string& url)
{
	// https://leetcode-cn.com/problemset/all/
	size_t pos1 = url.find(':');
	cout << url.substr(0, pos1) << " ";

	size_t pos2 = url.find('/', pos1 + 3);
	cout << url.substr(pos1 + 3, pos2 - (pos1 + 3)) << " ";

	cout << url.substr(pos2, url.size() - pos2) << endl;
}

void TestString7()
{
	string s("hello ");
	s += "world";// '\0'以空格显示
	cout << s << endl;// 使用运算符重载 operator<<
	cout << s.c_str() << endl;// 以C语言的方式打印

	// basic_string substr(size_type pos = 0,size_type n = npos) const  从pos位置开始打印n个字符
	string file1("string.cpp");
	size_t pos = file1.find('.');
	if (pos != string::npos)
	{
		cout << file1.substr(pos) << endl;
	}

	string file2("file.tar.zip");
	pos = file2.rfind('.');
	if (pos != string::npos)
	{
		cout << file2.substr(pos) << endl;

	}

	string url1("http://www.cplusplus.com/search.do?q=npos");
	string url2("https://leetcode-cn.com/problemset/all/");

	SplitUrl(url1);
	SplitUrl(url2);

}

代码运行结果如下:
在这里插入图片描述

🌲string类的非成员函数

在这里插入图片描述

  1. operator+ 传值返回加长后的string,尽量少用,因为传值返回会导致深拷贝的效率降低
  2. operator>> 重载输入运算符
  3. operator<< 重载输出运算符
  4. getline 获取一行字符串
  5. realational operators 大小比较

实例演示:

void TestString9()
{
	string s;
	cin >> s;
	cout << s << endl;
	s = s + " world";
	cout << s << endl;
}

代码运行结果如下:
在这里插入图片描述

🌏string类的模拟实现

🍯string类的成员

string类是字符串类,实现使用了一个C中的字符串,为了可以扩容改变空间大小,我们选择在堆上开辟空间,然后用一个字符指针指向这块空间,和我们之前数据结构中的顺序表实现有类似之处。

private:
	char*  _str;
	size_t _size;// 有效字符个数
	size_t _capacity;// 能存储的有效字符的个数 '\0'不是有效字符,是标识结束的字符
public:
	static size_t npos;
size_t string::npos = -1;

🍯string类的构造函数和析构函数

  1. 有参构造函数的实现,缺省值给空字符串,因为要存放’\0’,所以要多开一个空间的大小,’\0’是无效字符,不算有效大小
string(const char* str = "")
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];// 因为要存放'\0','\0'是无效字符,不算有效大小
	strcpy(_str, str);
}
  1. 析构函数的实现,清理空间资源
~string()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

🍯深浅拷贝

前面我们就提到过,浅拷贝就是简单的字节序拷贝,为了更好地理解深拷贝和浅拷贝,我来分开为大家讲解:

下面是我们用编译器的浅拷贝实现类对象的拷贝:

string s1("hello world");
string s2(s1);

代码运行结果如下:
在这里插入图片描述

编译器直接报错了,为什么会出现这种问题呢?其实我们前面有提到过
在这里插入图片描述
所以,为了避免释放两次空间,我们要进行深拷贝,深拷贝就是给新的对象出现开一块空间,这样就不是简单的字节序拷贝了

代码实现如下:

string(const string& s)
{
	// 深拷贝,另外开一块空间
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, s._str);
}

代码运行结果:
在这里插入图片描述
这次代码就很好地跑起来了。
operator=的实现 与拷贝构造的实现很相似,要进行深拷贝,否则会出问题。

string& operator=(const string& s)
{
	if (this != &s)// 防止自己给自己赋值
	{
		delete[] _str;
		_size = s._size;
		_capacity = s._capacity;
		_str = new char[_capacity + 1];
		strcpy(_str, s._str);	
	}
	return *this;
}

🍯string类的访问和迭代器的实现

  1. operator[]的实现 为了让这块空间的值可以被修改,所以我们选择传引用返回
char& operator[](size_t i)
{
	assert(i < _size);
	return _str[i];
}
  1. 迭代器 string类的迭代器的本质其实是char*的别名,就是一个指针的用法,注意其中begin和end不能写其他的单词,不然编译器识别不出迭代器就会导致范围for使用报错
typedef char* iterator;
iterator begin()
{
	return _str;
}
iterator end()
{
	return _str + _size;
}
  1. c_str 直接返回底层的字符指针即可
const char* c_str() const
{
	return _str;
}

🍯string类的增删操作的实现

reserve 预留出一片空间,我们要开一段新空间,然后把旧空间的数据拷贝到新空间上去,然后释放旧空间,空间都要多开一个,来存放’\0’

void reverse(size_t n)
{
	if (n > _capacity)
	{
		char* newStr = new char[n + 1];
		strcpy(newStr, _str);
		delete[] _str;
		_str = newStr;

		_capacity = n;
	}
}

push_back 我们以2倍的方式增容,复用reverse的代码

void push_back(char ch)
{
	// 扩容
	if (_size == _capcaity)
	{
		size_t newcapacity = _capcaity == 0 ? 2 : _capcaity * 2;
		reverse(newcapacity);
	}

	_str[_size] = ch;
	_size++;
	_str[_size] = '\0';//注意
	
}

append

void append(const char* str)
{
	size_t len = strlen(str);
	// 扩容
	if (len + _size > _capacity)
	{
		size_t newcapacity = len + _size;
		reverse(newcapacity);
	}
	strcpy(_str + _size, str);
	_size += len;

}

operator+= 这个函数有两个重载,一个是尾插字符串,一个是尾插字符,我们这里可以分别复用append和push_back的代码

string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}
string& operator+=(const char* s)
{
	append(s);
	return *this;
}

insert有两个重载,一个是在pos位置插入一个字符,一个是在pos位置插入一段字符串,同时我们还有判断pos的合理性,为了能够拷贝重叠区间的字符串,我们可以使用memmove库函数来操作,而不是memcpy。

string& insert(size_t pos, char ch)
{
	assert(pos <= _size);
	// 扩容
	if (_size == _capacity)
	{
		size_t newcapacity = _capacity == 0 ? 2 : _capacity * 2;
		reverse(newcapacity);
	}
	memmove(_str + pos + 1, _str + pos, sizeof(char) * (_size - pos));
	_str[pos] = ch;
	_size++;
	_str[_size] = '\0';// 注意

	return *this;
}
string& insert(size_t pos, const char* s)
{
	assert(pos <= _size);
	size_t len = strlen(s);
	// 扩容
	if (len + _size > _capacity)
	{
		size_t newcapcity = len + _size;
		reverse(newcapcity);
	}
	memmove(_str + pos + len, _str + pos, sizeof(char) * (_size - pos));
	// strcpy(_str + pos, s);  最后会把'\0'拷过去
	strncpy(_str + pos, s, len);
	_size += len;
	_str[_size] = '\0';// 注意

	return *this;
}

我们还可以思考这样一个问题,我们是不是可以复用insert的两个重载函数实现push_back和append,如下:

void push_back(char ch)
{
	insert(_size, ch);
}
void append(const char* str)
{
	insert(_size, str);
}

erase 在pos位置删去n个字符

string& erase(size_t pos, size_t len = npos)
{
	assert(pos < _size);
	if (len >= _size - pos)// 不能写成len+pos>=_size  因为当len是npos时,加上pos之后就变小了
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		memmove(_str + pos, _str + pos + len, sizeof(char) * (_size - (pos + len)));
		_size -= len;
		_str[_size] = '\0';
	}
	return *this;
}

🍯string类的大小操作的模拟实现

size和capacity

size_t size() const
{
	return _size;
}

size_t capacity() const
{
	return _capacity;
}

resize 要考虑改变后的大小是否大于当前空间,考虑是否需要扩容,记得最后加一个’\0’,表示字符串结尾

void resize(size_t n, char ch = '\0')
{
	if (n > _capacity)
	{
		size_t newcapacity = n;
		reverse(newcapacity);
	}
	// _size>=n时不进入循环
	for (size_t i = _size; i < n; ++i)
	{
		_str[i] = ch;
	}

	_size = n;
	_str[_size] = '\0';// 注意
}

🍯string类中find函数的实现

find函数也有两个重载,一个是从pos位置开始寻找一个字符,找到就范围字符位置,没找到就返回npos,还有一个就是从pos位置开始找一段字符串,我们可以使用strstr字符串查找函数,也可以使用KMP算法查找

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

下面是KMP算法的写法

int* GetNext(const char* str)
{
	int* next = new int[strlen(str)];
	next[0] = -1;
	next[1] = 0;
	int k = next[1];  // k = next[i-1] str2[i-1]== str2[k1] ==> next[i] = next[i-1] + 1 = k + 1
	int i = 2;
	while (i < (int)strlen(str))
	{
		if (k == -1 || str[i - 1] == str[k])
		{
			next[i] = k + 1;
			k = next[i];
			++i;
		}
		else
		{
			k = next[k];
		}
		
	}
	return next;
}
int KMP(const char* str1, const char* str2, size_t pos = 0)
{
	int len1 = strlen(str1);
	int len2 = strlen(str2);

	int* next = GetNext(str2);
	int i = 0;
	int j = (int)pos;
	while (i < len1 && j < len2)
	{
		if (j == -1 || str1[i] == str2[j])
		{
			++i;
			++j;
		}
		else// i不回退,j回退到特定的位置
		{
			j = next[j];
		}
	}
	if (j == len2)
	{
		delete[] next;
		return i - j;// 返回匹配的位置的下标
	}
	
	return -1;
}
size_t find(const char* s, size_t pos = 0) const
{
	if (pos >= _size)
		return npos;
	size_t index = KMP(_str, s, pos);
	if (index != npos)
	{
		return index;
	}
	else
	{
		return -1;
	}
}

🍯operator>>和operator<<的实现

这里不需要用到友元函数,因为没有访问私有成员

ostream& operator<<(ostream& out, const string& s)
{
	for (size_t i = 0; i < s.size(); i++)
	{
		out << s[i];
	}
	return out;
}
istream& operator>>(istream& in, string& s)
{
	s.clear();// 把之前的空间清理一下
	while (1)
	{
		char ch;
		ch = in.get();
		if (ch == ' ' || ch == '\n')
		{
			break;
		}
		else
		{
			s += ch;
		}
	}
	return in;
}

🍯非成员函数

这里比较简单,就不过多介绍

bool operator<(const string& s) const
{
	return strcmp(_str, s._str) < 0;
}

bool operator<=(const string& s) const
{
	return strcmp(_str, s._str) <= 0;
}

bool operator>(const string& s) const
{
	return strcmp(_str, s._str) > 0;
}

bool operator>=(const string& s) const
{
	return strcmp(_str, s._str) >= 0;
}

bool operator==(const string& s) const
{
	return strcmp(_str, s._str) == 0;
}

bool operator!=(const string& s) const
{
	return strcmp(_str, s._str) != 0;
}

🍯string类中拷贝构造和赋值重载的现代写法

我们首先实现一下swap函数

void swap(string& s)
{
	::swap(_str, s._str);// ::表示调用全局域的函数
	::swap(_size, s._size);
	::swap(_capacity, s._capacity);
}

我们先看一下现代写法

string(const string& s)
	:_str(nullptr)
		,_capacity(0)
		,_size(0)
{
	string tmp(s._str);
	swap(tmp);
}
string& operator=(string s)
{
	if (this != &s)
	{
		swap(s);
	}
	return *this;
}

拷贝构造的现代写法就是利用构造函数构造一个临时对象,然后利用swap把这个临时对象直接换给新的对象,是不是感觉这种方法很酷,很霸道,其实现代写法是简洁的,看起来也很舒服。所以推荐使用现代写法。

🌐总结

以上就是我也介绍的string类的全部内容,喜欢的话,欢迎点赞支持~

  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆兽学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值