前面的笔记都是行云流水,因为前面几章看过了,从这一讲开始,每一讲都是现学现做笔记的。
没人是天生的赢家,只有后天的loser。
由上一讲中的知识,我们了解到许多有关C++函数的知识,但需要学习的知识还很多。C++还提供许多新的函数特性,使之有别于C语言。
新特性包括内联函数、按引用传递变量、默认的参数值、函数重载(多态)以及模板函数。
这一讲中,我们将介绍C++在C语言基础上新增的特性,比前面各讲都多,这是我们进入(++)领域的重要一步。
【内联函数】
这是为提高程序运行速度所做的一项改进。
内联函数与常规函数的主要区别不在于编写方式,而在于C++编译器如何将它们组合到程序中。
要了解内联函数与常规函数之间的区别,必须深入到程序内部。
我们知道编译过程的最终产品是可执行程序——由一组机器语言指令组成。运行程序时,操作系统将这些指令载入到计算机内存中,因此每条指令都有特定的内存地址。计算机随后将逐步执行这些指令。有时(如有循环或分支语句时),将跳过一些指令,向前或向后跳到特定地址。常规函数调用也使程序调到另一个地址(函数的地址),并在函数结束时返回。
下面更详细地介绍这一过程的典型实现。
执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入到寄存器中),然后跳回到地址被保存的指令处(这与阅读文章时停下来看脚注,并在阅读完脚注后返回到以前阅读的地方类似)。来回跳跃位置意味着以前使用函数时,需要一定的开销。
此时,内联函数出现了,它提供了另一种选择。内联函数的编译代码与其他程序代码“内联”起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。
当然,我们不能盲目地使用内联函数,相反,我们应有选择地使用内联函数。
如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间将只占整个过程的很小一部分。如果代码执行时间很短,则内联调用就可以节省非内联调用使用的大部分时间。另一方面,由于这个过程相当快,因此尽管节省了该过程的大部分时间,但节省的时间绝对值并不大,除非该函数经常被调用。
上面说了一堆理论知识,那我们如何使用这项特性呢??
- 在函数定义前加上关键字inline
除此之外,我们通常省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。
- 知识点:内联函数不能递归
下面是一个计算参数的平方的程序:
#include<iostream>
//一个内联函数定义
inline double square(double x)
{
return x*x;
}
int main()
{
using namespace std;
double a = square(5.0);
cout<<"a = "<<a<<endl;
}
我们注意到上述程序中整个内联函数的定义只占用了几行,如果函数定义占用多行(假定没有使用冗长的标识符),则将其作为内联函数就不太合适。
上述程序亦表明,内联函数和常规函数一样,也是按值来传递参数的。
如果参数为表达式(如4.5+7.5),则函数将传递表达式的值(为12)。这使得C++的内联功能远远胜过C语言的宏定义。
【默认参数】
这将又是C++的另一项新内容,让我们慢慢欣赏吧。
默认参数——当函数调用中省略了实参时自动使用的一个值。
上面是书本的解释,我的看法如下:
假设我们创建一个函数如下:
void wow(int n)
如若我们要调用这个函数,我们应该在调用函数中这样调用:wow(2);或int a=2;wow(a);,调用语句的本质是实参不能为空。而如果我们设置成n有默认值为1:
void wow(int n = 1)
哈哈,这时我们可以在调用函数中这样调用:wow();,此时实参为空,但由于形参n有默认值为1,那么此调用语句等价于:wow(1); 。当然如果我们调用语句为:wow(2);,那么形参n就是2,而不是1。
好了,我的讲解完了,是不是仿佛知道了默认参数的用处啊。
- 我们只能通过函数原型设置参数的默认值,方法是将值赋给原型中的参数。这是由于编译器通过查看原型来了解函数所使用的参数数目,因此函数原型也必须将可能的默认参数告知程序。同时,只需要函数原型指定默认值,而函数定义与没有默认参数时完全相同。
每每学习一个知识点,总有它的要求:对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它右边的所有参数提供默认值。
下面我们举几个例子:
int chico(int n, int m = 6, int j); //错误 int harpo(int n, int m = 4, int j = 5); //harpo()原型允许调用该函数时提供1个、2个或3个参数: beeps = harpo(2); //same as harpo(2,4,5); beeps = harpo(1,8); //same as harpo(1,8,5); beeps = harpo(8,7,6); //没有使用默认参数
除此,我们还要知道实参按从左到右的顺序依次被赋给相应的形参,而不能跳过任何参数。下面的调用就是一个反例:
beeps = harpo(3, ,8); //错误,doesn't set m to 4
看到这,你可能吐槽默认参数的作用甚微,是的,它并非编程方面的重大突破,它只是提供了一种便捷的方式。而且,在设计类时我们将发现,通过使用默认参数,可以减少要定义的析构函数、方法以及方法重载的数量。
【函数重载】
在了解函数重载之前,我们先看一下函数多态。
函数多态是C++在C语言的基础上新增的功能。
学完了默认参数,我们知道它让我们能够使用不同数目的参数调用同一个函数,而函数多态(函数重载)让我们能够使用多个同名的函数。
术语“多态”:指的是有多种形式,因此函数多态允许函数可以有多种形式。
类似地,术语“函数重载”:指的是可以有多个同名的函数,因此对名称进行了重载。
这两个术语指的是同一回事,但我们通常使用函数重载。
简而言之,上面一大堆废话就是说:我们可以通过函数重载设计一系列同名的函数。
有多个同名的函数又怎么样??有什么用吗?
当然有用啦!!
这些同名函数使用了不同的参数列表,但它们却完成相同的工作。
所以函数重载的关键是函数的参数列表——也称为函数特征标。
两个函数的特征标相同:参数数目相同、参数类型相同、参数的排列顺序相同。
C++允许定义名称相同的函数,条件是它们的特征标不同。
- 请记住:是特征标,而不是函数类型使得可以对函数重载。其实,这个我们仔细想一想就明白了,试想,若两个同名函数的特征标相同,那么我们调用语句与这两个函数都匹配,编译器就不知道调用哪个了,所以特征标才是函数重载的关键。
关于重载的几个注意点。
double cube(double x); double cube(double &x);
这两个函数的特征标看上去不同,然而它们不能共存。因为如果有调用语句:cube(a);,参数a与double x原型和double &a原型都匹配,因此为避免这种混乱,编译器将把类型引用和类型本身视为同一个特征标。
/* 匹配函数时,并不区分const和非const变量 */ void dribble(char *bits); //用于常规指针 void dribble(const char *bits); //用于const指针 //下面两个函数不能重载 void dabble(char *bits); void drivel(const char *bits); //下面给出了各种函数调用对应的原型: const char p1[20] = "How's the weather?"; char p2[20] = "How's business?"; dribble(p1); //dribble(const char *); dribble(p2); //dribble(char *); dabble(p1); //no match dabble(p2); //dabble(char *) drivel(p1); //drivel(const char *) drivel(p2); //drivel(const char *)
dribble()函数有两个原型,一个用于const指针,另一个用于常规指针,编译器将根据实参是否为const来决定使用哪个类型。dabble()函数只与带非const参数的调用匹配,而drivel()函数可以与带const或非const参数的调用匹配,只因为将非const值赋给const变量是合法的,但反之则是非法的。
我们何时使用函数重载呢??
- 仅当函数基本上执行相同的任务,但使用不同形式的数据时。
【函数模板】
这又是C++新增的一项特性。
函数模板是通用的函数描述,也就是说,它们使用泛型来定义函数,其中的泛型可用具体的类型(如int或double)替换。
通过将类型作为参数传递给模板,可使编译器生成该类型的函数。
由于模板允许以泛型(而不是具体类型)的方式编写程序,因此有时也被称为通用编程。
由于类型是用参数表示的,因此模板特性有时也被称为参数化类型。
我承认这些都是废话。。。
下面我们来学习为何需要函数模板以及其工作原理。
看下面这个交换两个int值的程序:
#include<iostream>
using namespace std;
void swap(int *a, int *b)
{
int t; t = *a; *a = *b; *b = t;
}
int main()
{
int n1 = 3, n2 = 4;
swap(n1, n2);
}
那么,如果我们要交换两个double值,我们会想到复制swap函数的代码,并用double替换所有的int。同理,如果需要交换两个char值,我们可以再次使用同样的技术。显然,进行这种修改将浪费宝贵的时间,且容易出错。这时,我们在想有没有什么好的办法完成这项任务呢?
当然有咯!!C++的函数模板功能能自动完成这一过程,可以节省时间,而且更可靠。
函数模板允许以任意类型的方式来定义函数。
例如,我们可以建立一个交换模板:
template <typename T> //将类型命名为T,关键字typename可以用class代替
void swap(T *a, T *b)
{
T t; t = *a; *a = *b; *b = t;
}
我们要知道,模板并不创建任何函数,而只是告诉编译器如何定义函数。
需要交换int的函数时,编译器将按模板模式创建这样的函数,并用int代替T。同样,需要交换double的函数时,编译器将按模板模式创建这样的函数,并用double代替T。
更常见的情形是,将模板放在头文件中,并在需要使用模板的文件中包含头文件。
当我们需要多个对不同类型使用同一种算法的函数时,可使用模板。
但并非所有的类型都使用相同的算法,这是,我们可以像重载常规函数定义那样重载模板定义。
和常规重载一样,被重载的模板的函数特征标必须不同。如下面的程序:
template <typename T>
void swap(T *a, T *b);
template <typename T>
void swap(T a[], T b[], int n); //并非所有的模板参数都必须是模板参数类型(T)
template <typename T>
void swap(T *a, T *b)
{
T t; t = *a; *a = *b; *b = t;
}
template <typename T>
void swap(T a[], T b[], int n)
{
T t;
for(int i = 0; i < n; i++)
{
t = a[i];
a[i] = b[i];
b[i] = t;
}
}
上述程序中新增了一个交换模板,用于交换两个数组中的元素。原来的模板的特征标为(T &, T &),而新模板的特征标为(T[], T[], int)。
注意,在后一个模板中,最后一个参数的类型为具体类型(int),而不是泛型。并非所有的模板参数都必须是模板参数类型。
看到了模板函数的好处,它的局限性也比较显著。
假设有如下模板函数:
void f(T a, T b)
{
...
}
函数体内总会执行一些操作,这是不可否认的。假如,函数体内定义了赋值,但如果T为数组,这个模板函数就没用;再比如,函数体内定义了<,但如果T为结构,这个模板也没用。此外,还有好多好多。
总之,编写的模板函数很可能无法处理某些类型。
但我们可以想办法解决这个困难。例如,将两个包含位置坐标的结构相加是有意义的,虽然没有为结构定义运算符+。
此时,我们有两种解决方案:①C++允许我们重载运算符+,以便能够将其用于特定的结构或类,这样使用运算符+的模板便可处理重载了运算符+的结构;②为特定类型提供具体化的模板定义。
下面我们将来介绍第二种解决方案,因为第一种解决方案等我们学完运算符重载就会了啊!
我们来看这样一个结构:
struct job{
char name[20];
double salary;
int floor;
};
我们要交换两个这种结构的内容可以使用上面的第一种模板,因为C++允许将一个结构赋给另一个结构。
然而,如果我们只想交换salary成员,其他成员不做交换,那么就需要使用不同的代码,但swap()的参数将保持不变(两个job结构的引用),因此无法使用模板重载来提供其他的代码。
然而,可以提供一个具体化函数定义(称为显示具体化),这个函数包含所需的代码。当编译器找到与函数调用匹配的具体化定义时,将使用该定义,而不再寻找模板。
显然,具体化是需要一些特别说明的:
- 对于给定的函数名,可以有非模板函数、模板函数和显示具体化模板函数以及它们的重载版本;
- 显示具体化的原型和定义应以template<>打头,并通过名称来指出类型;
- 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
下面给出交换job结构的非模板函数、模板函数和具体化的原型:
//非模板函数 void swap(job &, job &); //模板函数 template <typename T> void swap(T &, T &); //显示具体化 template <> void swap<job>(job &, job &); //<job>可不写,因为参数列表已经表明这是job的一个具体化
上面总结了:如果有多个原型,则编译器在选择原型时,非模板版本优先于显示具体化和模板版本,而显示具体化优先于使用模板生成的版本。
例如,在下面的代码中,第一次调用swap时使用通用版本,而第二次调用使用基于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); //使用基于job类型的显示具体化
}
下面这个程序演示了显示具体化的工作方式:
1 // twoswap.cpp -- specialization overrides a template 2 #include <iostream> 3 template <typename T> 4 void Swap(T &a, T &b); 5 6 struct job 7 { 8 char name[40]; 9 double salary; 10 int floor; 11 }; 12 13 // explicit specialization 14 template <> void Swap<job>(job &j1, job &j2); 15 void Show(job &j); 16 17 int main() 18 { 19 using namespace std; 20 cout.precision(2); 21 cout.setf(ios::fixed, ios::floatfield); 22 int i = 10, j = 20; 23 cout << "i, j = " << i << ", " << j << ".\n"; 24 cout << "Using compiler-generated int swapper:\n"; 25 Swap(i,j); // generates void Swap(int &, int &) 26 cout << "Now i, j = " << i << ", " << j << ".\n"; 27 28 job sue = {"Susan Yaffee", 73000.60, 7}; 29 job sidney = {"Sidney Taffee", 78060.72, 9}; 30 cout << "Before job swapping:\n"; 31 Show(sue); 32 Show(sidney); 33 Swap(sue, sidney); // uses void Swap(job &, job &) 34 cout << "After job swapping:\n"; 35 Show(sue); 36 Show(sidney); 37 // cin.get(); 38 return 0; 39 } 40 41 template <typename T> 42 void Swap(T &a, T &b) // general version 43 { 44 T temp; 45 temp = a; 46 a = b; 47 b = temp; 48 } 49 50 // swaps just the salary and floor fields of a job structure 51 52 template <> void Swap<job>(job &j1, job &j2) // specialization 53 { 54 double t1; 55 int t2; 56 t1 = j1.salary; 57 j1.salary = j2.salary; 58 j2.salary = t1; 59 t2 = j1.floor; 60 j1.floor = j2.floor; 61 j2.floor = t2; 62 } 63 64 void Show(job &j) 65 { 66 using namespace std; 67 cout << j.name << ": $" << j.salary 68 << " on floor " << j.floor << endl; 69 }