◆博主名称:少司府

欢迎来到少司府的博客☆*: .。. o(≧▽≦)o .。.:*☆

数据结构系列个人专栏:

初阶数据结构_少司府的博客-CSDN博客

C++基础个人专栏:

C++初阶_少司府的博客-CSDN博客

琢玉成器终有时,笔底生花夺锦归

目录

一、标准库中的string类

        1.1 string类

        1.2 string类的实例化

        1.3 string类的遍历

        1.3.1 下标+[]

        1.3.2 迭代器 iterator

        1.3.3 范围for与auto

auto的使用

范围for

        1.4 const 迭代器与反向迭代器

二、capacity 相关接口

        2.1 lenth() 与 size()

        2.2 max_size()

        2.3 capacity()

        2.4 capacity 容量变化

三、reserve 与 clear

        3.1 reserve()

        3.2 clear()

四、modifiers 相关接口

        4.1 尾插

        4.1.1 push_back()

        4.1.2 append()

        4.1.3 +=

        4.2 insert()

        4.3 erase()

        4.4 replace()

        4.5 find() 和 rfind()

        4.6 replace() 扩容的测试与优化

        4.7 pop_back()

五、operations 相关接口

        5.1 c_str()

        5.2 substr()

六、非成员函数

        6.1 +、=

        6.2 getline()

七、string 类的模拟实现

        7.1 分文件开发

        7.1.1 基本框架

        7.1.2 构造函数

        7.1.3 重载运算符 =

        7.1.4 析构函数

        7.1.5 c_str()

        7.1.6 重载运算符 []

        7.2 插入与删除

        7.2.1 reserve() 模拟

        7.2.2 push_back() 模拟

        7.2.3 append() 模拟

        7.2.4 重载运算符 +=

        7.2.5 insert() 模拟

        7.2.6 erase() 模拟

        7.3 寻找与切割

        7.3.1 begin() 和 end()

        7.3.2 clear() 模拟

        7.3.3 find() 模拟

        7.3.4 substr() 模拟

        7.4 运算符重载

        7.4.1 <、>、=等

        7.4.2 <<、>>


一、标准库中的string类

        1.1 string类

        string类的文档介绍

        点击此链接可以转跳到官方文档。

在使用string类时,必须包含头文件<string>,以及命名空间std

        1.2 string类的实例化

        

如图,s1不传参数调默认构造,没有赋值;s2传入字符串 hello world ,s3调用拷贝构造s2;s4拷贝构造s2,从下标为6的字符开始,向后拷贝6个字符,不包括\0;s5代表从第一个字符开始向后拷贝5个字符。

        1.3 string类的遍历
        1.3.1 下标+[]

        和数组一样,string类可以利用下标进行遍历。

string s1;
string s2("hello world");
cout << s1 << s2 << endl; //下标+[]
for (int i = 0; i < s2.size(); i++)
{
	cout << s2[i] << ' ';
}
cout << endl;

如图,其中size() 是string类的一个接口,返回该对象中有效字符的个数。

        1.3.2 迭代器 iterator

1)、任何容器都有自己的迭代器

2)、迭代器使用时需要声明类域

3)、string类中,接口begin() 返回最初位置的迭代器(下标为0的位置);end() 返回最后一个有效字符的下一个位置( \0 的位置)。

4)、可以把迭代器类比为指针,需要 " 解引用 " 才能显示原来的元素

string::iterator it = s2.begin(); 
while (it != s2.end())			  
{
	cout << *it << ' ';
	++it;
}
cout << endl;

链表 list 也有自己的迭代器,如:

list<int> lt = { 1,2,3,4,5 };
list<int>::iterator lit = lt.begin(); 
while (lit != lt.end())
{
	cout << *lit << ' ';
	++lit;
}
cout << endl;

此处需要包含头文件<list>。

        1.3.3 范围for与auto
auto的使用

1)、auto是一个关键字,auto声明的变量必须由编译器在编译阶段推导得到

2)、auto声明指针类型时,用auto和auto*没有区别;auto声明引用时,必须用auto&

3)、在同一行声明多个变量时,必须是相同类型,因为编译器实际是对第一个类型进行推导,然后用推导出来的类型定义其他变量。

4)、auto不能作为函数的参数,但可以作为返回值,谨慎使用

5)、auto不能直接用于声明数组。

int a=10;
auto b=a; // 推导a的类型给b
cout << typeid(b).name() << endl; // 打印b的类型

auto aa=1,bb=2;
auto cc=3,dd=4.0; // error,同一行内,auto只能推导同一类型
范围for

1)、范围for,实际上是用auto来简化代码

2)、auto自动推导类型,自动迭代,自动判断结束,C++11支持底层就是迭代器

for (auto ch : s2) 
{
	cout << ch << ' '; //ch就是s2的拷贝,修改ch不改变s2
}
cout << endl;
for (auto& ch : s2) //ch是s2的别名
{
	ch += 2;
}
cout << s2 << endl;

如图,上述代码运行结果如下:

        1.4 const 迭代器与反向迭代器

反向迭代器是 reverse_iterator ,需要调用的接口改为 rbegin() 和 rend() 。

string::reverse_iterator rit = s2.rbegin();
while (rit != s2.rend())
{
	cout << *rit << ' ';
	++rit; // ++是倒着走的
}
cout << endl;

const迭代器:

1)、对于用const 修饰的string类对象,需要用到const迭代器

2)、const迭代器和反向迭代器可以合在一起,const反向迭代器

const string s3("hello world");
string::const_iterator cit = s3.cbegin(); // const迭代器,不能修改指向的内容
while (cit != s3.end())                   
{
	cout << *cit << ' ';
	++cit;
}
cout << endl;
string::const_reverse_iterator rcit = s3.rbegin();
while (rcit != s3.rend())
{
	cout << *rcit << ' ';
	++rcit;
}
cout << endl;

如图,分别是const迭代器和const反向迭代器。

其中,C++11规定cbegin返回对象是const类型迭代器,用begin也可以。

下面是运行结果:

二、capacity 相关接口

        2.1 lenth() 与 size()
string s2("hello world");
cout << s2.length() << endl; 
cout << s2.size() << endl;

都是返回有效字符的个数,两者没区别,但我们用size,length属于时代产物。

        2.2 max_size()
cout << s2.max_size() << endl; 

该接口返回 string 能开的最大的空间

        2.3 capacity()
cout << s2.capacity() << endl;

返回 s2 的容量空间但是不包括\0,所以实际容量比打印出来的多1。

        2.4 capacity 容量变化

我们先给一段代码来测试vs下string对象的容量的变化:

void TestPushBack()
{
	string s;
	size_t sz = s.capacity();
	cout << "capacity changed:" << sz << '\n';

	cout << "making s grow:\n";
	for (int i = 0; i < 100; ++i)
	{
		s.push_back('c');
		if (sz != s.capacity())
		{
			sz = s.capacity();
			cout << "capacity changed:" << sz << '\n';
		}
	}
}

如图,其中 push_back() 是尾插接口,当容量不够的时候,该接口会自动扩容。我们尾插一百次c字符,打印每次变化后的容量和最开始的容量。

结果如图:

打印出来的容量最开始是15,没有包含\0,实际上是16。

实际上,在vs上大概是这样实现string的:

class string
{
private:
	char _buff[16]; 
	char* _str;
	int _capacity;
	int _size;
};

vs上实现string,不超过16字节存在数组里面,超过就指针指向并扩容

三、reserve 与 clear

        3.1 reserve()
string s;
s.reserve(100);

1)、reserve 是保留的意思,语法上是提前开好100个空间实际比100大,且这100中不包含\0

2)、reserve 不改变size只修改capacity,如果 n < size ,那么vs下,s.reserve(n) 只能缩减到s.reserve(s.size())。对是否缩容(修改capacity)没有要求,取决于编译器的实现

3)、vs下不会缩容,例如 n < capacity 的话就不会改变

string s;
size_t sz = s.capacity();
s.reserve(2);
cout << "capacity changed:" << sz << '\n';
s.push_back('c');
s.push_back('c');
s.push_back('c');
cout << s.size() << endl;

如图,一开始没有给值的话,在vs下,capacity 就是底层给定的数组空间,实际上是16,capacity 返回15,因为\0 不算在其中。

        3.2 clear()

接口clear是清空数据的作用。

s.clear();
cout << endl << s.size() << ' ' << s.capacity() << endl;
if (s.empty()) 
	cout << "空的\n";

clear清除所有数据但不会删除容量

empty() 是一个判空的接口。

运行结果如图:

四、modifiers 相关接口

        4.1 尾插
        4.1.1 push_back()
string s("hello world");
s.push_back(' '); 
s.push_back('x');

前面讲过,push_back() 用于尾插,直接传入要尾插的字符。

        4.1.2 append()
s.append("......");

注意:尾插,push_back只能传单个字符,append可以传字符串

        4.1.3 +=
s += ' '; 
s += "11111111111";
cout << s << endl;

运算符 += 也是尾插,可以尾插单个字符,也可以是字符串。

        4.2 insert()

instert() 可以用于任意位置的插入。

s.insert(s.end(), '*');
s.insert(s.begin(), 'x');

如图,第一个参数是迭代器或下标位置,第二个是字符或字符串。

运行结果如图:

        4.3 erase()
s.erase(0, 1); 
s.erase(s.end() - 1); 
s.erase(6); // 把下标6的位置之后字符全删除
cout << s << endl; 

erase 删除,它有多个重构:

1)、第一个参数是删除的下标位置(删除的字符包括第一个参数下标位置),第二个是删除的个数

2)、参数是迭代器位置,删一个

3)、参数是下标,把下标的位置之后字符全删除

        4.4 replace()
s.replace(5, 1, "////"); // 把下标5位置开始的1个字符替换成////
cout << s << endl;

replace 是字符串替换,具体用法如图。

上述代码运行结果如图:

        4.5 find() 和 rfind()

这两个接口是字符查找函数,可以查找单个字符,也可以查找字符串。

size_t pos1 = s.find('/'); // find寻找字符,找到就返回第一次出现的位置,没找到就返回npso
size_t pos2 = s.find('/', 2); // 从第二字符开始找
size_t pos3 = s.rfind("//");
cout << pos1 << endl;
cout << pos2 << endl;
cout << pos3 << endl;

如图,代码运行结果如图:

        4.6 replace() 扩容的测试与优化
while (pos2 != string::npos)
{
	s.replace(pos2, 1, "%%"); // 扩容,挪动数据,有效率消耗
	pos2 = s.find('/', pos2 + 2);
}
cout << s << endl;

如图,从pos2位置开始的1个字符替换成字符串"%%",这实际上有挪动数据的时间消耗

可以做如下优化:

string tmp; //优化,以空间换时间
for (auto ch:s)
{
	if (ch == '%')
		tmp += "//";
	else
		tmp += ch;
}
cout << tmp << endl;

如图,定义一个tmp对象,找到要替换的字符就利用+=,实际上是尾插。

总的来说,replace、insert、erase 都是性能杀手

        4.7 pop_back()

这是 C++11 提供的尾删接口。

s.pop_back(); 
cout << s << endl;

代码运行结果:

五、operations 相关接口

        5.1 c_str()

c_str 返回的是string对象中底层的字符串指针,在某些只能适配c相关的接口的情况下适合使用。

string s("hello world");
cout << s.c_str() << endl; // c_str 返回s底层字符串的指针
        5.2 substr()

substr 是字符串分割函数,有多个重载。

string s1("test.cpp");
size_t pos = s1.rfind('.'); // rfind倒着找
string suffix = s1.substr(pos); // 从pos位置开始(包括pos)返回子串
string suffix1 = s1.substr(pos, 2);
cout << suffix << endl;
cout << suffix1 << endl;

如图,传入只传入一个参数pos,是从pos位置开始截取。传入下标位置、个数是从pos位置开始,截取多少个字符

代码运行结果:

六、非成员函数

        6.1 +、=

+、= 等重载运算符不是string类的成员函数。

string s1("hello");
string s2 = s1 + " world";
string s3 = "world " + s1;

cout << s2 << endl;
cout << s3 << endl;
        6.2 getline()

与cin、scanf 不同的是,getline的读取默认是遇到换行才停止,遇到空格不停止

getline(cin, s3); // getline 默认遇到空格不停止,遇到换行才停止
cout << s3 << endl;

当然,也可以指定停止的字符:

getline(cin, s3, '*'); // 遇到*才停止
cout << s3 << endl;

运行结果如图:

七、string 类的模拟实现

        7.1 分文件开发

和之前一样,在模拟实现的string类的时候,分成string.h 文件,string.cpp 文件和 test.cpp 文件。

        7.1.1 基本框架

如图,由于是自己实现的string类,需要封装在一个命名空间内。

迭代器可以用typedef 重命名实现,但是真实的迭代器 iterator 不是char*

        7.1.2 构造函数

注意默认构造和拷贝构造:

如图是普通构造函数的实现,一个是无参的默认构造,一个是带参的。

如图,注意默认构造 _str 需要指向 \0 ,和标准保持一致。

strcpy() 会把 \0 一起拷贝进去。

	string(const string& s)
	{
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}

如图,是拷贝构造的模拟实现。由于 capacity() 不包含\0,因此需要多开一个容量空间。

这是古早的实现方式,有一种现代写法:

如图,调用普通构造实现,将交换函数额外封装一下。

        7.1.3 重载运算符 =
// s2 = s1
string& operator=(const string& s)
{
	if (this != &s)
	{
		delete[] _str;
		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;

		return *this;
	}
	return *this;
}

如图,= 的写法和拷贝构造类似,需要注意的是应该先释放原本的空间,再 _str 指向新开的空间。

下面是赋值运算符的现代写法:

string& operator=(const string& s) // 现代写法
{
	if (this != &s)
	{
		string tmp(s);
		swap(tmp);
	}
	return *this;
}

如图,利用拷贝构造定义一个中间值tmp,再把tmp和this进行交换。

        7.1.4 析构函数
~string()
{
	delete[] _str;
	_str = nullptr;
	_size = _capacity = 0;
}

析构函数的实现比较简单,就是释放空间和置空。

        7.1.5 c_str()

该接口的实现也比较简单,主要是返回对应的字符串的指针

const char* c_str()
{
	return _str;
}

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

需要注意的是,我们可以针对普通对象和const对象分别来实现该接口。

        7.1.6 重载运算符 []
char& operator[](size_t pos)
{
	assert(pos < _size);

	return _str[pos];
}

const char& operator[](size_t pos) const
{
	assert(pos < _size);

	return _str[pos];
}

[] 返回的是pos 位置对应的字符,实现两类不同的[] 接口。

如图是给出的测试用例以及结果。

        7.2 插入与删除
        7.2.1 reserve() 模拟
void string::reserve(size_t n)
{
	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		delete[] _str;
		_str = tmp;
		_capacity = n;
	}
}

如图,此处 reserve() 的实现与vs的实现风格保持一致,不会缩容

先定义一个中间变量tmp,开n+1个空间,再利用 strcpy 拷贝原本的元素,释放旧空间,指向新空间

        7.2.2 push_back() 模拟
void string::push_back(char ch)
{
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}

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

如图,利用三目操作符,调用已经实现好的 reserve() 进行扩容,再进行插入逻辑。

        7.2.3 append() 模拟
void string::append(const char* str)
{
	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	strcpy(_str + _size, str);
	_size += len;
}

如图,具体的扩容和拷贝的思路和 push_back() 一样。

        7.2.4 重载运算符 +=
string& string::operator+=(char ch)
{
	push_back(ch);
	return *this;
}

string& string::operator+=(const char* str)
{
	append(str);
	return *this;
}

如图,+= 可以调用已经实现好的接口。

        7.2.5 insert() 模拟
void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);
	if (_size == _capacity)
	{
		reserve(_capacity == 0 ? 4 : _capacity * 2);
	}

	// 挪动数据
	size_t end = _size + 1;
	while (end > pos)
	{
		_str[end] = _str[end - 1];
		end--;
	}
	_str[pos] = ch;
	_size++;
}

单个字符插入的实现,如上图所示。先判断是否需要扩容,再处理挪动数据的逻辑。

将pos以及之后的数据向后挪动一个单位,再将数据插入到pos位置,令_size++。

void string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
	}

	size_t end = _size + len;
	while (end > pos + len - 1)
	{
		_str[end] = _str[end - len];
		end--;
	}
	memcpy(_str + pos, str, len);
	_size += len;
}

如图,这是字符串的插入,也是先判断扩容再挪动数据。

具体的挪动逻辑和单个字符的类似,将pos以及之后的字符串整体向后挪动len个单位,再插入字符串到pos位置。

        7.2.6 erase() 模拟
void string::erase(size_t pos, size_t len)
{
	assert(pos < _size);

	// 左闭右开减出来得到剩余字符
	if (len >= _size - pos)
	{
		_str[pos] = '\0';
		_size = pos;
	}
	else
	{
		for (int i = pos + len; i <= _size; i++)
		{
			_str[i - len] = _str[i];
		}
		_size -= len;
	}
}

如图,先判断删除的len个字符是否多于剩余字符,如果是,直接置空。

再不断将后面的数据往前面挪动

下面给出测试:

        7.3 寻找与切割
        7.3.1 begin() 和 end()
iterator begin()
{
	return _str;
}

iterator end()
{
	return _str + _size;
}

const_iterator begin() const
{
	return _str;
}

const_iterator end() const
{
	return _str + _size;
}

如图是不同版本的begin 和 end ,不用过多赘述。

        7.3.2 clear() 模拟
void clear()
{
	_str[0] = '\0';
	_size = 0;
}

clear() 清理数据如图。

        7.3.3 find() 模拟
	size_t string::find(char ch, size_t pos)
	{
		assert(pos < _size);

		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
				return i;
		}
		return npos;
	}

	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);

		const char* ptr = strstr(_str + pos, str); // 第二个参数是字串
		if (ptr == nullptr)
			return npos;
		return ptr - _str;
	}

如图分别是字符查找和字符串查找,两者思路类似。

	const size_t string::npos = -1;

如图,npos定义在类里面,在类外面初始化

        7.3.4 substr() 模拟
	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (len > _size - pos)
			len = _size - pos;

		string sub;
		for (int i = pos; i < pos + len; i++)
			sub += _str[i];

		return sub;
	}

如图,判断要截取的len是否合理,只能截取pos之后的有限个有效字符

然后再遍历返回。

样例测试:

        7.4 运算符重载
        7.4.1 <、>、=等
	bool operator<(const string& s1,const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 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);
	}
	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}
	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}

如图,是各类比较大小相关的运算符,只要实现其中两个,其他的就可以通过调用来实现。

        7.4.2 <<、>>
	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}

		return out;
	}

如图是流插入的重载,参数有一个 ostream 类型的对象。

	istream& operator>>(istream& in, string& s)
	{
		s.clear();

		const int N = 256;
		char buff[N];
		int i = 0;

		char ch;
		//in >> ch; // cin 提取不到空格和换行
		ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			buff[i++] = ch;
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			//s += ch;
			//in >> ch;
			ch = in.get();
		}

		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}

		return in;
	}

如图是流提取的重载,它需要一个 istream 类型的对象。

在提取之前,先用 clear 清空缓冲区。利用buff来进行缓存,减少扩容产生的消耗。

本期的分享就到这里,如果觉得博主的文章比较对胃口的话,可以点一个小小的关注~

您的三连是我持续更新的动力~

更多推荐