- 内容提要
- 前言
- 第 1 章 预备知识
- 第 2 章 开始学习 C++
- 第 3 章 处理数据
- 第 4 章 复合类型
- 第 5 章 循环和关系表达式
- 第 6 章 分支语句和逻辑运算符
- 第 7 章 函数——C++的编程模块
- 第 8 章 函数探幽
- 第 9 章 内存模型和名称空间
- 第 10 章 对象和类
- 第 11 章 使用类
- 第 12 章 类和动态内存分配
- 第 13 章 类继承
- 第 14 章 C++中的代码重用
- 第 15 章 友元、异常和其他
- 第 16 章 string 类和标准模板库
- 第 17 章 输入、输出和文件
- 第 18 章 探讨 C++新标准
- 附录 A 计数系统
- 附录 B C++保留字
- 附录 C ASCII 字符集
- 附录 D 运算符优先级
- 附录 E 其他运算符
- 附录 F 模板类 string
- 附录 G 标准模板库方法和函数
- 附录 H 精选读物和网上资源
- 附录 I 转换为 ISO 标准 C++
- 附录 J 复习题答案
8.2 引用变量
C++新增了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。例如,如果将 twain 作为 clement 变量的引用,则可以交替使用 twain 和 clement 来表示该变量。那么,这种别名有何作用呢?是否能帮助那些不知道如何选择变量名的人呢?有可能,但引用变量的主要用途是用作函数的形参。通过将引用变量用作参数,函数将使用原始数据,而不是其副本。这样除指针之外,引用也为函数处理大型结构提供了一种非常方便的途径,同时对于设计类来说,引用也是必不可少的。然而,介绍如何将引用用于函数之前,先介绍一下定义和使用引用的基本知识。请记住,下述讨论旨在说明引用是如何工作的,而不是其典型用法。
8.2.1 创建引用变量
前面讲过,C 和 C++使用&符号来指示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。例如,要将 rodents 作为 rats 变量的别名,可以这样做:
其中,&不是地址运算符,而是类型标识符的一部分。就像声明中的 char*指的是指向 char 的指针一样,int &指的是指向 int 的引用。上述引用声明允许将 rats 和 rodents 互换——它们指向相同的值和内存单元,程序清单 8.2 表明了这一点。
程序清单 8.2 firstref.cpp
请注意,下述语句中的&运算符不是地址运算符,而是将 rodents 的类型声明为 int &,即指向 int 变量的引用:
但下述语句中的&运算符是地址运算符,其中&rodents 表示 rodents 引用的变量的地址:
下面是程序清单 8.2 中程序的输出:
从中可知,rats 和 rodents 的值和地址都相同(具体的地址和显示格式随系统而异)。将 rodents 加 1 将影响这两个变量。更准确地说,rodents++操作将一个有两个名称的变量加 1。(同样,虽然该示例演示了引用是如何工作的,但并没有说明引用的典型用途,即作为函数参数,具体地说是结构和对象参数,稍后将介绍这些用法)。
对于 C 语言用户而言,首次接触到引用时可能也会有些困惑,因为这些用户很自然地会想到指针,但它们之间还是有区别的。例如,可以创建指向 rats 的引用和指针:
这样,表达式 rodents 和*prats 都可以同 rats 互换,而表达式&rodents 和 prats 都可以同&rats 互换。从这一点来说,引用看上去很像伪装表示的指针(其中,*解除引用运算符被隐式理解)。实际上,引用还是不同于指针的。除了表示法不同外,还有其他的差别。例如,差别之一是,必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值:
注意:
必须在声明引用变量时进行初始化。
引用更接近 const 指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。也就是说:
实际上是下述代码的伪装表示:
其中,引用 rodents 扮演的角色与表达式*pr 相同。
程序清单 8.3 演示了试图将 rats 变量的引用改为 bunnies 变量的引用时,将发生的情况。
程序清单 8.3 sceref.cpp
下面是程序清单 8.3 中程序的输出:
最初,rodents 引用的是 rats,但随后程序试图将 rodents 作为 bunnies 的引用:
咋一看,这种意图暂时是成功的,因为 rodents 的值从 101 变为了 50。但仔细研究将发现,rats 也变成了 50,同时 rats 和 rodents 的地址相同,而该地址与 bunnies 的地址不同。由于 rodents 是 rats 的别名,因此上述赋值语句与下面的语句等效:
也就是说,这意味着“将 bunnies 变量的值赋给 rat 变量”。简而言之,可以通过初始化声明来设置引用,但不能通过赋值来设置。
假设程序员试图这样做:
将 rodents 初始化为*pt 使得 rodents 指向 rats。接下来将 pt 改为指向 bunnies,并不能改变这样的事实,即 rodents 引用的是 rats。
8.2.2 将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。C++新增的这项特性是对 C 语言的超越,C 语言只能按值传递。按值传递导致被调用函数使用调用程序的值的拷贝(参见图 8.2)。当然,C 语言也允许避开按值传递的限制,采用按指针传递的方式。
图 8.2 按值传递和按引用传递
现在我们通过一个常见的的计算机问题——交换两个变量的值,对使用引用和使用指针做一下比较。交换函数必须能够修改调用程序中的变量的值。这意味着按值传递变量将不管用,因为函数将交换原始变量副本的内容,而不是变量本身的内容。但传递引用时,函数将可以使用原始数据。另一种方法是,传递指针来访问原始数据。程序清单 8.4 演示了这三种方法,其中包括一种不可行的方法,以便您能对这些方法进行比较。
程序清单 8.4 swaps.cpp
下面是程序清单 8.4 中程序的输出:
正如您预想的,引用和指针方法都成功地交换了两个钱夹(wallet)中的内容,而按值传递的方法没能完成这项任务。
程序说明
首先来看程序清单 8.4 中每个函数是如何被调用的:
按引用传递(swapr(wallet1, wallet2))和按值传递(swapv(wallet1, waller2))看起来相同。只能通过原型或函数定义才能知道 swapr( ) 是按引用传递的。然而,地址运算符(&)使得按地址传递(swapp(&wallet1, &wallet2))一目了然(类型声明 int * p 表明,p 是一个 int 指针,因此与 p 对应的参数应为地址,如&wallet1)。
接下来,比较函数 swapr( )(按引用传递)和 swapv( )(按值传递)的代码,唯一的外在区别是声明函数参数的方式不同:
当然还有内在区别:在 swapr( ) 中,变量 a 和 b 是 wallet1 和 wallet2 的别名,所以交换 a 和 b 的值相当于交换 wallet1 和 wallet2 的值;但在 swapv( ) 中,变量 a 和 b 是复制了 wallet1 和 waller2 的值的新变量,因此交换 a 和 b 的值并不会影响 wallet1 和 wallet2 的值。
最后,比较函数 swapr( )(传递引用)和 swapp( )(传递指针)。第一个区别是声明函数参数的方式不同:
另一个区别是指针版本需要在函数使用 p 和 q 的整个过程中使用解除引用运算符*。
前面说过,应在定义引用变量时对其进行初始化。函数调用使用实参初始化形参,因此函数的引用参数被初始化为函数调用传递的实参。也就是说,下面的函数调用将形参 a 和 b 分别初始化为 wallet1 和 wallet2:
8.2.3 引用的属性和特别之处
使用引用参数时,需要了解其一些特点。首先,请看程序清单 8.5。它使用两个函数来计算参数的立方,其中一个函数接受 double 类型的参数,另一个接受 double 引用。为了说明这一点,我们有意将计算立方的代码编写得比较奇怪。
程序清单 8.5 cubes.cpp
下面是该程序的输出:
refcube( ) 函数修改了 main( ) 中的 x 值,而 cube( ) 没有,这提醒我们为何通常按值传递。变量 a 位于 cube( ) 中,它被初始化为 x 的值,但修改 a 并不会影响 x。但由于 refcube( ) 使用了引用参数,因此修改 ra 实际上就是修改 x。如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。例如,在这个例子中,应在函数原型和函数头中使用 const:
如果这样做,当编译器发现代码修改了 ra 的值时,将生成错误消息。
顺便说一句,如果要编写类似于上述示例的函数(即使用基本数值类型),应采用按值传递的方式,而不要采用按引用传递的方式。当数据比较大(如结构和类)时,引用参数将很有用,您稍后便会明白这一点。
按值传递的函数,如程序清单 8.5 中的函数 cube( ),可使用多种类型的实参。例如,下面的调用都是合法的:
如果将与上面类似的参数传递给接受引用参数的函数,将会发现,传递引用的限制更严格。毕竟,如果 ra 是一个变量的别名,则实参应是该变量。下面的代码不合理,因为表达式 x + 3.0 并不是变量:
例如,不能将值赋给该表达式:
如果试图使用像 refcube(x + 3.0) 这样的函数调用,将发生什么情况呢?在现代的 C++中,这是错误的,大多数编译器都将指出这一点;而有些较老的编译器将发出这样的警告:
之所以做出这种比较温和的反应是由于早期的 C++确实允许将表达式传递给引用变量。有些情况下,仍然是这样做的。这样做的结果如下:由于 x + 3.0 不是 double 类型的变量,因此程序将创建一个临时的无名变量,并将其初始化为表达式 x + 3.0 的值。然后,ra 将成为该临时变量的引用。下面详细讨论这种临时变量,看看什么时候创建它们,什么时候不创建。
临时变量、引用参数和 const
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为 const 引用时,C++才允许这样做,但以前不是这样。下面来看看何种情况下,C++将生成临时变量,以及为何对 const 引用的限制是合理的。
首先,什么时候将创建临时变量呢?如果引用参数是 const,则编译器将在下面两种情况下生成临时变量:
- 实参的类型正确,但不是左值;
- 实参的类型不正确,但可以转换为正确的类型。
左值是什么呢?左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。在 C 语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字 const 之前的情况。现在,常规变量和 const 变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而 const 变量属于不可修改的左值。
回到前面的示例。假设重新定义了 refcube( ),使其接受一个常量引用参数:
现在考虑下面的代码:
参数 side、lens[2]、rd 和*pd 都是有名称的、double 类型的数据对象,因此可以为其创建引用,而不需要临时变量(还记得吗,数组元素的行为与同类型的变量类似)。然而,edge 虽然是变量,类型却不正确,double 引用不能指向 long。另一方面,参数 7.0 和 side + 10.0 的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让 ra 指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
那么为什么对于常量引用,这种行为是可行的,其他情况下却不行的呢?对于程序清单 8.4 中的函数 swapr( ):
如果在早期 C++较宽松的规则下,执行下面的操作将发生什么情况呢?
这里的类型不匹配,因此编译器将创建两个临时 int 变量,将它们初始化为 3 和 5,然后交换临时变量的内容,而 a 和 b 保持不变。
简而言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量,现在的 C++标准正是这样做的(然而,在默认情况下,有些编译器仍将发出警告,而不是错误消息,因此如果看到了有关临时变量的警告,请不要忽略)。
现在来看 refcube( ) 函数。该函数的目的只是使用传递的值,而不是修改它们,因此临时变量不会造成任何不利的影响,反而会使函数在可处理的参数种类方面更通用。因此,如果声明将引用指定为 const,C++将在必要时生成临时变量。实际上,对于形参为 const 引用的 C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
注意:
如果函数调用的参数不是左值或与相应的 const 引用参数的类型不匹配,则 C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
应尽可能使用 const
将引用参数声明为常量数据的引用的理由有三个:
使用 const 可以避免无意中修改数据的编程错误;
使用 const 使函数能够处理 const 和非 const 实参,否则将只能接受非 const 数据;
使用 const 引用使函数能够正确生成并使用临时变量。
因此,应尽可能将引用形参声明为 const。
C++11 新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用&&声明的:
新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。第 18 章将讨论如何使用右值引用来实现移动语义(move semantics)。以前的引用(使用&声明的引用)现在称为左值引用。
8.2.4 将引用用于结构
引用非常适合用于结构和类(C++的用户定义类型)。确实,引入引用主要是为了用于这些类型的,而不是基本的内置类型。
使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时使用引用运算符&即可。例如,假设有如下结构定义:
则可以这样编写函数原型,在函数中将指向该结构的引用作为参数:
如果不希望函数修改传入的结构,可使用 const:
程序清单 8.6 中的程序正是这样做的。它还通过让函数返回指向结构的引用添加了一个有趣的特点,这与返回结构有所不同。对此,有一些需要注意的地方,稍后将进行介绍。
程序清单 8.6 strtref.cpp
下面是该程序的输出:
1.程序说明
该程序首先初始化了多个结构对象。本书前面说过,如果指定的初始值比成员少,余下的成员(这里只有 percent)将被设置为零。第一个函数调用如下:
由于函数 set_pc() 的形参 ft 为引用,因此 ft 指向 one,函数 set_pc() 的代码设置成员 one.percent。就这里而言,按值传递不可行,因此这将导致设置的是 one 的临时拷贝的成员 percent。根据前一章介绍的知识,另一种方法是使用指针参数并传递地址,但要复杂些:
下一个函数调用如下:
由于 display() 显示结构的内容,而不修改它,因此这个函数使用了一个 const 引用参数。就这个函数而言,也可按值传递结构,但与复制原始结构的拷贝相比,使用引用可节省时间和内存。
再下一个函数调用如下:
函数 accumulate() 接收两个结构参数,并将第二个结构的成员 attempts 和 made 的数据添加到第一个结构的相应成员中。只修改了第一个结构,因此第一个参数为引用,而第二个参数为 const 引用:
返回值呢?当前讨论的函数调用没有使用它;就目前而言,原本可以将返回值声明为 void,但请看下述函数调用:
上述代码是什么意思呢?首先,将结构对象 team 作为第一个参数传递给了 accumulate()。这意味着在函数 accumulate() 中,target 指向的是 team。函数 accumulate() 修改 team,再返回指向它的引用。注意到返回语句如下:
光看这条语句并不能知道返回的是引用,但函数头和原型指出了这一点:
如果返回类型被声明为 free_throws 而不是 free_throws &,上述返回语句将返回 target(也就是 team)的拷贝。但返回类型为引用,这意味着返回的是最初传递给 accumulate() 的 team 对象。
接下来,将 accumulate() 的返回值作为参数传递给了 display(),这意味着将 team 传递给了 display()。display() 的参数为引用,这意味着函数 display() 中的 ft 指向的是 team,因此将显示 team 的内容。所以,下述代码:
与下面的代码等效:
上述逻辑也适用于如下语句:
因此,该语句与下面的语句等效:
接下来,程序使用了一条赋值语句:
正如您预期的,这条语句将 team 中的值复制到 dup 中。
最后,程序以独特的方式使用了 accumulate():
这条语句将值赋给函数调用,这是可行的,因为函数的返回值是一个引用。如果函数 accumulate() 按值返回,这条语句将不能通过编译。由于返回的是指向 dup 的引用,因此上述代码与下面的代码等效:
其中第二条语句消除了第一条语句所做的工作,因此在原始赋值语句使用 accumulate() 的方式并不好。
2.为何要返回引用
下面更深入地讨论返回引用与传统返回机制的不同之处。传统返回机制与按值传递函数参数类似:计算关键字 return 后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。请看下面的代码:
在第一条语句中,值 4.0 被复制到一个临时位置,然后被复制给 m。在第二条语句中,值 5.0 被复制到一个临时位置,然后被传递给 cout(这里理论上的描述,实际上,编译器可能合并某些步骤)。
现在来看下面的语句:
如果 accumulate() 返回一个结构,而不是指向结构的引用,将把整个结构复制到一个临时位置,再将这个拷贝复制给 dup。但在返回值为引用时,将直接把 team 复制到 dup,其效率更高。
注意:
返回引用的函数实际上是被引用的变量的别名。
3.返回引用时需要注意的问题
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。您应避免编写下面这样的代码:
该函数返回一个指向临时变量(newguy)的引用,函数运行完毕后它将不再存在。第 9 章将讨论各种变量的持续性。同样,也应避免返回指向临时变量的指针。
为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。程序清单 8.6 中的 accumulate() 正是这样做的。
另一种方法是用 new 来分配新的存储空间。前面见过这样的函数,它使用 new 为字符串分配内存空间,并返回指向该内存空间的指针。下面是使用引用来完成类似工作的方法:
第一条语句创建一个无名的 free_throws 结构,并让指针 pt 指向该结构,因此*pt 就是该结构。上述代码似乎会返回该结构,但函数声明表明,该函数实际上将返回这个结构的引用。这样,便可以这样使用该函数:
这使得 jolly 成为新结构的引用。这种方法存在一个问题:在不再需要 new 分配的内存时,应使用 delete 来释放它们。调用 clone( ) 隐藏了对 new 的调用,这使得以后很容易忘记使用 delete 来释放内存。第 16 章讨论的 auto_ptr 模板以及 C++11 新增的 unique_ptr 可帮助程序员自动完成释放工作。
4.为何将 const 用于引用返回类型
程序清单 8.6 包含如下语句:
其效果如下:首先将 five 的数据添加到 dup 中,再使用 four 的内容覆盖 dup 的内容。这条语句为何能够通过编译呢?在赋值语句中,左边必须是可修改的左值。也就是说,在赋值表达式中,左边的子表达式必须标识一个可修改的内存块。在这里,函数返回指向 dup 的引用,它确实标识的是一个这样的内存块,因此这条语句是合法的。
另一方面,常规(非引用)返回类型是右值——不能通过地址访问的值。这种表达式可出现在赋值语句的右边,但不能出现在左边。其他右值包括字面值(如 10.0)和表达式(如 x + y)。显然,获取字面值(如 10.0)的地址没有意义,但为何常规函数返回值是右值呢?这是因为这种返回值位于临时内存单元中,运行到下一条语句时,它们可能不再存在。
假设您要使用引用返回值,但又不允许执行像给 accumulate() 赋值这样的操作,只需将返回类型声明为 const 引用:
现在返回类型为 const,是不可修改的左值,因此下面的赋值语句不合法:
该程序中的其他函数调用又如何呢?返回类型为 const 引用后,下面的语句仍合法:
这是因为 display() 的形参也是 const free_throws &类型。但下面的语句不合法,因此 accumulate() 的第一个形参不是 const:
这影响大吗?就这里而言不大,因为您仍可以这样做:
另外,您仍可以在赋值语句右边使用 accumulate()。
通过省略 const,可以编写更简短代码,但其含义也更模糊。
通常,应避免在设计中添加模糊的特性,因为模糊特性增加了犯错的机会。将返回类型声明为 const 引用,可避免您犯糊涂。然而,有时候省略 const 确实有道理,第 11 章将讨论的重载运算符<<就是一个这样的例子。
8.2.5 将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。例如,可以通过使用引用,让函数将类 string、ostream、istream、ofstream 和 ifstream 等类的对象作为参数。
下面来看一个例子,它使用了 string 类,并演示了一些不同的设计方案,其中的一些是糟糕的。这个例子的基本思想是,创建一个函数,它将指定的字符串加入到另一个字符串的前面和后面。程序清单 8.7 提供了三个这样的函数,然而其中的一个存在非常大的缺陷,可能导致程序崩溃甚至不同通过编译。
程序清单 8.7 strquote.cpp
下面是该程序的运行情况:
此时,该程序已经崩溃。
程序说明
在程序清单 8.7 的三个函数中,version1 最简单:
它接受两个 string 参数,并使用 string 类的相加功能来创建一个满足要求的新字符串。这两个函数参数都是 const 引用。如果使用 string 对象作为参数,最终结果将不变:
在这种情况下,s1 和 s2 将为 string 对象。使用引用的效率更高,因为函数不需要创建新的 string 对象,并将原来对象中的数据复制到新对象中。限定符 const 指出,该函数将使用原来的 string 对象,但不会修改它。
temp 是一个新的 string 对象,只在函数 version1( ) 中有效,该函数执行完毕后,它将不再存在。因此,返回指向 temp 的引用不可行,因此该函数的返回类型为 string,这意味着 temp 的内容将被复制到一个临时存储单元中,然后在 main( ) 中,该存储单元的内容被复制到一个名为 result 的 string 中:
将 C-风格字符串用作 string 对象引用参数
对于函数 version1( ),您可能注意到了很有趣的一点:该函数的两个形参(s1 和 s2)的类型都是 const string &,但实参(input 和“***”)的类型分别是 string 和 const char *。由于 input 的类型为 string,因此让 s1 指向它没有任何问题。然而,程序怎么能够接受将 char 指针赋给 string 引用呢?
这里有两点需要说明。首先,string 类定义了一种 char *到 string 的转换功能,这使得可以使用 C-风格字符串来初始化 string 对象。其次是本章前面讨论过的类型为 const 引用的形参的一个属性。假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。例如,在本章前面,将 int 实参传递给 const double &形参时,就是以这种方式进行处理的。同样,也可以将实参 char *或 const char *传递给形参 const string &。
这种属性的结果是,如果形参类型为 const string &,在调用函数时,使用的实参可以是 string 对象或 C-风格字符串,如用引号括起的字符串字面量、以空字符结尾的 char 数组或指向 char 的指针变量。因此,下面的代码是可行的:
函数 version2( ) 不创建临时 string 对象,而是直接修改原来的 string 对象:
该函数可以修改 s1,因为不同于 s2,s1 没有被声明为 const。
由于 s1 是指向 main( ) 中一个对象(input)的引用,因此将 s1 最为引用返回是安全的。由于 s1 是指向 input 的引用,因此,下面一行代码:
与下面的代码等价:
然而,由于 s1 是指向 input 的引用,调用该函数将带来修改 input 的副作用:
因此,如果要保留原来的字符串不变,这将是一种错误设计。
程序清单 8.7 中的第三个函数版本指出了什么不能做:
它存在一个致命的缺陷:返回一个指向 version3( ) 中声明的变量的引用。这个函数能够通过编译(但编译器会发出警告),但当程序试图执行该函数时将崩溃。具体地说,问题是由下面的赋值语句引发的:
程序试图引用已经释放的内存。
8.2.6 对象、继承和引用
ostream 和 ofstream 类凸现了引用的一个有趣属性。正如第 6 章介绍的,ofstream 对象可以使用 ostream 类的方法,这使得文件输入/输出的格式与控制台输入/输出相同。使得能够将特性从一个类传递给另一个类的语言特性被称为继承,这将在第 13 章详细讨论。简单地说,ostream 是基类(因为 ofstream 是建立在它的基础之上的),而 ofstream 是派生类(因为它是从 ostream 派生而来的)。派生类继承了基类的方法,这意味着 ofstream 对象可以使用基类的特性,如格式化方法 precision( ) 和 setf( )。
继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。例如,参数类型为 ostream &的函数可以接受 ostream 对象(如 cout)或您声明的 ofstream 对象作为参数。
程序清单 8.8 通过调用同一个函数(只有函数调用参数不同)将数据写入文件和显示到屏幕上来说明了这一点。该程序要求用户输入望远镜物镜和一些目镜的焦距,然后计算并显示每个目镜的放大倍数。放大倍数等于物镜的焦距除以目镜的焦距,因此计算起来很简单。该程序还使用了一些格式化方法,这些方法用于 cout 和 ofstream 对象(在这个例子中为 fout)时作用相同。
程序清单 8.8 filefunc.cpp
下面是该程序的运行情况:
下述代码行将目镜数据写入到文件 ep-data.txt 中:
而下述代码行将同样的信息以同样的格式显示到屏幕上:
程序说明
对于该程序,最重要的一点是,参数 os(其类型为 ostream &)可以指向 ostream 对象(如 cout),也可以指向 ofstream 对象(如 fout)。该程序还演示了如何使用 ostream 类中的格式化方法。下面复习(介绍)其中的一些,更详细的讨论请参阅第 17 章。
方法 setf( ) 让您能够设置各种格式化状态。例如,方法调用 setf(ios_base::fixed) 将对象置于使用定点表示法的模式;setf(ios_base::showpoint) 将对象置于显示小数点的模式,即使小数部分为零。方法 precision( ) 指定显示多少位小数(假定对象处于定点模式下)。所有这些设置都将一直保持不变,直到再次调用相应的方法重新设置它们。方法 width( ) 设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为零,这意味着刚好能容纳下要显示的内容。
函数 file_it( ) 使用了两个有趣的方法调用:
方法 setf( ) 返回调用它之前有效的所有格式化设置。ios_base::fmtflags 是存储这种信息所需的数据类型名称。因此,将返回值赋给 initial 将存储调用 file_it( ) 之前的格式化设置,然后便可以使用变量 initial 作为参数来调用 setf( ),将所有的格式化设置恢复到原来的值。因此,该函数将对象回到传递给 file_it( ) 之前的状态。
了解更多有关类的知识将有助于更好地理解这些方法的工作原理,以及为何在代码中使用 ios_base。然而,您不用等到第 17 章才使用这些方法。
需要说明的最后一点是,每个对象都存储了自己的格式化设置。因此,当程序将 cout 传递给 file_it( ) 时,cout 的设置将被修改,然后被恢复;当程序将 fout 传递给 file_it( ) 时,fout 的设置将被修改,然后被恢复。
8.2.7 何时使用引用参数
使用引用参数的主要原因有两个。
- 程序员能够修改调用函数中的数据对象。
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么,什么时候应使用引用、什么时候应使用指针呢?什么时候应按值传递呢?下面是一些指导原则:
对于使用传递的值而不作修改的函数。
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向 const 的指针。
- 如果数据对象是较大的结构,则使用 const 指针或 const 引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
- 如果数据对象是类对象,则使用 const 引用。类设计的语义常常要求使用引用,这是 C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。
对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如 fixit(&x)这样的代码(其中 x 是 int),则很明显,该函数将修改 x。
- 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构,则使用引用或指针。
- 如果数据对象是类对象,则使用引用。
当然,这只是一些指导原则,很可能有充分的理由做出其他的选择。例如,对于基本类型,cin 使用引用,因此可以使用 cin>>n,而不是 cin >> &n。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论