上一篇介绍了函数的基本构成,主要对函数工作的原理,函数参数的相关知识做了总结。本章将总结一些比较新的,更复杂的函数知识。这些内容有的确实比较少用,但是如果你掌握了它,最起码可以做到见到之后并不觉得奇怪。
1.函数指针
简单讲,函数也有地址。函数的地址是存储其机器语言代码的内存的开始地址。通常,这些地址对用户而言无关紧要,但对程序而言,却很有用。常见的可以编写将另一个函数的地址作为函数的参数,如中断服务函数。
获取函数的地址很简单,其函数名即可表示其地址,注意带括号和参数的函数名将被认为是用其返回值。
process(add);//用函数地址作为参数
process(add());//用函数返回值作为参数
声明一个函数指针需要同时制定其返回值和参数列表(参数列表被称作特征标)。如函数原型为:
int add(int a, int b);
声明其对应的函数指针应为,也即将函数原型中的函数名换为(*pt),且这个括号不能省略,如果省略了,则为返回值是指针。
int (*pt)(int,int);//pt 指向一个有两个int参数、返回值为int的函数。
int *pt(int int);//函数名为pt,返回值为int *
将指针指向函数只要:当然前提是pt的声明和add的的特征标和返回类型必须相同。
pt = add;
在使用函数指针的时候,只要明确一点,函数指针也即函数名,(*pt)也即add,所以调用的时候add(1,2)与(*pt)(1,2)是一样的。但是也可以直接写作pt(1,2)。至于为什么pt会和(*pt)等价呢?虽然这在逻辑上是相反的,但C++折中了以上两种写法,认为都可以,所以这里没有道理可言,接收它就好,选择适合自己理解的方式书写。
2.函数递归
函数递归就是函数自己调用自己。这常见于一些诸如人工智能的算法中。
举个栗子:
void cut(int n);
int main()
{
cut(4);
return 0;
}
void cut(int n)
{
cout<<n<<endl;
if(n>0)
cut(n-1);//调用自己
cout<<n<<endl;
}
只要if中的条件n>0为真,则会循环调用cut函数,也即执行第一句cout。一直到n=0后,当前程序将执行第二个cout,至此当前的cut执行完毕,程序将控制权回到调用它的cut,以此类推。于是,第一个cout被函数调用的顺序执行5次,第二个cout将以相反的方向顺行执行5次。这里的一个重要原理是,每个递归调用都会创建一套变量,因此当程序达到第5次调用时,将由5个独立的n变量。其中每个n的不同。上述程序的输出为4 3 2 1 0 0 1 2 3 4。调试你会发现,同样值对应的n的地址是一样的,可以说明是“同一个n”。
3.内联函数
内联函数是在函数原型或定义之前加上关键字inline。通常的做法是将整个定义作为原型。它的实际功能很像C语言中的宏定义:
#define SQUARE(x) x*x; //将SQUARE(x)展开为x*x,通过文本替换的方式
inline double square(double x) { return x*x;} //内联函数的方式将以函数的按值传递
这两种方式区别从经常在考试题中出现的题目可以看出:
SQUARE(3+4); //被翻译为3+4*3+4=19;
square(3+4);//按值传递,7*7=49;
内联函数是C++为提高运行速度所做的一项改进。常规函数和内联函数编写方式并无区别,而在于编译器如何将它组合到程序中。有必要了解一下程序的运行原理。
程序执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此函数保留的内存块),跳到标记函数起点的内存单元,执行函数代码,然后跳回到地址被保存的指令处。这种来回跳跃并记录跳跃位置意味着使用函数会有一定的开销。
C++内联函数将以一种新的方式编译,使得程序无需跳到另外一个位置处执行代码,再跳回来。因此内联函数的运行速度比常规函数要快,但代价是需要占用更多内存。如果程序在10个地方调用10个内联函数,则该程序将包含该函数代码的10个副本,这是典型的以空间换取时间的工作方式。因此不一定所有的情况都要用内联函数。通常,内联函数要求行数不大于5行,一般为1-2行。
4.默认参数
默认参数是指在声明函数原型时提供某些参数的默认值,使得函数调用时可以省略实参,自动使用该默认值。如:
int add1(int a, int b=1);
int main()
{
int a=2;
int c = add1(2); //省略b的实参,将使用默认的1
return 0;
}
int add1(int a, int b)
{
return a+b;
}
默认参数只能在函数原型时提供,且必须从右到左添加默认值,也就是说要为某个参数设置默认值,则必须为它右边的参数提供默认值。而实参则按从左到右的顺序依次赋给相应的形参,不能跳跃任何参数。
5.函数重载
C++多了重载(多态)的概念。函数重载是其重要体现。函数重载的目的是可以使用多个同名函数。这些函数具有相同的功能,使用不同的参数列表。意思是这仍然是多个函数,你还是需要具体写多个函数的定义。函数重载仅仅是语法层面的,本质上他们还是不同的函数,占用不同的内存,入口地址也不一样。所以,函数重载的关键是函数的参数列表,这被称作函数特征标。需要明确一点是函数特征标不完全等价与函数参数的类型,比如引用类型和本身是一种特征标。
void test(int n){n++;}
void test(int & n){n--;}
如此当调用:
int m=5;
test(m);
编译则会报错:“对重载函数调用不明确”的类似信息。
函数重载也不能滥用。书中说仅当函数基本执行相同的任务,且使用不同的形式的数据时才用。所以重载的根本目的在于使用时得以不用考虑那么多,用同一个函数名实现不同的参数列表。例如swap函数,实现交换两个参数的值,但参数可以是int float double等等。传统的C语言只能些三个swap函数,并以swap1 swap2 swap3等不同的函数名命名。调用时用户还得知道1、2、3分别是转换什么的。但是函数重载使得你可以都定义为swap这个名,但是还是要具体定义三个函数。只是在调用时方便,不用考虑用哪个。
但我觉得,在实际应用时,函数重载被更多的理解为了函数匹配问题。
6.函数模板
上面的函数重载可能使得你很困惑,因为你还是要写很多个函数定义,那么有没有什么方法可以使得同样的函数功能,参数不同,也只需要写一个函数来满足呢?答案当然是有的,那就是函数模板。
函数模板是c++通用的函数表述,它们使用泛型来定义函数。在使用时通过将具体的类型作为参数传递给模板,使得编译器生成该类型对应的函数。这种编程方式也被称为通用编程。
上述swap函数的模板实现就如:
template <typename T>
void swap(T a, T b)
{
T tmp;
tmp = a;
a = b;
b = tmp;
}
int main()
{
int a=2, b=4;
swap(a, b);//编译器会将模板编译为int作为参数
return 0;
}
其中template和typename是关键字。有的地方用class关键字替代typename。模板不创建任何函数,而是告诉编译器如何定义函数。需要int的函数时,编译器将按模板形式创建适应int的函数,即用int代替T。所以当遇到将不同类型应用同一算法的函数,最好要使用模板。一般将函数模板放在头文件中,并在使用模板的地方包含这个头文件。
函数模板类似于函数,也可以进行模板重载,即用同一个函数名,适应不同的特征标。如:
template <typename T>
void swap(T &a, T &b);//用以交换基本类型
template <typename T>
void swap(T *a, T *b, int n);//用以交换数组元素
这使得用户可以在函数调用时用同一个函数名来适应不同的参数。
7.实例化和具体化
函数模板允许只定义一次函数的实现,即可实现不同类型的参数来调用该函数。这样可以减少代码的复杂度,也便于修改。在代码中包含模板本身并不会生成函数定义,它只是一个生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。比如,上述的swap函数,在main中调用时编译器生成了一个int类型的函数定义,这个函数定义即函数实例。这种实例化的方式被称为隐式实例化(implicit instantiation)。
除此之外,c++还可以进行显式实例化(explicit instantiation)。显式实例化可以直接命令编译器创建特定的实例,而不是让程序自己去判断。有两种显式声明的方式:
比如还是上述的swap模板,第一种方式是声明所需的种类,用<>符号来指示类型,并在声明前加上关键字template,如:
template void swap<int>(int, int);
第二种方式是直接爱程序中使用函数创建,如:
swap<int>(a,b);
下面来看显式具体化:
如果上述swap模板函数用来交换两个结构体的某个变量,则无法实现了。可以使用显式具体化来专门为该特定的类型显式定义函数定义。显式具体化不用模板类生成函数定义,而是要具体写函数的定义。编译器找到与函数调用匹配的具体化定义时,将使用该定义而不再寻找模板。
显式具体化的原型和定义都应以template<>开头,并通过名称来指明类型。下面的两种方式都可以:
template<> void swap(stru &a, stru &b);
template<> void swap<stru>( stru &a, stru &b);
通常,显式具体化要声明和定义都按上述格式书写,显式实例化在函数调用时用,也即第二种方式。注意,在同一文件中同时使用显示实例化和显式具体化将出错。
隐式实例化、显式实例化和显式具体化被统一称为具体化。他们的表示都是使用具体类型的函数定义,而不是通用描述。
8.编译器函数匹配
上述的函数重载、函数模板以及函数模板重载,C++必须有一个策略,来决定为函数调用使用哪一个函数定义。这个过程称为重载解析。
重载解析需要确定最佳的函数匹配原则,通常,从最佳到最差的顺序如下:
a.完全匹配,但常规函数优于模板,较具体的模板函数优先。
b.提升转换(如char和short自动转换为int, float自动转换为double)
c.标准转化(如int转char, long转为double);
d.用户定义的转换,如类声明中定义的转换。
如果不能够完成重载解析的过程,则编译器会报错,类似于“二义性”。
总之,重载解析将寻找最佳匹配的函数。如果只存在一个这样的函数,则选择它。如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数。如果存在多个适合的模板函数,但其中一个更加具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数的调用是不确定的,