第8章 阐述指针和引用

    C++最大的优点之一是,您既可使用它来编写不依赖于机器的高级应用程序,又可使用它来编写与硬件紧密协作的应用程序。事实上,C++让您能够在字节和比特级调整应用程序的性能。要编写高效地利用系统资源的程序,理解指针和引用是必不可少的一步。

    在本章中,您将学习:

    • 什么是指针;

    • 什么是自由存储区;

    • 如何使用运算符new和delete分配和释放内存;

    • 如何使用指针和动态分配编写稳定的应用程序;

    • 什么是引用;

    • 指针和引用的区别;

    • 什么情况下使用指针,什么情况下使用引用。

    8.1 什么是指针

    简单地说,指针是存储内存地址的变量。就像int变量用于存储整数值一样,指针变量用于存储内存地址,如图8.1所示。

    第8章 阐述指针和引用 - 图1

    图8.1 指针的可视化表示

    因此,指针是一个变量,与所有变量一样,指针也占用内存空间(在图8.1中,其地址为0x101)。指针的特殊之处在于,指针包含的值(这里为 0x558)被解读为内存地址,因此指针是一种指向内存单元的特殊变量。

    8.1.1 声明指针

    作为一种变量,指针也需要声明。通常将指针声明为指向特定的类型,如int,这意味着指针包含的地址对应的内存单元存储了一个整数。也可将指针声明为指向一个内存块,这种指针被称为void指针。

    作为一种变量,指针与所有变量一样,也需要声明:

    第8章 阐述指针和引用 - 图2

    与大多数变量一样,除非对指针进行初始化,否则它包含的值将是随机的。您不希望访问随机的内存地址,因此将指针初始化为NULL。NULL是一个可以检查的值,且不会是内存地址:

    第8章 阐述指针和引用 - 图3

    因此,声明int指针的代码如下:

    第8章 阐述指针和引用 - 图4

    第8章 阐述指针和引用 - 图5与您学过的所有数据类型一样,除非对指针进行初始化,否则它包含的将是垃圾值。对指针来说,这种垃圾值非常危险,因为指针包含的值被视为地址。未初始化的指针可能导致程序访问非法内存单元,进而导致程序崩溃。

    8.1.2 使用引用运算符(&)获取变量的地址

    在编程语言中,变量让您能够处理内存中的数据,这一概念在第3章详细阐述过。指针也是变量,但是一种特殊的变量,只用于存储内存地址。

    如果Varname是一个变量,&VarName将是存储该变量的内存的地址。

    因此,如果您使用下面这种您非常熟悉的语法声明了一个int变量:

    第8章 阐述指针和引用 - 图6

    &Age将是存储该变量的值(30)的内存的地址。程序清单8.1显示了存储变量值的内存的地址。

    程序清单8.1 获取int变量和double变量的地址

    第8章 阐述指针和引用 - 图7

    输出:

    第8章 阐述指针和引用 - 图8

    分析:

    注意到第9和10行使用了引用运算符(&)获取了变量Age和常量Pi的地址。作为一种约定,显示十六进制数时,应加上文本0x。

    第8章 阐述指针和引用 - 图9您知道,变量占用的内存量取决于其类型。在第3章,程序清单3.4使用sizeof()证明了int变量占用的内存为4字节(在笔者的系统上,使用笔者的编译器)。前面的输出表明, int变量Age的地址为0x0045FE00,而sizeof(int)为4,因此0x0045FE00-0x0045FE04的4字节内存由int变量Age占用。

    第8章 阐述指针和引用 - 图10引用运算符(&)也叫地址运算符。

    8.1.3 使用指针存储地址

    您知道如何声明指针以及如何获取变量的地址,还知道指针是用于存储内存地址的变量。现在该将它们关联起来,使用指针存储使用引用运算符(&)获取的地址。

    假设您声明了一个某种类型的变量:

    第8章 阐述指针和引用 - 图11

    要将该变量的地址存储到一个指针中,需要声明一个同样类型的指针,并使用引用运算符(&)将其初始化为该变量的地址:

    第8章 阐述指针和引用 - 图12

    因此,如果您使用自己非常熟悉的语法声明了一个int变量:

    第8章 阐述指针和引用 - 图13

    可像下面这样声明一个int指针来存储变量Age的地址:

    第8章 阐述指针和引用 - 图14

    程序清单8.2演示了如何使用指针来存储使用引用运算符(&)获取的地址。

    程序清单8.2 声明并初始化指针

    第8章 阐述指针和引用 - 图15

    输出:

    第8章 阐述指针和引用 - 图16

    分析:

    该程序清单的输出与前一个程序清单相同,因为它们都显示变量 Age的内存地址。不同之处在于,这里先将该地址赋给了一个指针(第6行),再在第9行使用cout显示该指针的值(地址)。

    第8章 阐述指针和引用 - 图17在您的输出中,地址可能不同。事实上,即使在同一台计算机上,每次运行该应用程序时输出的变量地址都可能不同。

    知道如何将地址存储到指针变量中后,就很容易想象得到,可将不同的内存地址赋给指针变量,让它指向不同的值,如程序清单8.3所示。

    程序清单8.3 给指针重新赋值,使其指向另一个变量

    第8章 阐述指针和引用 - 图18

    输出:

    第8章 阐述指针和引用 - 图19

    分析:

    这个程序表明,同一个int指针(pInteger)可指向任何int变量。第7行将该指针初始化为&Age,因此它包含变量Age的地址。第14行将&DogsAge赋给了该指针,因此它指向包含DogsAge的内存单元。相应地,输出表明这个指针的值(即它指向的地址)发生了变化,因为int变量Age和DogsAge在内存中的存储位置不同——分别是0x002EFB34和0x002EFB1C。

    8.1.4 使用解除引用运算符(*)访问指向的数据

    有了包含合法地址的指针后,如何访问这个地方,即如何获取或设置这个地方的数据呢?答案是使用解除引用运算符()。基本上,如果有合法的指针pData,要访问它包含的地址处存储的值,可使用pData。程序清单8.4演示了这种运算符(*)。

    程序清单8.4 使用解除引用运算符(*)来访问整数值

    第8章 阐述指针和引用 - 图20

    输出:

    第8章 阐述指针和引用 - 图21

    分析:

    像程序清单8.3一样,该程序清单也修改了指针存储的地址,还将解除引用运算符()用于指针变量pInteger,以打印存储在这两个地址处的值。在第18和24行,使用解除引用运算符()访问了pInteger指向的整数。由于第20行修改了pInteger包含的地址,因此第24行使用该指针访问的是变量DogsAge,即显示9。

    将解除引用运算符(*)用于该指针时,应用程序从它存储的地址开始,取回内存中4个字节的内容(因为该指针指向的是int变量,而sizeof(int)为4),因此指针包含的地址必须合法。第11行将指针初始化为&Age,确保它包含合法的地址。如果指针未初始化,它所在的内存单元将包含随机值,此时对其解除引用通常会导致非法访问(Access Violation),即访问应用程序未获得授权的内存单元。

    第8章 阐述指针和引用 - 图22解除引用运算符(*)也叫间接运算符。

    程序清单8.4使用指针读取它指向的内存单元中的值。程序清单8.5演示了将*pInteger用作左值(即给它赋值,而不是读取其值)的情况。

    程序清单8.5 使用指针和解除引用运算符操纵数据

    第8章 阐述指针和引用 - 图23

    输出:

    第8章 阐述指针和引用 - 图24

    分析:

    这里的关键步骤是第14行,它将用户提供的整数存储到指针pAge指向的地方。虽然存储输入时使用的是指针pAge,但第19行显示变量DogsAge时,显示的却是使用指针存储的值,这是因为第8行初始化了pAge,使其指向DogsAge。pAge指向存储DogsAge的内存单元,使用pAge和DogsAge中的一个修改该内存单元的内容时,另一个将受到影响。

    8.1.5 将sizeof()用于指针的结果

    您知道,指针是包含内存地址的变量。因此无论指针指向哪种类型的变量,其内容都是一个地址——一个数字。在特定的系统中,存储地址所需的字节数是固定的。因此,将sizeof()用于指针时,结果取决于编译程序时使用的编译器和针对的操作系统,与指针指向的变量类型无关,程序清单8.6演示了这一点。

    程序清单8.6 指向不同变量类型的指针的长度相同

    第8章 阐述指针和引用 - 图25

    输出:

    第8章 阐述指针和引用 - 图26

    分析:

    输出表明,虽然sizeof(char)为1字节,而sizeof(double)为8字节,但sizeof(pointer)总是为4字节。这是因为不管指针指向的内存单元是1字节还是8字节,存储指针所需的内存量都相同。

    第8章 阐述指针和引用 - 图27程序清单8.6的输出表明,将sizeof用于指针的结果为4字节,但在您的系统上结果可能不同。这里的输出是使用32位编译器编译代码时得到的,如果您使用的是64位编译器,并在64位系统上运行该程序,可能发现将sizeof用于指针的结果为64位,即8字节。

    8.2 动态内存分配

    如果在程序中使用下面这样的数组声明:

    第8章 阐述指针和引用 - 图28

    程序将存在下面两个问题。

    1.这限制了程序的容量,无法存储100个以上的数字。

    2.如果只需存储1个数字,却为100个数字预留存储空间,这将降低系统的性能。

    导致这些问题的原因是,数组的内存分配是静态和固定的。

    要编写根据用户需要使用内存资源的应用程序,需要使用动态内存分配。这让您能够根据需要分配更多内存,并释放多余的内存。为帮助您更好地管理应用程序占用的内存,C++提供了两个运算符——new和delete。指针是包含内存地址的变量,在高效地动态分配内存方面扮演了重要角色。

    8.2.1 使用new和delete动态地分配和释放内存

    您使用new来分配新的内存块。通常情况下,如果成功,new将返回指向一个指针,指向分配的内存,否则将引发异常。使用new时,需要指定要为哪种数据类型分配内存:

    第8章 阐述指针和引用 - 图29

    需要为多个元素分配内存时,还可指定要为多少个元素分配内存:

    第8章 阐述指针和引用 - 图30

    因此,如果需要给整型分配内存,可使用如下语法:

    第8章 阐述指针和引用 - 图31

    第8章 阐述指针和引用 - 图32new 表示请求分配内存,并不能保证分配请求总能得到满足,因为这取决于系统的状态以及内存资源的可用性。

    使用new分配的内存最终都需使用对应的delete进行释放:

    第8章 阐述指针和引用 - 图33

    这种规则也适用于为多个元素分配的内存:

    第8章 阐述指针和引用 - 图34

    第8章 阐述指针和引用 - 图35对于使用new[…]分配的内存块,需要使用delete[]来释放;对于使用new为单个元素分配的内存,需要使用delete来释放。

    不再使用分配的内存后,如果不释放它们,这些内存仍被预留并分配给您的应用程序。这将减少可供其他应用程序使用的系统内存量,甚至降低您的应用程序的执行速度。这被称为内存泄露,应不惜一切代价避免这种情况发生。

    程序清单8.7演示了如何动态地分配和释放内存。

    程序清单8.7 使用解除引用运算符(*)访问使用new分配的内存,并使用delete释放它

    第8章 阐述指针和引用 - 图36

    输出:

    第8章 阐述指针和引用 - 图37

    分析:

    第6 行运算符new请求为一个整型分配内存,而您打算使用它来存储用户输入的小狗年龄。new返回一个指针,因此将其赋给了一个指针变量。第10行使用cin和解除引用运算符()将用户输入的年龄存储在新分配的内存中。第13行使用解除引用运算符()显示存储的值,还显示了内存的地址。在第13行,pAge包含的地址与第6行的new返回的地址相同,这个地址始终未变。

    第8章 阐述指针和引用 - 图38不能将运算符delete用于任何包含地址的指针,而只能用于new返回的且未使用delete释放的指针。

    因此,程序清单8.6所示的指针虽然包含有效地址,但不应使用delete 来释放它们,因为这些地址并不是由new返回的。

    对于使用new[…]为一系列元素分配的内存,应使用delete[]来释放,如程序清单8.8所示。

    程序清单8.8 使用new[…]分配内存,并使用delete[]释放它们

    第8章 阐述指针和引用 - 图39

    输出:

    第8章 阐述指针和引用 - 图40

    分析:

    其中最重要的代码行是第14和23行,它们分别使用了运算符new[…]和delete[]。相比于程序清单8.7,这个程序清单的不同之处在于,为多个元素而不是单个元素分配内存块。对于为一系列元素分配的内存块,使用完毕后必须使用 delete[]来释放。第 11 行计算需要为多少个字符分配内存时,将用户输入的字符数加1,以便能够存储对C风格字符串来说至关重要的终止空字符。第4章解释了为何需要终止空字符。实际的复制工作是在第 17行使用 strcpy完成的,它将对 std::string Name调用 c_str()得到的结果作为输入,将其复制到char缓冲区CopyOfName中。

    第8章 阐述指针和引用 - 图41运算符new和delete分配和释放自由存储区中的内存。自由存储区是一种内存抽象,表现为一个内存池,应用程序可分配(预留)和释放其中的内存。

    8.2.2 将递增和递减运算符(++和—)用于指针的结果

    指针包含内存地址。例如,程序清单8.3的int指针包含0x002EFB34——int在内存中的地址。int本身长4字节,因此占用0x002EFB34-0x002EFB37的内存。将递增运算符用于该指针后,它指向的并不是0x002EFB35,因为指向int中间毫无意义。

    如果您对指针执行递增或递减运算,编译器将认为您要指向内存块中相邻的值(并假定这个值的类型与前一个值相同),而不是相邻的字节(除非值的长度刚好是1字节,如char)。

    因此,对于程序清单8.3中的指针pInteger,对其执行递增运算将导致它增加4字节,即sizeof(int)。将++用于该指针相当于告诉编译器,您希望它指向下一个int,因此递增后该指针将指向0x002EFB38。同样,将该指针加2将导致它向前移动两个int,即8字节。在本章后面,您将看到指针和数组索引之间的关系。

    使用运算符—将指针递减的效果类似:将指针包含的地址值减去它指向的数据类型的sizeof。

    将指针递增或递减的结果

    将指针递增或递减时,其包含的地址将增加或减少指向的数据类型的sizeof(并不一定是1字节)。这样,编译器将确保指针不会指向数据的中间或末尾,而只会指向数据的开头。

    如果声明了如下指针:

    第8章 阐述指针和引用 - 图42

    则执行++pType后,pType将包含(指向)Address + sizeof(Type)。

    程序清单8.9演示了对指针递增和添加偏移量的结果。

    程序清单8.9 根据需要动态地分配内存,并研究对指针递增和添加偏移量的结果

    第8章 阐述指针和引用 - 图43

    输出:

    第8章 阐述指针和引用 - 图44

    再次运行的输出:

    第8章 阐述指针和引用 - 图45

    分析:

    这个程序询问用户想输入多少个整数,再在第9行相应地分配内存。注意到第10行保存了该地址的拷贝,以便第26行使用它和delete来释放该内存块。这个程序演示了使用指针和动态内存分配相比于静态数组的优势:在用户需要存储较少的数字时,该应用程序占用的内存较少;在用户需要存储较多的数字时,占用的内存也较多,但从不浪费系统资源。由于内存是动态分配的,对可存储的整数数量没有限制,而只受限于可用的系统资源。第13~17行的for循环让用户输入数字,然后使用第16行的表达式将其存储到相邻的位置。这个表达式给指针增加从零开始的偏移量(Index),从而将用户提供的值存储到内存的合适位置,而不覆盖前一个值。换句话说,表达式(pNumber + Index)返回一个指针,指向内存中的第 Index + 1个整数(即 Index为 1时,返回的指针将指向第二个整数)。因此,cin语句中的表达式*(pNumber + Index)访问的是第 Index + 1个整数。第 20和 21行的 for循环与此类似,它显示前一个循环存储的值。这条 for 语句执行了多项初始化任务,其中之一是创建指针的拷贝并存储到pCopy中;第21行递增该拷贝以便显示值。

    第10行之所以创建指针的拷贝,是因为第二个循环使用递增运算符(++)修改了该指针。必须保留new最初返回的指针,因为第26行调用delete[]时,必须提供它,而不能随便提供一个值。

    8.2.3 将关键字const用于指针

    第3章介绍过,通过将变量声明为const的,可确保变量的取值在整个生命周期内都固定为初始值。这种变量的值不能修改,因此不能将其用作左值。

    指针也是变量,因此也可将关键字const用于指针。然而,指针是特殊的变量,包含内存地址,还可用于修改内存中的数据块。因此,const指针有如下三种。

    • 指针指向的数据为常量,不能修改,但可以修改指针包含的地址,即指针可以指向其他地方。

    第8章 阐述指针和引用 - 图46

    • 指针包含的地址是常量,不能修改,但可修改指针指向的数据。

    第8章 阐述指针和引用 - 图47

    • 指针包含的地址以及它指向的值都是常量,不能修改(这种组合最严格)。

    第8章 阐述指针和引用 - 图48

    将指针传递给函数时,这些形式的const很有用。函数参数应声明为最严格的const指针,以确保函数不会修改指针指向的值。这让函数更容易维护,在时过境迁和人员更迭后尤其如此。

    8.2.4 将指针传递给函数

    指针是一种将内存空间传递给函数的有效方式,其中可以包含值,也可以包含结果。将指针作为函数参数时,确保函数只能修改您希望它修改的参数很重要。例如,如果函数根据以指针方式传入的半径计算圆的面积,就不应允许它修改半径。为控制函数可修改哪些参数以及不能修改哪些参数,可使用const指针,如程序清单8.10所示。

    程序清单8.10 以指针方式将Pi和半径传递给计算圆面积的函数时,使用const限定相应的参数

    第8章 阐述指针和引用 - 图49

    输出:

    第8章 阐述指针和引用 - 图50

    分析:

    第3~5行演示了两种const指针,pRadius和pPi被声明为“指向const数据的const指针”,因此不能修改指针包含的地址,也不能修改它指向的数据。pArea显然是用于存储的参数,因为不能修改该指针的值(地址),但可修改它指向的数据。第8行在使用函数的指针参数前检查其有效性。在调用者不小心将这三个参数之一设置为 NULL 指针时,您不希望函数计算面积,因为这将导致应用程序崩溃。

    8.2.5 数组和指针的类似之处

    在程序清单 8.9 中,通过递增指针来访问内存中的下一个整数,这是不是与数组索引很像。当您声明下面的int数组时:

    第8章 阐述指针和引用 - 图51

    编译器将分配固定数量的内存,用于存储 5 个整数;同时向您提供一个指向数组中第一个元素的指针,而指针由您指定的数组名标识。换句话说, MyNumbers 是一个指针,指向第一个元素(MyNumber[0]),程序清单8.11演示了这种关系。

    程序清单8.11 数组变量是指向第一个元素的指针

    第8章 阐述指针和引用 - 图52

    输出:

    第8章 阐述指针和引用 - 图53

    分析:

    这个程序表明,可将数组变量赋给类型与之相同的指针,如第 9 行所示,这证明了数组与指针类似。第12和15行表明,存储在指针中的地址与数组第一个元素在内存中的地址相同。这个程序表明,数组是指向其第一个元素的指针。

    要访问第二个元素,可使用MyNumbers[1],也可通过指针pNumbers来访问,其语法为(pNumbers+1)。要访问静态数组的第三个元素,可使用MyNumbers[2],而要访问动态数组的第三个元素,可使用语法(pNumbers+2)。

    由于数组变量就是指针,因此也可将用于指针的解除引用运算符(*)用于数组。同样,可将数组运算符([])用于指针,如程序清单8.12所示。

    程序清单8.12 使用解除引用运算符(*)访问数组中的元素以及将数组运算符([])用于指针

    第8章 阐述指针和引用 - 图54

    输出:

    第8章 阐述指针和引用 - 图55

    分析:

    在这个应用程序中,第8行声明并初始化了一个包含5个int元素的静态数组。这个应用程序通过两种可相互替换的方式显示该数组的内容,一种是使用数组变量和间接运算符(*),如第15行所示;另一种方式是使用指针变量和数组运算符([]),如第19行所示。

    该程序表明,数组MyNumbers和指针pNumbers都具有指针的特点。换句话说,数组类似于在固定内存范围内发挥作用的指针。可将数组赋给指针,如第11行所示,但不能将指针赋给数组,因为数组是静态的,不能用作左值。

    第8章 阐述指针和引用 - 图56使用运算符new动态分配的指针仍需使用运算符delete来释放,虽然其使用语法与静态数组类似。牢记这一点很重要。

    如果忘记这样做,应用程序将泄露内存,这很糟糕。

    8.3 使用指针时常犯的编程错误

    C++让您能够动态地分配内存,以优化应用程序对内存的使用。不同于C#和Java等基于运行时环境的新语言,C++没有自动垃圾收集器对程序已分配但不能使用的内存进行清理。指针使用起来比较棘手,程序员很容易犯错。

    8.3.1 内存泄露

    这可能是 C++应用程序最常见的问题之一:运行时间越长,占用的内存越多,系统越慢。如果在使用new动态分配的内存不再需要后,程序员没有使用配套的delete释放,通常就会出现这种情况。

    确保应用程序释放其分配的所有内存是程序员的职责。绝不能让下面这样的情形发生:

    第8章 阐述指针和引用 - 图57

    8.3.2 指针指向无效的内存单元

    使用运算符*对指针解除引用,以访问指向的值时,务必确保指针指向了有效的内存单元,否则程序要么崩溃,要么行为不端。这看起来合乎逻辑,但一个非常常见的导致应用程序崩溃的原因就是无效指针。指针无效的原因很多,但都要归结于糟糕的内存管理。程序清单8.13演示了一种导致指针无效的典型情形。

    程序清单8.13 使用无效指针

    第8章 阐述指针和引用 - 图58

    输出:

    第8章 阐述指针和引用 - 图59

    再次运行的输出:

    第8章 阐述指针和引用 - 图60

    分析:

    这个程序的问题很多,有些已通过注释指出了。第14行分配内存并将其赋给指针,但这行代码仅在用户按y(表示yes)时才会执行。用户提供其他输入时,该if块都不会执行,因此指针pTemperature无效。第二次运行时,用户按 n,导致应用程序崩溃。因为 pTemperature 包含无效的内存地址,而第19行对这个无效的指针解除引用,导致应用程序崩溃。

    同样,第22行对这个指针调用delete,但并未使用new分配这个指针,这也是大错特错。如果有指针的多个拷贝,只需对其中一个调用delete(应避免指针拷贝满天飞)。

    要让程序清单8.13所示的程序更好、更安全、更稳定,应对指针进行初始化,确定指针有效后再使用并只释放指针一次(且仅当指针有效时才释放)。

    8.3.3 悬浮指针(也叫迷途或失控指针)

    使用delete释放后,任何有效指针都将无效。因此,在程序清单8.13中,即便用户按了y,在第22行前指针pTemperature是有效的,但第22行调用delete后,它也变成无效的了,不应再使用。

    为避免这种问题,很多程序员在初始化指针或释放指针后将其设置为NULL,并在使用运算符*对指针解除引用前检查它是否有效。

    8.4 指针编程最佳实践

    在应用程序中使用指针时,应遵守一些基本规则,这样您的工作将更轻松。

    第8章 阐述指针和引用 - 图61

    学习一些指针编程最佳实践后,该修改程序清单8.13中错误百出的代码了,如程序清单8.14所示。

    程序清单8.14 更安全的指针编程——程序清单8.13的修正版

    第8章 阐述指针和引用 - 图62

    输出:

    第8章 阐述指针和引用 - 图63

    再次运行的输出:

    第8章 阐述指针和引用 - 图64

    分析:

    主要差别在于,需要时(即用户按y时)才创建指针,并在创建时对其进行初始化,如第12行所示。在同一个代码块中释放了内存,避免了在没有赋给指针有效内存地址的情况下使用它(解除引用或调用delete)。

    8.4.1 检查使用new发出的分配请求是否得到满足

    除非请求分配的内存量特大,或系统处于临界状态,可供使用的内存很少,new 一般都能成功。有些应用程序需要请求分配大块的内存(如数据库应用程序),一般而言,不要假定内存分配能够成功,这很重要。C++提供了两种确保指针有效的方法,默认方法是使用异常,即如果内存分配失败,将引发std::bad_alloc异常。这导致应用程序中断执行,除非您提供了异常处理程序,否则应用程序将崩溃,并显示一条类似于“异常未处理”的消息。

    第 28 章将详细讨论如何解决这种问题。程序清单 8.15 演示了如何使用异常处理检查分配请求是否失败。

    程序清单8.15 异常处理——在new失败时妥善地退出

    第8章 阐述指针和引用 - 图65

    输出:

    第8章 阐述指针和引用 - 图66

    分析:

    在您的计算机上,这个程序的执行情况可能不同。在我的计算机上,无法为 536870911 个整型分配内存,如果没有编写异常处理程序(第14~17行的catch块),程序将以令人非常讨厌的方式结束。使用Microsoft Visual Studio以调试模式生成可执行文件,并在Microsoft Visual Studio外部执行它时,将出现如图8.2所示的输出。

    在调试模式下生成的可执行文件包含开发环境插入的异常处理程序,这导致出现图8.2所示的消息。在发布模式下生成时,操作系统(这里是Windows)将非常唐突地终止该应用程序,如图8.3所示。

    第8章 阐述指针和引用 - 图67

    图8.2如果删除异常处理代码,程序清单8.15所示的程序将崩溃(使用MSVC编译器以调试模式生成)

    第8章 阐述指针和引用 - 图68

    图8.3如果删除异常处理代码,程序清单8.15所示的程序将崩溃(以发布模式生成)

    应用程序像这样崩溃时,将被操作系统终止,如果没有异常处理程序,您都没有机会与用户说“再见”。

    通过使用异常处理程序,可让应用程序告诉用户遇到了问题,然后妥善地退出,而不是让操作系统显示一条崩溃消息。

    有一个 new 变种——new(nothrow),它不引发异常,而返回 NULL,让您能够在使用指针前检查其有效性,如程序清单8.16所示。

    程序清单8.16 使用new(nothrow),它在分配内存失败时返回NULL

    第8章 阐述指针和引用 - 图69

    输出:

    第8章 阐述指针和引用 - 图70

    分析:

    这个程序与程序清单 8.15 相同,但使用的是 new(nothrow),这样分配内存失败时,将返回NULL,而不是引发异常std::bad_alloc。这两种做法都可行,如何选择取决于您。

    8.5 引用是什么

    引用是变量的别名。声明引用时,需要将其初始化为一个变量,因此引用只是另一种访问相应变量存储的数据的方式。

    要声明引用,可使用引用运算符(&),如下面的语句所示:

    第8章 阐述指针和引用 - 图71

    要更深入地了解如何声明和使用引用,请参阅程序清单8.17。

    程序清单8.17 引用是相应变量的别名

    第8章 阐述指针和引用 - 图72

    输出:

    第8章 阐述指针和引用 - 图73

    分析:

    输出表明,无论将引用初始化为变量(如第9行所示)还是其他引用(如第12行所示),它都指向相应变量所在的内存单元。因此,引用是真正的别名,即相应变量的另一个名字。第 14 行显示了Ref2的值,结果与第6行显示的Original值相同,因为Ref2是Original的别名,它们位于内存的同一个地方。

    8.5.1 是什么让引用很有用

    引用让您能够访问相应变量所在的内存单元,这使得编写函数时引用很有用。第 7 章介绍过,典型的函数声明类似于下面这样:

    第8章 阐述指针和引用 - 图74

    调用函数DoSomething()的代码类似于下面这样:

    第8章 阐述指针和引用 - 图75

    上述代码导致将argument的值复制给Parameter,再被函数DoSomething()使用。如果argument占用了大量内存,这个复制步骤的开销将很大。同样,当 DoSomething()返回值时,这个值被复制给Result。如果能避免这些复制步骤,让函数直接使用调用者栈中的数据就太好了。为此,可使用引用。

    可避免复制步骤的函数版本类似于下面这样:

    第8章 阐述指针和引用 - 图76

    调用该函数的代码类似于下面这样:

    第8章 阐述指针和引用 - 图77

    由于argument是按引用传递的,Parameter不再是argument的拷贝,而是它的别名,这类似于程序清单8.17中的Ref。另外,接受引用参数的函数可使用这些参数返回值,如程序清单8.18所示。

    程序清单8.18 一个计算平方值并通过引用参数返回结果的函数

    第8章 阐述指针和引用 - 图78

    输出:

    第8章 阐述指针和引用 - 图79

    分析:

    计算平方的函数位于第3~6行。它通过引用参数接受一个要计算其平方的数字,并通过该参数返回结果。如果忘记将参数Number声明为引用(&),结果将无法返回到调用函数main(),因为ReturnSquare将使用 Number 的本地拷贝执行运算,而函数结束时该拷贝将被销毁。通过使用引用,可确保ReturnSquare()对main()中定义的Number所在的内存单元进行操作。这样,函数ReturenSquare()执行完毕后,也可以在main()中使用运算结果。

    在这个示例中,修改了输入参数。如果要保留这两个值——传入的数字及其平方,可让函数接受两个参数:一个包含输入;另一个提供平方值。

    8.5.2 将关键字const用于引用

    可能需要禁止通过引用修改它指向的变量的值,为此可在声明引用时使用关键字const:

    第8章 阐述指针和引用 - 图80

    8.5.3 按引用向函数传递参数

    引用的优点之一是,可避免将形参复制给形参,从而极大地提高性能。然而,让被调用的函数直接使用调用函数栈时,确保被调用函数不能修改调用函数中的变量很重要。为此,可将引用声明为 const的,如程序清单8.19所示。const引用参数不能用作左值,因此试图给它们赋值将无法通过编译。

    程序清单8.19 使用const引用确保被调用的函数不能修改按引用传入的值

    第8章 阐述指针和引用 - 图81

    输出:

    第8章 阐述指针和引用 - 图82

    分析:

    在前一个程序中,使用同一个参数来接受输入和存储结果,但这里使用了两个参数,一个用于接受输入,另一个用于存储运算结果。为禁止修改传入的值,必须使用关键字const将其声明为const引用,如第3行所示。这让Number自动变为输入参数——其值不能修改的参数。

    您可以尝试修改第5行,使其像程序清单8.18那样返回平方值:

    第8章 阐述指针和引用 - 图83

    这将导致编译错误,指出const值不能修改。const引用是C++提供的一个功能强大的工具,可用于将参数标识为输入参数,并禁止被调用的函数修改按引用传入的值。乍一看,这可能微不足道,但在多名程序员合作编程时,编写第一个版本的人和改进或修正的人可能不同,通过使用const引用可极大地提高编程质量。

    8.6 总结

    本章介绍了指针和引用。您学习了指针,它可用来访问和操纵内存,还是帮助动态分配内存的工具。您学习了运算符new和delete,它们可用于为单个元素分配和释放内存;还学习了变种new[…]和delete[],它们可用于为数组分配和释放内存。您简要地了解了指针编程和动态内存分配的陷进,知道释放动态分配的内存至关重要,有助于避免内存泄露。引用是别名,将参数传递给函数时,引用可很好地替代指针,因为引用总是有效的。您学习了const指针和const引用,知道声明函数时应尽可能提高参数的const程度。

    8.7 问与答

    问:既然使用静态数组无需释放内存,为何要动态分配内存?

    答:静态数组的长度是固定的,不能根据应用程序的需求增大或缩小,而动态内存分配可满足这样的需求。

    问:我声明了两个指针:

    第8章 阐述指针和引用 - 图84

    为释放内存,是否需要对它们都调用delete?

    答:这样做是错误的。对new返回的地址,只能调用delete一次。另外,最好避免让两个指针指向相同的地址,因为对其中一个调用 delete 将导致另一个无效。另外,编写程序时,应避免使用有效性不确定的指针。

    问:在什么情况下应使用new(nothrow)?

    答:如果不想处理异常std::bad_alloc,可使用new(nothrow),它在内存分配失败时返回NULL。

    问:下面是我编写的面积计算函数的两个版本:

    第8章 阐述指针和引用 - 图85

    请问哪个版本更好?

    答:使用引用的版本更好,因为引用不可能无效,而指针可能无效。另外,第二个版本也更简单。

    问:我编写了如下代码:

    第8章 阐述指针和引用 - 图86

    我知道,由于const声明,我不能使用指针pNumber来修改变量Number的值。我可以将pNumber赋给一个非const指针,再使用该指针来操纵变量Number的值吗?

    答:不能,您不能修改指针的const程度:

    第8章 阐述指针和引用 - 图87

    问:为何要按引用向函数传递值?

    答:可以不这样做,只要影响不大。然而,如果函数接受非常大的对象,则按值传递的开销将非常大,通过使用引用,可极大地提高函数调用的效率。别忘了将const用于引用参数,除非函数需要将结果存储在参数中。

    问:下面的两个声明有何不同?

    第8章 阐述指针和引用 - 图88

    答:MyNumbers是一个int数组,它指向这样的内存单元的开头,即其中存储了100个整数。它是静态的,可替换如下代码:

    第8章 阐述指针和引用 - 图89

    MyArrays是一个包含100个元素的指针数组,其中的每个指针都可指向一个int或int数组。

    8.8 作业

    作业包括测验和练习,前者帮助读者加深对所学知识的理解,后者提供了使用新学知识的机会。请尽量先完成测验和练习题,然后再对照附录D的答案。在继续学习下一章前,请务必弄懂这些答案。

    8.8.1 测验

    1.为何不能将const引用赋给非const引用?

    2.new和delete是函数吗?

    3.指针变量包含的值有何特征?

    4.要访问指针指向的数据,应使用哪种运算符?

    8.8.2 练习

    1.下面的语句显示什么?

    第8章 阐述指针和引用 - 图90

    2.下面三个重载函数有何相同和不同之处?

    第8章 阐述指针和引用 - 图91

    3.要让练习1中第3行的赋值非法,应如何修改第1行中pNum1的声明(提示:让pNum1不能修改它指向的数据)?

    4.查错:下面的代码有何错误?

    第8章 阐述指针和引用 - 图92

    5.查错:下面的代码有何错误?

    第8章 阐述指针和引用 - 图93

    6.修正练习5的代码后,其输出是什么?