说明
看《C++ Primer Plus》时整理的学习笔记,部分内容完全摘抄自《C++ Primer Plus》(第6版)中文版,Stephen Prata 著,张海龙 袁国忠译,人民邮电出版社。只做学习记录用途。
目录
复合类型是基于基本整型和浮点类型创建的,影响最为深远的复合类型是类,除类外,C++ 还支持几种更普遍的复合类型,它们都来自 C 语言,例如:数组、结构、指针等。
4.1 数组
数组能够存储多个同类型的值,计算机在内存中依次存储数组的各个元素。数组声明应包含三点:元素类型、数组名、数组中的元素总数,如下:
//数组声明
typeName arrayName[arraySize];
其中的表达式 arraySize
不能是变量,它必须是整型常量或const
值,也可以是常量表达式(如8*sizeof(int)
),即表达式中所有值在编译时都是已知的。C++ 标准模板库(STL)提供了一种数组替代品,模板类vector
,C++11 新增了模板类array
,这些替代品比内置复合类型数组更复杂、更灵活,这将在后面章节介绍。声明数组后,数组中的元素总数也可以使用以下方式计算出来:
//数组中的元素总数
arraySize = sizeof(arrayName)/sizeof(typeName);
4.1.1 数组访问
使用[index]
访问索引为 index
的数组元素,索引从 0 开始,最后一个元素的索引比数组长度小 1。
//声明长度为12的short数组
short months[12];
//访问它的第1个元素
months[0];
//访问它的最后一个元素
months[11];
注意:编译器不会检查索引是否有效,例如可以访问months[101]
或者给它赋值,编译器并不会指出错误(有时会给个警告,不是以错误的形式指出),但是程序运行后,这种赋值可能破坏数据或代码,也可能导致程序异常终止,因此需人为确保程序使用有效的索引值。
4.1.2 数组初始化及赋值
只有在定义数组时才能使用初始化,也不能将一个数组赋给另一个数组,但可以逐元素赋值。初始化数组时,提供的值可以少于数组的元素数目,剩余的元素编译器会自动初始化为 0。
//定义时初始化全部元素
int arr1[4] = {3, 6, 8, 10};
//定义时只指定第一个值,剩余元素初始化为0
int arr2[4] = {3};
//定义时全部初始化为0
int arr3[4] = {0};
//定义时指定全部元素,让编译器自动统计元素个数
int arr4[] = {3, 6, 8, 10};
//C++11初始化方式:可省略等号
int arr5[4]{3, 6, 8, 10};
//C++11初始化方式:默认初始化为0
int arr6[4] = {};
int arr7[4]{};
注意:使用大括号进行列表初始化时禁止缩窄转换。
4.2 字符串
C++ 处理字符串的方式有两种:第一种来自 C 语言,常被称为 C-风格字符串,第二种是下一节将介绍的string
类。
C-风格字符串将字符串存储在char
数组中,并以空字符结尾。空字符被写作\0
,其 ASCII 码为 0,用来标记字符串的结尾。很多处理字符串的函数(如cout
输出字符串)都逐个处理字符串中的字符,直到到达空字符为止,无论是否超过了char
数组的实际大小。
4.2.1 C – 风格字符串的初始化及拼接
常用的初始化方法如下:
//逐个字符初始化,需人为加空字符
char cat[8] = {'f','a','t','e','s','s','a','\0'};
//使用字符串常量进行初始化,自动添加空字符
char bird[11] = "Mr. Cheeps";
//字符数组剩余元素自动初始化为空字符
char boss[8] = "Bozo";
//让编译器计算字符数组长度,长度为8
char fish[] = "Bubbles";
//C++11字符串初始化
char fish[] = {"Bubbles"};
char fish[]{"Bubbles"};
注意:字符串常量(使用双引号)不能与字符常量(使用单引号)互换。在 ASCII 系统上,字符常量'S'
只是 83 的另一种写法,但"S"
不是字符常量,它表示的是两个字符(字符S
和字符\0
)组成的字符串,更糟糕的是,"S"
实际上表示的是字符串所在的内存地址。
C++ 允许将两个用引号括起来的字符串拼接成一个,任何两个由空白分隔的字符串常量都将自动拼接成一个。下面所有输出语句都是等效的:
//输出
cout << "Medical cotton swab.\n";
//使用空格分隔的拼接
cout << "Medical cot" "ton swab.\n";
//使用换行分隔的拼接
cout << "Medical cot"
"ton swab.\n";
注意:拼接时不会在被连接的字符串之间添加空格。
4.2.2 C – 风格字符串的使用
sizeof(arrayName)
指出整个字符数组的长度。strlen(arrayName)
指出字符数组中字符串的长度,且不把空字符计算在内。字符数组的长度不能短于strlen(arrayName)+1
。包含头文件<cstring>
后,可使用其中的字符串处理函数,常用的有,使用strcpy()
将字符串复制到字符数组中,使用strcat()
将字符串附加到字符数组末尾。还有对应的strncat()
和strncpy()
,它们接受指出目标数组最大允许长度的第三个参数。
//将字符串arrayName2复制到数组arrayName1中
strcpy(arrayName1,arrayName2);
//将字符串arrayName2添加到数组arrayName1末尾
strcat(arrayName1,arrayName2);
4.2.3 C – 风格字符串的输入与输出
C – 风格字符串的输出比较简单,按以下方式使用即可,它会在遇到第一个空字符时停止输出:
//C-风格字符串的输出
cout << arrayName;
C – 风格字符串的输入比较复杂,需根据不同需求使用不同方法:
// cin使用空白做结束标记,只读取一个单词,并丢弃空白
cin >> arrayName;
//getline()函数使用回车输入的换行符做结束标记,读取整行,并丢弃换行符
cin.getline(arrayName,arraySiz);
//get()函数与getline()相似,但不读取且不丢弃换行符,换行符仍在输入队列中
cin.get(arrayName,arraySiz);
//不带任何参数的get()函数读取下一个字符(包括换行符)
cin.get();
//读取一个字符到字符变量中(包括换行符)
cin.get(ch);
//读取整行并丢弃换行符,与getline()函数等效
cin.get(arrayName,arraySiz).get();
选用get()
而不用getline()
的原因主要有两点:老式实现没有getline()
、get()
检查错误更简单。可通过使用get()
检查下一字符是否是换行符来判断是否读取了整行。
注意:get()
函数读取空行后将设置失效位,接下来的输入将被阻断,此时可使用cin.clear()
来恢复输入。若输入行的字符数比指定的多,则getline()
和get()
将把余下的字符留在输入队列中,getline()
还会设置失效位,并关闭后面的输入。使用cin
获取数字输入后,回车键生成的换行符会留在输入队列中,可能会影响获取后面的输入。
4.3 string 类简介
C++98 标准添加了 string
类,它提供了将字符串作为一种数据类型的表示方法,使用起来要比数组简单,例如:不用担心字符串会导致数组越界,可以直接使用赋值运算符而不是函数strcpy()
。使用string
类必须包含头文件<string>
并指出名称空间std
。
4.3.1 string 对象的初始化
支持以下几种方式:
//创建空字符串
string str1;
//创建并初始化
string str2 = "panther";
//C++11方式
string str3 = {"The Bread Bowl"};
string str4{"Hank's Fine Eats"};
- 可以使用 C- 风格字符串来初始化
string
对象。 - 可以使用
cin
来将键盘输入存储到string
对象中。 - 可以使用
cout
来显示string
对象。 - 可以使用数组表示法来访问存储在
string
对象中的字符。 string
类设计让程序能够自动处理string
对象的大小。
4.3.2 string 对象的赋值、拼接和附加
可以将一个string
对象使用运算符 = 赋值给另一个string
对象,但 C – 风格字符串不能这样做。可以使用运算符 + 将两个string
对象拼接起来,还可以使用运算符 += 将字符串附加到string
对象的末尾。
//赋值
string str1;
string str2 = "panther";
str1 = str2;
//拼接
string str3 = str1 + str2;
//附加
str1 += str2;
4.3.3 string 对象的其他操作
可使用size()
获得string
对象的字符数:
//获得string对象的字符数
string str1;
int len = str1.size();
4.3.4 string 对象的输入与输出
可以使用cin
和运算符>>
来将输入存储到string
对象中,使用cout
和运算符<<
来显示string
对象,其句法与处理 C-风格字符串相同。但每次读取一行而不是一个单词时,使用的句法不同,此时getline()
函数并不是istream
的类方法,而是string
类的一个友元函数。
//读取整行到string对象
string str;
getline(cin, str);
4.3.5 其他形式的字符串常量
对于类型wchar_t
以及 C++11 新增的类型char16_t
和char32_t
,可分别使用前缀 L、u 和 U 来创建这些类型的数组和这些类型的字符串常量。
wchar_t title[] = L"Chief Astrogator";
char16_t name[] = u"Felonia Ripova";
char32_t cars[] = U"Humber Super Snipe";
C++11 还支持 Unicode 字符编码方案 UTF-8,字符可能存储为 1~4 个八位组,可使用前缀 u8 来表示这种类型的字符串常量。
C++11 还新增了一种类型:原始(raw)字符串。在原始字符串中,\n
不表示换行符,而表示两个常规字符\
和n
。原始字符串使用 “( 和 )” 作定界符,并使用前缀 R 来标识原始字符串。原始字符串语法允许自定义定界符:在开头 “ 和 ( 之间添加自定义字符后,也必须在结尾 ) 和 “ 之间添加一样的字符,添加的字符只能是基本字符,空格、左括号、右括号、斜杠和控制字符(如制表符、换行符)除外。还可将前缀 R 与其他字符串前缀结合使用,例如与wchar_t
类型的原始字符串结合时,可使用 RL 或 LR,前后顺序无规定。
//原始字符串,使用默认定界符
cout << R"(Jim is a "pig")";
//原始字符串,使用自定义定界符
cout << R"+*(Jim is a "pig")+*";
4.4 结构简介
结构是一种比数组更灵活的数据格式,同一个数组所有元素的类型必须相同,但同一个结构可以存储多种类型的元素。创建结构包括两步:首先,定义结构描述,它描述并标记了能够存储在结构中的各种数据类型;然后按描述创建结构变量(结构数据对象)。
4.4.1 定义和使用结构
使用关键字struct
定义结构,如下定义了一个新类型,类型名称为inflatable
,大括号中包含的是结构存储的数据类型列表,其中每一条都是一条声明语句。
//定义结构体,注意尾部的分号
struct inflatable
{
char name[20];
float volume;
double price;
};
使用结构时,可使用常规声明语句创建这种类型的变量,并使用成员运算符 (.) 来访问结构的各个成员。
//C语言风格创建结构变量,不省略struct
struct inflatable goose;
//C++风格创建结构变量,可省略struct
inflatable goose;
//访问结构成员变量
string str = goose.name;
char na = goose.name[1];
float vo = goose.volume;
double pr = goose.price;
4.4.2 结构初始化
结构定义可以放在函数外面,也可以放在函数里面,这将在第 9 章做更详细的介绍,C++ 不提倡使用外部变量,但提倡使用外部结构声明或在外部声明符号常量。结构常用的初始化方式如下:
//常规初始化
inflatable guest =
{
"Glorious Gloria",
1.88,
29.99
};
//可写在同一行
inflatable duck = {"Daphne",0.12,9.98};
//C++11风格,不允许缩窄转换
inflatable duck{"Daphne",0.12,9.98};
//C++11风格,成员变量全设为0或\0
inflatable duck{};
4.4.3 结构赋值及其他属性
可以使用赋值运算符 = 将结构赋给另一个同类型的结构,这样结构中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组,这种赋值被称为成员赋值。
//结构体赋值
inflatable choice;
inflatable duck = {"Daphne",0.12,9.98};
choice = duck;
可以同时完成定义结构和创建结构变量的工作,只需将变量名放在结束括号的后面,但是一般将结构定义和变量声明分开,使得程序更易于阅读和理解。
//定义并创建结构变量
struct perks
{
int key_number;
char car[12];
} mr_smith, ms_jones;
//定义并创建结构变量并初始化
struct perks
{
int key_number;
char car[12];
} mr_glitz =
{
7,
"Packard"
};
可以声明没有名称的结构类型,方法是省略名称,同时创建这种类型的变量,但这种类型没有名称,只可在定义时创建变量,以后再无法创建这种类型的变量。
//声明没有名称的结构类型
struct
{
int x;
int y;
} position;
相比于 C 结构,C++ 结构有更多的特性,它除了有成员变量外,还可以有成员函数,但这些高级特性一般用于类中,将在第 10 章介绍。
4.4.4 结构数组
可以创建元素为结构的数组,方法和创建基本类型数组完全相同。
//创建并初始化结构数组
inflatable guests[2] =
{
{"Bambi", 0.5, 21.99},
{"Godzilla", 2000, 565.99}
};
//访问数组中指定结构的成员
string str = guests[index].name;
char na = guests[index].name[1];
float vo = guests[index].volume;
double pr = guests[index].price;
4.4.5 结构中的位字段
与 C 语言一样,C++ 也允许指定占用特定位数的结构成员,这常用于低级编程中,创建与某个硬件设备上的寄存器对应的数据结构。字段的类型应为整型或枚举,接下来是冒号,冒号后面的数字指定了使用的位数。可以使用没有名称的字段来提供间距。每个成员都被称为位字段。
//使用位字段定义结构
struct torgle_register
{
unsigned int SN : 4;
unsigned int : 4;
bool goodIn : 1;
bool goodTorgle : 1;
};
//创建并初始化
torgle_register tr = {14,true,false};
//访问
tr.SN;
tr.goodIn;
tr.goodTorgle;
上述结构中的变量SN
内存量宽度为 4 位,接着有 4 位宽未使用到的内存,变量goodIn
的内存量宽度为 1 位,变量goodTorgle
的内存量宽度为 1 位。64 位 Windows 系统上对结构torgle_register
使用运算符sizeof()
得到的结果是该结构内存量宽度为 8 字节,具体原因可参考内存对齐的相关资料。
4.5 共用体
共用体能够存储不同的数据类型,但某个时刻只能存储其中的一种类型,其用途之一是:当数据项使用两种或更多格式(但不会同时使用时),可节省空间。共用体的语法与结构相似,但含义不同。
//定义共用体
union one4all
{
int int_val;
long long_val;
double double_val;
};
//创建共用体变量
one4all pail;
//使用共用体存储int值
pail.int_val = 15;
cout << pail.int_val;
//使用共用体存储double值,此时int值会丢失
pail.double_val = 1.38;
cout << pail.double_val;
上述例子共用体变量pail
有时是int
变量,有时是double
变量,它每次只能存储一个值,因此共用体的内存量宽度为其最宽成员类型的内存量宽度,更深一步地,可能还需要考虑内存对齐。最好另使用一个标志变量来标记共用体当前的数据类型,以防止取出来的值不正确。匿名共用体没有名称,其成员将成为位于相同地址处的变量。
//使用包含匿名共用体的结构
struct widget
{
char brand[20];
int type;
union
{
long id_num;
char id_char[20];
};
} prize;
//访问结构中的共用体成员
if(prize.type == 1)
cin >> prize.id_num;
else
cin >> prize.id_char;
4.6 枚举
C++ 的enum
工具提供了另一种创建符号常量的方式,这种方式可以代替const
,常用于定义switch
语句(后面第 6 章)中使用的符号常量。枚举的规则相当严格,它只定义了赋值运算符,没有定义算术运算符,在不进行强制类型转换的情况下,只能将定义枚举时使用的枚举量赋给这种枚举的变量。
4.6.1 枚举的定义
第一个枚举量的值默认为 0,后面的枚举量默认比其前面的枚举量大 1,可以人为指定枚举量的值,早期的 C++ 只能将int
的值赋给枚举,现在的 C++ 可以使用long
甚至long long
类型的值。
//显式设置全部枚举量的值
enum bits{one = 1, two = 2, four = 4, eight = 8};
//显式设置部分枚举量的值
enum bigstep{first, second = 100, third};
//可以创建多个值相同的枚举量
enum bigsame{zero, null = 0, one, numro_uno = 1};
//若只使用枚举常量,而不使用枚举变量,可省略名称
enum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};
//使用默认方式设置枚举量
enum spectrum{red, orange, yellow, green, blue, violet, indigo, ultraviolet};
4.6.2 枚举的取值范围
每个枚举都有取值范围,通过强制类型转换,可以将取值范围内中的任何整数值赋给枚举变量,即使这个值不是定义中的枚举值。
-
枚举取值范围上限的计算方式如下:找到大于最大枚举量的、最小的 2 的幂,将它减去 1 即可。
-
枚举取值范围下限的计算方式如下:找到最小枚举量,若它不小于 0,则取值范围的下限为 0,否则,采用与上限相同的计算方式,但加上负号。
例如:最大枚举量为 101,则枚举取值范围的上限为\(2^7-1=127\),最小枚举量为 -6,则枚举取值范围的下限为\(-(2^3-1)=-7\)。
4.6.3 枚举变量的赋值及其他属性
枚举变量的声明与基本类型相似,赋值时最好使用枚举定义中的值,将一个不适当的值通过强制类型转换赋给枚举变量的结果是不确定的。枚举量在表达式中进行算术运算时将被提升为整型,通常是int
,但整型不能自动转化为枚举类型,需使用强制类型转换。
//声明枚举变量
spectrum band;
//枚举变量的赋值
band = blue;
//取值范围内的整数需进行强制类型转换
band = spectrum(3);
枚举类型的内存量宽度由编译器决定,对于取值范围较小的枚举,使用一个字节或更少的空间,而对于包含long
类型的枚举,则使用 4 个字节甚至 8 个字节,通常情况下,枚举类型内存量宽度为 4 个字节。
4.7 指针
指针是一种特殊的数据类型,其存储的是值的地址,而不是值本身,对常规变量应用地址运算符 & 就可以获得它的位置。指针变量的内存量宽度与所指数据类型无关,只与计算机系统有关,32 位系统上为 4 字节,64 位系统上为 8 字节,使用 cout
显示值的地址时,通常采用十六进制表示法(有些实现可能采用十进制),例如 0x0065fd40(32 位系统)、0x000000A9186FFC14(64 位系统),使用cout
显示字符数组的地址时,需要对其做强制类型转换(int *)
,否则cout
将输出字符数组的内容(字符串)。面向对象编程(OOP)与传统的过程性编程的区别在于:OOP 强调的是在运行阶段(而不是编译阶段)进行决策。指针类型为OOP 提供了很大的灵活性,例如,考虑为数组分配内存的情况,若每次运行程序时实际使用到的数组长度是不固定的,比如有时需要 20 长度,有时需要 80 长度,最长时需要 200 长度,传统方法是在创建数组时就为数组指定最大长度 200,这个长度在编译阶段就是已知且固定不变的,这样,程序在大多数情况下都浪费了内存。为了能在每次运行程序时根据需求创建不同大小的数组,需要将创建数组的时机推迟到运行阶段,可使用关键字new
在程序运行阶段请求正确数量的内存,然后使用指针来跟踪新分配的内存位置。
4.7.1 声明和初始化指针
指针名表示的是地址,对指针使用间接值运算符(也称解除引用运算符)* 可以得到该地址处存储的值(C++ 根据上下文来确定所指的是乘法还是解除引用)。和数组一样,指针都是基于其他类型的,因此指针声明时必须指定指针指向的数据类型,声明时运算符 * 两边的空格是可选的。
//声明指向int的指针,C风格
int *ptr;
//声明指向int的指针,C++风格
int* ptr;
//其他声明格式
int*ptr;
int * ptr;
//同时声明两个
int *ptr1, *ptr2;
在 C++ 中创建指针时,计算机只分配指针变量的内存(通常是 4 字节或 8 字节),不分配指针所指数据的内存。若在声明时没有对指针变量进行初始化,则指针所指向的地方是不确定的,最好在声明时就对指针变量初始化。
//初始化指针指向指定变量
int higgens = 5;
int* ptr = &higgens;
//初始化指针为空指针,C风格
int* ptr = NULL;
int* ptr = 0;
//初始化指针为空指针,C++风格
int* ptr = nullptr;
//初始化为指定地址值,需要强制类型转换
int* ptr = (int*) 0xB8000000;
指针ptr
指向变量higgens
后,记号ptr
与&higgens
等效,都表示地址,记号*ptr
与higgens
等效,都表示值。注意:不要使用未经初始化的指针来访问内存。
4.7.2 使用 new 来分配内存
变量是在编译时分配的有名称的内存,指针只是为可以通过名称直接访问的内存提供了一个别名,指针的真正用武之地是在运行阶段分配未命名的内存以存储值。在 C 语言中,可以用库函数 malloc()
在运行阶段分配内存,用free()
来释放内存。在 C++ 中仍可以这样做,但它提供了一个更好的方式,使用运算符new
来分配内存,运算符delete
来释放内存。成功分配内存后,new
返回该内存的首地址,当没有足够的内存满足new
的请求时,new
通常会抛出一个异常,老式实现的new
会返回 0。为一个数据对象分配内存的通用格式如下:
typeName * pointer_name = new typeName;
分配出来的内存只能通过指针pointer_name
进行访问。需要注意的是,常规变量声明分配的内存块一般是在称为栈的内存区中,而new
从被称为堆或自由存储区的内存区域分配内存。
4.7.3 使用 delete 来释放内存
使用完内存后,应该用运算符delete
来释放内存,将内存归还给内存池。使用delete
时,后面需加上指向内存块的指针(这些内存块最初是用new
分配的),这个操作只会释放指针所指向的内存,而不会删除指针,后面可以让指针指向别的数据。有以下几点需注意:
delete
只能用来释放new
分配的内存。- 对空指针使用
delete
是安全的。 - 不要尝试释放已经释放的内存块,这样做的结果是不确定的。
- 最好不要创建两个指向同一个由
new
分配的内存块的指针,这会增加delete
两次的可能性。 new
出来的内存使用结束后,必须配对使用delete
释放,不然会产生内存泄漏,造成被分配的内存再也无法使用了。
//配对使用new和delete
typeName * pointer_name = new typeName;
...
delete pointer_name;
//只能释放一次
int* pa = new int;
int* pb = pa;
delete pb;
4.7.4 使用 new 创建动态数组
对于小型数据对象(如int
、double
等),使用new
通常使程序变得复杂,一般不用;对于大型数据对象(如数组、结构等),可根据实际需求来决定是否使用new
。在编译阶段指定数组长度并给其分配内存被称为静态联编,在运行阶段自由选择数组长度并给其分配内存被称为动态联编,动态联编创建的数组被称为动态数组,动态数组可使用new
来创建,但无法用sizeof()
运算符获得动态数组的总字节数。
//分配动态数组内存的通用格式
typeName * pointer_name = new typeName[arraySize];
//访问动态数组元素
pointer_name[0];
pointer_name[1];
...
pointer_name[arraySize-1];
//释放动态数组内存
delete[] pointer_name;
delete
使用的注意事项增加两点:
- 使用
new
分配的内存,必须配对使用delete
进行释放。 - 使用
new[
]分配的内存,必须配对使用delete[]
进行释放。
new
和delete
格式不匹配导致的后果是不确定的。
4.7.5 使用 new 创建动态结构
需要在程序运行时为结构分配空间,可以使用new
运算符来完成,C++ 的类也可以仿照结构来使用new
运算符。创建动态结构的语法与前面相似,但访问结构成员需要使用成员运算符 -> ,该运算符由连字符和大于号组成,可用于指向结构的指针,就像点运算符用于结构名一样。
//定义结构体
struct inflatable
{
char name[20];
float volume;
double price;
};
//创建动态结构
inflatable * ptr = new inflatable;
//访问结构成员,方式一
ptr->volume = 1.66;
//访问结构成员,方式二
(*ptr).volume = 1.66;
//释放内存
delete ptr;
4.7.6 内存管理
C++ 有 3 种管理数据内存的方式:自动存储、静态存储和动态存储(也称自由存储空间或堆)。
- 自动存储:在函数内部定义的常规变量使用自动存储空间,被称为自动变量,自动变量是一个局部变量,其作用域为包含它的代码块(代码块指包含在花括号中的一段代码)。自动变量通常存储在栈(stack)中,执行代码块时,其中的变量将依次加入到栈中,离开代码块时,将按相反的顺序自动释放这些变量,即后进先出(LIFO),这种机制使得栈的内存通常是连续的。
- 静态存储:静态变量将存在于程序的整个生命周期,而不是代码块内,使变量成为静态变量的方式有两种:在函数外面定义它、声明变量时加上关键字
static
。 - 动态存储:
new
和delete
管理一个内存池,这在 C++ 中被称为自由存储空间(free store)或堆(heap)。该内存池同自动存储、静态存储是分开的,数据的生命周期完全由程序员控制,可以在一个函数中分配内存,而在另一个函数中释放它,这使得自由存储空间通常是不连续的。
C++11 新增了第 4 种方式:线程存储,这将在第 9 章学习。
4.8 指针算术和数组名
4.8.1 指针算术
C++ 允许将指针和整数相加,指针加 1 的结果等于原来的地址值加上指向的对象占用的总字节数;还可以将指针相减,获得两个指针的差,最后得到一个整数,指针减法仅当两个指针指向同一个数组时,运算结果才有意义。如下示例中假设short
宽 2 字节,数组首地址为 0x0028ccf0:
//声明并初始化数组
short tacos[10] = {5,2,8,4,1,2,2,4,6,8};
//声明并初始化指针为 0x0028ccf0
short* ptr = tacos;
short* ptrb = tacos;
//指针和整数相加
ptr = ptr + 1; //结果为0x0028ccf2
//指针和整数相加
ptr = ptr + 3; //结果为0x0028ccf8
//指针和指针相减
int dif = ptr - ptrb; //结果为4
4.8.2 数组名
C++ 将数组名解释为其第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。从数字上而言,这两个地址相同,无需区分;但从概念上特别是需要运用指针算术时,需要明白两者的区别。如下示例中假设short
宽 2 字节,系统为 32 位,数组首地址为0x0028ccf0,指针变量ptr
和ptrc
的区别如下:
- 变量
ptr
的类型是short*
,存储的是一个 2 字节内存块的地址,它指向的对象是short
类型,记号*ptr
与tacos[0]
等价。 - 变量
ptrc
的类型是short(*)[10]
,存储的是一个 20 字节内存块的地址,它指向的对象是包含 10 个元素的short
数组,记号*ptrc
与tacos
等价。
//声明并初始化数组
short tacos[10] = {5,2,8,4,1,2,2,4,6,8};
//声明并初始化指针
short *ptr = tacos;
short (*ptrc)[10] = &tacos;
//访问数组第三个元素
cout << tacos[2]; //结果为8
cout << *(tacos+2); //结果为8
cout << ptr[2]; //结果为8
cout << *(ptr+2); //结果为8
cout << (*ptrc)[2]; //结果为8
cout << *(*ptrc+2); //结果为8
//应用sizeof()获得内存量大小
cout << sizeof(tacos); //结果为20
cout << sizeof(ptr); //结果为4
cout << sizeof(ptrc); //结果为4
上述例子中数组名tacos
和指针变量ptrc
以及ptr
的区别如下:
- 数组名
tacos
是常量,值不能修改;ptrc
以及ptr
是变量,值可以修改。 - 对数组名
tacos
使用sizeof()
得到的是整个数组的内存量宽度;对ptrc
以及ptr
使用sizeof()
得到的是指针变量的内存量宽度。
实际上,上述访问方式中,C++ 编译器会自动将 tacos[2]
转换为*(tacos+2)
,将ptr[2]
转换为*(ptr+2)
,将(*ptrc)[2]
转换为*(*ptrc+2)
,前者称为数组表示法,后者称为指针表示法。
4.8.3 指针和字符串
在多数 C++ 表达式中,char
数组名、char
指针以及用双引号括起来的字符串常量都被解释为字符串第一个字符的地址。一般来说,编译器在内存内会留出一些空间,以存储程序源代码中所有用双引号括起来的字符串,并将每个被存储的字符串与其地址关联起来。C++ 不能保证字符串常量被唯一地存储,例如:若在程序中多次使用了字符串常量"tomato"
,则编译器将可能存储该字符串的多个副本,也可能只存储一个副本。
4.9 类型组合
可以用各种方式组合复合类型以及基本类型。比如结构中可以含有数组、指针,可以创建结构数组、指针数组,可以创建指向指针数组的指针。
//创建基本类型变量
int a1 = 110;
int a2 = 120;
int a3 = 130;
int a4 = 140;
//创建指针数组
int* arr[4] = {&a1, &a2, &a3, &a4};
//创建指向指针数组(第一个元素)的指针
int** ptr = arr;
//创建指向(整个)指针数组的指针
int* (*ptrc)[4] = &arr;
//访问指针数组第三个元素所指的值
cout << *arr[2]; //结果为130
cout << **(arr+2); //结果为130
cout << *ptr[2]; //结果为130
cout << **(ptr+2); //结果为130
cout << *(*ptrc)[2]; //结果为130
cout << **(*ptrc+2); //结果为130
//应用sizeof()获得内存量大小(32位系统)
cout << sizeof(arr);//结果为16
cout << sizeof(ptr);//结果为4
cout << sizeof(ptrc);//结果为4
这个例子看似复杂,实则与前一个short
数组例子的原理一样,只是多了一个解除引用运算符 *。
4.10 数组的替代品
模板类 vector 和 array 可以用做数组的替代品。
4.10.1 模板类 vector
模板类vector
是一种动态数组,它自动使用new
和delete
来管理内存,vector
对象存在于自由存储区或堆中,使用时需#include<vector>
并指出名称空间std
。
//使用模板vector创建动态数组
vector<typeName> arrayName(arraySize);
其中的表达式 arraySize
可以是整型常量,也可以是整型变量。
4.10.2 模板类 array(C++11)
模板类vector
由于使用了动态内存分配,其效率会比使用传统数组低。C++11 新增了数组长度固定的模板类array
,它的对象存在于栈中(静态联编),使用时需#include<array>
并指出名称空间std
。
//使用模板array创建定长数组
array<typeName,arraySize> arrayName;
其中的表达式 arraySize
只能是整型常量。
4.10.3 比较数组、vector 对象和 array 对象
见以下例子:
//声明并初始化传统数组
double arr_C[4] = {1.2, 2.4, 3.6, 4.8};
//声明并初始化vector对象:C++98不支持{}初始化
vector<double> arr_vector(4);
arr_vector[0] = 1.2;
arr_vector[1] = 2.4;
arr_vector[2] = 3.6;
arr_vector[3] = 4.8;
//C++11支持{}初始化
vector<double> arr_vector(4) = {1.2, 2.4, 3.6, 4.8};
//声明并初始化array对象
array<double,4> arr_array = {1.2, 2.4, 3.6, 4.8};
//访问数组第三个元素
cout << arr_C[2];
cout << arr_vector[2];
cout << arr_array[2];
cout << arr_vector.at(2);
cout << arr_array.at(2);
访问数组元素时,中括号表示法和成员函数at()
的差别在于:
- 成员函数
at()
可捕捉非法索引并在给定非法索引时中断程序,额外代价是运行时间会更长。 - 中括号表示法不检查越界错误,可以访问如
[-2]
、[10000000]
不在数组大小范围内的值。