目录
一、导入
二、接口学习
1.默认成员函数
2.迭代器相关的函数iterator
3.与容量相关的函数Capacity系列
4.与成员权限相关的函数Element access:
5.修改器相关的函数Modifiers:
6.字符串操作接口函数String operations:编辑
三、扩展
一、导入
学习过C++类的相关知识之后,我们便可以进行类的进阶学习。在C++的标准库中,有一些内置的类,string便是其中之一。它定义在头文件<string>
中。这个类提供了丰富的接口来操作字符串,比如字符串的拼接、查找、替换、插入、删除等。
string也是一个模板,被typedef过。最开始是basic_string这样一个类。
值得一提的是,在学习string的过程中可能会有些许“逆风”。但是学习完这个类之后,再学习其他类,往后便是“大顺风”。
二、接口学习
string - C++ Reference (cplusplus.com),在CPP的官网中,提供了大量的关于string接口函数的介绍,本文旨在了解并熟悉使用string类的接口函数。
String class
Strings are objects that represent sequences of characters. 这是官网中对u有string类的介绍,说明了string类是一个管理字符数组的类。
在网站中对于string类的接口函数列举如下。
其中有默认成员函数Member functions,有迭代器相关的函数Iterators,有容量相关的函数Capacity,访问成员的接口函数:Element access,有负责修改的修改器函数Modifiers,还有String operations函数用来进行字符串操作,以及内部的一个成员常量npos和一些全局的函数Non-member function overloads。
可以看出,string的接口是非常繁多的,甚至有一些可以用冗杂形容。下面将以上述列举顺序为序,进行接口的一一列举学习。
1.默认成员函数
文献中列出了三个默认成员函数,constructor构造、destructor析构、operator=赋值重载。
constructor
这是构造函数内部的介绍,总共实现了7中函数重载,其中重要的有默认构造、拷贝构造……多种多样的函数,提供了大量不同的初始化方式。初始化可以传字符、字符串、对象……
(1) (4)是一个带参数、一个不带参数的类型。
这是关于函数的解释。(1)会被初始化为0。
演示:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1;
string s2("hello world");
cin >> s1;
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
在上述代码中,我们利用string类新建了两个对象,s1调用了默认构造中的缺省函数,s2调用了函数(4)。
由于string类中完成了 流提取和流插入 的重载,因此可以直接使用这两个操作符。当我们屏蔽s1的流提取操作之后,打印是这样的。
取消屏蔽之后,便可以完成打印输入的内容。
当然,内部也包括拷贝构造
int main()
{
string s2("hello world");
string s3(s2);
cout << s3 << endl;
cout << s2 << endl;
return 0;
}
这段代码便是利用s2去拷贝构造s3。
我们也可以用字符串的一部分去传参。
对于hello world,假如我们只想传入world,便可以用这个函数。
int main()
{
string s2("hello world");
string s3(s2, 6, 5);
cout << s3;
return 0;
}
w的下标是6,字符串的长度是5。
对于这个长度5,我们也不需要手动计算。
string s3(s2, 6, s2.size() - 6);
我们只需要让总的长度 - w的下标即可。size()接口计算的是大小(即长度length,不包含0)。
注意到,len参数给了一个缺省值,nops,这是定义在string中的一个成员。下面是string::npos的解释
给出的值是-1。但是由于是size_t类型,所以这是整数的最大值:42亿。但显然,这会造成越界。因此下面给出的解释中, means "until the end of the string".表示,如果字符串小于npos的长度,就到达字符串的末尾。
当然,我们给函数传入缺省参数时,必须是从右到左缺省!
还可以用n个字符去初始化。
还可以采用迭代器(下面介绍)去初始化。
传入迭代器的一段区间去初始化。当上述初始化时,由于迭代器支持++、--操作,我们去掉了首尾。
当然,最多的:默认构造、拷贝构造、字符串构造
destructor析构函数
这是这是析构函数,用来完成资源的清理。由于析构函数自动调用,所以不需要过多处理。
operator=赋值重载
可以采用字符、字符串、对象传参,去完成赋值运算。其返回值是*this。
实例:
2.迭代器相关的函数iterator
引入
遍历字符串时,我们可以有两种方式,但是学习完迭代器之后,方式便多了一种。
方式一:下标法
由于string重载了[]操作符,我们可以直接用[]去完成资源的访问。
int main()
{
string s2("hello world");
for (size_t i = 0; i < s2.size(); i++)
{
cout << s2[i] << ' ';
}
cout << '\n';
return 0;
}
方法二:迭代器法
string提供了begin 和 end 两个成员函数,函数简介中说明了,返回的是一个迭代器的beginning和end,用来遍历字符串。begin和end的行为和指针类型类似,但是只能说是用起来类似,却不是完全一致!
这是关于begin和end的函数介绍
值得一提的是,end指向字符串最后一个字符的迭代器,即返回的是/0的位置,而不是最后一个输入的有效字符。因为字符串除了存储我们需要的有效数据,还会增加一个/0。
他们的返回类型都是iterator和const修饰后的iterator两种。因此我们在使用的时候,应该用iterator类型来建立变量。由于迭代器是众多类都有的,因此我们应该声明,这是string的迭代器。
实例:
int main()
{
string s2("hello world");
string::iterator bg = s2.begin();
string::iterator end = s2.end();
while (bg != end)
{
cout << *bg << ' ';
bg++;
}
cout << endl;
return 0;
}
在上述遍历中,我们采用了解引用*和++操作。这两种操作都是指针在遍历时常常用到的操作,所以说迭代器的行为类似指针。
注意,我们写的是bg != end。这种写法主要为了和别的迭代器完成形式的统一。
其实string的迭代器可以写成bg < end;也可以。原因是string的字符串内部的存储空间是连续的。
但是到了vector这种类,内部的空间不连续时,我们只能使用!=,而不是<。
对于不连续的空间,指针减法、指针加法都是未定义的! 在后续的容器中,指针++是允许的,这是因为进行了操作符的重载。否则要进行cur = cur->next;的操作。
这就体现了C++的封装!只需要给你接口,让你用就好!
当然,上述的迭代器由于没有被const修饰,因此可以完成写的操作。
我们将字符串修改成了a。
当然也有被const修饰过的迭代器,来完成只读操作。
这时候,迭代器的返回类型就成了const_iterator类型。
int main()
{
const string s2("hello world");
string::const_iterator bg = s2.begin();
string::const_iterator end = s2.end();
while (bg != end)
cout << *bg++;
cout << endl;
return 0;
}
这就是只读操作。迭代时,先用*bg,再bg++。当然,对于这种复杂的类型,我们可以直接用auto代替。
int main()
{
const string s2("hello world");
auto bg = s2.begin();
auto end = s2.end();
while (bg != end)
cout << *bg++;
cout << endl;
return 0;
}
方式三:采用范围for
int main()
{
string s2("hello world");
for (auto& ch : s2)
{
ch = 'a';
cout << ch;
}
cout << endl;
return 0;
}
范围for只需要用 变量 :容器,就可以完成容器中内容的读取。范围for的特点是:自动迭代、自动结束。此处可以用&类型,也可以用auto ch : s2进行迭代,但是无法完成数据的修改!
当然,如果我们观察反汇编,就会观察到其实范围for的本质还是迭代器!
在begin和end的下面,还有两个
rbegin和rend表示的是reverse,表示的是逆置的迭代器。
int main()
{
string s2("hello world");
auto bg = s2.rbegin();
auto end = s2.rend();
while (bg != end)
cout << *bg++;
return 0;
}
通过上述的使用,就实现了逆置的迭代。
因此迭代器有const修饰和非const修饰、正向、逆向四个版本。
其实每个容器都有自己的迭代器,但是这些迭代器的行为都是十分相似!
c修饰的这几个迭代器则是C++11才支持的语法。c表示const,但是我们基本不用,由于原来的begin本身就是有重载过const过的,我们只需要auto去推断类型就可以。
3.与容量相关的函数Capacity系列
Part 1
这里面已经有我们之前用过的size()函数。
我们为什么说string设计的十分冗杂,这里也能体现。
其实size()与length()接口都是计算长度的,只不过为了和其他容器统一(string出现的比较早,出现string时,STL还没有问世呢!),才出现了size()接口。
Part 2
下一个时max_size函数,这个函数用来返回string字符串的最大容量。Returns the maximum length the string can reach.但其实一般来说,我们很少去用这个函数,主要是对于最大容量的需求极少。
resize接口:
当我们建立好一个string对象之后,可以用这个函数去提前开辟一个空间,来防止多次扩容的影响。
int main()
{
string s;
s.resize(10);
cout << s.size() << endl;
return 0;
在上述代码中,我们将s的空间初始化为10。我们也可以借助这个函数去完成初始化。
int main()
{
string s;
s.resize(10, 'a');
cout << s.size() << endl;
cout << s << endl;
return 0;
}
这段代码,就把s字符串的空间初始化为了10个‘a'。
capacity接口。
对于string的实现,我们姑且可以认为是由三部分组成:_str、_size、_capacity。其中的_capacity就是给_str开辟的容量。我们可以利用capacity接口去访问容量。
int main()
{
string s;
s.resize(10, 'a');
cout << s.capacity() << endl; //15
cout << s.size() << endl; //10
return 0;
}
在上述的代码中,capacity被开辟到了15字节,size为10字节。
Part 3
reserve
reserve(保存、留存)不同于reverse(逆置)。reserve是用来申请容量的变化的。
int main()
{
string s;
s.resize(10, 'a');
cout << s.capacity() << endl; //15
s.reserve(20);
cout << s.capacity(); //31
return 0;
}
可以看到,我们申请了20个字节,但是最终给我们了31个字节。这就跟reserve的特性有关。
当我们进行资源申请时,我们至少会得到n个字节的内存,甚至可能会多给我们一些内存。
clear接口
这个接口是用来清楚string的内容的。他会消除内容,但不会释放空间。
int main()
{
string s;
s.resize(10, 'a');
cout << s.capacity() << endl; //15
s.clear();
cout << s.size() << endl;
cout << s.capacity(); //15
return 0;
}
empty接口
就像是栈、堆等empy函数,这个函数也是用来判断有没有内容的。
shrink_to_fit接口
这个接口是用来减少容量的。它会将容量减少到适宜的空间大小。但一般我们不用用这个接口,毕竟减少容量再去扩容是由风险的!
4.与成员权限相关的函数Element access:
1.[]的运算符重载
[]的重载支持在string中直接yong[]来访问内部内容。
2.at
at函数就是[]的另一种形式,也是用来访问内部元素的
int main()
{
string s("hello world");
cout << s.at(4) << endl; //打印0
return 0;
}
back函数:
返回最后一个字符的索引。
front函数:
返回第一个字符的索引。
5.修改器相关的函数Modifiers:
Part 1:
1.operator+=
这是用来尾插的运算符重载。
int main()
{
string s("hello world");
s += "ace";
cout << s;
return 0;
}
完成了尾插ace。
2.append(附加)接口
append是用来追加字符串的。类似于+=
int main()
{
string s("hello world");
s.append(" hello");
cout << s;
return 0;
}
3.push_back接口
同理,也是完成+=的尾插操作的。
Part 2:
1.assign函数
Assigns(分派) a new value to the string, replacing its current contents。用一个新的值,去覆盖原来字符串的值。注意,此处的覆盖是完全覆盖,需要抹去之前的内容。
int main()
{
string s("hello world");
s.append(" hello");
s.assign("ni hao");
cout << s;
return 0;
}
同理,我们可以用一部分值去替换。
int main()
{
string s("hello world");
string s2("nihao");
s.assign(s2, 2, 2);
cout << s;
return 0;
}
用s2串的下标为2处,用长度为2的字符串去替换原来的字符串。
insert接口
类比栈和队列的insert,用来完成任意位置处数据的插入。
既可以在任意位置插入想要的内容,也可以在任意位置插入想要的某个字符串的子串(2)。
int main()
{
string s("hello world");
string s2("ni hao ");
s.insert(0, s2);
cout << s;
return 0;
}
在上述代码中,我们将s2插入到s的头部。
当我们只想把hao插入到s中时,需要用(2)这个接口。
int main()
{
string s("hello world");
string s2("ni hao ");
s.insert(0, s2, 3, 4);
cout << s;
return 0;
}
当然,也支持用迭代器去操作。
由于接口繁多,我们在使用的时候,查阅文档就好。同时,对于大部分修改内容、查阅内容的接口,都是支持迭代器的。
但是需要注意的是,insert能不用就不用,因为牵扯到数据的挪动,消耗极大。
replace接口
replace不同于assign,replace是“替换” ,而assign是“分配” 。
假如我们想让ni hao替换hello,只需要用(3)就可以
int main()
{
string s("hello world");
s.replace(0, 5, "ni hao ");
cout << s;
return 0;
}
当然,我们也可以直接传入对象来替换。
当然,replace与insert都是不建议经常使用的,否则内部会进行数据的挪动,消耗极大。
erase接口
如其他的erase,这个接口就是用来完成删除操作的。
1)传入位置与长度 2).传入一个迭代器,来删除一个字符 3).传入迭代器的一段区间去删除。
当然,由于数据的删除也是进行数据的挪动,因此也是消耗极大,尽量少使用。
int main()
{
string s("hello world");
s.erase(0, 3);
cout << s << endl;
return 0;
}
swap接口
这是用来交换字符串的内容的。
int main()
{
string s("hello world");
string s2("ni hao ma ");
s.swap(s2);
cout << s << endl;
cout << s2 << endl;
return 0;
}
当然,string还定义了一个全局的swap。
由于还存在模板swap,所以在string这个地方,就有三个swap可以使用。
但是由于存在全局的swap,并且参数匹配,string对象优先使用内部的swap,而不会使用全局swap。
这就省去了拷贝构造,大大提高了效率。
int main()
{
string s("hello world");
string s2("ni hao ma ");
swap(s, s2);
cout << s << endl;
cout << s2 << endl;
return 0;
}
pop_back接口
用来尾删的接口。
6.字符串操作接口函数String operations:
part 1:
c_str接口
我们都知道,C++是兼容C的语言,那我们在使用C的一些函数的时候,他只允许传入字符串,而不是传入自定义类型string对象,那该怎么办呢?这时候C++就提供了一个接口,c_str(),他能将string对象变成具有字符串一样行为的能力。
当我们使用strlen时,会报错,但是用这个接口之后,就不会报错。
同理,下一个接口
date接口
也是得到string字符串数据的,但是我们一般还是c_str()接口用的更多。
part 2:
get_allocator接口
get_allocator()
是 string
类的一个成员函数,它返回与 string
容器关联的分配器对象的副本。
分配器(Allocator)是C++标准库中的一个组件,它负责在容器(例如 vector
、string
、deque
、list
等)中分配和释放内存。每个容器都有一个关联的分配器,用于管理容器的内存需求。
get_allocator()
函数允许你在容器之外使用相同的分配策略来分配内存,这样可以保证程序中内存分配的一致性。这在某些情况下是有用的,比如当你需要在容器之外分配与容器内部数据结构兼容的内存时,或者当你需要测量或控制内存使用时。
使用 get_allocator()
可以确保分配的内存与 string
内部使用的内存来自相同的内存池,这可以减少内存碎片,提高内存使用效率。
由于返回类型及其复杂,我们可以用auto去自动推导。
#include <iostream>
#include <string>
int main() {
std::string s("Hello, world!");
std::allocator<char> alloc = s.get_allocator();
// 使用与string相同的分配器来分配内存
char* p = alloc.allocate(10);
// ... 使用分配的内存 ...
// 释放内存
alloc.deallocate(p, 10);
return 0;
}
这是STL的六大组件:
copy接口
在C++中,string
类提供了一个 copy
成员函数,用于将字符串中的字符复制到指定的字符数组中。这个函数的用法比较直接,下面是一个简单的例子来展示如何使用 string
类的 copy
函数:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World!";
// 创建一个字符数组来接收复制的字符串
char buffer[20];
// 使用copy函数将字符串复制到buffer中
// 注意:copy函数不会自动添加空字符('\0')到复制的字符串末尾
str.copy(buffer, 5, 0); // 从字符串的第一个字符开始复制5个字符
// 手动添加空字符以终止字符串
buffer[5] = '\0';
std::cout << "Copied string: " << buffer << std::endl;
return 0;
}
在这个例子中,str.copy(buffer, 5, 0)
调用将 str
中的前5个字符复制到 buffer
中。copy
函数的前两个参数分别是目标数组和要复制的字符数量。第三个参数是可选的,表示从源字符串的哪个位置开始复制(偏移量)。如果省略第三个参数,默认从字符串的开头开始复制。
需要注意的是,copy
函数不会自动在复制的字符串末尾添加空字符(\0
),因此在上面的例子中,我们需要手动添加空字符来确保 buffer
是一个以空字符结尾的字符串。如果不添加空字符,buffer
可能包含未初始化的数据,这可能导致未定义的行为,特别是当你尝试使用 std::cout
或其他字符串处理函数时。
可以看得出来,这其实是比较鸡肋的函数。
part 3 : find系列
在C++中,string
类提供了一个 find
成员函数,用于在字符串中查找子字符串或字符。find
函数返回找到子字符串或字符的第一个实例的位置,如果没有找到,则返回一个特殊的常量 std::string::npos
。
使用 find
时,你应该总是检查返回值是否为 std::string::npos
,以确定是否找到了匹配的字符。
他的返回类型是size_t,返回的是数据的下标。
int main()
{
string s1("a b c d e f g");
cout << s1.find("d e f") << endl;
return 0;
}
在上述代码中,打印了第一次出现d e f出现时的下标,打印为6。
find是正向查找,那么rfind就是反向查找
查找的是对应的内容最后一次出现时的位置。
find_first_of接口
find_first_of
函数的返回值是一个 size_t
类型的值,它表示在字符串中找到的匹配字符的第一个实例的位置。这个位置是从字符串的开头开始的索引,从0开始计数(但不一定返回0)。
同时也提供了查找的位置。
int main()
{
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_first_of("aeiou");
while (found != std::string::npos) //找到时
{
str[found] = '*';
//从found + 1继续查找
found = str.find_first_of("aeiou", found + 1); //found是找到的索引,found + 1,表示往后查找
}
cout << str << '\n';
return 0;
}
在上述代码中,我们不断查找位置,然后替换成*
find_last_of 是倒着查找。find_first_not_of 、find _last_not_of则是找到不是指定内容的对应内容。
下一部分
substr接口
substr接口的返回类型是string类型,因此可以用来取出一部分去初始化另一个string对象。
int main()
{
string str("Please, replace the vowels in this sentence by asterisks.");
string s2(str.substr(0, 6));
cout << s2 << endl;
return 0;
}
compare接口
Compares the value of the string object (or a substring) to the sequence of characters specified by its arguments.它可以比较两个字符串或者两个字串,相同返回0,<0 和 >0是字符串的大小比较。
实例:
int main()
{
string str("Please, replace the vowels in this sentence by asterisks.");
string s2(str.substr(0, 6));
cout << s2.compare(str) << endl;
return 0;
}
7.全局的函数
除了我们知道的swap、<< 、>> 。还有加法,用来完成string类对象的相加,但是加法也得少用,由于是传值返回,消耗比较大。
关系操作符的重载
内部主要是一些常用的关系操作符,有 == >= <= < > != 。
输入控制:getline
这也是一个非成员的函数重载
我们都知道,在进行多次输入的时候,通常会把空格或者换行符作为两个string对象的间隔,此时当我们想要规定使用换行作为间隔时,可以用这个接口。
他的返回类型是i流,也就是说支持连续的输入。参数中的str是需要存储的字符串。
当我们想让s1存储 i love you ,s2 存储 about you ?时,就不能使用简单的cin >> s1 >>s2;因为>>会把空格当成分隔符,这时候就需要getline。
int main()
{
string s1, s2;
getline(cin, s1);
getline(cin, s2);
//cin >> s1 >> s2;
cout << s1 << endl;
cout << s2 << endl;
return 0;
}
三、扩展
关于utf-8和gbk的介绍:
UTF-8 和 GBK 是两种不同的字符编码方案,它们用于将字符转换为计算机可以理解的字节序列。
UTF-8
UTF-8(Unicode Transformation Format - 8-bit)是一种可变长度的编码方案,可以表示 Unicode 标准中的任何字符。UTF-8 使用 1 到 4 个字节来表示一个字符,具有以下特点:
- 对于 ASCII 字符(代码点在 0-127 之间),UTF-8 编码与 ASCII 编码相同,使用一个字节。
- 它向前兼容 ASCII,这意味着任何仅包含 ASCII 字符的文本文件也是有效的 UTF-8 编码。
- 它为每个字符提供了明确的字节顺序,因此不需要字节序标记(BOM)。
- 它是一种自同步编码,即使在传输过程中丢失或篡改了某些字节,也容易恢复同步。
- 它在全球范围内被广泛使用,特别是在互联网标准和开放源代码社区中。
GBK
GBK(汉字内码扩展规范)是一种双字节的编码方案,最初是为了在简体中文版的 Windows 操作系统中支持更多的汉字而设计的。GBK 编码具有以下特点:
- 它使用两个字节来表示一个汉字,兼容 GB2312 编码。
- GBK 编码包含了 GB2312 的所有字符,并且增加了更多的汉字和符号,总共可以表示超过 70000 个字符。
- GBK 在中国大陆地区被广泛使用,尤其是在一些旧的软件和文档中。
- GBK 不兼容 ASCII 编码,这意味着纯 ASCII 文本在 GBK 编码下可能会被错误地解释。
区别
- 编码方式:UTF-8 是可变长度的,而 GBK 是固定长度的。
- 字符集:UTF-8 可以表示 Unicode 标准中的所有字符,而 GBK 主要用于中文字符和一些符号。
- 兼容性:UTF-8 向前兼容 ASCII,而 GBK 不兼容 ASCII。
- 使用范围:UTF-8 在全球范围内使用,特别是互联网和开放源代码项目。GBK 主要在中国大陆地区使用。
在选择编码方案时,如果需要支持多种语言和全球化的应用,通常会选择 UTF-8。如果应用主要面向中文用户,且需要兼容旧的系统和软件,可能会考虑使用 GBK。随着 Unicode 和 UTF-8 的普及,GBK 的使用正在逐渐减少。