第4章 复合类型——字符串(一)C风格字符串

本文章是作者根据史蒂芬·普拉达所著的《C++ Primer Plus》而整理出的读书笔记,如果您在浏览过程中发现了什么错误,烦请告知。另外,此书由浅入深,非常适合有C语言基础的人学习,感兴趣的朋友可以自行阅读此书籍。

字符串

字符串是存储在内存的连续字节中的一系列字符。

C++处理字符串的方式有两种:

  1. 来自于C语言的C风格字符串。
  2. 基于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";

在数组中使用字符串

要将字符串存储到数组中,最常用的方法有两种:
  1. 将数组初始化为字符串常量
  2. 将键盘或文件输入读入到数组中
#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()的操作。

posted @ 2024-03-07 19:32  superbmc  阅读(22)  评论(0编辑  收藏  举报