第 3 章 复合类型
3.1 数组
-
C++11 数组的初始化方法
之前提到过,C++11 将使用大括号的初始化(列表初始化)作为一种通用的初始化方式,可用于所有类型。数组以前就可使用列表初始化,但 C++11 中的列表初始化新增了一些功能。
-
初始化数组时,可以省略等号
double earnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4};
-
可以不在大括号内包含任何东西,这将把所有元素都设置为 0 0 0
unsigned int counts[10] = {}; float balances[100] {};
-
列表初始化禁止缩窄转换
long plifs[] = {25,92,3.0}; //not allowed char slifs[] = {'h','i',1122011,'\0'}; //not allowed
上述代码中,第一句不能通过编译,因为将浮点数转换为整型是缩窄操作。第二句也不能通过编译,因为 1122011 1122011 1122011 超出了
char
变量的取值范围。但是,这两句在 C 语言中都可以通过编译。
-
3.2 字符串
C++ 处理字符串的方式有两种。第一种来自 C 语言,被称为 C 风格字符串;第二种基于 C++ string
类库。C 风格字符串具有一种特殊的性质:以空字符 \0
结尾。
3.2.1 字符串输入
#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 << " for you, " << name << ".\n";
return 0;
}
该程序的意图很简单:读取用户名和用户喜欢的甜点,并输出。下面是程序的运行情况:
Enter your name:
Baymax Guo
Enter your favorite dessert:
I have some delicious Guo for you, Baymax.
用户还没有输入甜点,程序就已经读取并显示出来了。这和 cin
确定字符串结尾的方式有关。cin
使用空白(空格、制表和换行)来确定字符串的结束位置。这意味着 cin
一次只能读取一个单词,并不能读入整行,和 scanf("%s")
类似。
3.2.2 读取一行字符串
要将整行而不是一个单词作为字符串输入,需要采用另一种字符串读取方法。具体地说,需要采用面向行而不是面向单词的方法。iostream
中的类提供了一些面向行的类成员函数:getline()
和 get()
。这两个函数都读取一行输入,直到到达换行符。随后,getline()
将丢弃换行符,而 get()
将换行符保留在输入序列中。
getline()
getline()
函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。要调用这种方法,可以使用 cin.getline()
。该函数有两个参数:第一个是数组名,第二个是读取的字符数。如果第二个参数是
20
20
20,则函数最多读取
19
19
19 个字符(还有一个空间用于储存空字符)。getline()
成员函数在读取指定数目的字符或遇到换行符时停止读取。使用该函数修改上述程序:
#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, 20);
cout << "Enter your favorite dessert:\n";
cin.getline(dessert, 20);
cout << "I have some delicious " << dessert << " for you, " << name << ".\n";
return 0;
}
修改后,得到正确输出:
Enter your name:
Baymax Guo
Enter your favorite dessert:
ice cream
I have some delicious ice cream for you, Baymax Guo.
getline()
函数每次读取一行。它通过换行符来确定行尾,但不保存换行符,而是用空字符 \0
替换换行符。
-
get()
istream
类中有另一个名为get()
的成员函数,该函数有几种变体。其中一种变体的工作方式与getline()
类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但get()
不再读取并丢弃换行符,而是将其留在输入队列中。假设我们连续两次调用
get()
:cin.get(name, Arsize); cin.get(dessert, Arsize);
由于第一次调用后,换行符将留在输入队列中,因此第二次调用时看到的第一个字符便是换行符,
get()
认为已到达行尾,而没有发现任何可读取的内容。如果不借助帮助,get()
将不能跨过该换行符。幸运的是,
get()
另一种变体。使用不带任何参数的cin.get()
,将从输入中读取一个字符(相当于getchar()
)。因此可以用它来处理换行符,为读取下一行做好准备:cin.get(name, Arsize); cin.get(); cin.get(dessert, Arsize);
另一种使用
get()
的方式是将两个类成员函数拼接起来,如下所示:cin.get(name, Arsize).get();
之所以可以这样做,是因为
cin.get(name, Arsize)
返回一个cin
对象,该对象随后调用get()
函数。同样,也可以下面这样调用getline()
:cin.getline(name1, Arsize).getline(name2, Arsize);
需要指出的是,C++ 允许函数有多个版本,条件是这些版本的参数列表不同。这种特性叫做函数重载。
-
混合输入字符串和数字
混合输入时,可能会导致问题:
#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"; return 0; }
该程序的运行情况如下:
What year was your house built? 1966 What is its street address? Year built: 1966 Address: Done!
用户根本没有输入地址的机会。问题在于当
cin
读取年份后,将回车键生成的\n
留在了输入队列中。后面的cin.getline()
看到换行符后将认为是一个空行,并将一个空字符串赋给address
数组。所以应该在读入year
后把换行处理掉://solution 1 cin >> year; cin.get(); //solution 2 (cin >> year).get();
3.3 string
类简介
C++ 可以使用 string
类的对象来储存字符串。要使用 string
类,必须引头文件 #include <string>
。string
类位于名称空间 std
中,其定义隐藏了字符串的数组性质。考虑下面程序:
#include <iostream>
#include <string>
int main()
{
using namespace std;
char charr1[20];
char charr2[20] = "jaguar";
string str1;
string str2 = "panther";
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;
}
下面是程序运行情况:
Enter a kind of feline: ocelot
Enter another kind of feline: tiger
Here are some felines:
ocelot jaguar tiger panther
The third letter in jaguar is g
The third letter in panther is n
从这个示例可知:在很多方面,使用 string
对象的方式和使用字符数组相同:
- 可以使用 C 风格字符串来初始化
string
对象 - 可以使用
cin
读入string
对象,用cout
输出string
对象 - 可以使用数组表示法来访问存储在
string
对象中的字符(通过下标访问)
上述程序表明,string
对象和字符数组的主要区别是,可以将 string
对象声明为简单变量,而不是数组。
类设计让程序能够自动处理 string
的大小。例如,str1
的声明创建一个长度为
0
0
0 的 string
对象,但当读入 str1
时,将自动调整 str1
的长度。与使用字符数组相比,使用 string
对象更方便,安全。
3.3.1 赋值、拼接和附加
使用 string
类时,某些操作比使用数组时更简单。例如,不能将一个数组赋给另一个数组,但能将一个 string
对象赋值给另一个 string
对象:
char charr1[20];
char charr2[20] = "jaguar";
string str1;
string str2 = "panther";
charr1 = charr2; //错误,不能将数组赋值给另一个数组
str1 = str2; //正确
string
类简化了字符串合并操作。可以使用 +
将两个 string
对象合并起来,还可以使用 +=
将字符串附加到 string
对象的末尾。也可以使用 C 风格字符串和 string
对象相加,或附加到 string
对象的末尾:
string str3;
str3 = str1 + str2;
str1 += str2;
str3 = str1 + "abc";
str1 += "defg";
3.3.2 string
类 I/O
当 string
需要每次读取一行而不是一个单词时,需要使用以下方法读取:
string str;
getline(cin, str);
对于 string
,不能用 cin.getline()
来给 string
赋值,因为 cin.getline()
的第一个参数的类型为 char *
。应该使用 getline()
函数,该函数不是类方法,它将 cin
作为参数,指出到哪里去查找输入。另外,也没有指出字符串长度的参数,因为 string
对象会根据字符串长度自动调整大小。
3.3.3 其他形式的字符串字面值
之前说过,除了 char
之外, C++ 还有类型 wchar_t
;而 C++11 新增了类型 char16_t、char32_t
,下面是一个使用这些类型的例子:
wchar_t title[] = L"Chief Astrogator";
char16_t name[] = u"Felonia Ripova";
char32_t car[] = U"Humber Super Snipe";
C++11 新增了另一种类型:原始字符串。在原始字符串中,字符表示的就是自己。例如,\n
不表示换行符,而表示两个常规字符\
和 n
。因此在屏幕上打印时,将显示这两个字符;此外,可以在字符串中直接使用 "
,而不需要再转义。当然,既然可以在字符串中包含 "
,就不能再使用它来表示字符串的开头和结尾。因此,原始字符串将使用 "(
和 )"
用作定界符,用前缀 R
来标识原始字符串:
cout << R"(Jim "King" Tutt uses "\n" instead of endl.)";
上述代码将输出:Jim "King" Tutt uses "\n" instead of endl.
如果要在原始字符串中包含 )"
,该怎么办呢?编译器见到第一个 )"
会认为字符串到此结束,但原始字符串语法允许在表示字符串开头的 "
和 (
之间添加其他字符,这意味着表示字符串结尾的 )
和 "
之间也要包含这些字符。因此,下面语句:
cout << R"+*("(Who wouldn't?)", she whispered.)+*";
将输出:
"(Who wouldn't?)", she whispered.
自定义定界符时,在默认定界符之间可以添加任何数量的基本字符,但空格、括号、斜杆和控制字符(如换行符)除外。
3.4 结构体
与 C 不同,C++ 允许在声明结构时省略 struct
关键字:
struct stu
{
string name;
int age;
}
struct stu a; //C 风格
stu b; //C++ 风格
3.4.1 C++11 结构初始化
与数组一样, C++11 也支持将列表初始化用于结构,且 =
是可选的:
stu a {"baymax", 20};
stu b = {"cbetula", 20};
其次,如果打括号内未包含任何东西,则各个成员都将被设置成 0 0 0。
最后,不要忘了列表初始化不允许缩窄。
3.4.2 其它结构属性
C++ 允许使用赋值运算符 `=` 将结构赋给另一个同类型的结构,这样结构中的每个成员都将被设置成另一个结构中相应成员的值,即使成员是数组。这种赋值被称为**成员赋值**。
C++ 还可以声明没有名称的结构类型,方法是省略名称,同时定义一种结构类型和一个这种类型的变量:
struct //省略结构名称
{
int x,y;
} position;
这样将创建一个名为 position
的结构体变量。但这种类型没有名称,因此以后无法创建这种类型的变量。
与 C 不同, C++ 结构还允许有成员函数,将在讨论类的时候再描述。
3.5 共用体(联合体)
共用体是一种数据格式,它能存储不同的数据类型,但只能同时存储其中的一种类型。也就是说,结构体可以同时存储 int、long
和 double
,而共用体只能存储 int、long
和 double
。例如:
union one4all
{
int int_val;
long long_val;
double double_val;
}
可以用 one4all
变量来存储 int、long
或 double
,条件是在不同的时间进行:
one4all pail;
pail.int_val = 15;
cout << pail.int_val;
pail.double_val = 1.38;
cout << pail.double_val;
成员名称标识了变量的容量。由于共用体一次只能存储一个值,因此它必须有足够的空间来存储最大的成员。所以共用体的长度为其最大成员的长度。
共用体的主要用途是节省空间。
3.6 枚举
C++ 的 enum
工具提供了另一种创建符号常量的方式,这种方式可以代替 const
。使用 enum
的句法与使用结构相似。例如:
enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};
这条语句完成两项工作:
1. 让 `spectrum` 成为新类型的名称,其被称为枚举
1. 将 `red、orange、yellow` 等作为符号常量,对应整数值 $0\sim 7$。这些常量叫做枚举量。
枚举具有一些特殊属性:
-
在不进行强制类型转换的情况下,只能将枚举量赋给枚举变量:
spectrum band; band = blue; //正确 band = 2000; //错误,2000不是枚举量
-
对于枚举,只定义了赋值运算符,而没有定义算术运算:
band = orange; band++; //错误,没有给枚举定义++运算符 band = orange + red; //错误,没有给枚举定义假发
-
枚举量是整型,可被提升为
int
类型,但int
类型不能自动转换为枚举类型:int color = blue; //正确 band = 3; //错误, 3不能自动转换为枚举类型 color = 3 + red; //正确, red提升为整型,可以和3相加
-
如果
int
值在枚举的范围内,则可以通过强制类型转换将其赋值给枚举变量:band = spectrum(3); //正确,枚举变量范围是 0~7
如果
int
值不在枚举的范围内,结果是不确定的,但编译器并不会报错。
3.6.1 设置枚举量的值
可以使用赋值运算符来显示地设置枚举量的值,指定的值必须是整数,也可以只显示地定义其中一部分枚举量的值:
enum bits{one = 1, two = 2, four = 4, eight = 8};
enum bigstep{first, second = 100, third}; //first=0,third=101
后面未被初始化的枚举量的值将比前面的枚举量大 1 1 1。
可以创建多个值相同的枚举变量:
enum {zero, null=0, one, numero_uno=1};
3.6.2 枚举的取值范围
枚举的取值范围定义如下。首先找到枚举量的最大值,再找到大于这个最大值的、最小的 2 2 2 的幂,再减去 1 1 1,得到的便是枚举取值的上限。下限类似,例如:
enum {a = -6, b = 101};
最大枚举值为 101 101 101,满足条件的 2 2 2 的幂为 128 128 128,减去 1 1 1 得枚举上限为 127 127 127;最小枚举值为 − 6 -6 −6,满足条件的 2 2 2 的幂为 − 8 -8 −8,加上 1 1 1 得下限为 − 7 -7 −7。
在枚举的取值范围内,通过强制转换,可以将取值范围中的任何整数值赋值给枚举变量,即使这个值不是整数值,如:
enum bits{one = 1, two = 2, four = 4, eight = 8};
bits myflag = bits(6);
3.7 指针和自由存储空间
3.7.1 使用 new
分配内存
在 C 语言中,可以用 malloc()
来分配内存;在 C++ 中仍然可以这样做,但还有更好的方法——使用 new
运算符。程序员要告诉 new
为哪种数据类型分配内存;new
将找到一个长度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋给一个指针,例如:int * pn = new int
。
为一个数据对象(可以是结构,也可以是基本类型)分配内存的通用格式如下:
typeName * pointer_name = new typeName;
需要指出的是,new()
和 malloc()
一样,都是从堆(自由存储区)的内存区域分配内存的。
3.7.2 使用 delete
释放内存
使用 delete
可以释放用 new
分配的内存:
int * ps = new int;
...
delete ps;
这将释放 ps
指向的内存,但不会删除指针 ps
本身,可重新令 ps
指向另一片内存。一定要配对地使用 new
和 delete
,否则将发生内存泄漏。
不要尝试释放已经释放的内存块,C++ 标准指出这样做的结果是不确定的,这意味着什么都有可能发生。另外,不能使用 delete
来释放由声明变量所获得的内存:
int * ps = new int;
delete ps;
delete ps; //未定义的行为
int a = 5;
int b* = &a;
delete b; //错误,不能释放由声明变量所获得的内存
警告:只能用
delete
来释放使用new
分配的内存。然而,对空指针使用delete
是安全的。
注意,使用 delete
的关键在于:将它用于 new
分配的内存。这并不意味着只能用于分配内存时的指针,只要是指向 new
分配的内存的指针都可以,如:
int * ps = new int;
int * pq = ps;
delete pq; //正确
3.7.3 使用 new
来创建动态数组
在 new
后面加上方括号,包含元素数目,即可创建动态数组:int * arr = new int [10];
。
对于使用 new
创建的数组,应使用另一种格式的 delete
释放:delete [] arr;
。
[]
告诉程序,应该释放整个数组,而不仅仅是指针指向的元素。
使用 new
和 delete
时,应该遵循以下规则:
-
不要使用
delete
释放不是new
分配的内存 -
不要使用
delete
释放同一片内存两次 -
如果使用
new []
为数组分配内存,则应使用delete []
来释放;如果使用new
时没有方括号,使用delete
时也不应该带上 -
对空指针使用
delete
是安全的
3.8 指针算术
-
数组的地址
数组名一般被认为是首元素的地址,除了以下两种情况:
-
sizeof(arr)
这种情况下,
sizeof(arr)
将返回整个数组的大小 -
&arr
&arr
取出的是整个数组的地址:short tell[10]; cout << tell << endl; cout << &tell << endl;
从数字上说,这两个地址相同;但从概念上说,
tell
是一个 2 2 2 字节内存块的地址,而&tell
是一个20
字节内存块的地址。因此,表达式tell+1
将地址值加 2 2 2,表达式&tell+1
将地址值加20
。换句话说,
tell
的类型是short *
,而&tell
的类型是short (*)[10]
。
-
-
const
修饰指针const
在*
的前面:不可以修改指针指向的值,但可以修改指针指向的位置。const
在*
的后面:不可以修改指针指向的位置,但可以修改指针指向的值。int a = 5; const int * p = &a; *p = 6; //error p = new int; //valid int * const r = &a; *r = 6; //valid r = new int;//error
-
自动存储、静态存储和动态存储
根据用于分配内存的方法,C++ 主要有 3 3 3 种管理数据内存的方式:自动存储、静态存储和动态存储。
-
自动存储
相当于存储在栈区空间中。
-
静态存储
静态存储是整个程序执行期间都存在的存储方式。使变量成为静态的方式有两种:一种是在函数外面定义它(全局变量);另一种是在声明变量时使用关键字
static
。static double fee = 56.50;
-
动态存储
new
和delete
运算符管理的是称为堆的内存池。
-
3.9 数组的替代品
3.9.1 模板类 vector
模板类 vector
类似于 string
类,也是一种动态数组。可以在运行阶段设置 vector
对象的长度,也可以在末尾附加新数据,还可以在中间插入新数据。这是通过 new
和 delete
来管理内存实现的,但这种工作是自动完成的。
下面介绍一些基本的实用知识。首先,要使用 vector
对象,必须包含头文件 #include <vector>
。其次, vector
包含在名称空间 std
中。再次,需要向模板类提供存储的数据类型。最后,vector
类使用不同的语法来指定元素数。如:
#include <vector>
...
using namespace std;
vector<int> vi;
int n;
cin >> n;
vector<double> vd(n);
由于 vector
对象可以在插入和添加值时自动调整长度,因此可以将 vi
的长度设置为零。但要调整长度,需要使用 vector
类的方法,将在以后介绍。
3.9.2 模板类 array
vector
类的功能比数组强大,但代价是效率稍低。如果需要长度固定的数组,使用 array
类是一个更好的选择。与数组一样,array
对象的长度是固定的,且使用栈区分配内存,效率和普通数组相同,但更加方便安全。
要创建 array
对象,需包含头文件 #include <array>
。例如:
#include <array>
...
using namespace std;
array<int, 5> ai;
array<double, 4> ad = {1.2, 2.1, 3.43, 4.3};
要创建包含 n_elem
个类型为 typename
的元素,应使用以下语法:
array<typeName, n_elem> arr;
可以将 array
对象赋给另一个 array
对象;而对于数组,只能逐元素复制数据。
此外,可以通过成员函数 at()
访问 array
和 vector
的元素,arr.at(i)
和 arr[i]
等价,但前者可以禁止如 i=-1
时产生的非法行为,更加安全可靠。