C++学习笔记(9)——函数探幽

上一篇介绍了函数的基本构成,主要对函数工作的原理,函数参数的相关知识做了总结。本章将总结一些比较新的,更复杂的函数知识。这些内容有的确实比较少用,但是如果你掌握了它,最起码可以做到见到之后并不觉得奇怪。

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.用户定义的转换,如类声明中定义的转换。

如果不能够完成重载解析的过程,则编译器会报错,类似于“二义性”。

总之,重载解析将寻找最佳匹配的函数。如果只存在一个这样的函数,则选择它。如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数。如果存在多个适合的模板函数,但其中一个更加具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数的调用是不确定的,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bjtuwayne

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值