字符数组和string字符串:从C语言到C++的演变
在C语言和C++的编程中,字符数组和字符串(string)是非常重要的基础数据类型。它们在实际编程中常用于存储和操作文本数据,但是这两种类型的处理方式有所不同。在这篇博客中,我们将详细讲解字符数组和string字符串,从C语言的字符数组到C++中的string字符串,分析它们的区别、演变过程及其输入输出方式。
本文零基础也能看,最细节的讲解,讲透每个点! ❤️ ⭐️ 👻
一、C语言中的字符数组
在C语言中,字符数组用于存储字符串数据。C语言并没有专门的字符串类型,字符串其实是由字符数组组成的。字符数组是以 '\0'
(空字符)结尾的,这个空字符表示字符串的结束。
注意:
- 空字符是
'\0'
,ASCII值为0
,在字符数组中作为结束标志。 - 空白字符是指一类特殊字符,主要用于分隔代码中的标记 (tokens是编译器将源代码分解为的最小可识别单元。这些单元是语法分析的基础,用于构建程序的语法树并最终生成可执行代码),但本身不会影响程序的逻辑功能。这些字符通常不可见,用来提升代码的可读性或作为输入数据的一部分。
C语言中的空白字符包括以下几种:- 空格(Space):
' '
(ASCII值:32) - 水平制表符(Horizontal Tab):
'\t'
(ASCII值:9) - 换行符(Newline):
'\n'
(ASCII值:10) - 垂直制表符(Vertical Tab):
'\v'
(ASCII值:11) - 回车符(Carriage Return):
'\r'
(ASCII值:13) - 换页符(Form Feed):
'\f'
(ASCII值:12)
- 空格(Space):
1.1 字符数组的定义(初始化)
定义字符数组时,必须指定数组的大小或通过初始化让编译器推断大小。
//直接在程序内储存数据
char str1[100] = "hello world"; // 手动定义长度:定义一个长度为100的字符数组,并用str1来储存"hello world"这个c语言风格的字符串
char str2[] = "hello world"; //自动适应长度:不直接填写字符数组的具体大小,直接通过c语言风格字符串来初始化str2
//通过标准输入流存储数据
char str4[100]; //需要指定字符数组的大小
cin >> str3;
char str5[]; //直接写 char str5[]; 会导致编译错误,因为编译器不知道数组需要多大
cin >> str5; //err
解释:C语言中没有专门的string类,用双引号将一串字符括起来表示字符串,这种方式在C++中也是支持的,但是我们通常叫它C语言风格的字符串。
由此可见,字符数组在定义时通常指定一个足够大的空间来存储字符串。如果定义时不预先知道字符串的长度,可以选择一个较大的固定值,确保空间足够。这样以来就显得很麻烦且手动控制字符数组的长度很容易出错,并且’\0’也是个常常容易犯错的点,于是在C++中封装了string字符串这个类,下面会讲到。
显式地初始化字符数组有两种方式:
//方式1:字符串字面值初始化
char str1[100] = "abcdef"; //初始化值长度小于字符数组长度,剩余的空间全部用\0来填充
char str2[] = "abcdef";
//方式2:字符列表初始化
char str3[100] = {'a','b','c','d','e','f'}; //剩余的空间全部被自动初始化为\0
char str4[] = {'a','b','c','d','e','f'};
- 方式1 字符串字面值初始化字符数组会自动在存储内容的末尾加上一个
'\0'
作为结束标志。 - 方式2 字符列表初始化字符数组不会自动添加
'\0'
,指定的哪几个字符,存储的就是这几个字符,不会多也不会少(前提是空间要够)。
1.2 字符数组的长度,字符串长度
1.2.1 字符数组的长度
求字符数组的长度用 sizeof(数组名)/sizeof(数据类型)
即可,这样求出来的是整个字符数组的大小,也就是它的最大长度。
注意:sizeof(数组名)/sizeof(数据类型)
会计算\0
的大小。
#include <iostream>
using namespace std;
int main()
{
char str1[100] = "abcdef";
cout << sizeof(str1)/sizeof(char) << endl; //输出100
char str2[] = "abcdef";
cout << sizeof(str2)/sizeof(char) << endl; //输出7 因为前面讲过这样初 始化字符串会在末尾自动添加一个\0,这个\0也是要占据空间的
}
1.2.2 字符串长度
字符数组的长度怎么求知道了,字符数组中存放的字符串的长度怎么求?这时候\0
就起到了关键作用,C/C++标准库中有个函数strlen
是专门来求字符串长度的,它的原理就是通过计算\0
前面所有元素的个数来得到字符串的总长度(在C++中需要包含头文件是<cstring>,在C语言是<string.h>,C语言中的头文件在C++都进行了改进或直接套壳,这些头文件的名字由.h结尾变成了c开头)。
注意:strlen
函数计算的长度不包含\0
,这是一个初学者经常会犯的错误。
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char arr[20] = "abcdef";
cout << "数组的长度:" << sizeof(arr)/sizeof(arr[0]) << endl; //注释1
//输出20
cout << "字符串的长度:" << strlen(arr) << endl;
//输出6
return 0;
}
注释1:这里使用字符数组的sizeof(arr[0])
作为分母也是可以的,sizeof(第一个元素)
求的就是这个元素的大小,元素的大小又是由它的类型决定的,为什么使用第一个元素而不使用第二个元素?一个数组被合法地创建出来,它一定是有第一个元素的,但不保证有第二个元素(假如数组大小只有1),所以使用第一个元素更加安全。
1.3 字符数组的输入
1.3.1 输入没有空格的字符串
输入没有空格的字符串那就非常简单了,没有任何陷阱,选择使用scanf或者cin读取即可。
使用scanf读取:
#include <cstdio>
int main()
{
char arr[20] = { 0 };
scanf("%s", arr); //输⼊abcdef
printf("%s", arr); //输出abcdef
return 0;
}
使用cin读取:
#include <iostream>
using namespace std;
int main()
{
char arr[20] = { 0 };
cin >> arr; //输⼊abcdef
cout << arr << endl; //输出abcdef
return 0;
}
指定位置存放
上面两个都是读什么就输出什么,非常简单,不必过多演示。但是它们都是从数组的起始位置开始存储的,可以指定位置存放吗?当然可以,比如从数组的第二个元素的位置(下标为1的位置)开始存放。
#include <iostream>
using namespace std;
int main()
{
char arr[20] = { 0 };
//输⼊
cin >> arr + 1;//arr表⽰数组的起始位置,+1意思是跳过⼀个元素,就是第⼆个元素的位置
//可以通过调试观察arr的内容
cout << arr + 1;
return 0;
}
使用 scanf 函数也一样的:
#include <cstdio>
int main()
{
char arr[20] = { 0 };
scanf("%s", arr+2); //输⼊ 从arr+2的位置开始存放
printf("%s", arr+2); //输出 从arr+2的位置开始打印
return 0;
}
想从哪开始存储数据就跳过多少个元素。
注意:如果是往整型数组里面存数据,必须逐个读取数组的元素并存入数据,要搭配循环来存储数据。
#include <iostream>
using namespace std;
int main()
{
int arr[20] = { 0 };
//循环输⼊
for(int i=0;i<20;++i)
{
cin >> arr[i] ; //将数据依次存储到arr[0],arr[1],arr[2]...
}
cout << arr ;
return 0;
}
Q:为什么对于字符数组就可以 cin >> 字符数组名
来一次性读取所有数据,对于整型数组就要挨个读取,并且只能使用 cin >> 整型数组名[下标]
不能使用 cin >> 整型数组名
,我知道 scanf
读取并存储数据是要取变量的地址的,那么cin >> 后面跟的参数到底是变量还是变量的地址?
A:cin >> 后面跟的参数是变量,不需要取变量的地址。
为什么两种数组读数据方式有区别?说简单点就是,字符数组可以连续读数据,整型数组只能一个个读,所以 cin >> 字符数组名
这里的字符数组名表示整体数组这个变量,cin >> 整型数组名[下标]
整型数组名[下标]表示每个元素变量,怎么写跟他们的读取方式有关。
再讲复杂点,cin>>字符数组名
是可以一次性将输入流中的多个连续字符作为字符串读取并存入字符数组中的,所以直接 cin>>字符数组名
即可,这里将整个字符数组看作一个整体,而不是表示首元素的地址,这里的数组名表示整个字符数组这个变量,只不过恰巧数组名又是这个数组首元素的地址,不要理解错了。
对于 cin >>
整型数组和字符数组的区别:
- 字符数组 被 cin >> 特殊处理:它认为字符数组表示一个字符串,并会从输入流中读取多个字符,直到遇到空白字符(空格、回车等)。
- 整型数组 没有这样的特殊处理:cin >> 只能一次读取一个整数,而不是填充整个数组。
1.3.2 输入含有空格的字符串
1.3.2.1 scanf(%s)
和 cin
能读取含有空格的字符串吗?
直接说结论:scanf(%s)
和 cin
在读取带空格的字符串的时候不能读取全部内容,会在第一个空格处停止,这是因为 scanf(%s)
和 cin
在读取字符串时都以空白字符作为结束标志,并且会在字符串末尾加上一个 \0
(这是对于字符数组,在string中去掉了以\0为结束标志,所以往string中存储字符串时不会加\0)。
以下演示 scanf(%s) 和 cin >> 不能完整读取含有空格的字符串:
1.3.2.2 解决方法
1.3.2.2.1 gets
和 fgets
gets
函数能够读取整行字符串,包括空格,从第一个字符一直读到 '\n'
,但不会包含 '\n'
,读取结束后会在读取内容末尾自动加上一个 \0
。
char * gets ( char * str ) ;
str:字符数组的指针,用于存储读取的字符串。
gets
不安全,容易造成缓冲区溢出,从 C11 标准 开始已被移除。
建议使用更安全的替代方法,如 fgets
。
char * fgets ( char * str, int num, FILE * stream );
返回值:fgets 在成功读取一行时返回一个指向缓冲区的指针。如果遇到错误或到达文件末尾而没有读取任何内容,则返回 NULL。这里只作了解。
str:字符数组的指针,用于存储读取的字符串。
num:要读取的最大字符数(包括末尾自动添加的 \0)最多读取 num-1 个字符。当 size 为 100 时,最多读取 99 个字符,要留一个空间给 \0 。
stream:文件流,通常为 stdin(标准输入)。从文件读取时,stream 可以是文件指针。
注意:一般情况,fget 会以 ‘\n’ 为结束标志,读取整行字符串,并且包含 ‘\n’ 。除非空间不够就不会读取 \n ,只预留一个元素的空间来储存 \0 ,比如说定义了一个长度为10的字符数组,当使用 fgets 读取了一个长度为 9 个字符的字符串的时候,只剩下一个空间,优先保留给 \0 ,要保证字符串中有结束标志,这是更为重要的。
输入 abcdef 六个字符,空间绰绰有余,可以包含 \n:
输入 abcdefghi 九个字符,最多读取 num-1 即 9 个字符,这时候就不能包含 \n:
补充:如果想要移除字符数组 ch 中的换行符(‘\n’),可以这样:
str[strcspn(str, "\n")] = '\0';
这行代码的作用就是:找到字符串 str 中第一个 \n 的位置,然后将这个位置的元素替换成 \0 。
函数解析:strcspn
size_t strcspn(const char *s1, const char *s2);
返回值:返回字符集合 s2 中的任意一个字符第一次个出现在字符串 s1 中的下标(0-based);如果 s2 中的字符在 s1 中不存在,返回 s1 的长度。
s1:要搜索的字符串。
s2:要匹配的字符集合。s2中可放入多个字符去匹配(以C语言风格字符串或者字符数组的形式传入),但是我们通常只传入一个字符去匹配。
1.3.2.2.2 在 scanf
中使用格式控制符
使用 scanf 的指定格式可以读取含空格的字符串,例如 %[^\n]
,表示读取一行数据,直到遇到 \n
为止。
示例代码:
#include <stdio.h>
int main() {
char str[100];
scanf("%[^\n]", str); // 读取直到换行符的字符串 //输入Dante 798
printf("您输入的是:%s\n", str); //输出Dante 798
return 0;
}
补充(通过字符集合自定义匹配):
- %[a-zA-Z]:读取所有字母。
- %[0-9]:读取所有数字。
- %[^0-9]:读取所有非数字字符。
注意: 这些格式控制符虽然语法上有类似正则表达式的特性,但是它们的实现并不是基于正则表达式,而是 C 标准库对格式说明符的一种支持,它通过解析输入流并与指定的格式匹配来工作。
1.3.2.2.3 getchar
和 fgetc
int getchar(void);
返回值:返回读取到的字符的ASCII值,读取失败返回EOF。
参数:无参数。
int fgetc(FILE *stream);
返回值:返回读取到的字符的ASCII值,读取失败返回EOF。
stream:文件流,通常为 stdin。
#include <iostream>
using namespace std;
int main()
{
char str[100];
int i = 0; //等会用来控制下标,挨个填充到str中
char ch = 0; //用来临时保存getchar每次读到的字符
while((ch = getchar()) != '/n' && ch != EOF)
{
str[i++]=ch;
}
str[i] = '\0'; //最容易忘记的的一步:添加字符串结束符,很多人不加\0就直接打印去了,就导致未定义行为
cout << str << endl;
return 0;
}
注意:while 循环的条件 ((ch = getchar()) != '/n' && ch != EOF)
中 ch != EOF
建议要写上,虽然说大多数文件末尾都是以换行符结尾,但是避免不了有少数个别情况,加上这个条件可以使我们的代码更健壮,避免出现异常,这也是一种企业级编程的思维。
如果文件末尾没有换行符(或者文件结尾是其他字符),那么 getchar() 会继续尝试读取,最终返回 EOF,可能导致写入无效数据到 str 或者导致未定义行为(在数组越界等情况下)。
1.4 字符数组的输出
字符数组的输出主要就是三种方法:
- printf 函数中使用 %s 占位符来打印字符串
- cout 打印字符串
- 通过循环逐个字符打印字符串
代码演示:
//⽅法1
//printf和cout
#include <iostream>
#include <cstdio>
using namespace std;
int main()
{
char a[] = "hello world";
cout << a << endl;
printf("%s\n", a);
return 0;
}
//⽅法2.1
//单个字符的打印,直到\0字符,\0不打印
#include <iostream>
using namespace std;
int main()
{
char a[] = "hello world";
int i = 0;
while (a[i] != '\0')
{
cout << a[i];
i++;
}
cout << endl;
return 0;
}
//⽅法2.2
//单个字符打印,根据字符串⻓度来逐个打印
//strlen可以求出字符串的⻓度,不包含\0
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char a[] = "hello world";
int i = 0;
for (i = 0; i < strlen(a); i++)
{
cout << a[i];
}
cout << endl;
return 0;
}
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
char str[10] = "hello";
for(char ch:str)
{
cout << ch << ' ';
}
return 0;
}
运行结果:
补充:
在C++11中引入了 范围for循环
(range-based for loop) ,用来简化对容器的遍历。char []
作为一个字符字符数组,支持这种简化遍历方式。
提示:在DevC++
中使用gcc4.9
编译器时,默认是不支持范围for循环的,使用范围for循环的时候要在百年一起选项中加入-std=c++11
这条命令。
1.5 strcpy
和 strcat
strcpy
和 strcat
是两个用于操作字符串的标准库函数,分别用于字符串复制和字符串拼接。它们的原型定义在 <string.h>
头文件中。
1.5.1 strcpy 函数
char *strcpy(char *dest, const char *src);
参数和返回值
dest
:目标字符串的指针,用于存储复制后的字符串。必须指向一块足够大的内存空间以容纳源字符串及其结束符\0
。src
:源字符串的指针,表示要复制的字符串。- 返回目标字符串
dest
的指针。这种返回值允许链式操作,例如将多个 字符串操作函数 组合在一行代码中使用。
功能
功能
将 src
指向的字符串内容复制到 dest
,包括字符串的结束符 \0
。原来 dest
中的内容会被覆盖。
注意事项
dest
的内存空间必须足够大,至少需要strlen(src) + 1
字节,否则可能导致缓冲区溢出。src
和dest
不能重叠(不能指向同一个数组,也不能有地址重叠,这和 strcpy 的底层实现原理有关,如果地址重叠,在拷贝的时候会覆盖掉重叠的数据发生数据丢失),重叠会导致未定义行为。
示例代码
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, World!";
char dest[50];
strcpy(dest, src);
printf("复制后的字符串:%s\n", dest);
return 0;
}
输出:
复制后的字符串:Hello, World!
1.5.2 strcat 函数
函数原型
char *strcat(char *dest, const char *src);
参数和返回值
dest
:目标字符串的指针,表示要拼接到的字符串。dest
必须有足够的空间以容纳拼接后的字符串。src
:源字符串的指针,表示要拼接的字符串。- 返回目标字符串
dest
的指针。
功能
将 src
指向的字符串内容追加到 dest
指向的字符串末尾,并在拼接后自动添加字符串结束符 \0
。
注意事项
dest
必须有足够的空间以容纳拼接后的字符串,总大小应至少为strlen(dest) + strlen(src) + 1
字节。- 如果
dest
和src
有重叠,行为是未定义的。 - 拼接操作会从
dest
的第一个\0
位置开始,因此dest
必须是一个以\0
结尾的有效字符串。
示例代码
#include <stdio.h>
#include <string.h>
int main() {
char dest[50] = "Hello, ";
char src[] = "World!";
strcat(dest, src);
printf("拼接后的字符串:%s\n", dest);
return 0;
}
输出:
拼接后的字符串:Hello, World!
注意: 这两个函数均不进行边界检查,使用时需确保内存安全,推荐在需要安全性时使用 strncpy
和 strncat
,它们和原本的函数功能和参数大致是一样的,只是在最后加了一个参数用来控制拷贝或者追加的字符个数。
放一张函数原型,非常简单,会使用就行。
char *strncpy( char *strDest, const char *strSource, size_t count );
char *strncat( char *strDest, const char *strSource, size_t count );
//count为拷贝或者追加的字符个数
但是使用 strncpy
时请注意:函数并不会在拷贝后的字符串的末尾自动加上 \0
,
#include <iostream>
#include <cstring>
using namespace std;
int main()
{
char ch1[10] = "abcdef";
char ch2[10];
strncpy(ch2, ch1, 6);
cout << ch2 << endl;
return 0;
}
示例代码调试
输出信息
这样在没有 \0 的情况下打印 ch2 字符串的内容会打印出各种随机值,锟斤拷烫烫烫。
但是如果我一开始就对 ch2 数组显式初始化了,那么 ch2 中存储的全部都是 0 ,这时再从 ch1 拷贝数据到 ch2 中时,就不会出现没有结束标志的情况了,由此可见随手初始化的重要性,这也是一种良好的编程习惯。
字符数组部分就到这里了,讲了这么多,总是绕不开数组长度
的控制和\0
问题,这就是字符数组储存字符串的弊端。于是,在C++中封装了 string 这个类来表示字符串,虽然他不是基本数据类型,但是内置了丰富的方法,使得其使用起来非常方便。接下来我们讲解 string 。
二、C++ 中的 string
string
是 C++ 标准库中的一个类,用于处理字符串。与传统的 字符数组(char[]
)相比,string
提供了更灵活和安全的操作,能够自动管理内存、支持动态扩展,并且提供了丰富的成员函数来简化字符串的处理。
2.1 string
概念
C++ 中将字符串视作一种类型(string
),由 string
关键字创建的对象就是 C++ 中的字符串。但是要明确的是 string
并不是基本数据类型,string str;
和 int a;
创建变量是有区别的,前者是由类创建了一个对象,后者是基本数据类型(int
)变量的声明。
string
封装了对字符数组的管理,并提供了多种操作字符串的方法。使用 string
可以避免许多手动内存管理的麻烦,支持更丰富的字符串操作。
2.2 string
的常见操作
2.2.1 创建和初始化字符串
string
支持多种创建和初始化方式:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str1; // 默认构造,创建空字符串
string str2("Hello"); // 使用字符串字面量初始化
string str3 = "World"; // 使用赋值初始化
string str4(str2); // 使用另一个字符串初始化
cout << str1 << " " << str2 << " " << str3 << " " << str4 << endl;
return 0;
}
示例代码调试:
像 str2 一样, string 字符串不再以 \0 作为结束标志。
2.2.2 字符串的输入
可以使用 cin
或 getline()
从标准输入中读取字符串:
2.2.2.1 输入没有空格的字符串使用cin
可以直接使用 cin >>
来给一个 string
类型的变量输入一个字符串数据并存储。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str;
cin >> str; //输入
cout << str << endl; //输出
return 0;
}
输入1:abc 输入2:abc def
输出1:abc 输出2:abc
还是同 字符数组的输入 一样,cin
无法输入含有空格的字符串。
2.2.2.2 输入含有空格的字符串使用getline
getline
可以在输入流中读取一整行文本,并将其储存为字符串。
getline
有两种定义,区别是第二种多了一个参数用来控制getline
读取文本的结束标志。
istream& getline (istream& is, string& str);
istream& getline (istream& is, string& str, char delim);
补充:
istream 是输入流类型, cin 是 istream 类型的标准输⼊流对象。
ostream 是输出流类型, cout 是 ostream 类型的标准输出流对象。
参数:
- getline 函数是在输入流中读取一行文本信息,所以如果是在标准输入流(键盘)中读取数
据,就可以传 cin 给第一个参数。 - str:存放读取到的信息的字符串。
- delim:自定义的结束标志。
示例代码1:
//代码1
#include <iostream>
#include <string>
using namespace std;
int main ()
{
string name;
getline (cin, name); //默认以'\n'为结束标志
cout << name << endl;
return 0;
}
运行结果1:
示例代码2:
//代码2
#include <iostream>
#include <string>
using namespace std;
int main ()
{
string name;
getline (cin, name, 'x'); //自定义以字符'x'为结束标志
cout << name << endl;
return 0;
}
运行结果2:
2.2.3 length()
和 size()
length()
和 size()
是 string 的内置方法,需要调用内置方法时,就要使用.
点运算符。
演示代码:
#include <iostream>
#include <string> //添加string头⽂件
using namespace std;
int main()
{
string s1;
string s2 = "hello";
string s3 = "hello world";
string s4 = "Dante hello world";
cout << "s1:" << s1.size() << endl;
cout << "s2:" << s2.size() << endl;
cout << "s3:" << s3.size() << endl;
cout << "s4:" << s4.size() << endl;
cout << "s1:" << s1.length() << endl; //注意是length,不要写成了lenth
cout << "s2:" << s2.length() << endl;
cout << "s3:" << s3.length() << endl;
cout << "s4:" << s4.length() << endl;
return 0;
}
可以通过 .size()
获取字符串的长度,然后通过循环遍历字符串的每个字符。
#incldue <iostream>
#include <string>
using namespace std;
int main()
{
string s = "abcdef";
int i = 0;
for(i = 0; i < s.size(); i++)
{
cout << s[i] << " ";
}
return 0;
}
注意:string
类型的字符串是可以通过下标(索引)访问每个元素的,每个元素的类型是 char
,string
其实是一个字符序列,内部存储的是一系列的字符。
补充1:通过下标访问 string
时,如果索引超出了字符串的有效范围(即索引大于等于 str.size()
),会导致未定义行为。为了避免这种情况,你可以使用 at()
方法,它会在越界时抛出异常。
at()
示例代码:
try {
char ch = str.at(20); // 如果索引超出范围,将抛出 std::out_of_range 异常
} catch (const std::out_of_range& e) {
std::cout << "Out of range error: " << e.what() << std::endl;
}
补充2:string
是可修改的:如果你希望修改字符串中的字符,可以通过下标来直接修改它。
修改string
示例代码:
std::string str = "Hello";
str[0] = 'h'; // 修改第一个字符
std::cout << str << std::endl; // 输出: hello
2.2.4 迭代器(iterator)
迭代器
是 C++ STL(标准模板库)中的一个重要概念,用于遍历容器(如数组、链表、集合、映射等)中的元素。它类似于指针(可以用指针来理解,通过迭代器访问它指向的元素的时候需要解引用),可以用来访问容器中的元素,并且提供了统一的接口来遍历各种类型的容器(迭代器的操作和方法适用于支持迭代器的所有容器)。
string
作为一个序列容器,支持迭代器。这些迭代器可以让你访问字符串中的每个字符。
以下是 string
类型中迭代器的常见操作:
2.2.4.1 获取迭代器(适用于所有类型容器的迭代器)
- begin():返回指向容器第一个元素的迭代器。
- end():返回指向容器尾部之后位置的迭代器(一个虚拟位置,表示遍历结束)。
- rbegin():返回指向容器最后一个元素的反向迭代器。
- rend():返回指向容器开头之前位置的反向迭代器(这个位置也是虚拟的,在第一个元素之前)。
2.2.4.2 遍历操作
string
中的迭代器是可以进行加减整数运算的,也可以进行大小比较。同一个容器中的两个迭代器是可以相减,结果的绝对值表示两个迭代器之间的距离(元素个数)。
以下是常见可行操作,不要把迭代器想的很神秘,我们先熟悉迭代器支持什么运算,到后面用的熟练了就熟悉它的特性了。
- 递增递减运算符:
it++
将迭代器移动到下一个元素,it--
将迭代器移动到下一个元素。 - 解引用运算符:用
*
访问迭代器当前指向的元素。迭代器是不能直接访问的,要想进行修改、打印等操作必须解引用当前迭代器。 - 比较操作:如
it != end()
,it < end()
用来判断是否到达迭代器的结束位置。 - 可以通过迭代器修改容器中的元素( string 允许修改),例如
*it = value
。
示例代码1(通过调取迭代器,迭代器自增,迭代器比较来遍历string):
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Hello";
// 使用统一接口遍历
for (string::iterator it = str.begin(); it != str.end(); ++it) //这里的string::iterator类型可以写成auto,让程序自己判断类型
{
cout << *it << " ";
}
cout << endl;
return 0;
}
示例代码1运行结果:
示例代码2(遍历修改字符串内容):
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "abcdef";
cout << str << endl;
for (string::iterator it = str.begin(); it != str.end(); ++it)
{
*it = 'x';
}
cout << str << endl;
return 0;
}
示例代码2运行结果:
补充:在C++11中引入了 范围for循环
(range-based for loop) ,用来简化对容器的遍历。string
作为一个字符序列,支持这种简化遍历方式。
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = "hello";
for(char ch:s)
{
cout << ch << ' ';
}
return 0;
}
运行结果:
总结(string字符串的遍历方式):
-
传统 for 循环(基于索引)
string str = "example"; for (size_t i = 0; i < str.size(); ++i) { char ch = str[i]; cout << ch << " "; }
-
基于范围的 for 循环(C++11 及以上)
string str = "example"; for (char ch : str) { cout << ch << " "; }
-
使用迭代器
string str = "example"; // 使用普通迭代器(可对字符进行修改) for (string::iterator it = str.begin(); it != str.end(); ++it) { cout << *it << " "; } // 使用常量迭代器(只读) for (string::const_iterator it = str.cbegin(); it != str.cend(); ++it) { cout << *it << " "; }
-
反向迭代器(从后往前遍历字符串)
string str = "example"; // 使用反向迭代器 for (string::reverse_iterator it = str.rbegin(); it != str.rend(); ++it) { cout << *it << " "; } // 使用常量反向迭代器 for (string::const_reverse_iterator it = str.crbegin(); it != str.crend(); ++it) { cout << *it << " "; }
-
索引方式与范围结合(C++20 起更高效,结合索引和值操作)
string str = "example"; for (size_t i = 0; auto& ch : str) { cout << "Index " << i++ << ": " << ch << endl; }
重点(命名空间前缀讲解):
-
iterator
要想在一个程序中使用是要使用命名空间前缀的,如果这个程序没有使用任何namespace
那么 iterator 类型应该这样写std::string::iterator
,这里使用了完整的命名空间前缀,因为iterator
是 –std::string
定义的一种类型,必须通过std::string::iterator
来访问。
Q:为什么要使用std::string::
前缀?
A:iterator 是 std::string 类定义的嵌套类型。如果不提供 std::string:: 的限定,编译器无法确定你是指哪个 iterator。std::vector 也有自己的 iterator,即 std::vector::iterator。如果你只写 iterator,编译器无法知道是 std::string::iterator 还是其他容器的 iterator。 -
如果我们使用了
using namespcae std;
那么使用iterator
类型就可以省略std::
写成string::iterator
。 -
std::string::iterator
中的string
是不可省略的,只能通过using
或者typedef
在全局或函数内来为std::string::iterator
创建一个类型别名。// 今天是圣诞节,Merry Christmas各位 #include <iostream> #include <string> int main() { using StringIterator = std::string::iterator; //为std::string::iterator创建别名 //using也可以写到全局 std::string str = "Hello, World!"; for (StringIterator it = str.begin(); it != str.end(); ++it) { std::cout << *it << " "; } return 0; }
2.2.5 push_back()
和 pop_back()
2.2.5.1 push_back()
函数原型:
void push_back(char ch);
功能:
push_back()是一个尾插函数,实现在 string 内部,作用是将一个字符插到字符串的末尾。
在使用 push_back() 的时候,会发生那些事呢?
- 将一个字符添加到字符串的末尾。
- 修改字符串的大小(size() 增加 1)。
- 如果字符串的容量不足以容纳新字符,会自动扩容。
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Hello";
// 在字符串末尾添加字符
str.push_back('!');
cout << str << endl; // 输出:Hello!
return 0;
}
注意:如果需要添加一个完整的字符串(而不是单个字符),请使用 operator+= 或 append()
std::string str = "Hello";
str += " World"; // 添加字符串
str.append("!!!!"); // 添加字符串
2.2.5.2 pop_back()
函数原型:
void pop_back();
功能:
- 删除字符串末尾的一个字符。
- 修改字符串的大小(size() 减少 1)。
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Hello!";
// 删除字符串末尾的一个字符
str.pop_back();
cout << str << endl; // 输出:Hello
return 0;
}
注意:调用 pop_back() 前,字符串不能是空的,否则会导致未定义行为。
安全的写法:
if (!str.empty()) //检查非空,非空empty()返回false,空返回true
{
str.pop_back();
}
或者:
if (s.size() > 0) //通过size()函数来得到字符串的⻓度
{
s.pop_back();
}
如果需要获取再删除最后一个字符,可以使用以下方式:
char lastChar = str.back(); // 获取最后一个字符
str.pop_back(); // 删除最后一个字符
2.2.6 字符串的加法运算
string
类型的字符串是支持 +
和 +=
操作的,其原理就是重载了 operater+=
这个操作符。
注意:字符串支持加法运算,既可以在一个字符串后面或者前面加上单个字符
,也可以加上一个string类型的字符串
,也可以加上C语言风格的字符串
(直接用双引号引起来的字符串
),也可以加上一个用字符数组表示的字符串
。
很多人都以为加法不能加单个字符,这是错误的。
当加单个字符时:字符串 + 单个字符
就等同于 字符串.push_back()
;
当加上多个字符时,这就是字符串的加法运算的特色。
Q:有一个非常模糊的点,我们常常会将字符串与一个(用char强转后的)整数相加(不强转会报错),这时候到底加上的是这个整数的字符,还是这个整数在ASCII值中对应的字符?
A:如果这个整数在ASCII值范围内(常见范围0-127),就转换成对应的字符。如果超出了范围就是无意义的操作。要想加上这个整数(int 类型)对应的字符串(string 类型),就要用 to_string
函数进行转换。
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "123"; //给定一个初始字符串,接下来都是在它身上做加法试验
string s1 = str + char(1); //加上一个强转后的1,相当于加上一个空字符
cout << s1 << endl;
s1 = str + char(97); //加上一个强转后的97,相当于加上一个字符'a'
cout << s1 << endl;
s1 = str + char(49); //加上一个强转后的49,相当于加上一个字符'1'
cout << s1 << endl;
cout << endl;
string s2 = str + to_string(1); //用to_string函数将1转换成了字符'1'
cout << s2 << endl;
cout << endl;
char ch_arr[] = "abcdef";
string s3 = str + ch_arr; //string同样可以加上一个字符数组
cout << s3 << endl;
cout << endl;
string s4 = str + "csdn"; //string同样可以加上一个C语言风格的字符串
cout << s4 << endl;
return 0;
}
2.2.7 insert()
功能
insert() 函数用于在字符串的指定位置插入字符或子字符串。
函数原型(std::string::insert)
string& insert(size_t pos, const string& str); // 插入字符串
string& insert(size_t pos, const char* s); // 插入C语言风格字符串
string& insert(size_t pos, size_t n, char c); // 插入多个字符
string& insert(size_t pos, const string& str, size_t subpos, size_t sublen); // 插入字符串的一部分
参数
- pos:插入位置的索引 (从 0 开始)。
- str:要插入的字符串。
- s:C语言风格的字符串(const char*)。
- n:要插入的字符数。
- c:要插入的字符。
- subpos 和 sublen:表示插入的子字符串的起始位置和长度。
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "Dante 798";
string s = " and";
str.insert(5,s);
cout << str << endl; //插入string,想要插入单个字符时,string内就存储单个字符
str = "Dante 798";
str.insert(0,"Hello "); //插入C语言风格的字符串
cout << str << endl;
str = "Dante 798";
str.insert(3,5,'x'); //在"Dan"后面插入5个'x'
cout << str << endl;
str = "Dante 798";
string sub_str = "Hello world!"; //想要将"world"插入"Dan"后面
str.insert(3,sub_str,6,5); //从"Hello world!"的下标6开始往后截取5个长度
cout << str << endl;
return 0;
}
运行结果:
解释(&引用操作): string&
的意思是该函数返回一个 std::string
类型的引用,而不是一个新的 std::string
对象的副本。返回引用意味着函数返回的是原始对象的别名,因此可以通过返回的引用直接修改原始对象的内容。就类似于C语言中返回值为 char*
、int*
等指针 ,返回原本数据类型的地址,而不是创建一个副本,这样以来就可以访问和修改原本数据类型中的数据。无论是 C 语言中的指针,还是 C++ 中的引用,它们都允许你访问或修改原始对象。
指针与引用的区别:
-
指针可以为空,但引用必须初始化到一个有效的对象: 指针可以指向
nullptr
,表示它不指向任何有效的内存地址。引用不能像其他类型那样仅仅进行变量声明
,引用只要被定义就必须要显式初始化
,引用必须绑定到一个有效的对象,不能为空。int* p = NULL;
表示指针不指向任何地方。int& r = x;
必须绑定到一个有效的对象 x。
-
指针可以修改指向的对象,但是引用不能被修改: 指针变量本身可以修改,改变它指向不同的内存地址。
int* p = &a; p = &b;
可以让 p 指向不同的内存地址。- 引用不能修改指向的对象,一旦引用绑定到某个对象后,就无法再改变它引用其他对象。
int& ra = a; ra = b;
这个语句的意思不是将ra
从引用a
修改成引用b
,而是将b
的值赋给a
。
-
引用更加安全,并且在函数传参中引用更快:
引用在定义时必须绑定到一个有效的对象,而指针可能是空的,容易出现空指针错误(null pointer)。引用的使用语法更接近于直接操作对象,更加直观和安全。
我们知道在给函数传实参的时候,形参是实参的一份临时拷贝,这样以来,就会造成临时变量内存的开销,在时间和空间上都有损耗,而采用引用传参的话,就是直接对原变量进行操作,没有多余空间的开辟。 -
指针需要解引用,引用则直接使用: 指针需要使用 * 操作符来解引用访问指向的对象。
int* p = &a; *p = 10;
通过解引用修改a
。- 引用则直接使用,不需要解引用。
int& r = a; r = 10;
直接修改a
。
补充(NULL 和 nullptr):
特性 | NULL | nullptr |
---|---|---|
类型 | 它是一个宏,通常定义为 0 ,是一个整数常量 | 它是一个关键字,类型是 std::nullptr_t |
类型安全 | NULL 可能导致类型不匹配的问题,特别是在重载函数中 | nullptr 具有类型安全性,避免了类型不匹配的问题 |
语法 | NULL 可以与 0 互换使用,可能不清晰 | nullptr 是明确的空指针表示法 |
与整数的比较 | NULL 是 0 ,在某些情况下可能与整数 0 混淆 | nullptr 不是整数,不能直接与整数 0 比较 |
推荐使用 | 在现代 C++ 中不推荐使用 NULL | 推荐使用 nullptr ,尤其是在 C++11 及以上版本 |
2.2.8 find()
find
函数用于查找子字符串或字符在主字符串中的位置。如果找到匹配项,则返回匹配的位置的起始下标;如果找不到,则返回 std::string::npos
,npos
是 string 中定义的一个静态常量,是一个 size_t
类型的值。我们常用 find
函数的返回值与 npos
比较,看是否相等,来判断是否找到了目标。
npos定义
static const size_t npos = -1;
find()函数原型
size_t find(const string& str, size_t pos = 0) const; // 查找子字符串
size_t find(const char* s, size_t pos = 0) const; // 查找 C 风格字符串
size_t find (const char* s, size_t pos, size_t n) const; //从字符串的pos这个位置开始向后查找C⻛格的字符串s中的前n个字符,
size_t find(char c, size_t pos = 0) const; // 查找单个字符
参数
- pos:开始查找的位置,默认为 0。
- str:要查找的子字符串。
- s:C 风格字符串(const char*)。
- c:要查找的字符。
示例代码(查找字符串):
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Hello C++ World";
size_t pos = str.find("C++");
if (pos != string::npos)
{
cout << pos << endl; // 输出: 6
}
else
{
cout << "'C++' not found" << endl;
}
return 0;
}
示例代码(查找字符):
string str = "Hello World";
size_t pos = str.find('o');
if (pos != string::npos)
{
cout << pos << endl; // 输出: 4
}
2.2.9 substr
substr
函数用于截取字符串的一部分,返回一个新的字符串。
函数原型
string substr(size_t pos = 0, size_t len = npos) const;
参数
- pos:开始截取的位置 (默认值为 0)。
- len:截取的长度(默认值为 npos,表示一直截取到字符串末尾)。
用法:
substr() :如果函数不传参数,就是从下标为0的位置开始截取,直到结尾,得到的是整个字符串;
substr(pos) :从指定下标 pos 位置开始截取子串,直到结尾;
substr(pos, len) :从指定下标 pos 位置开始截取长度为 len 的子串。
示例代码
#include <iostream>
#include <string>
using namespace std;
int main() {
string str = "Hello C++ World";
string sub = str.substr(6, 3); // 从位置6开始,截取3个字符
cout << sub << endl; // 输出:C++
return 0;
}
2.3 string的关系运算
在 C++ 中,std::string
支持一系列的关系运算符比较运算符,可以用来对两个字符串进行比较。这些关系运算符包括:==、!=、<、<=、>、>= 。
//(1) s1 == s2
bool operator== (const string& lhs, const string& rhs);
bool operator== (const char* lhs, const string& rhs);
bool operator== (const string& lhs, const char* rhs);
//(2) s1 != s2
bool operator!= (const string& lhs, const string& rhs);
bool operator!= (const char* lhs, const string& rhs);
bool operator!= (const string& lhs, const char* rhs);
//(3) s1 < s2
bool operator< (const string& lhs, const string& rhs);
bool operator< (const char* lhs, const string& rhs);
bool operator< (const string& lhs, const char* rhs);
//(4) s1 <= s2
bool operator<= (const string& lhs, const string& rhs);
bool operator<= (const char* lhs, const string& rhs);
bool operator<= (const string& lhs, const char* rhs);
//(5) s1 > s2
bool operator> (const string& lhs, const string& rhs);
bool operator> (const char* lhs, const string& rhs);
bool operator> (const string& lhs, const char* rhs);
//(6) s1 >= s2
bool operator>= (const string& lhs, const string& rhs);
bool operator>= (const char* lhs, const string& rhs);
bool operator>= (const string& lhs, const char* rhs);
string
通过对关系运算符进行重载,使得 string 和 string
,string 和 C语言风格字符串
,C语言风格字符串 和 string
之间都能相互比较。
string
大小的比较本质上是从第一个字符开始向后依次比较每个字符的ASCII值大小。
例如:
"abc" < "abd" //'c'的ASCII值比'd'小
"100" < "5" //'1'的ASCII值比'5'小
"abcdefgh" < "abcdz" //'e'的ASCII值比'z'小
示例代码:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s1 = "abcd";
string s2 = "abbcdef";
char s3[] = "bbc";
if (s1 > s2)
cout << "s1 > s2" << endl;
else
cout << "s1 <= s2" << endl;
if (s1 == s2)
cout << "s1 == s2" << endl;
else
cout << "s1 != s2" << endl;
if (s1 <= s3)
cout << "s1 <= s3" << endl;
else
cout << "s1 > s3" << endl;
return 0;
}
2.4 和string相关的函数
stoi
、stol
、stod
、stof
和 to_string
都是用来进行字符串与数字之间的转换的函数。它们提供了将字符串转换为数值类型,或将数值转换为字符串的功能。
2.4.1 stoi
(string to int)
stoi
函数用于将 字符串 转换为 整数(int)。
函数原型:
int stoi(const std::string& str, size_t* pos = 0, int base = 10);
参数
- str:要转换的字符串。
- pos:可选参数,不需要的时候可以不填也可以传空指针。pos 是一个输出型参数,它是一个指针,需要在函数外创建一个size_t类型的变量,并取地址传给pos参数,stoi函数会在内部通过这个指针修改指针指向的外面的这个变量,来达到输出数据的效果,这就是输出型参数。这个参数会返回第一个
字符未被转换成数字
的位置。比如说"1457abc123"
,stoi
匹配到'a'
就无法继续匹配下去了,因为a
无法转换成数字。如果不需要这个信息,可以忽略它。 - base:可选参数,不需要改变的时候可以不填也可以填默认值10。base 用来指定进制,默认值为 10,表示解析的字符串是十进制。
如果传递的是 2,表示被解析的字符串中是 2 进制的数字,最终会转换成 10 进制。
如果传递的是 8,表示被解析的字符串中是 8 进制的数字,最终会转换成 10 进制。
如果传递的是 16,表示被解析的字符串中是 16 进制的数字,最终会转换成 10 进制。
如果传递的是 0,会根据字符串的内容的信息自动推导进制,比如:字符串中有0x
,就认为是 16 进制;如果是以0
开头,会被认为是 8 进制,最终会转换成 10 进制。
不管传什么值,这个函数最终会将被解析的字符串转换为 10 进制的数字。
示例代码1
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "12345";
int num = stoi(str); //也可以这样写:int num = stoi(str,NULL,10);
cout << "Converted integer: " << num << endl; // 输出:Converted integer: 12345
return 0;
}
示例代码2(将x进制的数字转换为十进制数)
#include <iostream>
#include <string>
using namespace std;
int main()
{
int x;
string s;
cin >> x >> s; //输入一个整数x表示当前进制 和 一个字符串s表示被解析的数字(以字符串格式接收)
cout << stoi(s,NULL,x); //转换为十进制数
return 0;
}
模拟实现 stoi(仅实现base参数,不包括pos的实现)
#include <iostream>
#include <string>
#include <cmath>
using namespace std;
void change(string s,int base) // 1 =< base <= 36
{
int sz = s.size();
int j=0;
int ret = 0;
for(int i=sz-1;i>=0;--i)
{
if(s[i]<='9')
{
ret += (s[i]-'0')*pow(base,j++); //基于十进制各个数位的权重,处理原字符串中的每个数位再相加到一起
}
else
{
ret += (s[i]-'A'+10)*pow(base,j++);
}
}
cout << ret;
}
int main()
{
int base;
string s;
cin >> base >> s;
change(base,s);
return 0;
}
注意:
- 如果字符串无法转换为有效的整数,stoi 会抛出一个 invalid_argument 异常。
- 如果转换结果超出了 int 类型的范围,stoi 会抛出 out_of_range 异常。
2.4.2 stol
(string to long)
stol
函数用于将 字符串 转换为 长整型(long)。
函数原型:
long stol(const string& str, size_t* pos = 0, int base = 10);
参数
见stoi
参数部分。
示例代码
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "123456789012345";
long num = stol(str);
cout << "Converted long: " << num << endl; // 输出:Converted long: 123456789012345
return 0;
}
2.4.3 stod
(string to double) / stof
(string to float)
stod 函数用于将 字符串 转换为 双精度浮点数(double)。
stof 函数用于将 字符串 转换为 浮点数(float)。
函数原型:
double stod(const string& str, size_t* pos = 0);
float stof(const string& str, size_t* pos = 0);
参数
str:要转换的字符串。
pos:可选参数,指向一个位置,表示第一个未被转换的字符的位置。
示例代码
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str = "123.45";
double num = stod(str);
cout << "Converted double: " << num << endl; // 输出:Converted double: 123.45
return 0;
}
2.4.4 to_string
(number to string)
to_string 可以将多种数值类型(如 int、long、double 等)转换为 string
函数原型
string to_string(int val);
string to_string(long val);
string to_string(long long val);
string to_string(unsigned val);
string to_string(unsigned long val);
string to_string(unsigned long long val);
string to_string(float val);
string to_string(double val);
string to_string(long double val);
示例代码
#include <iostream>
#include <string>
using namespace std;
int main()
{
int num = 123;
string str = to_string(num);
cout << "Converted string: " << str << endl; // 输出:Converted string: 123
return 0;
}
注意: 该函数不会抛出异常,但如果你传入了无法表示的数值(例如无法表示的数值Not a Number或者无穷大Infinity对于浮点数),它将返回特定的字符串(如 “nan” 或 “inf”)。
#include <iostream>
#include <string>
using namespace std;
int main()
{
double num1 = 0.0 / 0.0; // 产生 NaN
double num2 = 1.0 / 0.0; // 产生 Infinity
cout << "NaN as string: " << to_string(num1) << endl; // 输出:nan
cout << "Infinity as string: " << to_string(num2) << endl; // 输出:inf
return 0;
}