[C++学习笔记] 第 5 章 函数探幽

第 5 章 函数探幽

​ 从这一章开始,将会进入到 C++ 中比较核心的部分。

5.1 内联函数

内联函数是 C++ 为提高程序运行速度所做的一项改进。常规函数和内联函数之间的主要区别不在于编写方式,而在于 C++ 编译器如何将它们组合到程序中。

​ 对于常规函数,执行到函数调用指令时,程序将跳到标记函数起点的内存单元,执行函数代码,然后再跳回到原来的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到原来阅读的地方类似)。来回跳跃并记录跳跃位置意味着需要一定的开销。

​ C++ 内联函数提供了另一种选择。内联函数的编译代码与其他程序代码内联起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。

​ 要使用这项特性,必须采取以下措施:在函数声明或者函数定义前加上关键字 inline。通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。

​ 程序员请求将函数作为内联函数时,编译器不一定会满足这种要求。它可能认为该函数过大或者调用了自己(内联函数不能递归),因此不能作为内联函数,下面是一个使用内联函数的例子:

#include <iostream>
inline double square(double x) { return x * x; }
int main()
{
    using namespace std;
    double a, b,c=13.0;
    a = square(5.0);
    b = square(4.5 + 7.5);
    
    cout << "a= " << a << endl;
    cout << "b= " << b << endl;
    cout << "c= " << c << " , c^2= " << square(c++) << endl;
    cout << "Now, c= " << c;
}

//输出
a= 25
b= 144
c= 13 , c^2= 169
Now, c= 14

​ 输出表明,内联函数和常规函数一样,也是按值来传递参数的。如果参数为表达式,如 4.5+7.5,则函数将传递表达式的值。这使得 C++ 的内联功能远远胜过 C 的宏定义。

内联与宏

inline 工具是 C++ 新增的特性。C 语言使用预处理器语句 #define 来提供宏——内联代码的原始实现。例如,下面是一个计算平方的宏:

#define SQUARE(X) X*X

​ 这并不是通过传递参数来实现的,而是通过文本替换来实现的——X 是参数的符号标记。如:

a = SQUARE(5.0); 		// 等价于 a = 5.0*5.0;
b = SQUARE(4.5+7.5);	// 等价于 b = 4.5+7.5*4.5+7.5;
d = SQUARE(c++);		// 等价于 d = c++ * c++; 

​ 上述代码中只有第一个可以正常工作。可以通过使用括号来改进宏:

#define SQUARE(X) ((X)*(X))

​ 通过改进后,示例中的前两个代码可以正常工作,但 SQUARE(c++) 仍会使 c 递增两次,不能正常工作。

​ 因此,如果使用了 C 的宏执行类似函数的功能,应考虑将它们转换为 C++ 内联函数。

5.2 引用变量

​ C++ 新增了一种复合类型——引用变量。引用是已定义变量的别名,主要用途是作为函数的形参。通过将引用变量作为函数的参数,函数将使用原始数据,而不是其副本。这样,除了指针之外,引用也为函数处理大型结构提供了一种非常方便的途径。

5.2.1 创建引用变量

​ C++ 给 & 符号赋予了另一个含义,将其用来声明引用。例如,要将 rodents 作为 rats 的别名,可以这样做:

int rats;
int & rodents = rats;

​ 其中,& 不是地址运算符,而是类型标识符的一部分。就像声明 char* 指的是指向 char 的指针一样,int& 指的是指向 int 的引用。上述引用声明允许将 ratsrodents 互换——它们指向相同的值和内存单元。例如:

#include <iostream>
int main()
{
    using namespace std;
    int rats = 101;
    int &rodents = rats;
    cout << "rats = " << rats << endl;
    cout << "rodents = " << rodents << endl;
    rodents++;
    cout << "After rodents++,\n";
    cout << "rats = " << rats << endl;
    cout << "rodents = " << rodents << endl;

    cout << "rats address = " << &rats << endl;
    cout << "rodents address = " << &rodents << endl;
}

//输出如下:
rats = 101
rodents = 101
After rodents++,
rats = 102
rodents = 102
rats address = 0xd64adffa44
rodents address = 0xd64adffa44

​ 对于 C 语言用户而言,首次接触到引用时可能会感到困惑,因为引用看起来很类似指针。实际上,引用还是不同于指针的。除了表示法不同外,还有其他的差别。例如,必须在声明引用时将其初始化,而不能像指针那样先申明再赋值:

int rats = 101;
int & rodents;
rodents = rat; //错误,必须在声明引用时将其初始化

​ 引用更接近 const 指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。也就是说:

int & rodents = rats;
//实际上是下面代码的伪装表示:
int * const pr = &rats;
//其中,rodents扮演的角色与表达式 *pr 相同

引用变量是谁的别名只看引用变量的初始化声明,一旦声明后就无法再更改。

5.2.2 将引用用作函数参数

​ 引用经常被用作函数参数,使得函数中的变量名成为调用程序中变量的别名,这种传递方式称为按引用传递。

​ 下面程序展示了按照传值、传址、传引用交换变量的函数:

#include <iostream>
using namespace std;
void swap1(int &a, int &b)
{
    int temp = a;
    a = b;
    b = temp;
}
void swap2(int *p,int *q)
{
    int temp = *p;
    *p = *q;
    *q = temp;
}
void swap3(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
}

​ 只有前两个函数才能成功交换变量的值。按引用传递(swap1)和按值传递(swap3)的函数看起来相同,只能通过原型或函数定义才能知道 swap1 是按引用传递的。

5.2.3 引用的属性和特别之处
  • 如果想要让函数使用传递给它的信息而不对其进行修改,同时又想使用引用,则应使用常量引用(使用 const 修饰引用变量):

    double refcube(const double &ra);
    
  • 按值传递的函数可以使用多种类型的实参,而传递引用的限制更加严格(只允许传递左值),如:

    //cube() 为按值传递的函数
    double z = cube(x + 2.0);
    z = cube(8.0);
    int k = 10;
    z = cube(k);
    
    //refcube() 为传递引用的函数
    double z = refcube(x + 3.0); //错误,x+3.0不是可修改的左值
    //这好比令 x + 3.0 = 5.0,是错误的
    
  • 临时变量、引用参数和 const

    当参数为 const 引用时,如果实参与引用参数不匹配,C++ 将在以下两种情况下创建临时变量:

    • 实参的类型正确,但不是左值;
    • 实参的类型不正确,但可以转换为正确的类型。

​ 左值是什么呢?左值参数是可被引用的数据对象,包括常规变量和 const 变量。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。

​ 假设定义了函数 refcube 如下:

double refcube(const double &ra)
{
    return ra*ra*ra;
}

​ 考虑以下代码:

double side = 3.0;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens[4] = {2.0, 5.0, 10.0, 12.0};
double c1 = refcube(side);			//ra为side
double c2 = refcube(lens[2]);		//ra为lens[2]
double c3 = refcube(rd);			//ra为side
double c4 = refcube(*pd);			//ra为side
double c5 = refcube(edge);			//ra为临时变量
double c6 = refcube(7.0);			//ra为临时变量
double c7 = refcube(side + 10.0);	//ra为临时变量

​ 上述代码中,后三种情况下,编译器都将生成一个临时匿名变量,并让 ra 指向它,这些临时变量只在函数调用期间存在。

​ 总而言之,如果声明将引用指定为 const,C++ 将在必要时生成临时变量。实际上,对于形参为 const 引用的 C++ 函数,如果实参不匹配,则其行为类似于按值传递。

应尽可能使用 const

将引用参数声明为 const 引用的理由有三个:

  • 可以避免无意中修改数据的编程错误
  • 使用 const 使函数能够处理 const 和 非 const 实参,否则将只能接受非 const 数据
  • 使用 const 引用使函数能够正确生成并使用临时变量
5.2.4 将引用用于结构

​ 引用非常适合用于结构和类(引入引用主要就是为了用于这些类型),考虑下面的的程序:

#include <iostream>
#include <string>
struct free_throws
{
    std::string name;
    int made;
    int attempts;
    float percent;
};

void display(const free_throws &ft)
{
    using std::cout;
    cout << "Name: " << ft.name << '\n';
    cout << " Made: " << ft.made << '\t';
    cout << "Attempts: " << ft.attempts << '\t';
    cout << "Percent: " << ft.percent << '\n';
}
void set_pc(free_throws &ft)
{
    if (ft.attempts != 0)
        ft.percent = 100.0f * float(ft.made) / float(ft.attempts);
    else
        ft.percent = 0;
}
free_throws &accumulate1(free_throws &target, const free_throws &source)
{
    target.attempts += source.attempts;
    target.made += source.made;
    set_pc(target);
    return target;
}

int main()
{
    free_throws one = {"Ifelsa Branch", 13, 14};
    free_throws two = {"Andor Knott", 10, 16};
    free_throws three = {"Minnie Max", 7, 9};
    free_throws four = {"Whily Looper", 5, 9};
    free_throws five = {"Long Long", 6, 14};
    free_throws team = {"Throwgoods", 0, 0};
    free_throws dup;

    set_pc(one);
    display(one);
    accumulate1(team, one);
    display(team);

    display(accumulate1(team, two));
    accumulate1(accumulate1(team, three), four);
    display(team);

    dup = accumulate1(team, five);
    std::cout << "Displaying team:\n";
    display(team);
    std::cout << "Displaying dup after assignment:\n";
    display(dup);
    set_pc(four);

    accumulate1(dup, five) = four;
    std::cout << "Displaying dup after ill-advised assignment:\n";
    display(dup);
    return 0;
}
//输出如下:
Name: Ifelsa Branch
 Made: 13       Attempts: 14    Percent: 92.8571
Name: Throwgoods
 Made: 13       Attempts: 14    Percent: 92.8571
Name: Throwgoods
 Made: 23       Attempts: 30    Percent: 76.6667
Name: Throwgoods
 Made: 35       Attempts: 48    Percent: 72.9167
Displaying team:
Name: Throwgoods
 Made: 41       Attempts: 62    Percent: 66.129
Displaying dup after assignment:
Name: Throwgoods
 Made: 41       Attempts: 62    Percent: 66.129
Displaying dup after ill-advised assignment:
Name: Whily Looper
 Made: 5        Attempts: 9     Percent: 55.5556
  1. 程序说明

​ 上述代码中,accumulate1 函数的返回类型为 free_throws &,这意味着返回的是最初传给 accumulate1 函数的 team 对象。

​ 接下来,程序中使用了一条赋值语句:dup = accumulate1(team, five),这条语句返回的是指向 team 的引用,将把 team 对象的值赋给 dup。注意,虽然返回的是引用,但 dup 并不是 team 的引用,也就是说改变 dup 并不会改变 team,这里不能弄混了。

​ 最后,程序以独特的方式使用了 accumulate1accumuate1(dup, five) = four。这是可行的,因为函数的返回值是一个引用,相当于:

accumulate1(dup, five);
dup = four;

​ 第二条语句消除了第一条语句做的工作,因此在原始赋值语句使用 accumulate1() 的方式并不好。

  1. 为何要返回引用

    考虑语句 dup = accumulate1(team, five),如果 accumulate1() 返回结构,而不是指向结构的引用,则该结构会被复制到一个临时的位置,再将这个拷贝复制给 dup但在返回值为引用时,将直接把 team 复制到 dup,效率更高

  2. 返回引用时需要注意的问题

    返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。例如:

    const free_throws & clone2(free_throws & ft)
    {
        free_throws = newguy;
        newguy = ft;
        return newguy;
    }
    

    该函数返回一个指向临时变量的引用,但函数运行完毕后他将不复存在。为避免这种问题,最简单的方法是返回一个作为参数传递给函数的引用。另一种方法是用 new 来分配新的存储空间,如:

    const free_throws & clone(free_throws & ft)
    {
        free_throws *pt = new free_throws;
        *pt = ft;
        return *pt;
    }
    

    上述代码将返回 pt 指向的结构的引用。这样,便可以这样使用函数:free_throws & jolly = clone(three);。这使得 jolly 成为新结构的引用。这种方法存在一个问题:当不再需要 new() 分配的内存时,应使用 delete 来释放它们。调用 clone() 隐藏了对 new 的调用,这使得以后很容易忘记使用 delete 来释放内存。以后将使用智能指针来解决这种情况。

  3. 为何将 const 用于引用返回类型

    上述程序包含如下语句:

    accumulate1(dup, five) = four;
    

    这条语句会让人觉得很奇怪,但又是合法的。假设我们需要使用引用返回值,但又不允许像给 accumulate1() 赋值这样的操作,只需将返回类型声明为 const 引用,使该语句不再合法。

5.2.5 将引用用于类对象

​ 将类对象传递给函数时,C++ 通常的做法是使用引用。下面是一个例子:

#include <iostream>
#include <string>
using namespace std;
string version1(const string &s1, const string &s2)
{
    string temp = s2 + s1 + s2;
    return temp;
}
const string &version2(string &s1, const string &s2)
{
    s1 = s2 + s1 + s2;
    return s1;
}
const string &version3(string &s1, const string &s2) //bad
{
    string temp = s2 + s1 + s2;
    return temp;
}

int main()
{
    string input, copy, result;
    cout << "Enter a string: ";
    getline(cin, input);
    copy = input;
    cout << "Your string as entered: " << input << endl;
    result = version1(input, "***");
    cout << "Your string enhanced: " << result << endl;
    cout << "Your original string: " << input << endl;

    result = version2(input, "###");
    cout << "Your string enhanced: " << result << endl;
    cout << "Your original string: " << input << endl;

    cout << "Resetting original string.\n";
    input = copy;
    result = version3(input, "@@@");
    cout << "Your string enhanced: " << result << endl;
    cout << "Your original string: " << input << endl;

    return 0;
}

//输出如下:
Enter a string: It's not my fault.
Your string as entered: It's not my fault.
Your string enhanced: ***It's not my fault.***
Your original string: It's not my fault.
Your string enhanced: ###It's not my fault.###
Your original string: ###It's not my fault.###
Resetting original string.
程序崩溃

​ 上述程序中有三个字符串拼接函数。其中,version1 最简单:它接受两个 string 参数,这两个参数都是 const 引用。如果使用 string 对象,最终结果将不变:

string version4(string s1, string s2);

​ 在这种情况下,s1、s2string 对象。使用引用的效率更高,因为函数不需要创建新的 string 对象,并将原来对象中的数据复制到新对象中。

​ 函数 version1 中的 temp 是一个新的 string 对象,只在函数内部有效,当函数执行完毕后,它将不复存在。因此,返回指向 temp 的引用是不可行的。

将 C 风格字符串用作 string 对象引用参数

​ 对于函数 version1,该函数的两个形参 s1、s2 的类型都是 const string &,但程序调用时实参(input"***")的类型分别是 stringconst char *。为什么程序可以接受将 char 指针赋值给 sting 引用呢?

​ 有两方面原因。首先,string 类定义了一种 char*string 的转换功能,这使得可以使用 C 风格字符串来初始化 string 对象。其次,之前已经讨论过类型为 const 引用的形参的一个属性:假设实参的类型与引用参数类型不匹配,但可被转换为引用类型,程序将创建一个正确类型的临时变量,使用转换后的实参值来初始化它,然后传递一个指向该临时变量的引用。

​ 这种属性的结果是,如果形参类型为 const string &,在调用函数时,使用的实参可以是 string 对象或 C 风格字符串。

​ 函数 version2 不创建临时 string 对象,而是直接修改原来的 string 对象。

​ 函数 version3 将引起程序崩溃,因为程序试图引用已经释放的内存。

5.2.6 对象、继承和引用

​ 之前提到过,ofstream 对象可以使用 ostream 类的方法。使得能够将特性从一个类传递给另一个类的语言特性被称为继承。简单地说,ostream 类是基类,而 ofstream 类是派生类。派生类继承了基类的方法,这意味着 ofstream 对象可以使用基类的特性,如方法 precision()setf()

​ 继承的另一个特征是:基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的一个结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为参数,也可以将派生类对象作为参数。例如,参数类型为 ostream & 的函数可以接受 ostream 对象(如 cout)或声明的 ofstream 对象作为参数。如:

#include <iostream>
#include <fstream>
#include <cstdlib>
using namespace std;

void file_it(ostream & os, double fo, const double fe[], int n)
{
    ios_base::fmtflags initial;
    initial = os.setf(ios_base::fixed);
    os.precision(0);
    os << "Focal length of objective: " << fo << " mm\n";
    os.setf(ios::showpoint);
    os.precision(1);
    os.width(12);
    os << "f.l. eyepiece";
    os.width(15);
    os << "magnification" << endl;

    os.setf(initial);
}

​ 对于该程序,最重要的一点就是参数 os 既可以指向 ostream 对象,也可以指向 ofstream 对象。

​ 下面介绍一些 ostream 类中的格式化方法:

  • setf()

    方法 setf() 能够设置各种格式化状态。例如,方法调用 setf(ios_base::fixed) 将对象置于使用定点表示法的模式;setf(ios_base::showpoint) 将对象置于显示小数点的模式,即使小数部分是 0 0 0。方法 setf() 返回调用它之前的所有格式化设置。ios_base::fmtflags 是存储这种信息所需的数据类型名称。因此,setf(initial) 会将所有的格式化设置恢复到原来的值。

  • precision()

    方法 precision() 指定显示多少位小数(假设对象处于定点模式下)。

    这些设置将一直保持不变,直到再次调用相应方法重新设置它们。

  • width()

    方法 width() 设置下一次输出操作使用的字段宽度,这种设置只在显示下一个值时有效,然后将恢复到默认设置。默认的字段宽度为 0 0 0,这意味着刚好能容纳下要显示的内容。

5.2.7 何时使用引用参数

​ 使用引用参数的主要原因有两个:

  • 程序员能够修改作为函数参数的数据对象
  • 通过传递引用而不是整个数据对象,可以提高程序的运行速度

​ 当数据对象较大时,第二个原因最重要。

5.3 默认参数

​ 下面介绍 C++ 的另一项新内容——默认参数。默认参数指的是当调用函数时省略了实参后自动使用的值。设置默认值必须通过函数原型,如 char * left(const char * str, int n = 1)

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

int harpo(int n, int m = 4, int j = 5);
int chico(int n, int m = 6, int j); //错误
int groucho(int k = 1, int m = 2, int n = 3);

​ 函数 harpo() 原型允许调用该函数时提供 1 、 2 1、2 12 3 3 3 个参数:

beeps = harpo(2);		//等价于 harpo(2,4,5)
beeps = harpo(1,8);		//等价于 harpo(1,8,5)
beeps = harpo(8,7,6);

​ 实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。因此,下面的调用是不允许的

beeps = harpo(3, ,8);

​ 默认参数并非编程方面的重大突破。而只是提供了一种便捷的方式。

5.4 函数重载

​ 函数多态是 C++ 在 C 语言的基础上新增的功能。默认参数允许用户使用不同数目的参数调用同一个函数,而函数多态(函数重载)允许用户使用多个同名的函数。

​ 函数重载的关键是函数的参数列表——也称为函数特征标。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而变量名是无关紧要的。C++ 允许定义名称相同的函数,条件是它们的特征标不同。例如:

void print(const char * str, int width);	//#1
void print(double d, int width);			//#2
void print(long l, int width);				//#3
void print(int i, int width);				//#4
void print(const char *str);				//#5

​ 使用 print() 函数时,编译器将根据所采取的用法使用相应的原型:

print("Pancakes", 15);	//调用#1
print("Syrup");			//调用#5
print(1999.0, 10);		//调用#2
print(1999, 12);		//调用#4
print(1999L, 15);		//调用#3

​ 使用被重载的函数时,需要使用正确的数据类型,例如:

unsigned int year = 3210;
print(year, 6);

print() 调用将不与任何原型匹配。没有匹配的原型并不会自动停止使用其中的某个函数,因为 C++ 将尝试使用标准类型转换强制进行匹配。如果 #2 原型是 print() 唯一的原型,则函数调用 print(year, 6) 将把 year 转换为 double 类型。但在上面的代码中,有 3 3 3 个将数字作为第一个参数的原型,因此有 3 3 3 种转换 year 的方式。在这种情况下,C++ 将拒绝这种函数调用,并将其视为错误。

​ 此外,为避免混乱,编译器在检查函数特征标时,将把类型引用和类型本身视为同一个特征标,下面这两个函数是不能共存的:

double cube(double x);
double cube(double &x);

​ 匹配函数时,要区分 const 和非 const 变量:

void f1(char * bits);
void f1(const char * cbits);
void f2(char * bits);
void f3(const char * bits);

//下面为函数调用
const char p1[20] = "How's the weather?";
char p2[20] = "How's business?";
f1(p1);	//f1(char * bits);
f1(p2);	//f1(const char * cbits);
f2(p1);	//无法调用
f2(p2);	//f2(char * bits);
f3(p1);	//f3(const char * bits);
f3(p2);	//f3(const char * bits);

​ 函数 f2f3 之所以在调用上有差别,主要是因为将非 const 值赋给 const 变量是合法的,反之则是非法的

​ 特征标相同、函数返回类型不同的函数不能进行重载,如以下两个函数不能共存:

long gronk(int n, float m);
double gronk(int n, float m);

重载引用参数

考虑以下几个函数:

void stove(double & r1);
void stove(const double & r2);
void stove(double && r3);

上面三个函数在,与 r1r3 匹配的参数都与 r2 匹配。这就带来了一个问题:如果重载使用这三种参数的函数,结果将如何?答案是将调用最匹配的版本。例如,

double x = 55.5;
const double y = 32.0;
stove(x);	//调用stove(double & r1);
stove(y);	//调用stove(const double & r2);
stove(x+y);	//stove(double && r3);

如果没有定义函数 stove(double &&),则 stove(x+y) 将调用函数 stove(const double &)

名称修饰

​ C++ 如何跟踪每一个重载函数呢?它给这些函数指定了秘密身份。使用 C++ 开发工具中的编辑器编写和编译程序时,C++ 编译器将执行一些神奇的操作——名称修饰(或名称矫正),它根据函数原型中指定的形参类型对每个函数名进行加密。通过这种操作,C++ 编译器便能区分不同的同名函数。

​ 补充:笔者发现下面这种情况也是不能进行函数重载的:

#include <iostream>
using namespace std;
struct stu
{
	int a, b;
};
void show(stu a)
{
	cout << "hehe";
}
void show(const stu  a)
{
	cout << "hehe";
}

​ 这段代码的问题在于定义了两个具有相同参数类型的 show 函数。一个参数类型是 stu,另一个参数类型是 const stu,这实际上是相同的,因为参数是按值传递的。按值传递时,参数的常量性(const)对函数重载不起作用。编译器会认为这是两次定义了同一个函数,从而导致函数重载冲突。

5.5 函数模板

​ 函数模板是通用的函数描述,也就是说它们使用泛型来定义函数,其中的泛型可用具体的类型(如 intdouble)替换。通过将类型作为参数传递给模板,可使编译器生成该类型的函数。

​ 函数模板允许以任意类型的方式来定义函数。例如,可以这样建立一个交换模板:

template <typename AnyType>
void swap(AnyType &a, AnyType &b)
{
    AnyType temp = a;
    a = b;
    b = temp;
}

​ 上述代码中,关键字 templatetypename 是必需的,除非可以使用关键字 class 代替 typename。另外,必须使用尖括号 <>。类型名可以任意选择(这里为 AnyType),只要遵守 C++ 命名规则即可。

​ 模板并不创建任何函数,而只是告诉编译器如何定义函数。例如,需要交换 int 的函数时,编译器将按模板模式创建这样的函数,并用 int 代替 AnyType

​ 要让编译器知道程序需要一个特定形式的交换函数,只需在程序中使用 swap() 函数即可。下面程序是一个示例:

#include <iostream>
template <typename T>
void Swap(T &a, T &b);

int main()
{
    using namespace std;
    int i = 10, j = 20;
    cout << "i = " << i << ", j = " << j << endl;
    cout << "Using Swap:\n";
    Swap(i, j);
    cout << "i = " << i << ", j = " << j << endl;

    double x = 24.5, y = 81.7;
    cout << "x = " << x << ", y = " << y << endl;
    cout << "Using Swap:\n";
    Swap(x, y);
    cout << "x = " << x << ", y = " << y << endl;

    return 0;
}

template <typename T>
void Swap(T &a, T &b)
{
    auto temp = a;
    a = b;
    b = temp;
}

​ 函数模板并不能缩短可执行程序,但它能使生成多个函数定义更简单、更可靠。

5.5.1 函数模板重载

​ 需要多个对不同类型使用同一种算法的函数时,可以使用模板。然而,并非所有的类型都使用相同的算法,在这种情况下,可以像重载常规函数那样重载模板定义。例如:

template <typename T>
void Swap(T &a, T &b);

template <typename T>
void Swap(T *a, T *b, int n);

​ 注意,在后一个模板中,最后一个参数的类型为具体类型 int,而不是泛型。并非所有的模板参数都必须是模板参数类型。

5.5.2 显式具体化

​ 假设定义了如下结构:

struct job
{
    char name[40];
    double salary;
    int floor;
}

​ 假设希望能够交换这样的两个结构,原来的模板使用下面的代码来完成交换:

temp = a;
a = b;
b = temp;

​ 由于 C++ 允许将一个结构赋给另一个结构,因此上述代码可以完成交换结构的任务。然而,假设只想交换 salaryfloor 成员而不交换 name 成员,将无法使用该模板进行交换。

​ 这种情况下,可以提供一个具体化函数定义——称为显式具体化。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。

​ 具体化机制如下:

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

​ 下面是用于交换 job 结构的非模板函数、模板函数和具体化原型:

//非模板函数
void Swap(job &, job &);
//模板函数
template <typename T>
void Swap(T &, T &);
//具体化原型
template <> void Swap<job> (job &, job &);

​ 正如前面指出的,如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。例如:

template <typename T>
void Swap(T &, T &);

template <> void Swap<job>(job &, job &);
int main()
{
    double u,v;
    Swap(u,v);	//使用模板版本
    job a,b;
    Swap(a,b);	//使用显式具体化版本
}

Swap<job> 中的 <job> 是可选的,也可以像下面这样编写:

template <> void Swap(job &, job &);
5.5.3 实例化和具体化

​ 为进一步了解模板,必须理解术语实例化和具体化。在代码中包含函数模板本身并不会生成函数定义,模板只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例。例如,对于 int 类型变量 a、bSwap(a,b) 将生成一个使用 int 类型实例的 Swap 函数。这种实例化方式被称为隐式实例化

​ 还有一种实例化方式,为显式实例化。这意味着可以直接命令编译器创建特定的实例。其语法是,声明所需的种类——用 <> 符号指定类型,并在声明前加上关键字 template

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

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

int m = 6, n = 5;
Swap<int<(m,n);

​ 隐式实例化、显式实例化和显式具体化统称为具体化。引入显示实例化后,必须使用新的语法——在声明中使用前缀 templatetemplate <>,以区分显式具体化和显式实例化。

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

​ 对于函数重载、函数模板和函数模板重载,C++ 具有一个定义良好的策略来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析,其大致流程如下:

  1. 创建候选函数列表。
  2. 使用候选列表创建可行函数列表(参数数目正确)。
  3. 确定是否有最佳的可行函数。如果有,则调用它,否则该函数调用出错。

​ 例如,有函数调用:may('B')

​ 首先,编译器将寻找候选者,即名称为 may() 的函数和函数模板,然后寻找那些可以用一个参数调用的函数。例如,下面的函数符合要求:

void may(int);							//#1
float may(float, float = 3);			//#2
void may(char);							//#3
char * may(const char *);				//#4	
char may(const char &);					//#5
template<class T> void may(const T &);	//#6
template<class T> void may(T *);		//#7

​ 注意,寻找时只考虑下标,而不考虑返回类型。其中, 4 4 4 7 7 7 不可行,因为整数类型不能被隐式地转换为指针类型。

​ 接下来,编译器必须确定哪个可行函数是最佳的。通常,从最佳到最差的顺序如下所述:

  1. 完全匹配,但常规函数优先于模板
  2. 提升转换(例如,charshort 自动转换为 intfloat 自动转换为 double
  3. 标准转换(例如,int 转换为 charlong 转换为 double
  4. 用户定义的转换,如类声明中定义的转换

​ 例如,函数 1 1 1 优于函数 2 2 2,因为 charint 的转换是提升转换,而 charfloat 的转换是标准转换。函数 3 、 5 、 6 3、5、6 356 优于函数 1 、 2 1、2 12,因为它们都是完全匹配的。函数 3 、 5 3、5 35 优于函数 6 6 6,因为函数 6 6 6 是模板。

​ 上面的分析有一个问题:什么是完全匹配?如果有两个函数都完全匹配,会怎么样呢?通常,有两个函数完全匹配是一种错误,但这个规则有一些例外。下面将探讨完全匹配:

  1. 完全匹配和最佳匹配

    ​ 进行完全匹配时,C++ 允许某些无关要紧的转换,如下表所示:

    从实参到形参
    TypeType &
    Type &Type
    Type [ ]Type *
    Type (argument-list) (传递函数作为实参)Type (*) (argument-list) (函数指针)
    Typeconst Type
    Typevolatile Type
    Type *const Type *
    Type *volatile Type *

    ​ 例如,假设有以下代码:

    struct blot {int a; char b[10];}
    blot ink {25, "spots"};
    ...
    recycle(ink)
    

    ​ 在这种情况下,下面的原型都是完全匹配的:

    void recycle(blot);			//#1
    void recycle(const blot);	//#2
    void recycle(blot &);		//#3
    void recycle(const blot &);	//#4
    

    ​ 如果有多个匹配的原型,则编译器将无法完成重载解析过程;如果没有最佳的可行函数,则编译器将生成一条错误信息,该消息可能会使用诸如 a m b i g u o u s ambiguous ambiguous 这样的词语。

    ​ 然而,有时候即使两个函数都完全匹配,仍可以完成重载解析:

    1. 指向非 const 数据的指针和引用优先与非 const 指针和引用匹配。

      例如,在 recycle() 示例中,如果只定义了函数 3 、 4 3、4 34,则编译器将优先选择 3 3 3,因为变量 int 没有被声明为 const。然而,const 和非 const 之间的区别只适用于指针和引用指向的数据。也就是说,如果只定义了函数 1 、 2 1、2 12,则将出现二义性错误。

    2. 一个是模板函数,另一个是非模板函数。

      这种情况下,非模板函数将优先于模板函数(包括显式具体化)。

    3. 如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。

      术语最具体并不一定意味着显示具体化,而是指编译器推断使用哪种类型时执行的转换最少。例如,下面有两个模板:

      template<class T> void recycle (T t);
      template<class T> void recycle (T *t);
      

      假设调用函数代码如下:

      struct blot {int a; char b[10];};
      blot ink = {25, "spots"};
      ...
      recycle(&ink);
      

      该函数调用将认为第二个函数是更具体的,因为在生成过程中,它需要进行的转换更少。

      简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且它们都是模板函数,但其中有一个函数比其它函数更具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个比其它函数更具体,则函数调用将是不确定的,因此是错误的。

  2. 创建自定义选择

    在有些情况下,可通过编写合适的函数调用,来引导编译器做出用户希望的选择。例如下面的程序:

    #include <iostream>
    using namespace std;
    
    template <class T>          //#1
    T lesser(T a, T b) { 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()
    {
        int m = 20, n = -30;
        double x = 15.5, y = 25.9;
        cout << lesser(m, n) << endl;       //调用#2
        cout << lesser(x, y) << endl;       //调用#1
        cout << lesser<>(m, n) << endl;     //调用#1,lesser<int>
        cout << lesser<int>(x, y) << endl;  //调用#1,lesser<int>
        return 0;
    }
    

    语句 lesser<>(m,n) 中的 <> 指出,编译器应该选择模板函数,而不是非模板函数。语句 lesser<int>(x, y) 要求进行显式实例化,xy 将被强制转换为 int 类型,再返回一个 int 值。

  3. 多个参数的函数

    将有多个参数的函数调用与有多个参数的原型进行匹配时,情况将非常复杂。编译器必须考虑所有参数的匹配情况。如果找到比其他可行函数都合适的函数,则选择该函数。一个函数要比其他函数都合适,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。

5.5.5 一些特殊情况

​ 在编写模板函数时,一个问题是并非总能知道应在声明中使用哪种类型。下面是一个示例:

template<class T1, class T2>
void ft(T1 x, T2 y)
{
    ...
    ?type? xpy = x + y;
    ...
}

xpy 应该是什么类型?无法预先知道。例如,T1、T2 分别为 int、double 类型,则结果为 double 类型;T1、T2 分别为 int、short 类型,则结果为 int 类型。为了解决这种情况,C++11 新增了关键字 decltype

​ 假设有如下声明:

decltype(expression) var; 

​ 则 var 的类型应该按照下面的规则确定:

  1. 如果 expression 是一个没有用括号括起来的标识符,则 var 的类型与该标识符的类型相同,包括 const 等限定符。例如:

    double x = 5.5;
    double y = 7.9;
    double &rx = x;
    const double * pd;
    decltype(x) w; 		// 等价于 int w;
    decltype(rx) u = y; // 等价于 double & u = y;
    decltype(pd) v;		// 等价于 const double * v;
    
  2. 如果 expression 为函数调用,则 var 与函数的返回类型相同:

    long indeed(int);
    decltype(indeed(3)) m; //等价于 long m;
    

    注意:并不会实际调用函数。编译器通过查看函数的原型来获悉返回类型,而无需实际调用函数。

  3. 如果 expression 是一个左值,则 var 为指向其类型的引用。

    这好像意味着前面的 w 应为引用类型,因为 x 是一个左值。但别忘了,这种情况已经在第一步处理过了。要进入第三步,一种常见的情况是 expression 是用括号括起来的标识符:

    double xx = 4.4;
    decltype((xx)) r2 = xx;	//等价于 double & r2 = xx;
    decltype(xx) r3 = xx	//等价于 double r3 = xx;
    
  4. 如果前面的情况都不满足,则 var 类型与 expression 相同:

    int j = 3;
    int &k = j;
    int &n = j;
    decltype(j+6) a;	//等价于 int a;
    decltype(100L) b;	//等价于 long b;
    decltype(k+n) c;	//等价于 int c;
    

    注意,虽然 kn 都是引用,但表达式 k+n 不是引用,它是两个 int 的和,因此为 int 类型。

    如果需要多次声明,可结合使用 typedefdecltype

    template<class T1, class T2>
    void ft(T1 x, T2 y)
    {
        ...
        typedef decltype(x+y) xytype;
        xytype xpy = x+y;
        xytype & rxy = x;
        ...
    }
    

​ 还有一个问题是 decltype 无法解决的,如下:

template<class T1, class T2>
?type? ft(T1 x, T2 y)
{
    ...
 	return x+y;
}

​ 同样,无法预先知道 x+y 的类型。好像可以将返回类型设置为 decltype(x+y),但不幸的是此时还未声明参数 xy,它们不在作用域内。为此,C++ 定义了一种声明和定义函数的新语法:后置返回类型。例如:

double h(int x, float y);
//可以写成
auto h(int x, float y) -> double;

​ 这将返回类型移到了参数声明的后面,->double 称为后置返回类型。其中 auto 是一个占位符,表示后置返回类型提供的类型。

​ 通过这种语法,便可以解决上面的问题:

template<class T1, class T2>
auto ft(T1 x, T2 y) -> decltype(x+y)
{
    ...
 	return x+y;
}

​ 现在,decltype 在参数声明后面,因此 x、y 位于作用域内,可以使用它们。

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值