第4章 复合类型——字符串(一)C风格字符串
本文章是作者根据史蒂芬·普拉达所著的《C++ Primer Plus》而整理出的读书笔记,如果您在浏览过程中发现了什么错误,烦请告知。另外,此书由浅入深,非常适合有C语言基础的人学习,感兴趣的朋友可以自行阅读此书籍。
字符串
字符串是存储在内存的连续字节中的一系列字符。
C++处理字符串的方式有两种:
- 来自于C语言的C风格字符串。
- 基于string类库的方法。
本文首先了解C风格字符串。
C风格字符串
基础知识
存储在连续字节中的一系列字符意味着可以将字符串存储在char数组中,其中每个字符都位于自己的数组元素中。 C风格字符串具有一种特殊的性质:以空字符(null)结尾,空字符被写作'\0'(读作“反斜杠0”),其ASCII码也为0,用来标记字符串的结尾。 例如,如下两个声明: ``` char dog[5] = {'g','o','u','z','i'}; //不是个字符串,但是字符数组 char cat[5] = {'m','i','a','o', '\0'}; //是字符串,也是字符数组 ```这两个数组都是char数组,但只有第二个数组是字符串。空字符对C风格字符串而言至关重要。
例如,C++有很多处理字符串的函数,其中包括cout使用的那些函数。他们都逐个地处理字符串中的字符,直到到达空字符为止。
但这种使用单引号的方式,实在令人难以忍受。不仅要将每个字符都带上单引号,还需要记住末尾的'\0'。幸好,有更加方便的写法:
char cat[5]="miao";
char dog[]="gouzi";
用括号括起的字符串隐式地包括结尾的空字符,因此不用显式地包括它。但因为'\0'要占一位,因此,cat这个字符数组要想成为字符串,那么就最大只能存储4个字符。dog没有限制大小,它的大小由编译器在编译阶段确认。这两种字符串也被称为字符串常量。
需要注意的是,字符串常量(使用双引号)不能与字符常量(使用单引号)互换。
如下面的示例:
char ch = 'S'; //正确
char ch = "S"; //错误
"S"是两个字符'S'和'\0'组成的字符串,另外它实际表示的是字符串所在的内存地址,在C++中,地址是一种独立的类型,因此编译器不允许这种错误的做法。
将第二句修改成下面的样子:
char *p = "s";
这个时候p是一个char类型的指针,它可以指向一个地址,而"s"正好表示了字符串所在的内存地址,因此这种做法是正确的。
拼接字符串常量
C++允许拼接字符串常量,即将两个用双引号括起的字符串合并为一个。任何两个由空白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个。如下,所有的语句都是等效的:
cout << "I’d like to watch The Jackie Chan Adventures\n";
cout << "I’d like to watch " "The Jackie Chan Adventures\n";
cout << "I’d like to watch "
"The Jackie Chan Adventures\n";
在数组中使用字符串
要将字符串存储到数组中,最常用的方法有两种:- 将数组初始化为字符串常量
- 将键盘或文件输入读入到数组中
#include <iostream>
#include <cstring> //为了使用c风格函数strlen
using namespace std;
int main()
{
const int Size = 10;
char name1[Size] = "gouzi";
char name2[Size];
cout << "输入:";
cin >> name2;
cout << "name1: " << name1 << endl;
cout << "name2: " << name2 << endl;
cout << "sizeof(name1): " << sizeof(name1) << endl;
cout << "sizeof(name2): " << sizeof(name2) << endl;
cout << "strlen(name1): " << strlen(name1) << endl;
cout << "strlen(name2): " << strlen(name2) << endl;
name1[0] = 'm';
name1[1] = 'i';
name1[2] = 'a';
name1[3] = 'o';
name1[4] = '\0';
cout << "name1: " << name1 << endl;
name2[1] = '\0';
cout << "name2: " << name2 << endl;
return 0;
}
运行程序后结果如下:
输入:miao
name1: gouzi
name2: miao
sizeof(name1): 10
sizeof(name2): 10
strlen(name1): 5
strlen(name2): 4
name1: miao
name2: m
...
我们看到这两个数组的sizeof()计算出来的大小一样,1个char在内存中占1个字节,Size是10,所以占10个字节。但是strlen("miao")和strlen("gouzi")的实际长度不一样,一个是4,一个是5,需要注意的是,它们都没有算'\0',因此,使用strlen计算数组长度来开辟空间的话,需要strlen(str) + 1个长度,为'\0'预留一位。
另外,由于name1和name2都是数组,因此可以使用索引来访问数组中各个字符。例如可以name1的前5个字节都替换成'm','i','a','o','\0',也可以单独将name2的第2个字节替换成'\0',这就打印name2,就只会打印第一个字节了。
字符串输入
关于cin的隐藏问题,看如下示例:#include <iostream>
using namespace std;
int main()
{
const int Size = 10;
char name1[Size];
char name2[Size];
cout << "inpur name1: " << endl;
cin >> name1;
cout << "inpur name2: " << endl;
cin >> name2;
cout << "name1: " << name1 << endl;
cout << "name2: " << name2 << endl;
return 0;
}
运行程序后结果如下:
inpur name1:
> gou zi
inpur name2:
name1: gou
name2: zi
两个疑问:为什么第一个name1打印出来是个"gou",而不是"gou zi",第二个不让输入,为什么打印了个"zi"?
原来cin使用空白(空格、制表符和换行符)来确定字符串的结束位置。这意味着cin在获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。
上面的示例就是,如果输入了"gou zi",第一个cin读到了"gou",接着读到了后面的第一个空格,那么就把"gou"赋值给name1,"zi"仍然留在输入队列中,第二个cin会继续从输入队列中获取到"zi",同时也获取到了第一次输入"gou zi"时末尾的换行符,那么就把"zi"赋值给了name2, 因此继续往下执行。
那么有什么办法可以解决这个问题呢?
每次读取一行字符串输入
我们希望使用cin的时候能存储完整的字符串,比如"gou zi"就是"gou zi",而不是"gou"。因此我们需要采用另一种字符串读取方法,面向行而不是面向单词。istream中的类提供了一些面向行的类成员函数:getline()和get(),下面将详细介绍它们:
1,面向行的输入: getline()
geline读取整行,它使用通过回车键输入的换行符来确定输入结尾。使用方式为:
cin.getline(参数1,参数2)
//参数1是用来存储输入行的数组的名称,参数2是要读取的字符数。
如下示例:
#include <iostream>
using namespace std;
int main()
{
const int Size = 10;
char name1[Size];
char name2[Size];
cout << "inpur name1: " << endl;
cin.getline(name1, Size);
cout << "inpur name2: " << endl;
cin.getline(name2, Size);
cout << "name1: " << name1 << endl;
cout << "name2: " << name2 << endl;
return 0;
}
运行程序后结果如下:
inpur name1:
> gou zi
inpur name2:
> miao
name1: gou zi
name2: miao
getline()函数每次读取一行。它通过换行符来确定行尾,但不保存换行符。同时在存储字符串时,它用空字符来替换换行符。
2,面向行的输入:get()
istream类有另一个名为get()的成员函数,该函数有几种变体。其中一种变体的工作方式与getline()类似,格式与getline()也一致。不同的是,get()不读取并丢弃换行符,而是将其留在输入队列中。假设连续两次调用get():
cin.get(name1, Size);
cin.get(name2, Size); //有问题
由于第一次调用后,换行符将留在输入队列中,因此第二次调用时看到的第一个字符便是换行符。因此get()认为已到达行尾,而没有发现任何可读取的内容。
幸运的是,get()有另一种变体。使用不带任何参数的cin.get()调用可读取下一个字符,因此可以用它来处理换行符,为读取下一行做准备。因此可以这么做:
cin.get(name1, Size);
cin.get(); //处理换行符
cin.get(name2, Size);
cin.get(); //处理换行符
也可以将两个类成员函数拼接起来:
cin.get(name1, Size).get();
cin.get(name2, Size).get();
之所以可以这样做,是由于cin.get(name1, Size)返回一个cin对象,该对象随后将被用来调用cin.get()函数。getline()也可以这样操作:
cin.getline(name1, Size).getline(name2, Size);
下面的程序使用了函数拼接的方法:
#include <iostream>
using namespace std;
int main()
{
const int Size = 10;
char name1[Size];
char name2[Size];
cout << "inpur name1: " << endl;
cin.get(name1, Size).get();
cout << "inpur name2: " << endl;
cin.get(name2, Size).get();
cout << "name1: " << name1 << endl;
cout << "name2: " << name2 << endl;
return 0;
}
运行程序后结果如下:
inpur name1:
>gou zi
inpur name2:
>miao
name1: gou zi
name2: miao
cin.get()根据不同的参数,来确定是读取一串字符,还是读取一个字符(换行符),是利用了C++函数重载的特性。简单来说,C++允许函数有多个版本,条件是这些版本的参数列表不同。
那么问题来了, 我们要使用getline()还是get()?
如果我们想知道停止读取的原因,是读取了整行,还是因为数组已经被填满了。就需要用get(),因为get可以查看下一个输入字符,如果是换行符,说明已经读取了整行;否则说明该行中还有其他输入。总之,getline()使用起来更简单,但get()检查错误更简单。
3,空行问题
上面的程序有个问题,如果输入是个空行,当get()读取空行后将设置失效位,因此接下来的输入将被阻断。但可以使用下面的命令来恢复输入:
cin.clear();
因此可以改写程序:
#include <iostream>
using namespace std;
int main()
{
const int Size = 10;
char name1[Size];
char name2[Size];
cout << "inpur name1: " << endl;
cin.get(name1, Size);
if(cin.fail())
{
cin.clear();
}
cin.get();
cout << "inpur name2: " << endl;
cin.get(name2, Size);
if(cin.fail())
{
cin.clear();
}
cin.get();
cout << "name1: " << name1 << endl;
cout << "name2: " << name2 << endl;
return 0;
}
运行程序结果如下:
inpur name1:
inpur name2:
>miao
name1:
name2: miao
原理如下,在读取操作后,if(cin.fail())检查cin的状态。当cin遇到错误或者读到个空行时,其内部的eofbit或failbit标志位会被置为真,使得cin.fail()为真,接着进行cin.clear()的操作。
本文来自博客园,作者:superbmc,转载请注明原文链接:https://www.cnblogs.com/superbmc/p/18057713
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具