数组拾遗
数组运用的已经较为熟练了,但是还有些细节没注意。本文后面主要是字符串。
1.数组初始化
初始化数组时,提供的值可以少于数组的元素数目。例如,下面的语句只初始化hotelTips的前两个元素:
1 | float hotelTips[5]={5.0,2.5}; |
如果只对数组的一部分进行初始化,则编译器将把其他元素设置为0。因此,将数组中所有的元素都初始化为0 非常简单—— 只要显式地将第一个元素初始化为0,然后让编译器将其他元素都初始化为0即可:
1 | long totals[500]={0}; |
如果初始化数组时方括号内([])为空,C++编译器将计算元素个数。例如,对于下面的声明:
1 | short things[]={1,5,3,8}; |
编译器将使things数组包含4 个元素。
2.C++11数组初始化方法
C++11将使用大括号的初始化(列表初始化)作为一种通用初始化方式,可用于所有类型。数组以前就可使用列表初始化,但 C++11中的列表初始化新增了一些功能。
首先,初始化数组时,可省略等号(=):
1 | double earning[4] {1.2e4,1.6e4,1.1e4,1.7e4}; |
其次,可不在大括号内包含任何东西,这将把所有元素都设置为零:
1 | unsigned int counts[10] = {}; |
第三,列表初始化禁止缩窄转换。
1 | long plifs[] = {25,92,3.0}; //错误 |
在VS和code block中均出现警告或错误。
在上述代码中,第一条语句不能通过编译,因为将浮点数转换为整型是缩窄操作,即使浮点数的小数点后面为零。第二条语句也不能通过编译,因为1122011超出了char变量的取值范围(这里假设char变量的长度为8 位)。第三条语句可通过编译,因为虽然112是一个int值,但它在char变量的取值范围内。
3.字符串
1 | char cat[4] = {'c','a','t','\0'}; //a string |
这两个数组都是char数组,但只有第二个数组是字符串。空字符对C-风格字符串而言至关重要。例如, C++有很多处理字符串的函数,其中包括cout使用的那些函数。它们都逐个地处理字符串中的字符,直到到达空字符为止。如果使用cout显示上面的cat这样的字符串,则将显示前3个字符,发现空字符后停止,但是,如果使用cout显示上面的cat2数组(它不是字符串),cout将打印出数组中的8个字母,并接着将内存中随后的各个字节解释为要打印的字符,直到遇到空字符为止。于空字符(实际上是被设置为0 的字节)在内存中很常见,因此这一过程将很快停止。但尽管如此,还是不应将不是字符串的字符数组当作字符串来处理。在VS的调试中,观察到cat2出现了“烫烫”的乱码,改为cat2[5]后,出现“字符串无效”。
有一种更好的、将字符数组初始化为字符串的方法—— 只需使用一个用引号括起的字符串即可,这种字符串被称为字符串常量(string constant)或字符串字面值(string literal), 如下所示:
1 | char bird[11] = "Mr.Cheeps"; |
用引号括起的字符串隐式地包括结尾的空字符,因此不用显式地包括它(参见图4.2)。另外,各种C++输入工具通过键盘输入,将字符串读入到char数组中时,将自动加上结尾的空字。
应确保数组足够大,能够存储字符串中所有字符——包括空字符。使用字符串常量初始化字符数组是这样的一种情况,即让编译器计算元素数目更为安全。让数组比字符串长没有什么害处,只是会浪费一些空间而己。这是因为处理字符串的函数根据空字符的位置,而不是数组长度来进行处理。C++对字符串长度没有限制。
注意,字符串常量(使用双引号)不能与字符常量(使用单引号)互换。字符常量(如’S’) 是字符串编码的简写表示。在 ASCII系统上,’S‘只是83的另一种写法,因此,下面的语句将83赋给shirt_size:
1 | char shirt_size = 'S'; |
更加值得注意的是:”S”表示的是地址,而不是字符!
对于char
型,只有单引号;
对于char
型数组,有单引号和双引号;
1 | char cat[4] = {'c','a','t','\0'}; |
进一步的探讨在指针部分。
4.在数组中使用字符串
1 | char str1[10]; |
不需要像其他类型数组那样,一个一个地给元素赋值。
cin使用空白(空格、制表符和换行符)来确定字符串的结束位置,这意味着cin在获 取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。
这种读取方式有时会带来一些麻烦。
(1).面向行的输入:cin.getline()
该函数有三个参数。第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数,第三个是终止字符,默认是’\n’。如果这个参数为20 ,则函数最多读取19个字符,余下的空间用于存储自动在结尾处添加的空字符。getline()成员函数在读取指定数目的字符或遇到换行符时停止读取。
(2).面向行的输入:cin.get()
istream类有另一个名为get()的成员函数,该函数有几种变体。其中一种变 体的工作方式与getline()类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但get并不再读取并丢弃换行符,而是将其留在输入队列中。假设我们连续两次调用get():
1 | cin.get(name,ArSize); |
由于第一次调用后,换行符将留在输入队列中,因此第二次调用时看到的第一个字符便是换行符。因此 get()认为已到达行尾,而没有发现任何可读取的内容。
使用不带任何参数的cin.get()调用可读取下一个字符(即使是换行符), 因此可以用它来处理换行符,为读取下一行输入做好准备。
1 | cin.get(name,ArSize); |
另一种使用get()的方式是将两个类成员函数拼接起来(合并),如下所示:
1 | cin.get(name,ArSize).get(); |
之所以可以这样做,是由于cin.get (name, ArSize) 返回一个cin对象,该对象随后将被用来调用get()函数。
Tips:缓冲区
缓冲区是输入时非常重要的一环,当我们在键盘上按下name
并回车时,实际上是进入了缓冲区,现在里面的字符是name
和\n
。’\n‘代表了回车。每当cin读取字符时,读的就是缓冲区里的东西。很多时候输入是复杂的,空格,回车,换行夹杂在一起,要正确的读取输入关键就是明确使用的函数的终止符和是否会丢弃终止符。例如:cin
遇到空格,回车,换行就停止,同时还将它们丢弃(排除),所以读两行输入时,没有像cin.get()
那样要读取掉一个换行符。
5.string类
Tips:
strlen
函数用来确定字符串长度,需要cstring
头文件,老式的则是string.h
,string类则需要string
头文件。在C/C++中,头文件要么是开头加``c要么结尾加
.h,或者都不加。C一般是前面两种,C++是后面这种。也有例外,如
stdio.h和
iostream`。
要使用string类,还需要名称空间std。使用时,string是将字符串作为一种数据类型的。string类定义隐藏了字符串的数组性质,能够像处理普通变量那样处理字符串。
1 |
|
类设计让程序能够自动处理string的大小。例如,str1
的声明创建一个长度为0 的 string对象,但程序 将输入读取到str1
中时,将自动调整str1
的长度。
这使得与使用数组相比,使用string对象更方便,也更安全。从理论上说,可以将char数组视为一组用于存储一个字符串的char存储单元,而 string类变量是一个表示字符串的实体。
同时,string类与变量一样,可以赋值,拼接和附加。
在新增string类之前,要把一个字符串赋给另一个字符串是不能直接用等号的。附加字符到末尾也不能用’+=‘。而是要用到cstring
中的函数。
1 |
|
注意,以上代码在VS中会有两个警告和两个错误:
第一个可能丢失数据是因为str1.size
的返回值类型是size_t
,查看定义发现是unsigned long long int
型,将len1
和len2
改为这种类型就可以,不改也没关系。
第三个是因为在VS看来strcpy
和strcat
这种老式的函数存在安全问题,微软重新定义了一个一样的但更安全的函数strcpy_s
和strcat_s
。改成这样就可以了。当然,也可以关闭这种安全检查。错误信息里已经提示了。
可以看到,string类的使用比C函数方便多了,也更安全。例如在C中:
1 | char site[10] = "house"; |
这将会覆盖相邻内存导致一些错误。而string类可以自动调整大小。C 函数库确实提供了与strcat()和 strcpy()类似的函数— stmcat()和 stmcpy() , 它们接受指出目标数组最大允许长度的第三个参数,因此更为安全,但使用它们进一步增加了编写程序的复杂度。
string类的I/O
1 |
|
在用户输入之前,该程序指出数组charr中的字符串长度为27 ,这比该数组的长度要大。这里要两点需要说明。首先,未初始化的数组的内容是未定义的;其次,函数strlen()从数组的第一个元素开始计算字节数,直到遇到空字符。在这个例子中,对于未被初始化的数据,第一个空字符的出现位置是随机的。
注意到第13行,string类读取行时,与第十行的有着显著的区别。这里没有使用句点表示法,这表明这个getline()不是类方法。它 将 cin作为参数,指出到哪里去查找 输入。另外,也没有指出字符串长度的参数,因为string对象将根据字符串的长度自动调整自己的大小。
为何第十行是istream的类方法,第十三行的不是呢?这是因为string类是C++98才出现的,更早的istream在设计时没有考虑到string类型。但是这种代码为何可行呢?
1 | cin>>str;//str is a string object |
这里涉及到string类的一个友元函数,在函数部分在讲。