C-风格字符串
字符串是存储在内存的连续字节中的一系列字符。C++处理字符串的方式有两种。第一种来自C语言,常被称为C-风格字符串(C-style string);另一种是基于string类库的方法。
本文介绍C-风格字符串。
存储在连续字节中的一系列字符意味着可以将字符串存储在char数组中,其中每个字符都位于自己的数组元素中。C-风格字符串具有一种特殊的性质:以空字符(null character)结尾,空字符被写作\0,起ASCII码为0,用来标记字符串的结尾。例如:
char dog[8] = {'b','e','a','u','x',' ','I','I'}; //不是字符串
char cat[8] = {'f','a','t','e','s','s','a','\0'}; //是字符串
这两个都是char数组,但只有第二个是字符串。空字符对C-风格字符串至关重要。C++有很多处理字符串的函数,包括cout使用的那些函数,它们都逐个地处理字符串中的字符,直到发现空字符为止。
如果使用cout显示上面的dog数组(它不是字符串),cout将打印出数组中的8个字母,并接着将内存中随后的各个字节解释为要打印的字符,直到遇到空字符为止。
在cat数组示例中,将数组初始化为字符串的方法要使用大量的单引号,且必须记住加上空字符,这种方法十分麻烦。有一种更好的将字符数组初始化为字符串的方法——只需使用一个用双引号阔气的字符串即可,这种字符串被称为字符串常量(string constant)或字符串字面值(string literal),如下所示:
char bird[11] = "Mr. Cheeps"; //结尾自动包含\0
char fish[] = "Bubbles"; //让编译器自动计数
用双引号括起的字符串隐式地包含结尾的空字符,各种C++输入工具输入字符串到char数组中时,将自动加上结尾的空字符。
应确保数组足够大,能够存储字符串中的所有字符——包括空字符。使用字符串常量初始化字符数组时,让编译器自动计算元素数目更为安全。
字符串常量(使用双引号)与字符常量(使用单引号)不能互换。
字符常量(如’S’)是字符串编码的简写表示,而”S”不是字符常量,它表示字符S和\0组成的字符串。而且,”S”实际上表示的是字符串所在的内存地址。
char shirt_size = 'S'; //合理,将83赋给shirt_size
char shirt_size = "S"; //类型不匹配,试图将一个内存地址赋给shirt_size
1.拼接字符串常量
有时候,字符串很长,无法放到一行中。C++允许拼接字符串常量,讲讲两个用双引号括起的字符串合并为一个。事实上,任何两个由空白(空格、tab和换行符)分隔的字符创常量都将自动拼接成一个。因此,西面所有的输出语句都是等效的:
cout << "I'd give my right arm to be" " a great violinist.\n";
cout << "I'd give my right arm to be a great violinist.\n";
cout << "I'd give my right ar"
"m to be a great violinist.\n";
注意:拼接时不会在被连接的字符串之间添加空格,第二个字符串的第一个字符将紧跟在第一个字符串的最后一个字符(不考虑\0)后面。第一个字符串中的\0将被第二个字符串的第一个字符取代。
2.在数组中使用字符串
程序4.2
#include<iostream>
#include<cstring> //使用strlen()函数
int main()
{
using namespace std;
const int Size = 15;
char name1[Size]; //空数组
char name2[Size] = "C++owboy"; //初始化数组
cout << "Howdy! I'm " << name2;
cout << "! What's your name?\n";
cin >> name1;
cout << "Well, " << name1 << ", your name has ";
cout << strlen(name1) << " letters and is stored\n";
cout << "in an array of " << sizeof(name1) << " bytes.\n";
cout << "Your initial is " << name1[0] << ".\n";
name2[3] = '\0'; //设置空字符
cout << "Here are the first 3 charaters of my name: ";
cout << name2 << endl;
cin.get();
cin.get();
return 0;
}
运行结果:
sizeof()返回整个数组的长度:15字节;
strlen()返回存储在数组中的字符串的长度,而不是数组本身的长度。strlen()只计算可见的字符,而不把空字符计算在内。
程序4.2将name2[3]设置为空字符,使得该字符串在第三个字符后就结束。
3.字符串输入
程序4.2有一个缺陷,但该缺陷由于精心选择输入而被掩盖掉了。
程序4.3
#include<iostream>
int main()
{
using namespace std;
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
cout << "Enter your name:\n";
cin >> name;
cout << "Enter your favorite dessert:\n";
cin >> dessert;
cout << "I have some delicious " << dessert;
cout << " for you, " << name << ".\n";
cin.get();
cin.get();
return 0;
}
运行结果:
在输入完name(CSDN blog)后,还没有输入dessert,程序便将它显示出来了,并立即显示了最后一行。
cin如何确定字符串输入的完成呢?
由于不能用键盘输入空字符,因此cin使用空白(空格、制表符和换行符)来确定字符串的结束位置。
这意味着cin在获取字符数组输入时只读取一个单词,读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。
因此,程序4.3中,cin把CSDN作为第一个字符串,并将它放到name数组中,并自动在结尾添加空字符。这把blog留在了输入队列中,当cin在输入队列中搜索dessert时,发现了blog,因此cin读取blog并存在dessert中。
另一个问题是,输入字符串的长度可能比目标数组长。
4. 每次读取一行字符串输入
每次读取一个单词通常不是最好的选择。要将整条短语而不是一个单词作为字符串输入,需要采用面向行而不是面向单词的方法。
istream中的类(如cin)提供了一些面向行的类成员函数:getline()和get()。这两个函数都能读取一行,直到到达换行符。不同的是,getline()将丢弃换行符,而get()将换行符保留在输入序列中。
1. 面向行的输入:getline()
使用cin.getline()函数有两个参数,第一个参数是用来存储输入行的数组名称;第二个参数是要读取的字符数,如果这个参数为20,则最多读取19个字符,余下的空间用于存储在动在结尾处添加的空字符。getline()成员函数在读取指定数目的字符或遇到换行符时停止读取。
cin.getline(name,20); //将输入读入到一个包含20个元素的name数组中
程序4.4
#include<iostream>
int main()
{
using namespace std;
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
cout << "Enter your name:\n";
cin.getline(name, ArSize); //读取一行
cout << "Enter your favorite dessert:\n";
cin.getline(dessert, Arsize);
cout << "I have some delicious " << dessert;
cout << " for you, " << name << ".\n";
cin.get();
cin.get();
return 0;
}
2. 面向行的输入:get()
cin.get()与getline()类似,它们的参数相同。但到行尾时,get()不丢弃换行符,而是将换行符保留在输入队列中。假设我们连续两次调用get():
cin.get(name, Arsize);
cin.get(dessert, Arsize); //第二次调用看到的第一个字符便是换行符,因此get()认为已到达行尾,
//而没有发现任何可读取的内容.
get()有另一种变体,使用不带任何参数的cin.get()调用可读取下一个字符(即使是换行符),因此可以用它来处理换行符,为读取下一行做准备,如:
cin.get(name, Arsize);
cin.get();
cin.get(dessert, Arsize);
或者将两个类成员函数合并起来:
cin,get(name, Arsize).get();
之所以可以这样做,是因为cin.get(name,Arsize)返回一个cin对象,该对象随后调用get()函数。同样,下面的语句将连续输入的两行分别读入到数组name1和name2中,其效果与两次调用cin.getline()相同:
cin.getline(name1, Arsize).getline(name2, Arsize);
程序4.5
#include<iostream>
int main()
{
using namespace std;
const int ArSize = 20;
char name[ArSize];
char dessert[ArSize];
cout << "Enter your name:\n";
cin.get(name, ArSize).get(); //读取一行
cout << "Enter your favorite dessert:\n";
cin.get(dessert, ArSize).get();
cout << "I have some delicious " << dessert;
cout << " for you, " << name << ".\n";
cin.get();
cin.get();
return 0;
}
3. 空行和其他问题
当get()读取空行后,将设置失效位(failbit),接下来的输入将被阻断,但可以用下面的命令来恢复输入:
cin.clear();
如果输入行包含的字符数比指定的多,则getline()和get()将把余下的字符留在输入队列中。getline()还会设置失效位,并关闭后面的输入。
5. 混合输入字符串和数字
混合输入数字和面向行的字符串会导致问题。
程序4.6
#include<iostream>
int main()
{
using namespace std;
cout << "What year was your house built?\n";
int year;
cin >> year;
cout << "What is its street address?\n";
char address[80];
cin.getline(address, 80);
cout << "Year built: " << year << endl;
cout << "Address: " << address << endl;
cout << "Done!\n";
cin.get();
cin.get();
cin.get();
return 0;
}
运行结果:
用户根本没有输入address的机会。问题在于,当cin读取年份是,将回车键生成的换行符留在了输入队列中。后面的cin.getline()看到换行符后,将认为是一个空行,并将一个空字符串赋给address数组。
解决办法是,在读取地址之前先丢弃换行符。如:
cin>>year;
cin.get(); //or cin.get(ch);
(cin>>year).get(); //or (cin>>year).get(ch);
C++程序常用指针(而不是数组)处理字符串。