C++ PrimerPlus第8章 函数探幽

C++ PrimerPlus第8章 函数探幽

C++新特性:内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及模板函数。

8.1 C++内联函数

为了提高程序运行速度。和常规函数的主要区别在于C++编译器如何将它们组合到程序中。
编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。
应当选择地使用内联函数。如果代码执行时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
要使用这项特征,必须采取下述措施之一:
在函数声明前加上关键字inline;
在函数定义前加上关键字inline。
通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
程序员请求将函数作为内联函数时,编译器不一定会满足这种要求。它可能认为该函数过大或注意到函数调用了自己(内联函数不能递归),因此不将其作为内联函数;而有些编译器没有启用或实现这种特性。
如果函数定义占用多行(假定没有使用冗长的标识符),则将其作为内联函数就不太合适。

8.2 引用变量

c++新增了一种复合类型——引用变量。引用是已定义的变量的别名(另一个名称)。引用变量的主要用途是用作函数的形参。

8.2.1 创建引用变量
前面讲过,C和C++使用&符号来指示变量的地址。C++给&符号赋予了另一个含义,将其用来声明引用。
引用和指针的不同:
1.表示方法不同
2.引用必须在声明引用时将其初始化,而不能像指针那样,先声明,再赋值。
3.引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。

8.2.2将引用用作函数参数
引用经常被用作函数参数,使得函数中的变量名成为调用程序中的变量的别名。这种传递参数的方法称为按引用传递。按引用传递允许被调用的函数能够访问调用函数中的变量。
C++ PrimerPlus第8章 函数探幽_第1张图片
程序清单8.4
主要用来比较如下三个函数的方法:
1.void swapr(int & a, int & b );//按引用传递
2.void swapp(int * p, int * q);//按指针传递
3.void swapv(int a,int b);//按传递
1和2可以改变原始变量,3不可。
在swapr()中,变量a和b是复制了wallet1和wallet2的别名,所以交换a和b相当于交换wallet1和wallet2的值;
在swapv()中,变更a和b是复制了wallet1和waller2的值的新变量,因此不会影响wallet1和wallet2的值。
传递引用时,函数将可以使用原始数据。
地址运算符(&)使得地址传递一目了然。

8.2.3 引用的属性和特别之处
临时变量、引用参数和const
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。
生成临时变量的情况:
- 实参的类型正确,但不是左值;
- 实参的类型不正确,但可以转换为正确的类型。
左值参数是可以被引用的数据对象,如变量、数组元素、结构成员、引用和解除引用的指针。
非左值包括字面常量(用引用括起的字符串除外,它们由其地址表示)和包含多项的表达式。
常规变量属于可修改的左值,而const变量属于不可修改的左值。

注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。

应尽可能使用const
将引用参数声明为常量数据的引用的理由有三个:

  • 使用const可以避免无意中修改数据的编程错误;
  • 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
  • 使用const引用使函数能够正确生成并使用临时变量。
    因此,应尽可能将引用形参声明为const。
    C++11新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用&&声明的:
    C++ PrimerPlus第8章 函数探幽_第2张图片
    新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。第18章将讨论如何使用右值引用来实现移动语义(move semantics)。以前的引用(使用&声明的引用)现在称为左值引用。

8.2.4 将引用用于结构
引用非常适合用于结构和类(C++的用户定义类型),因为引如引用主要是为了用于这些类型的,而不是基本的内置类型。
使用结构引用参数的方式与使用基本变量引用相同,只需在声明结构参数时引用运算符&即可。
为何要返回引用
引用与传统返回机制的不同
传统返回机制与按值传递参数类似:计算关键字return后面的表达式,并将结果返回给调用函数。从概念上说,这个值被复制到一个临时位置,而调用程序将使用这个值。
**注意:**返回引用的函数实际上是被引用的变量的别名。
返回引用时需要注意的问题
返回引用时最重要的一点是,应避免返回函数终止时不再存在内存单元引用。避免编写下面这样的代码
C++ PrimerPlus第8章 函数探幽_第3张图片
为了避面这种问题,以下两种方法:
- 返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
- 用new来分配新的存储空间。(这种方法存在一个问题:在不再需要new分配的内存时,应使用delete来释放它们。调用clone()隐藏了对new的调用,这使得以后很容易忘记使用delete来释放内存。
为何将const用于引用返回类型:要使用引用返回值,但又不允许执行像给accumulate()赋值这样的操作,需要将返回类型声明为const引用。

8.2.5 将引用用于类对象
将类对象传递给函数时,C++通常的做法是使用引用。

8.2.6 对象、继承和引用
继承:使得能够将特性从一个类传递给另一个类的语言特性。
继承的另一个特征:基类引用可以指向派生类对象,而无需进行强制类型转换。

8.2.7 何时使用引用参数
使用引用参数的主要原因有两个:
- 程序员能够修改调用函数中的数据对象。
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
当数据对象较大时(如结构和类对象),第二个原因最重要。这些也是使用指针参数的原因。这是有道理的,因为引用参数实际上是基于指针的代码的另一个接口。那么,什么时候使用引用、什么时候应使用指针呢?什么时候应按值传递呢?下面是一些指导原则:

对于使用传递的值而不作修改的函数。
- 如果数据对象很小,如内置数据类型或小型结构,则按值传递。
- 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针。
- 如果数据对象是较大的结构,则使用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间。
- 如果数据对象是类对象,则使用const引用。类设计的语义常常要求使用引用,这是C++新增这项特性的主要原因。因此,传递类对象参数的标准方式是按引用传递。

对于修改调用函数中数据的函数:
- 如果数据对象是内置数据类型,则使用指针。如果看到诸如fixit(&x)这样的代码(其中x是int),则很明显,该函数将修改x。
- 如果数据对象是数组,则只能使用指针。
- 如果数据对象是结构,则使用引用或指针。
- 如果数据对象是类结构,则使用引用。

8.3 默认参数

默认参数指的是当函数调用中省略了实参时自动使用的一个值。

通过函数原型设置默认值。默认参数值是初始化值。

对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。

8.4 函数重载

默认参数能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让您能够使用多个同名的函数。通过函数重载来设计一系列函数——它们完成相同的工作,但使用不同的参数列表。

函数重载的关键是函数的参数列表——也称函数特征标(function signature)。

8.4.1 重载示例
8.4.2 何时使用函数重载
仅当函数基本上执行相同的任务,但使用不同形式的数据时,才应采用函数重载。

什么是名称修饰:
为了跟踪每一个重载函数。使用C++开发工具中的编译器编写和编译程序时,C++编译器将执行一些神奇的操作——名称修饰(name decoration)或名称矫正(name mangling),它根据函数原型中指定的形参类型对每个函数名进行加密。对原始名称进行的表面看来无意义的修饰将对参数数目和类型进行编码。添加的一组符号随函数特征标而异,而修饰时使用的约定随编译器而异。

8.5 函数模板

函数模板是通用的函数描述,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。由于类型是参数表示的,因此模板特性有时也被称为参数化类型(parameterized types)。

要建立模板,关键字template和typename是必须的,除非可是使用关键字class代替typename。另外,必须使用尖括号。

template <typename T>
void 函数名称(T 参数名称){}
or
template <class T>
void 函数名称(T 参数名称){}

提示:如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename而不使用class。

注意,函数模板不能缩短可执行程序,最终仍将两个独立的函数定义,就像以手工方式定义了这些函数一样。最终的代码不包含任何模板,而只包含了为程序生成的实际函数。使用模板的好处是,它使生成多个函数定义更简单、更可靠。

8.5.1 重载的模板
可以像重载常规函数定义那样重载模板定义。

8.5.2 模板的局限性
编写的模板函数很可能无法处理某些类型。有时候通用化是有意义的,但C++语法不允许这样做。

8.5.3 显式具体化(explicit specialization)
当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

C++标准定义形式:
1.第三代具体化(ISO/ANSI C++标准)
试验其他具体化方法后,C++98标准选择了下面的方法。

  • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
  • 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。
  • 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。

8.5.4 实例化和具体化
在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。
显式具体化在关键字template后包含<>,而显式实例化没有。
显式实例化:
函数调用导致编译器成的实例,为隐式实例化(implicit instantiation)。
直接命令编译器创建特定的实例为显式实例化,声明所需的种类——用<>符号指示类型,并在声明前加上关键字template:

template void Swap<int>(int,int);

显式具体化:

template <> void Swap<int>(int &, int &);
or
template <> void Swap(int &, int &);

区别在于,这些声明的意思是“不要使用Swap()模板来生成函数定义,而应使用专门为int类型显示地定义的函数定义”。这些原型必须有自己的函数定义。显式具体化声明在关键字template后包含<>,而显式实例化没有。

警告:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错。

还可以通过在程序中使用函数来创建显式实例化。

隐式实例化、显式实例化和显式具体化统称为具体化(specialization)。它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。

8.5.5 编译器选择使用哪个函数版本
重载解析(overloading resonlution):决定为函数调用使用哪一个函数定义。

8.5.6 模板函数的发展

8.6总结

C++扩展了C语言的函数功能。通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数。也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用。只有在函数很短时才能采用内联方式。

引用变量是一种伪装指针,它允许为变量创建别名(另一个名称)。引用变量的主要被用作处理结构和类对象的函数的参数。通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类(如ofstream)是从另一个类(如ostream)派生出来的,则基类引用可以指向派生类对象。

C++原型让您能够定义参数的默认值。如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值)。只能在参数列表中从右到左提供默认参数。因此,如果为某个参数提供了默认值,则必须为该参数提供默认值。

函数的特征标是其参数列表。程序员可以定义两个同名函数,只要其特征标不同。这被称为函数多态或重载。通常,通过重载函数来为不同的数据类型提供相同的服务。

函数模板自动完成重载函数的过程。只需使用泛型和具体算法来定义函数,编译器将为程序中使用的特定参数类型生成正确的函数定义。

你可能感兴趣的:(C++,PrinmerPlus,c++)