第四章 复合类型
4.1 数组
数组(array)是一种数据格式,能够存储多个同类型的值。每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素。
要创建数组,可使用声明语句。数组声明应指出以下三点:
- 存储在每个元素中值的类型
- 数组名
- 数组中的元素数
声明数组的通用格式如下:
typeName arrayName[arraySize];
注意:arraySize它必须是整型常数或const值,也可以是个常量表达式,即所有值在编译的时候都是已知的。
数组可以单独访问数组元素,方法是通过使用下标或索引来对元素进行编号。(C++数组从0开始编号。)
小tips:编译器不会检查使用的下标是否有效。但是程序运行后,下标无效可能会引发问题,它可能会破坏数据结构或代码,也可能会导致程序的终止。所以必须确保程序只使用有效的下标值。
下面这段代码说明了数组的一些属性,包括声明数组、给数组元素赋值以及初始化数组。
#include<iostream>
using namespace std;
int main()
{
int yams[3];//创建一个含三个元素的数组
yams[0] = 7;//依次对第一个第二个第三个元素进行赋值操作,不能对yams[3]赋值,为无效下标
yams[1] = 8;
yams[2] = 6;
int yamcosts[3] = { 20,30,5 };//对yamcosts这个数组进行初始化及赋值操作
cout << "Total yams=";
cout << yams[0] + yams[1] + yams[2] << endl;
cout << "The package with " << yams[1] << " yams costs";
cout << yamcosts[1] << "cents per yam." << endl;
int total = yams[0] * yamcosts[0] + yams[1] * yamcosts[1] + yams[2] * yamcosts[2];
cout << "The total yam expense is " << total << " cents.\n";
cout << endl;
cout << "size of yams array=" << sizeof yams << "bytes" << endl;
cout << "size of one element=" << sizeof yams[0] << "bytes" << endl;
return 0;
}
注意:sizeof运算符用于数组名的时候,得到的将是整个数组中的字节数。但如果将sizeof用于数组元素,则得到的将是元素的长度(单位为字节)。这表明yams是一个数组,而yams[1]只是一个int 变量。
4.1.1 数组的初始化规则
只有在定义数组的时候才能使用初始化,此后就不能对数组进行初始化操作了。也不能将一个数组赋给另一个数组。然而,可以使用下标分别给数组中的元素赋值。初始化数组的时候,提供的值可以少于数组的元素数目。(int yams[5]={1,2,3};编译器会把未提供值的元素设为0)
如果初始化数组时方括号内([])为空,C++编译器将计算元素个数。(通常情况下让编译器计算元素个数是种很糟的做法。如果主要关心的问题是程序,而不是自己是否知道数组的大小,则可以这样做: short things[]={1,5,3,8}; int num_elements=sizeof things/sizeof(short);)
4.1.2 C++11数组初始化方法
C++11将使用大括号的初始化作为一种通用初始化方式,可用于所有类型。
C++11中的列表初始化新增了一些功能:
- 初始化数组的时候可以省略等号。例如int yams[5]={1,2,3,4,5};int photos[3]{1,2,3};
- 可以不在大括号中包含任何东西,这将把所有元素都设置为0。
- 列表初始化禁止缩窄转换。(缩窄转换是指:将浮点数转换为整数,从取值范围大的浮点数转换为取值范围小的浮点数(在编译期可以计算没有超出范围的表达式可以),从整数转换为浮点数,从取值范围大的整数转换为取值范围小的整数(在编译器可以计算没有超出范围的表达式),还有不能超出char型(数组类型)的取值范围。)
4.2 字符串
字符串是存储在内存的连续字节中的一系列字符。
C-风格字符具有一种特殊的性质:以空字符结尾,空字符被写作\0,其ASCII码为0,用来标记字符串的结尾。
char dog[3]={'a','b','\0'};//是字符串
char dog[3]={'a','b','c'};//不是字符串
在cout打印的时候,遇到空字符就停止。
char birds[11]="Mr.Cheeps";
char fish[]="Bubbles";
以上两个初始化方式都可以,用引号括起的字符串隐式地包括结尾的空字符。
注意:在确定存储字符串所需的最短数组时,别忘了将结尾的空字符计算在内。
注意:字符串常量(使用双引号)不能与字符常量(使用单引号)互换。字符常量如‘A’。但“A”不是字符常量,它表示的是两个字符A和\0。
4.2.1 拼接字符串常量
有时候字符串很长,无法放到一行中。C++允许拼接字符串字面值,即将两个用引号括起来的字符串合并为一个。事实上,任何两个由空白(空格、制表符和换行符)分隔打的字符串常量都将自动拼接称一个。
注意:拼接时不会在被连接的字符串之间添加空格。
4.2.2 在数组中使用字符串
要将字符串存储到数组中,最常用的方法有两种——将数组初始化为字符串常量、将键盘或文件输入读入到数组中。
下面一段代码,使用了strlen()和sizeof可以看一下它们的区别。
#include<iostream>
#include<cstring>
using namespace std;
int main()
{
const int size = 15;
char name1[size];
char name2[size] = "C++owboy";
cout << "Hody:I'm" << name2;
cout << "What's your name?" << endl;
cin >> name1;
cout << "well," << name1 << ",your name has";
cout << strlen(name1) << "letters and is stored" << endl;
cout << "in an array of" << sizeof(name1) << "bytes" << endl;
cout << "Your initial is " << name1[0] << '.' << endl;
name2[3] = '\0';
cout << "here are the first 3 characters of my name:";
cout << name2 << endl;
return 0;
}
strlen()只计算可见的字符,而不把空字符计算在内。
4.2.3 字符串的输入
字符串的输入中存在一个缺陷。下面程序将揭示该缺陷。
#include<iostream>
#include<cstring>
using namespace std;
int main()
{
const int size = 100;
char name[size];
char dessert[size];
cout << "Enter your name:" << endl;
cin >> name;
cout << "Enter your favourite dessert:" << endl;
cin >> dessert;
cout << "I have some delicious " << dessert;
cout << "for you, " << name << endl;
return 0;
}
cin使用空白(空格、制表符和换行符来确定字符串的结束为止),这意味着cin在获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串放在数组中,并自动在结尾处添加空字符。
4.2.4 每次读取一行字符串输入
下面将介绍两个成员函数分别是getline()和get()。
1.面向行的输入:getline()
getline()函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。如,cin.getline(name,20)将输入的值读取到一个包含20个元素,名字为name的数组中。
注意:getline()函数每次读取一行。它通过换行符来确定行尾,但不保存换行符。相反,在存储字符串的时候,它用空字符来替换换行符。
下面一段代码将展示如何调用getline()。
#include<iostream>
using namespace std;
int main()
{
const int size = 100;
char name[size];
char dessert[size];
cout << "Enter your name:" << endl;
cin.getline(name, size);
cout << "Enter your favorite dessert " << endl;
cin.getline(dessert, size);
cout << "I have some delicious " << dessert;
cout << "for you, " << name << endl;
return 0;
}
-----------------------------2021.7.30 更新-------------------------------------------------------------------------------
2.面向行的输入:get()
该函数有几种变体,其中一种变体的工作方式与工作方式与getline()类似,它们接受的参数相同,解释参数的方式也相同。并且都读取到行尾。但get并不再读取并丢弃换行符,而是将其留在输入队列中。
下面举一段例子。
cin.get(name, ArSize);
cin.get(dessert, ArSize);// 在输入的过程中出现问题
由于第一次调用后,换行符将留在队列里,因此第二次调用时看到的第一个字符便是换行符。因此get认为此时已达到行尾,而没有发现任何可读取内容。如果不借助于帮助,get()将不能跨过该换行符。
但是get()有另一种变体。使用不带任何参数的cin.get()调用可读取下一个换行符(即使是换行符)。因此,可以使用它来处理换行符,为读取下一行输入做好准备。例如,
cin.get(name, ArSize);
cin.get();
cin.get(dessert, ArSize);
另一种使用get()的方式是将两个类成员函数拼接起来(合并),例如,cin.get(name, ArSize).get();可以这样做的原因是cin.get()返回一个cin对象,该对象随后被用来调用get()函数。
#include<iostream>
using namespace std;
int main() {
const int size = 20;
char name[size];
char dessert[size];
cout << "Enter your name:\n";
cin.get(name, size).get();
cout << "Enter your favoriate dessert:\n";
cin.get(dessert, size).get();
cout << "I have some delicious " << dessert;
cout << " for you," << name << "." << endl;
return 0;
}
注意:如果使用的是cin.get(),则编译器知道是要读取一个字符。如果使用的是cin.get(name, size),则编译器知道是要将一个字符串放入数组中。
总之,getline使用起简单点,但get()使得检查错误更简单些。
3.空行和其他问题
当getline()或get()读取空行时,会发生什么情况?最初的做法是吓一跳输入语句将在前一条getline()或get()结束读取的位置开始读取。但当前的做法是,当get()读取空行后将设置失效位。这意味着接下来的输入将被阻断。但可以使用cin.clear();来恢复输入。
另一个潜在的问题是,输入字符串可能比分配的空间长。如果输入行包含的字符数比指定的多,则getline()和get()将把余下的字符留在输入队列中,而getline()还会设置失效位,并关闭后面的输入。
4.2.5 混合输入字符串和数字
请看下面一段代码。
#include<iostream>
using namespace std;
int main()
{
cout << "What year was your house built?\n";
int years;
cin >> years;
cout << "What is its street address?\n";
char adress[100];
cin.getline(adress, 100);
cout << "Year built:" << years << endl;
cout << "Adress:" << adress << endl;
return 0;
}
可以看到这一段的代码adress的值为空,这是因为当cin读取年份,将回车键生成的换行符留在了输入队列中。后面的cin.getline()看到换行符后将认为这是一个空行,并将空字符赋给adress数组。改进的方法是:cin>>years; cin.get(); 或 (cin>>years).get();
4.3 string类简介
下面一段代码将介绍string对象与字符数组之间的一些相同点和不同点。
#include<iostream>
#include<string>
using namespace std;
int main() {
char charr1[20];
char charr2[20]="candle";
string str1;
string str2 = "candle";
cout << "Enter a kind of feline:";
cin >> charr1;
cout << "Enter another kind of feline:";
cin >> str1;
cout << "Here are some felines:\n";
cout << charr1 << " " << charr2 << " " << str1 << " " << str2 << endl;
cout<<"The third letter in"<< charr2 << " is " << charr2[2] << endl;
cout << "The third letter in" << str2 << " is " << str2[2] << endl;
return 0;
}
string对象和字符数组之间的主要区别是,可以将string对象声明为简单变量,而不是数组。类设计让程序能够自动处理string的大小(将根据输入自动调整string的长度)。这使得与使用数组相比,使用string对象更方便,也更安全。
4.3.1 C++11字符串初始化
char first_date[]={"Le Chapon Dodu};
char first_date[]{"a bc d e"};
string str1={"The big park"};
string str1{"the beautiful flower"};
4.3.2 赋值、拼接和附加
一个数组不能赋给另一个数组,但一个string对象可以赋给另一个string对象。
string类简化了字符串合并操作。可以使用运算符+将两个string对象合并起来,还可以使用运算符+=将字符串附加到string对象的末尾。
下面一段代码展示了如何使用+和+=。
#inlcude<iostream>
using namespace std;
int main()
{
string s1 = "penguin";
string s2, s3;
cout << "you can assign one string object to another: s2=s1\n";
s2 = s1;
cout << "you can assign a C- style string to a string object.\n";
s2 = "buzzard";
cout << "s2:" << s2 << endl;
cout << "you can concatenate strings: s3=s1+s2\n";//concatenation n.一系列
s3 = s2 + s1;
cout << "s3:" << s3 << endl;
cout << "you can append strings.\n";
s1 += s2;
cout << "s1+=s2 yields s1=" << s1 << endl;
s2 += " for a day";
cout << "s2+=\" for a day\" yields s2= " << s2 << endl;//此处\"表示双引号而不是字符串的结尾。
return 0;
}
4.3.3 string类的其他操作
头文件cstring提供了函数strcpy()将字符串复制到字符数组中,使用函数stract()将字符串附加到字符组末尾。
下面一段代码对用于string对象的技术和用于字符数组技术进行了比较。
#include<cstring>
#include<iostream>
#include<string>
using namespace std;
int main()
{
char charr1[20];
char charr2[20] = "jaguar";
string str1;
string str2 = "panther";
str1 = str2;
strcpy_s(charr1, charr2);
str1 += " paste";
strcat_s(charr1, " juice");
int len1 = str1.size();
int len2 = strlen(charr1);
cout << "the string " << str1 << " contains " << len1 << " characters." << endl;
cout << "the string " << charr1 << " contains " << len2 << " characters." << endl;
return 0;
}
4.3.4 string类I/O
可以使用cin和运算符>>来将输入存储到string对象中,使用cout和运算符<<来显示string对象。但每次读取一行而不是一个单词时,使用的句法不同。
下面一段代码将说明如何读取一行。
#include<cstring>
#include<string>
#include<iostream>
using namespace std;
int main()
{
char charr[20];
string str;
cout << "length of string in charr before input: " << strlen(charr) << endl;
cout << "length of string in str before input: " << str.size() << endl;
cin.getline(charr, 20);
cout << "you entered: " << charr << endl;
cout << "length of string in charr after input: " << strlen(charr) << endl;
getline(cin, str);
cout << "you entered: " << str << endl;
cout << "length of string in str after input: " << str.size() << endl;
return 0;
}
注意:在用户输入之前,该数组charr的字符串长度是不确定的,因为第一个空字符的出现位置是随机的,因此在运行的时候,很可能得到不同的初始长度数值。
4.3.5 其他形式的字符串字面值
下面是关于如何使用前缀的例子。
wchar_t title[]=L"fleg charlet";
char16_t name[]=u"Felonia Ripova";
char32_t car[]=U"humber super";
C++11新增的另一种类型是原始字符串。原始字符串将"(和)"用作定界符,并使用前缀R来标识原始字符串。例如,cout<<R"(Jim "KING" Tutt uses "\n" instead of endl.)"<<'\n';
上述代码将显示为:Jim "KING" Tutt uses \n instead of endl.
如果原始字符串中包含)",则可使用R"+*(... )+*"来标识。可将前缀R与其他字符串前缀结合使用,以标识wchar_t等类型的原始字符串,如UR,Ru,LR等。