第08章 函数探幽

本章内容包括:

  • 内联函数
  • 应用变量
  • 如何按引用传递函数参数
  • 默认参数
  • 函数重载
  • 函数模板
  • 函数模板具体化

8.1 C++内联函数

  • 常规函数和内联函数之间的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中.
  • 内联函数的编译代码与其他程序代码”内联”起来了.编译器将使用相应的函数代码替换函数调用.内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存.所以应该有选择的使用内联函数.
  • 要是用这项特性,必须采取下述措施之一: 
    1. 在函数声明前加上关键字inline
    2. 在函数定义前加上关键字inline
  • 通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方.
  • 程序员请求将函数作为内联函数时,(有些)编译器不一定会满足这种要求.过大或调用自己,内联函数不能递归.
  • 程序清单8.1 inline.cpp 
    • 尽管程序没有提供独立的原型,但C++原型特性仍在起作用.这是因为在函数首次使用前出现的整个函数定义充当了原型.
  • 内联与宏:inline工具是C++新增的特性.C语言使用预处理器语句#define来提供宏–内联代码的原始实现.宏不能按值传递.需要指出的是,如果使用C语言的宏执行了类似函数的功能,应考虑将他们转换为C++内联函数.

8.2 引用变量

  • 引用变量的主要用途是用作函数的形参.通过将引用变量用作参数,函数将使用原始数据,而不是其副本.

8.2.1 创建引用变量

  • C和C++使用&符号来指示变量的地址.C++给&符号赋予了另一个含义,将其用来声明引用.
int rats;
int & reodents = rats;
  • 程序清单8.2 firstref.cpp
  • 注意:必须在声明引用变量时进行初始化.
  • 引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它.
  • 程序清单8.3 sceref.cpp

8.2.2 将引用用作函数参数

  • 程序清单8.4 swaps.cpp

8.2.3 引用的属性和特别之处

  • 程序清单8.5 cubes.cpp
  • 如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用.如果这样做,当编译器发现代码修改了方法里的值时,将生成错误消息.
  • 临时变量,应用参数和const:如果实参与引用参数不匹配,C++将生成临时变量.
  • 如果引用参数是const,则编译器将在下面两种情况下生成临时变量: 
    • 实参的类型正确,但不是左值;
    • 实参的类型不正确,但可以转换为正确的类型.
  • 如果接受引用参数的函数和意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现.解决方法是,禁止创建临时变量,现在的C++标准正是这样做的(然而,在默认情况下,有些编译器仍将发出警告,而不是错误消息,因此如果看到了有关临时变量的警告,请不要忽略).
  • 注意:如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量.
  • 应尽可能使用const:将引用参数声明为常量数据的引用的理由有三个: 
    1. 使用const可以避免无意中修改数据的编程错误.
    2. 使用const使函数能够处理const和非const实参,否则将只能接受非const数据.
    3. 使用const引用使函数能够正确生成并使用临时变量.
  • C++11新增了另一种引用—右值引用.这种引用可指向右值,是使用&&声明的.主要目的是,让库设计人员能够提供有些操作的更有效实现.以前的引用(使用&声明的引用)现在称为左值引用.

8.2.4 将引用用于结构

  • 程序清单8.6 strt_ref.cpp 
    1. 程序说明
    2. 为何要返回引用 
      • 注意:返回引用的函数实际上是被引用的变量的别名.
    3. 返回引用时需要注意的问题 
      • 应避免返回函数终止时不再存在的内存单元引用.
    4. 为何将const用于引用返回类型

8.2.5 将引用用于类对象

  • 将类对象传递给函数时,C++通常的做法是使用引用.
  • 程序清单8.7 strquote.cpp

8.2.6 对象,继承和引用

  • 程序清单 filefunct.cpp
//filefunct.cpp -- function with ostream & parameter
#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;
void file_it(ostream & os, double fo, const double fe[],int n);
const int LIMIT = 5;
int main()
{
    ofstream fout;
    const char * fn = "ep-data.txt";
    fout.open(fn);
    if (!fout.is_open())
    {
        cout << "Can't open " << fn << ". Bye.\n";
        exit(EXIT_FAILURE);
    }
    double objective;
    cout << "Enter the focal length of your "
            "telescope objective in mm: ";
    cin >> objective;
    double eps[LIMIT];
    cout << "Enter the focal lengths, in mm, of " << LIMIT
         << " eyepieces:\n";
    for (int i = 0; i < LIMIT; i++)
    {
        cout << "Eyepiece #" << i + 1 << ": ";
        cin >> eps[i];
    }
    file_it(fout, objective, eps, LIMIT);
    file_it(cout, objective, eps, LIMIT);
    cout << "Done\n";
    // cin.get();
    // cin.get();
    return 0;
}
void file_it(ostream & os, double fo, const double fe[],int n)
{
    // save initial formatting state
    ios_base::fmtflags initial;
    initial = os.setf(ios_base::fixed, ios_base::floatfield);
    std::streamsize sz = os.precision(0);
    os << "Focal length of objective: " << fo << " mm\n";
    os.precision(1);
    os.width(12);
    os << "f.l. eyepiece";
    os.width(15);
    os << "magnification" << endl;
    for (int i = 0; i < n; i++)
    {
        os.width(12);
        os << fe[i];
        os.width(15);
        os << int (fo/fe[i] + 0.5) << endl;
    }
    // restore initial formatting state
    os.setf(initial, ios_base::floatfield);
    os.precision(sz);
}
  • 方法setf()让您能够设置各种格式化状态.

8.2.7 何时使用引用参数 
8.3 默认参数

  • 默认参数指的是当函数调用汇总省略了实参时自动使用的一个值.
  • 如何设置默认值呢?必须通过函数原型.由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也 必须将可能的默认参数告知程序.方法是将值赋给原型中的参数.例如:left()的原型如下:
char * left(const char * str, int n=1);
  • 实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数.
  • 程序清单8.9 left.cpp

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

8.4 函数重载

  • 函数重载的关键是函数的参数列表—也称为函数特征标function signature.如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的.
  • 请记住,是特征标,而不是函数类型使得可以对函数进行重载.
  • 重载引用参数:类设计和STL经常使用引用参数,因此知道不同引用类型的重载很有用.

8.4.1 重载示例

  • 程序清单8.10 leftover.cpp

8.4.2 何时使用函数重载

  • 什么是名称修饰:C++如何跟踪每一个重载函数呢?它给这些函数制定了秘密身份.使用C++开发工具种的编辑器编写和编译程序时,C++编译器将执行一些神奇的操作—名称修饰或名称矫正,它根据函数原型中制定的形参类型对每个函数名进行加密.对原始名称进行的表面看来无意义的修饰(或矫正,因人而异)将对参数数目和类型进行编码.添加的一组符号随函数特征标而异,而修饰时使用的约定随编译器而异.

8.5 函数模板

  • 函数模板是通用的函数描述,也就是回溯哦,他们使用泛型来定义函数,其中的泛型可用具体的类型替换.
  • 函数模板允许以任意类型的方式来定义函数.
  • typename关键字使得参数AnyType表示类型这一点更为明显;然而,有大量代码库是使用关键字class开发的.在这种上下文中,这两个关键字是等价的.
  • 提示:如果需要多个将同一种算法用于不同类型的函数,请使用模板.如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename而不使用class.
  • 程序清单8.11 funtemp.cpp 
    • 注意:函数模板不能缩短可执行程序.
  • 更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件.

8.5.1 重载的模板

  • 和常规重载一样,被重载的模板的函数特征标必须不同.
  • 程序清单8.12 twotemps.cpp

8.5.2 模板的局限性 
8.5.3 显式具体化

  1. 第三代具体化(ISO/ANSI C++标准) 
    • 试验其他具体方法后,C++98标准选择了下面的方法. 
      • 对于给定的函数名,可以有费模板函数,模板函数和显式具体化模板函数以及他们的重载版本.
      • 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型.
      • 具体化优先于常规模板,而非模板函数优先与具体化和常规模板.
  2. 显式具体化示例 
    • 程序清单8.13 twoswap.cpp

8.5.4 实例化和具体化

  • 记住,在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案.编译器使用模板为特定类型生成函数定义时,得到的是模板实例instantiation.
  • 隐式实例化和显式实例化
  • 显式具体化声明在关键字template后包含<>,而显式实例化没有
  • 警告:试图在同一个文件(或转换单元)中使用同一种类型的显式实例和显式具体化将出错.
  • 隐式实例化,显式实例化和显式具体化统称为具体化specialization.他们的相同之处在于,他们表示的都是使用具体类型的函数定义,而不是通用描述.引入显式实例化后,必须使用新的语法—在声明中使用前缀template和template<>,以区分显式实例化和显式具体化.

8.5.5 编译器选择使用哪个函数版本

  • 第1步:创建候选函数列表.其中包含与被调用函数的名称相同的函数和模板函数.
  • 第2步:使用候选函数列表创建可行函数列表.这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况.
  • 第3步:确定是否有最佳的可行函数.如果有,则使用它,否则该函数调用出错.
  • 通常,从最佳到最差的顺序如下所述: 
    1. 完全匹配,但常规函数优先于模板.
    2. 提升转换
    3. 标准转换
    4. 用户定义的转换,如类声明中定义的转换.
  • 更深入的探讨: 
    1. 完全匹配和最佳匹配 
      • 如果有多个匹配的原型,则编译器将无法完成重载解析过程;如果没有最佳的可行函数,则编译器将声称一条错误消息,该消息可能会使用注入”ambiguous(二义性)”这样的词语.
      • 一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个 不是.在这种情况下,非模板函数将由于模板函数(包括显式具体化).
      • 如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先.
      • 术语”最具体”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少.
      • 用于找出最具体的模板的规则被称为函数模板的部分排序规则.和显式示例一样,这也是C++98新增的特性.
    2. 部分排序规则示例 
      • 程序清单8.14 tempover.cpp
      • 重载解析将寻找最匹配的函数.如果之存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且他们都为模板函数,但其中有一个函数比其他函数根据题,则选择该函数.如果有多个同样何时的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然,如果不存在匹配的函数,则也是错误.
    3. 自己选择 
      • 程序清单8.15 choices.cpp
// choices.cpp -- choosing a template
#include <iostream>
template<class T>
T lesser(T a, T b)         // #1
{
    return a < b ? a : b;
}
int lesser (int a, int b)  // #2
{
    a = a < 0 ? -a : a;
    b = b < 0 ? -b : b;
    return a < b ? a : b;
}
int main()
{
    using namespace std;
    int m = 20;
    int n = -30;
    double x = 15.5;
    double y = 25.9;
    cout << lesser(m, n) << endl;       // use #2
    cout << lesser(x, y) << endl;       // use #1 with double
    cout << lesser<>(m, n) << endl;     // use #1 with int
    cout << lesser<int>(x, y)  << endl; // use #1 with int
    // cin.get();
    return 0;
}

4.多个参数的函数

8.5.6 模板函数的发展

  1. 是什么类型 
    • 在C++98中,编写模板函数时,一个问题是并非总能指导应在声明中使用哪种类型.
  2. 关键字decltype(C++11) 
    • 注意:并不会实际调用函数.编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数.
  3. 另一种函数声明语法(C++11后置返回类型) 
    • 原型:double h(int x,float y);使用新增的语法可编写成这样:auto h(int x,float y) -> double;这将返回类型移到了参数声明后面.->double被称为后置返回类型.其中auto是一个占位符,表示后置返回类型提供的类型,这是C++11给auto新增的一种角色.
    • 通过结合使用这种语法和decltype,便可给gt()制定返回类型,如:
template<class T1, class T2>
auto gt(T1 x, T2 y)  -> decltype(x + y)
{
    return x + y;
}

8.6 总结

  • C++扩展了C语言的函数功能.通过将inline关键字用于函数定义,并在首次调用该函数前提供其函数定义,可以使得C++编译器将该函数视为内联函数.也就是说,编译器不是让程序跳到独立的代码段,以执行函数,而是用相应的代码替换函数调用.只有在函数很短时才能采用内联方式.
  • 引用变量是一种伪装指针,它允许为变量创建别名(另一个名称).引用变量主要被用作处理结构和类对象的函数的参数.通常,被声明为特定类型引用的标识符只能指向这种类型的数据;然而,如果一个类是从另一个类派生出来的,则积累引用可以指向派生类对象.
  • C++原型让您能够定义参数的默认值.如果函数调用省略了相应的参数,则程序将使用默认值;如果函数调用提供了参数值,则程序将使用这个值(而不是默认值).只能在参数列表中从右到左提供默认参数.因此,如果为某个参数提供了默认值,则 必须为该参数右边所有的参数提供默认值.
  • 函数模板自动完成重载函数的过程.只需使用泛型和具体算法来定义函数,编译器将为程序中使用特定参数类型生成正确的函数定义.

8.7 复习题 
8.8 编程练习

附件:本章源代码下载地址

猜你喜欢

转载自blog.csdn.net/weixin_39345003/article/details/82110149