C++:string(底层,常用接口,具体题目,模拟实现)

1.为什么学习string类

2.标准库中的string
3.string类的模拟实现

1.为什么学习string类
1.1 C语言中的字符串

C语言中我们知道字符串以'\0'为结尾的集合,同时C语言标准库中提供一些str系列的库函数,但这些库函数是与字符串分离的,底层还需要自己管理。容易出现越界访问(无提示)
C++的 std::string 就很好的弥补C字符串的短板。它本质是字符顺序表 ,封装字符数组 、size、capacity,提供一整套成员函数。下面我们就从底层原理出发,把string的重点、易错点梳理清楚。

2.标准库中的string
2.1auto和范围for
2.1.1auto关键字

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器 的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

  • 用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
  • 当在同一行声明多个变量 时,这些变量必须是相同的类型 ,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
  • auto不能作为函数的参数 ,可以做返回值 ,但是建议谨慎使用
  • auto不能直接用来声明数组
2.2string类的常用接口

在使用string类时,必须包含#include头文件以及using namespace std;

cpp 复制代码
//底层顺序表
class string 
{
	char& operator[](size_t pos)
	{
		assert(pos <_size);
		return _str[pos];
	}
private:
	char* _str;
	size_t _size;
	size_t capacity;
};

!!!这里要用assert去判断是否越界

这是我们手动写的底层顺序表的基础构造,接下来我们看下C++如何利用string

2.2.1string类对象的常见构造

|---------------------------------|-------------------------------------------|
| (constructor)函数名称 | 功能说明 |
| string()(重点) | 构造空的string类对象,即空字符串 |
| string(const char* s)(重点) | 用C-string来构造string类对象 |
| string(size_t n, char c) | string类对象中包含n个字符c |
| string(const string&s)(重点) | 拷贝构造函数 |

cpp 复制代码
void test_string1() {
	string s1;
	string s2("hello world");
	string s3(s2);
	string s4 = s2;
	string s5 = "开心abcd01";
	cout << s2.size() << endl;
	cout << s5.size() << endl;
	cin >> s1;
	cout << s1 << endl;
	list<string> lt;
	string s6("张三");
	lt.push_back(s6);
	lt.push_back("李四");
	lt.push_back("李四xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
}
int main() {
test_string1();
return 0;
}

我们输入3,输出结果如下:

这里的s1 就是利用了类string的构造函数 (无参数),而s2 则是有参的string类对象 ,s3和s4则是利用类对象s2 对s3和s4进行的构造,通过用size我们就可以发现它利用了构造函数实现初始化。

同时我们又利用了list 创建一个链表来存放string类型的数据 ,而这里我们可以先构造s6在传给It ,也可以直接利用李四进行隐式类型转换 实现构造的目的。同时这里我们还可以发现这里如果我们传的值过大也会进行动态扩容

2.2.2Test2
cpp 复制代码
void test_string2() {
	string s1("hello worldxxxxxxxxxxxxxx");
	string s2(s1, 6, 3);
	cout << s2 << endl;
	//string s3(s1, 6, s1.size()-6);
	//string s3(s1, 6, 1000);
	string s3(s1, 6);
	cout << s3 << endl;
string s4("hello worldxxxxxxxxxxxxxx", 7);
cout &lt;&lt; s4 &lt;&lt; endl;
string s5(10,'*');
cout &lt;&lt; s5 &lt;&lt; endl;

s4 = s5;
cout &lt;&lt; s4 &lt;&lt; endl;

s4 = "##################";
cout &lt;&lt; s4 &lt;&lt; endl;

s4 = "!";
cout &lt;&lt; s4 &lt;&lt; endl;
}
int main() {
test_string2();
return 0;
}

输出结果如下:

这里s2利用下面这个截断一部分的代码 ,从第6个位置往后取3个值 。如果我们不传3就会直接复制到最后也就是s3

s4则是利用下面的函数进行复制前7个字符

s5则是利用下面的函数进行将10个*填充到s5

接下来的代码则是利用下面的函数赋值运算 符的作用,它直接覆盖全部原有内容

2.2.3Test3
cpp 复制代码
// 编译器:检查官+翻译器
void test_string3() {
	string s1("12345");
	s1[0]++; // s1.operator[](0)
	s1[2] = '4';
	//cout << s1[30] << endl; // 越界报错
	cout << s1[3] << endl;
	cout << s1 << endl;
char arr[] = "12345";
arr[0]++; // *(arr+0) 
//cout &lt;&lt; arr[30] &lt;&lt; endl; // 越界不报错

s1 += "xxxxxx";
cout &lt;&lt; s1 &lt;&lt; endl;
}
int main() {
test_string3();
return 0;
}

输出结果如下:

这里我们利用上面的运算符重载,我们可以想利用数组一样去定位字符。s10++相当于把第一个字符加1,s12='4'则是将第3个字符换成4

!!!这里如果出现越界的情况它不会报错,这里我们就需要利用assert去查看其是否越界

剩下的**+=** 则是利用下面的复合赋值运算符重载去实现对其末尾的叠加赋值

2.2.4Test4
cpp 复制代码
void test_string4() {
	string s1("12345");
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl << endl;
	s1 += "11111111111";
	cout << s1.size() << endl;
	cout << s1.capacity() << endl << endl;
	//s1.clear();
	//cout << s1.size() << endl;
	//cout << s1.capacity() << endl << endl;
//cout &lt;&lt; s1.max_size() &lt;&lt; endl;

for (size_t i = 0; i &lt; s1.size(); i++) {
	s1[i] += 1;
}
cout &lt;&lt; s1 &lt;&lt; endl;

for (size_t i = 0; i &lt; s1.length(); i++) {
	cout &lt;&lt; s1[i] &lt;&lt; '|';
}
}
int main() {
test_string4();
return 0;
}

输出结果如下:

这里的size函数就是为了统计有效字符的数量

而capacity是为了统计底层缓冲区大小,我们通过开始的capacity可以发现初始的内存只有15个字符,当我们初始size的后面添加一个新字符使其大于capacity,这样就会发生扩容也就是开辟一块新内存,在把原来的数据拷贝至新内存,释放旧内存

!!!这里的内存扩容规律由编译器自己实现,但底层是字符数组的扩容规则。同时扩容后记得添加上尾部的'\0'

接下来我们看一下添加注释后的代码

cpp 复制代码
void test_string4() {
	string s1("12345");
	cout << s1 << endl;
	cout << s1.size() << endl;
	cout << s1.capacity() << endl << endl;
	s1 += "11111111111";
	cout << s1.size() << endl;
	cout << s1.capacity() << endl << endl;
	s1.clear();
	cout << s1.size() << endl;
	cout << s1.capacity() << endl << endl;
cout &lt;&lt; s1.max_size() &lt;&lt; endl;
}
int main() {
test_string4();
return 0;
}

输出结果如下:

这里clear函数 作用是为了清除所有字符即把size变成0,同时这里的max_size是为了计算出这里的string能装多少个char字符(上限)

2.2.5Test5
cpp 复制代码
void test_string5() {
	string s1("12345");
// 下标+[]  最常用
for (size_t i = 0; i &lt; s1.length(); i++) {
	cout &lt;&lt; s1[i] &lt;&lt; '|';
}
cout &lt;&lt; endl;

// 迭代器
string::iterator it = s1.begin();
while (it != s1.end()) {
	cout &lt;&lt; *it &lt;&lt; '%';
	++it;
}
cout &lt;&lt; endl;

for (auto&amp; ch : s1) {
	ch++;
}
cout &lt;&lt; endl;

// C++11 语法糖
// 范围for
// 自动依次取容器数据给对象
// 自动判断开始和结束位置,自动迭代
// for (char ch : s1) {
for (auto ch : s1) {
	cout &lt;&lt; ch &lt;&lt; '%';
}
cout &lt;&lt; endl;

list&lt;int&gt; lt(10, 0);
//list&lt;int&gt;::iterator lit = lt.begin();
auto lit = lt.begin();
while (lit != lt.end()) {
	cout &lt;&lt; *lit &lt;&lt; '%';
	++lit; // lit.operator++()
}
cout &lt;&lt; endl;

for (auto x : lt) {
	cout &lt;&lt; x &lt;&lt; '@';
}
cout &lt;&lt; endl;

auto x1 = 1;
auto x2 = 0.1;
int x3 = 1;
cout &lt;&lt; typeid(x1).name() &lt;&lt; endl;
cout &lt;&lt; typeid(x2).name() &lt;&lt; endl;
cout &lt;&lt; typeid(it).name() &lt;&lt; endl;
cout &lt;&lt; typeid(lit).name() &lt;&lt; endl;

std::map&lt;std::string, std::string&gt; m;
//std::map&lt;std::string, std::string&gt;::iterator mit = m.begin();
auto mit = m.begin();

int a = 0;
auto p1 = &amp;a;
auto* p2 = &amp;a;
// auto* p3 = a; // 报错,因为auto*代表必须是指针初始化
auto&amp; r1 = a;  // r1是a的引用
const auto&amp; r2 = a;  // r2是a的const引用

// auto array[3] = { 4, 5, 6 }; auto [3]": 数组不能具有其中包含"auto"的元素类型

// C++11
int array[] = { 1, 2, 3, 4, 5 };
for (auto&amp; e : array) // 推荐
	e *= 2;

// for (auto e : array)
for (const auto&amp; e : array) // 推荐
	cout &lt;&lt; e &lt;&lt; " " &lt;&lt; endl;
}
int main() {
test_string5();
return 0;
}

输出结果如下:

我先分析下面的输出结果

这里我们利用了重载运算符\[\] 和for循环来进行输出每个数据。通过下标访问的方式,我们可以像操作普通数组一样逐个取出string中的字符,这种方式直观且高效,是日常开发中最常用的遍历手段之一。for循环配合length()函数可以精确控制遍历的范围,确保不会越界访问。

下面则是迭代器 的两个函数(begin和end)

string中的begin 是为了返回指向第一个字符的迭代器 ,end 是为了返回指向最后一个字符的下一个位置的迭代器 。这里需要特别注意的是,end指向的是最后一个字符的下一个位置,而不是最后一个字符本身,这是一个左闭右开的区间设计,与C++标准库中其他容器的迭代器设计保持一致。这样的设计使得循环判断条件可以统一写成it != s1.end(),当迭代器走到end位置时说明遍历已经完成。

而这里我们又写了一个范围for 来遍历打印字符,它不仅可以自动取容器的数据 ,还可以自动刷新开始和结束位置来实现自动迭代 ,但它其实本质是迭代器 ,底层 也是通过迭代器来实现遍历数据。范围for是C++11引入的语法糖,编译器会在底层自动将其转换为迭代器的begin()和end()调用,因此我们在写范围for时不需要手动管理迭代器的递增和结束判断,代码更加简洁易读。需要注意的是,范围for中的变量默认是值拷贝,如果希望修改容器中的元素,需要使用auto&引用形式。

通过begin和end两个函数,我们可以轻松完成对string的遍历,迭代器在容器的作用如下

迭代器在容器里面非常重要,所有容器都可以通过迭代器来实现遍历数据 ,就像下面的list(双向循环链表)的版本,可以向迭代器传指针,然后通过解引用来得到数据。迭代器实际上是对指针操作 的一种泛化封装,它屏蔽了不同容器底层存储结构的差异,为上层提供了统一的访问接口。无论是连续存储的vector、string,还是链式存储的list、map,我们都可以用相同的方式通过迭代器进行遍历、查找和修改操作,这正是STL设计思想的精髓所在。对于list这样的双向链表,迭代器内部封装了节点的指针,通过operator++和operator--可以实现在链表节点间的移动,而解引用操作符*则返回当前节点存储的数据。
再分析下面的输出结果

这里我们可以通过auto 来自动推算出对象的类型 。比如这里的x1和x2会自动推算出他们的类型,随后利用typeid这个运算符得到数据类型,然后又利用.name成员函数来返回字符串类型 (也能分辨出迭代器 ,例如这里的it 和lit 分别是string 和list的迭代器)

这里的p1的auto是int*类型,p2的auto*则是int类型

!!!这里如果auto后加* ,则传的数据必须是地址 。这里auto后面加**&** 则是引用 ,加上const就是const引用即不能修改元素。

因此我们就可以利用auto&引用来避免拷贝,提升效率。

!!!这里我们如果要修改数据就不用加const ,但如果要打印 就可以加上const防止数据被改变

2.2.6Test6
复制代码
void func(const string& s) {
	// 遍历容器时,可读不可写
	string::const_iterator it = s.begin();
	while (it != s.end()) {
		// (*it)++;
		cout << *it << '%';
		++it;
	}
	cout << endl;
//string::const_reverse_iterator rit = s.rbegin();
auto rit = s.rbegin();
while (rit != s.rend()) {
	cout &lt;&lt; *rit &lt;&lt; '%';
	++rit;
}
cout &lt;&lt; endl;
}
void test_string6() {
string s1("12345");
// 遍历容器时,可读可写
string::iterator it = s1.begin();
while (it != s1.end()) {
(*it)++;
cout << *it << '%';
++it;
}
cout << endl;
// 从后往前遍历,可读可写
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend()) {
	(*rit)++;
	cout &lt;&lt; *rit &lt;&lt; '%';
	++rit;
}
cout &lt;&lt; endl;

func(s1);
}
int main() {
test_string6();
return 0;
}

输出结果如下

这里的rbegin则是begin的相反面,begin指向第一个元素,rbegin则是指向最后一个元素 。而rend也是指向第一个元素,这样我们就可以像利用begin和end一样利用rbegin和rend来反向输出数据

这里的func函数 是我们用来遍历和打印数据 的(有const不可修改),这就与我们写在test中的不一样,test中的可读可写 ,而func中的只能读不可写

2.2.7Test7
cpp 复制代码
void test_string7() {
	string s1("12345");
	reverse(s1.begin(), s1.end());
list&lt;int&gt; lt = { 1,2,3,4,5 };
reverse(lt.begin(), lt.end());

string s2("hello");
s2.push_back(' ');
s2.push_back('w');
s2.push_back('o');
cout &lt;&lt; s2 &lt;&lt; endl;
s2.append("rld");
cout &lt;&lt; s2 &lt;&lt; endl;
s2.append(5, '!');
cout &lt;&lt; s2 &lt;&lt; endl; 

s2.append(s1.begin(), s1.end());
cout &lt;&lt; s2 &lt;&lt; endl;

s2.append(++s1.begin(), --s1.end());
cout &lt;&lt; s2 &lt;&lt; endl;

string s3("hello");
s3 += ' ';
cout &lt;&lt; s3 &lt;&lt; endl;

s3 += "world";
cout &lt;&lt; s3 &lt;&lt; endl;

s3 += s3;
cout &lt;&lt; s3 &lt;&lt; endl;

//s3.assign("xxxxxx");
s3 = "xxxxxxx";
cout &lt;&lt; s3 &lt;&lt; endl;
}
int main() {
test_string7();
return 0;
}

输出结果如下

我们利用reverse 来实现从first到last的元素 (左闭右开)的反转

我们利用push_back来实现在尾部追加一个元素 (不用挪动已知数据)。

而append 不止可以追加一个元素 ,也可以追加n个一样的元素 ,同时还可以追加一段迭代器区间 。同时对于迭代器区间 我们还可以对其**++** 和**--** 来控制区间长度。

这里的重载运算符+= 则是直接在尾部追加元素 (单个字符,字符串,string对象)

为了修改这里的s3,我们不仅可以利用重载运算符= 来实现整体覆盖赋值,清空原有内容,替换成新内容 ,而assign 的作用和重载运算符=相同。

它们的区别在于,重载运算符=只有3个常用重载 ,assign则重载更多 ,参数类型和个数更丰富

2.2.8Test8
cpp 复制代码
void test_string8() {
	string s1("12345");
	cout << s1 << endl;
// insert慎重使用,因为挪动数据会有效率损失
s1.insert(0, "xxxx"); // 头插
cout &lt;&lt; s1 &lt;&lt; endl;

s1.insert(0, 1, 'y');
s1.insert(s1.begin(), 'y');
cout &lt;&lt; s1 &lt;&lt; endl;

s1.insert(5, 1, 'y');
cout &lt;&lt; s1 &lt;&lt; endl;

s1.insert(s1.begin() + 5, 'y');
cout &lt;&lt; s1 &lt;&lt; endl;

string s2("123456789");
cout &lt;&lt; s2 &lt;&lt; endl;
s2.erase(0, 1); // 头删
cout &lt;&lt; s2 &lt;&lt; endl;

s2.erase(4, 2); // 中间
cout &lt;&lt; s2 &lt;&lt; endl;

s2.erase(4);  // 给定位置删到结尾 
cout &lt;&lt; s2 &lt;&lt; endl;

string s3("123456789");
cout &lt;&lt; s3 &lt;&lt; endl;
s3.replace(3, 3, "abc");
cout &lt;&lt; s3 &lt;&lt; endl;
// 谨慎使用,多替换少,或者少替换多,都涉及挪动数据
s3.replace(3, 5, "xxx");
cout &lt;&lt; s3 &lt;&lt; endl;

// C++11之前,s1.swap(s3);效率更好,C++11以后swap(s1, s3);优化了,他们效率都很好,都可以用
s1.swap(s3);
swap(s1, s3);

string s4("hello world hello world hello world hello world hello world hello world"); // 把空格换成%%
string s5;
// reverse 逆置/反转
s5.reserve(s4.size()); // 保留储备
for (auto ch : s4) {
	if (ch != ' ')
		s5 += ch;
	else
		s5 += "%%";
}
cout &lt;&lt; s5 &lt;&lt; endl;
s4.swap(s5);
cout &lt;&lt; s4 &lt;&lt; endl;

//int n = 10000000;
//size_t begin1 = clock();
//string s6;
//size_t old = s6.capacity();
//for (size_t i = 0; i &lt; n; i++) {
//	s6 += 'x'; // 不断扩容

//	if (s6.capacity() != old) {
//		cout &lt;&lt; i &lt;&lt; "s6扩容" &lt;&lt; endl;
//		old = s6.capacity();
//	}
//}
//size_t end1 = clock();
//cout &lt;&lt; end1 - begin1 &lt;&lt; endl;

//size_t begin2 = clock();
//string s7;
//s7.reserve(n);
//old = s7.capacity();
//for (size_t i = 0; i &lt; n; i++) {
//	s7 += 'x';
//	if (s7.capacity() != old) {
//		cout &lt;&lt; i &lt;&lt; "s7扩容" &lt;&lt; endl;
//		old = s7.capacity();
//	}
//}
//size_t end2 = clock();
//cout &lt;&lt; end2 - begin2 &lt;&lt; endl;
}
int main() {
test_string8();
return 0;
}

输出结果如下

这里的insert 函数是为了帮助我们通过传递一个插入的第几个位置和插入内容位置进行插入,这样我们就可以通过传0来实现头插。

!!!这里的插入是直接移动插入位置后的时候后插入

同时我们还可以添加一个size_t类型的数据 实现添加几个相同的字符 。

我们还可以传递一个迭代器来实现插入数据

这里的erase则是通过传入一个删除的位置 和删除数据的个数 来实现删除数据。

如果我们不传删除数据的个数 则是从删除位置到结尾都删除

而replace 函数则是通过传入3个参数(替换位置,替换数据个数,替换内容 )来实现对数据的替换

!!!replace的替换内容 个数如果大于 替换内容的个数,就会出现替换内容替换后 ,但没达到替换个数就会删除数据 (直到达到替换内容个数)

s1.swap(s3)是调用strimg里的函数实现交换数据 ,而swap(s1,s3)则是通过库里面的函数实现交换数据。

!!!C++11之前 ,s1.swap(s3);效率更好 ,C++11以后swap(s1, s3);优化了,他们效率都差不多

这里我们如果要实现对s4中的空格都换成%% ,就可以通过开一个一样大的空间来通过传递数据对空格进行处理。

而reserve 就是为了实现s5和s4的capacity相同来提前控制好空间 以便接下来进行传递

我们还是利用范围for实现遍历和传递数据,这里如果遇见空格就可以对其进行修改为%%再传递,当所有数据都传递完后 再通过swap函数进行数据没交换实现s4的要求

接下来是注释部分的解析

接下来是对于扩容的分析,对于这里初始capacity的s6 ,我们不断在尾部追加数据来实现不断扩容(先拷贝在扩容)

!!!这里的扩容是当size=capacity时再进行扩容直至数据完成拷贝

而对于这里的s7 则是先开辟了n的空间,再进行数据拷贝

通过最后输出的两个由clock计算的时间我们发现,当我们知道要拷贝数据个数大小时,可以先开辟空间再进行拷贝,这样效率会提高

2.2.9Test9
cpp 复制代码
void test_string9() {
	string s1("12345xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
	cout << s1 << endl;
	s1.reserve(10);
	s1.reserve(100); // 扩容
// 大于size
// 1、扩容+插入数据
string s2;
//s2.resize(100);
s2.resize(100, 'x');

// 小于size
// 2、删除数据
s2.resize(10);

// 大于size
// 3、部分插入
s2.resize(50, 'y');
}
int main() {
test_string9();
return 0;
}f

这里是reserve和resize的区别

对于s1的reserve,它只改变capacity,但我们初始化如果capacity小就直接扩容了,接下来如果我们的reserve(10),它的容量不会缩小而如果reserve(100),如果capacity<100,capacity就会扩容到100。

对于s2,我们就可以利用resize来实现初始化和扩容,这里就新增100个有效字符,填充为'x'。

如果resize小于原size,就会只保留前10个字符,后面90个字符直接删除。

而如果resize大于原size,就会在保留原字符的基础上扩容

!!!resize可以改变size的大小,同时还可以对其进行扩容并初始化。

总结resize

1. resize 用于修改string的有效字符数量size。
2. 尺寸变大时会填充字符,空间不足则扩容改变capacity。
3. 尺寸变小时截断尾部字符,但不会主动缩小底层容量。

2.2.10Test10
cpp 复制代码
void split(const string& url) {
	size_t i1 = url.find(':');
	string protocol = url.substr(0, i1);
size_t i2 = url.find('/', i1 + 3);
string domain = url.substr(i1 + 3, i2 - (i1 + 3));

string uri = url.substr(i2 + 1);

cout &lt;&lt; protocol &lt;&lt; endl;
cout &lt;&lt; domain &lt;&lt; endl;
cout &lt;&lt; uri &lt;&lt; endl;
}
void test_string10() {
string filename("string.cpp");
FILE* fout = fopen(filename.c_str(), "r");
if (fout == NULL) {
perror("fopen fail");
return;
}
/*char ch = fgetc(fout);
while (ch != EOF) {
	cout &lt;&lt; ch;
	ch = fgetc(fout);
}*/

string url1("http://legacy.cplusplus.com/reference/string/string/find/");
string url2("https://www.baidu.com/s?wd=%E7%BB%BF%E6%B0%B4%E9%9D%92%E5%B1%B1%E5%86%B0%E5%A4%A9%E9%9B%AA%E5%9C%B0%E9%83%BD%E6%98%AF%E9%87%91%E5%B1%B1%E9%93%B6%E5%B1%B1&amp;sa=fyb_n_homepage&amp;rsv_dl=fyb_n_homepage&amp;from=super&amp;cl=3&amp;tn=baidutop10&amp;fr=top1000&amp;rsv_idx=2&amp;hisfilter=1");
split(url1);
split(url2);

string filename1("string.cpp");
string filename2("string.tar.zip");
cout &lt;&lt; filename1.substr(filename1.rfind('.')) &lt;&lt; endl;
cout &lt;&lt; filename2.substr(filename2.rfind('.')) &lt;&lt; endl;

// str中aeiou变成*
std::string str("Please, replace the vowels in this sentence by asterisks.");
std::size_t found = str.find_first_of("aeiou");
while (found != std::string::npos)
{
	str[found] = '*';
	found = str.find_first_of("aeiou", found + 1);
}

std::cout &lt;&lt; str &lt;&lt; '\n';
}
int main() {
test_string10();
return 0;
}

输出结果如下

这里的c_str的作用是为了将C++的string类型 转换为const char* (以\0结尾)。是为了将其用C语言的fopen来读文件。结合注释的代码就可以将其利用C语言的fgets得到字符并打印下来

copy在string的作用是将从***s** 的pos位置 开始将len个字符插入到里面

find可以查找单个字符 ,字符串 ,子串str。同时还可以控制查找的起始位置,并返回下标。

如果没有找到,就返回npos

**!!!**这里对于字符串还可以控制查找数量

substr的作用是取出pos位置的len个字符,如果len大于剩余字符个数,剩余字符有多少取多少

**!!!**这里有缺省参数npos,如果不传参数它就从pos位置一直取到结束

这里我们利用url的两个网页地址(需要协议+域名+资源路径)来调用split自定义函数来了解find和substr,find得到要找数据的下标,substr取出这段数据。并通过cout打印下来

**!!!**下标刚好是我们要去数据的个数,因此不用改变

rfind的作用是反向查找数据并返回下标,没想到也是返回npos。

我们就可以利用rfind反向取结尾的数据,就比如这里的filename1和2就利用rfind反向取尾部的数据

find_first_of的作用是查找要查找的任意位置的字符 (我们传递的),并返回下标 。如果要查找一串字符如果要查找的字符一个数据也没有就返回npos

!!!这里与find的区别是它要找的一串字符会一个一个找, 不会想find的一样找一串

我们就可以利用这里的逻辑将一串字符中的要查找字符变成别的数据,就像这里改变aeiou为*这样

2.2.11Test11
cpp 复制代码
void test_string11() {
	string s1("string.cpp");
	string s2("aaaa");
	const char* p1 = "bb";
	cout << (s1 < s2) << endl;
	cout << (s2 < p1) << endl;
	cout << (p1 < s2) << endl;

	cout << s1 + p1 << endl;
	cout << p1 + s1 << endl;
	cout << s1 + p1 + s2 + p1 << endl;

	/*int a, b;
	cin >> a >> b;
	cout << a<<" " <<b<< endl;*/

	// 输入hello world 得到hello 遇到空格终止了
	/*cin >> s1;
	cout << s1 << endl;

	cin >> s2;
	cout << s2 << endl;*/

	// 输入hello world 得到hello world
	getline(cin, s1);
	cout << s1 << endl;

	getline(cin, s2, '#');
	cout << s2 << endl;
}

int main() {
	test_string10();

	return 0;
}

输出结果如下:

这里这个运算符重载是用来ascall码值的大小 一个一个比较。满足条件返回1,不满足返回0

!!!这里的运算符重载不是成员函数,是因为它如果变成成员函数(它的左操作数必须是string类型 ),就无法满足左操作数 是char类型的函数

因为这里的运算符优先级高低,我们对于重载运算符必须加上括号才能使用,否则会因为优先级的高低导致无法正常运行

这里的重载运算符+也不是成员函数 ,它的作用是创建一个新的字符串并把两个内容拼接并拷贝

**!!!**它支持连续+,因此可以连续使用+来拼接内容

对于这里的getline ,我们先理解cin的机制,它在输入法时候如果遇见空格就终止,在空格后的字符会进入缓冲区,输入就提取不到完整字符,下次调用函数时会直接给那个对象

而getline的作用是**遇见空格不停止,遇见换行在停止。**因此它可以完全打印hello world,同时我们还可以通过传一个终止字符来控制停止,比如这里的'#',我们输入时遇见'#'才停止。

2.3类型之间的转换

C++11 之后,就可以调用上面的函数把string类型 转换成其他的类型 ,下面两个是将其他类型转换成string的类型和wstring类型

3.string类的模拟实现
3.1string.h
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#pragma once

#include<string.h>
#include<assert.h>

#include<iostream>
using namespace std;

namespace qz {
	class string {
	public:
		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; }
		
		string();
		string(const char* str);
		string(const string& s);
		//string& operator=(const string& s);
		string& operator=(string tmp);
		~string();
		
		void reserve(size_t n=0);
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);
		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos = 0, size_t len = string::npos);
		size_t find(char ch, size_t pos = 0) const;
		size_t find(const char* str, size_t pos = 0) const;
		string substr(size_t pos = 0, size_t len = npos) const;
		void swap(string& s);


		const char* c_str() const { return _str; }
		const size_t size() const{ return _size; }
		size_t capacity() const { return _capacity; }

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

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

		//static size_t npos;
		// 特殊,只有整形的const静态成员支持这么写
		static const size_t npos = -1;

		// 其他类型不支持
		//static const double x = 1.1;

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};

	//下面的都是非成员函数
	string operator+ (const string& lhs, const string& rhs);
	string operator+ (const string& lhs, const char* rhs);
	string operator+ (const char* lhs, const string& rhs);

	bool operator== (const string& lhs, const string& rhs);
	bool operator!= (const string& lhs, const string& rhs);
	bool operator>(const string& lhs, const string& rhs);
	bool operator<(const string& lhs, const string& rhs);
	bool operator>= (const string& lhs, const string& rhs);
	bool operator<= (const string& lhs, const string& rhs);

	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);
	istream& getline(istream& in, string& s,char dalim='\n');
}

//放在类外面是为了能在main中调用时显示
void swap(qz::string& s1, qz::string& s2);
3.2string.cpp
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include"string.h"


namespace qz {

	string::string()
		:_str(new char[1])
		, _size(0)
		, _capacity(0) {
		_str[0] = '\0';
	}
	string::string(const char* str)
		//这里运用多次strlen,而strlen时间复杂度是O(n)
		/*:_str(new char[strlen(str) + 1])
		, _size(strlen(str))
		, _capacity(strlen(str)) {

		strcpy(_str, str);
	}*/
		:_size(strlen(str)) {

		_capacity = _size;
		_str = new char[_size + 1];
		strcpy(_str, str);
	}

	//传统写法
	//这里如果不写拷贝构造,会有深浅拷贝的问题
	//string::string(const string& s) 
	//	:_str(new char[s._capacity+1])
	//	,_size(s._size)
	//	,_capacity(s._capacity){
	//	strcpy(_str, s._str);//记得把值传回来
	//}

	//现代写法
	string::string(const string& s) {
		string tmp(s._str);
		swap(tmp);
	}


	//传统写法
	//string& string::operator=(const string& s) {
	//	//释放旧空间,重新开跟s一样的空间
	//	if (this != &s) {
	//			delete[]_str;
	//			_str = new char[s._capacity];
	//			strcpy(_str, s._str);
	//			_size = s._size;
	//			_capacity = s._capacity;
	//	}

	//	return *this;
	//}

	////现代写法
	//string& string::operator=(const string& s) {
	//	if (this != &s) {
	//		string tmp(s._str);
	//		string tmp(s);

	////		this指向s1取到tmp拷贝的空间,并将要释放的空间给tmp,tmp出作用域调用析构
	//		swap(tmp);
	//	}
	//	return *this;
	//}

	//最简洁的写法
	string& string::operator=(string tmp) {
		swap(tmp);
		return *this;
	}

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

	}
	
	//扩容n
	void string::reserve(size_t n) {
		if (n > _capacity) {
			char* tmp = new char[n + 1];

			strcpy(tmp, _str);
			delete[](_str);
			_str = tmp;
			_capacity = n;
		}
	}


	//尾插
	void string::push_back(char ch) {
		//扩容
		if (_size == _capacity) {
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';
	}


	//尾插插入一段数据
	void string::append(const char* str) {
		int len = strlen(str);
		//扩容 
		if (_size+len > _capacity) {
			reserve(_size + len >= 2*_capacity ? _size + len : _capacity * 2);
		}
		strcpy(_str + _size, str);
		_size += len;
	}

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


	//插入一个字符
	void string::insert(size_t pos, char ch) {
		assert(pos <= _size);
		//扩容
		if (_size == _capacity) {
			reserve(_capacity == 0 ? 4 : _capacity * 2);
		}

	/*	int end = _size;
		while (end >= (int)pos){
			_str[end + 1] = _str[end];
			--end;
		 }*///这里pos必须强制类型转换,防止size_t导致出现-1的情况
		size_t end = _size+1;
		while (end > pos) {
			_str[end] = _str[end-1];
			--end;
		}
		_str[pos] = ch;
		++_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 : _capacity * 2);
		}

	/*	int end = _size;
		while (end >= (int)pos) {
			_str[end + len] = _str[end];
			--end;
		}*/
				
		size_t end = _size + len;
		while (end > pos+len-1) {
			_str[end] = _str[end - len];
			--end;
		}
		for (size_t i = 0; i < len; i++) {
			_str[pos + i] = str[i];
		}
		_size += len;
	}


	//删除
	void string::erase(size_t pos , size_t len) {
		assert(pos <= _size);
		//pos以后都要删
		if (len == npos || len >= _size - pos) {
			_str[pos] = '\0';
			_size = pos;
		}
		else {
			for (size_t i = pos+len; i <=_size ; i++) {
				_str[pos++] = _str[i];
			}
			_size -= len;
		}

	}

	//交换
	void string::swap(string& s) {
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}


	//查找一个字符
	size_t string::find(char ch, size_t pos) const {
		assert(pos <= _size);
		for (size_t i = pos; i < _size; i++) {
			if (ch == _str[i])
				return i;
		}
		return npos;
	}


	//查找一段字符
	size_t string::find(const char* str, size_t pos) const {
		assert(pos <= _size);
		const char* ptr = strstr(_str+pos, str);
		if (ptr == nullptr) {
			return npos;
		}
		else {
			return ptr - _str;
		}
			
	}


	//取从pos开始的len个数据
	string string:: substr(size_t pos, size_t len) const {
		assert(pos <= _size);
		//len大于剩余字符长度
		if (len == npos || len >= _size - pos) {
			len = _size - pos;
		}
		string sub;
		sub.reserve(len);
		for (size_t i = 0; i < len; i++) {
			sub += _str[pos + i];
		}
		return sub;
	}
	//size_t string:: npos = -1;

	//////////////////////////////////////////////////////////////////////////////////////////////////////////
	string operator+ (const string& lhs, const string& rhs) {
		string  ret(lhs);
		ret += rhs.c_str();
		return ret;
	}
	string operator+ (const string& lhs, const char* rhs) {
		string  ret(lhs);
		ret += rhs;
		return ret;
	}
	string operator+ (const char* lhs, const string& rhs) {
		string  ret(lhs);
		ret += rhs.c_str();
		return ret;
	}

	bool operator== (const string& lhs, const string& rhs) {
		return strcmp(lhs.c_str(), rhs.c_str())==0;
	}

	bool operator<(const string& lhs, const string& rhs) {
		return strcmp(lhs.c_str(), rhs.c_str()) < 0;
	}

	bool operator!= (const string& lhs, const string& rhs) {
		return !(lhs == rhs);
	}

	bool operator>(const string& lhs, const string& rhs) {
		return !(lhs <= rhs);
	}
	
	bool operator>= (const string& lhs, const string& rhs) {
		return !(lhs < rhs);
	}

	bool operator<= (const string& lhs, const string& rhs) {
		return lhs < rhs||lhs==rhs;
	}


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


	istream& operator>>(istream& in, string& s) {
		//清除原有数据,防止在其后面叠加
		s.erase();
		//输入支持一个一个读
		char ch;
		//in >> ch;
		ch = in.get();
		//这里设置一个buff来存放数据,每存127个数据(末尾放'\0')就给s
		char buff[128];
		int i = 0;

		while (ch != EOF && ch != ' ' && ch != '\n') {
			buff[i++] = ch;
			//buff满了
			if (i == 127) {
				buff[i] = '\0';
				s += buff;
				i = 0;
			}
			//s += ch;
			//in >> ch;
			ch = in.get();
		
		}
		if (i > 0) {
			buff[i] = '\0';
			s += buff;
		}


		return in;
	}


	//遇见空格继续读,遇到换行终止                   dalim可以让我们自定义结束符
	istream& getline(istream& in, string& s, char dalim) {
		char ch;
		ch = in.get();
		while (ch != EOF && ch != dalim) {
			s += ch;
			ch = in.get();
		}
		return in;
	}
	
}

//防止调用到库里面的swap
void swap(qz::string& s1, qz::string& s2) {
	s1.swap(s2);
}
3.3Test.cpp
cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;

#include"string.h"


void teststring1(){
	std::string s3;
	cout << s3.c_str() << endl;

	qz::string s2;
	cout << s2.c_str() << endl;

	qz::string s1("hello world");
	cout << s1.c_str() << endl;

	for (size_t i = 0; i < s1.size(); i++) {
		s1[i]++;
		cout << s1[i] << '|';
	}
	cout << endl;

	for (auto& ch : s1) {
		ch--;
	}
	cout << endl;

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

void teststring2() {
	/*std::string s1("hello");
	s1.append("xxxxxxxxxxxx");

	cout << s1.c_str() << endl;

	s1 += ' ';
	s1 += "yyyyyyyyyyyyyy";
	cout << s1.c_str() << endl;*/

	string s1("string.cpp");
	string s2("aaaa");
	const char* p1 = "bb";
	//cout << (s1 < s2) << endl;
	//cout << (s2 < p1) << endl;
	//cout << (p1 < s2) << endl;

	cout << s1 + p1 << endl;
	cout << p1 + s1 << endl;
	cout << s1 + p1 + s2 + p1 << endl;
}

void teststring3() {
	qz::string s1("hello world");
	s1.insert(5, ' ');
	s1.insert(0, 'x');
	cout << s1.c_str() << endl;

	qz::string s2;
	s2.insert(0, 'x');
	cout << s2.c_str() << endl;

	qz::string s3("hello world");
	s3.insert(5, "xxx");
	s3.insert(0, "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy");
	cout << s3.c_str() << endl;

	qz::string s4("hello worldxxxxxxxxxxxx");
	//s4.erase(11, 20);
	//s4.erase(11);
	cout << s4.c_str() << endl;

	s4.erase(6, 3);
	cout << s4.c_str() << endl;

	qz::string s5("abcabexxxx");
	cout << s5.find("abe") << endl;
	cout << s5.find("abx") << endl;

	qz::string ret = s5.substr(3, 4);
	//cout << &ret << endl;
	cout << ret.c_str() << endl;

	qz::string s6(s5);
	cout << s5.c_str() << endl;
	cout << s6.c_str() << endl;

	s6 = s3;
	cout << s6.c_str() << endl;
	cout << s3.c_str() << endl;

	s6 = s6;
	cout << s6.c_str() << endl;
}

void teststring4() {
	qz::string s1("hello world");
	qz::string s2("xxxx");
	const char* str = "yyyy";
	cout << s1 + s2 << endl;
	cout << s1 + str << endl;
	cout << str + s1 << endl;

	swap(s1, s2);
	s1.swap(s2);

	qz::string s3;
	cin >> s3;
	//getline(cin, s3);
	cout << s3 << endl;
}

void teststring5() {
	qz::string s1("hello world");
	qz::string s2(s1);
	cout << s1  << endl;
	cout << s2  << endl;

	qz::string s4("hello worldyyyyyyyyyyyyyyyyyyyyyyyyyy");
	s1 = s4;
	cout << s1 << endl;
	cout << s4 << endl;

	qz::string s5("hello world");
	//cin >> s5;
	//cout << s5 << endl;

	qz::getline(cin, s5,'#');
	cout << s5 << endl;


	qz::string s10;
	qz::string s11("hello world");
	qz::string s12("hello worldyyyyyyyyyyyyyyyyyyyyyy");
	cout << sizeof(s10) << endl;
	cout << sizeof(s11) << endl;
	//这里对于sizeof的结果不是12,而是其他值。因为vs在这里会有一个Buf数组(可以存15个字符)会先存数据,等到字符个数大于15就会直接放在堆空间
}


int main() {
	teststring5();

	return 0;
}
3.4总结

对于这里的string的函数模拟实现 ,这里对其进行了声明和定义分离的方式,同时又利用了Test.cpp进行了测试。

注意∶这里我们把自定义string 都写在了命名空间qz 中的头文件,同时在string.cpp 中也加了命名空间qz 。这样两个命名空间 虽然在两个文件中但都是一个命名空间。当我们要写成员函数时要用命名空间∶∶。不是成员函数的则不用。
这里还有一个特殊的函数swap,它的解析如下

这里有3个swap函数 ,最下面的那个是std中带有模板的swap,这样对于任意类型都能调用,而第一个swap函数内部是为了调用第二个swap函数,而第二个swap函数才是类中的函数。这是因为大多数人在调用swap函数 时是用了**()**去调用,而这里如果不写第一个swap函数,调用swap函数就要用对象调用。

因此我们在模拟实现swap函数时也要放在命名空间外面声明 (为了在main中显示),防止和std中的冲突。所以我们在.cpp函数中定义也要放在命名空间。

相关推荐
NiceBlueChai1 小时前
Qt 跨线程传递多容器嵌套结构:一次 Release 实测
开发语言·qt
念何架构之路1 小时前
go-grpc逻辑架构图
开发语言·后端·golang
SEO_juper2 小时前
AI 爬虫 robots.txt 检查工具:你的站是不是把 AI 挡在门外了
开发语言·人工智能·爬虫·python·外贸独立站
木圭的AI时代指南2 小时前
实测 MiniMax M3.1 Flash:同一道 Three.js 大题,两个 agent 差出 4.6 倍
开发语言·javascript·ecmascript
星恒讯工业路由器2 小时前
工业路由器在中小 IoT 场景的选型与适配实践:从标准品到定制化的技术路径
开发语言·网络·物联网·php·工业路由器选型·工业网关odm·链路冗余切换
OKkankan2 小时前
python 进阶(三):面向对象进阶——从“会写 class”到设计可维护的后端对象
开发语言·数据结构·python
魔都吴所谓2 小时前
【Python】 多线程 & 多进程&协程 常见题 + 应用场景整理
开发语言·python
king13598745502 小时前
LiteRT Tensor API:C++ 编写端侧模型图,XNNPACK、GPU 与 NPU 怎么选?
开发语言·c++·人工智能·chatgpt·架构·aigc
geovindu2 小时前
rust: Template Method Pattern(续)
开发语言·后端·设计模式·rust·行为模式·板方法模式