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

Posted 呆呆兽学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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. reverse reverse(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)
	以上是关于C++初阶第九篇——string类(string类中一些常见接口的用法与介绍+string类的模拟实现)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构初阶第九篇——八大经典排序算法总结(图解+动图演示+代码实现+八大排序比较)

C++初阶第八篇——模板初阶(泛型编程+函数模板+类模板)

C++初阶第六篇——类和对象(下)(初始化列表+explicit关键字+static成员+友元+内部类)

C++从入门到入土第九篇:string相关OJ练习

C++初阶:string类string类 | 浅拷贝和深拷贝(传统写法和现代写法) | string类的模拟实现

C++初阶第四篇——类和对象(上)(类的定义+封装+this指针)