函数又叫方法、子例程或程序,等等。它是一组一起执行一个任务的语句。每个 C++ 程序都至少有一个函数,即主函数 main() ,所有程序都可以定义其他额外的函数。
您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行的。
函数的连接性
c++中的所有的函数声明连接性都是内部的。意思是说你在file1中定义的函数,只有在file1中才能使用,在file2中是无法使用的,因为它看不见。
函数原型(声明)
函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。
函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。使用函数声明好处有如下几点
1、编译器正确处理函数的返回值
2、编译器检查使用的参数是否正确
3、编译器检查使用的参数是否正确。如果不正确,则转换为正确的类型(如果可以转换的话)
函数声明包括以下几个部分:
return_type function_name( parameter list );
比如声明函数 max(),以下是函数声明:
int max(int num1, int num2);
在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:
int max(int, int);
当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。这个和变量很相似,请参考变量之前讲到的连接性
通常情况下函数声明还有一个好听的名字——原型。总结一下
1、函数原型 不需要提供函数名 有类型 就够了,变量名只是占位符,
2、如果在使用函数之前编译器知道函数长什么样子的话就可以不不写函数原型了,如下
#include <iostream>
using namespace std;
void f(){//这就定义了
cout<<"anything not to do"<<endl;
};
int main ()
{
f();//编译器从上往下扫描 知道f()是什么样子的了
return 0;
}
3、在c语言中函数参数括号中不写void 代表在定义中指出参数列表 ,但是c++表示没有没有参数,但是c++就算你不写void也是表示没有的
定义函数
C++ 中的函数定义的一般形式如下:
return_type function_name( parameter list )
{
body of the function
}
在 C++ 中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:
回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字void。
函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
参数:参数就像是占位符。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
函数主体:函数主体包含一组定义函数执行任务的语句。
示例
以下是 max() 函数的源代码。该函数有两个参数 num1 和 num2,会返回这两个数中较大的那个数:
// 函数返回两个数中较大的那个数
int max(int num1, int num2)
{
// 局部变量声明
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
类函数的定义
class A {
public :
int f1();
int f2();
int f3() {};
};
A::f1() {
return 0;
}
inline A::f2() {
return 0;
}
类中的函数 我们一般再勒种声明再类外定义(通过全::)。如果再类中声明由定义了函数,那么该函数就是内联函数。
c++程序在运行到调用某个函数时 通常是在存储这个函数信息的一个表中寻找函数地址,然后去调用实际的函数代码 ,而内联函数就是在调用函数的地方直接书写函数代码的一种技术
定义
1在函数声明前加上关键字inline
2在函数定义前加上关键字inline
Note:
内联函数的实现权是属于编译器的,你声明的inline只是给编译器一种建议罢了,如果编译认为函数太大了 还不如调用呢。 就算你声明了inline 编译器还是不会这么做的,
还有有一点要注意的是如果函数递归了自己 那么它是不会被编译成内联函数的
函数参数
如果函数要使用参数,则必须声明接受参数值的变量。这些变量称为函数的形式参数。
形式参数就像函数内的其他局部变量,在进入函数时被创建,退出函数时被销毁,所以形式参数的作用域和生存期都是函数的{}中。
Tips:
本质上来说c++向函数传递参数的时候,是把参数的实际值复制给函数的形式参数。比如指针复制的是地址,引用也可以理解为是复制的地址,结构(除了数组)也是整体复制一遍给形式参数,所以c++只有按值传递,但是由于指针,引用的特殊性我们才分开讲的,这点要注意。
传值调用
向函数传递参数的传值调用方法,把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。函数swap() 定义如下:
// 函数定义
void swap(int x, int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */
return;
}
现在,让我们通过传递实际参数来调用函数 swap():
#include <iostream>
using namespace std;
// 函数声明
void swap(int x, int y);
int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
// 调用函数来交换值
swap(a, b);
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
交换前,a 的值: 100
交换前,b 的值: 200
交换后,a 的值: 100
交换后,b 的值: 200
上面的实例表明了,虽然在函数内改变了 a 和 b 的值,但是实际上 a 和 b 的值没有发生变化。
指针调用
向函数传递参数的指针调用方法,把参数的地址复制给形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。
按指针传递值,参数指针被传递给函数,就像传递其他值给函数一样。因此相应地,在下面的函数 swap() 中,您需要声明函数参数为指针类型,该函数用于交换参数所指向的两个整数变量的值。
// 函数定义
void swap(int *x, int *y)
{
int temp;
temp = *x; /* 保存地址 x 的值 */
*x = *y; /* 把 y 赋值给 x */
*y = temp; /* 把 x 赋值给 y */
return;
}
现在,让我们通过指针传值来调用函数 swap():
#include <iostream>
using namespace std;
// 函数声明
void swap(int *x, int *y);
int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
/* 调用函数来交换值
* &a 表示指向 a 的指针,即变量 a 的地址
* &b 表示指向 b 的指针,即变量 b 的地址
*/
swap(&a, &b);
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
交换前,a 的值: 100
交换前,b 的值: 200
交换后,a 的值: 200
交换后,b 的值: 100
数组传递
这点非常重要,因为如果按照c++中都是按值传递的理论。数组应该和其他结构一样,整体复制给形式参数,但是实际情况完全不是。可以怎么说对于函数的形式参数而言是没有数组这个概念的,所有的都是指针在管理。
#include <iostream>
using namespace std;
int f1(int a [3]){
return a;// [Error] invalid conversion from 'int*' to 'int'
};
void f2(int a[]){
cout<<a<<endl;
};
void f3(int [3]){
};
int main ()
{
int a[10]={};
int b=0;
int c [3]={};
f1(a);
f2(b);//[Error] invalid conversion from 'int' to 'int*'
f3(&c);//[Error] cannot convert 'int (*)[3]' to 'int*' for argument '1' to 'void f(int*)'
return 0;
}
根据上面我们可以知道在c++函数形式参数中数组和指针是一会事(而在别的地方数组和指针还是又区别的),所以以下是都是相同的。
int f(int a[])
int f(int * a)
int f(int a [3])//如果你传递一个 int(*)[]的参数他会报错,这就是为什么c++的形式参数中没有数组这个概念了
又因为在c++中数组使用指针管理的。所以你可以在函数中可以使用[]来访问数组外面的世界 。但是这样我们还是不知道数组的大小 这样是很危险的 。
#include <iostream>
using namespace std;
void f(int a[3]){
cout<<a[100]<<endl;//这个是可以运行的但是非常危险。
};
int main ()
{
int a[3];
f(a);
return 0;
}
所以我们可以在传递参数的时候也传递数组大小。
int f(int a[],int arraysSize)
我们把数组大小也一同传入其中,还有一种做法就是把数组起始地地址指针和 起始指针加上数组长度-1 传入参数列表。
二维数组
int f(int a[][]) //error c++编译器无法确定a++是多大
int f(int a[][4]) //ok
int f(int (*a)[4]) //ok
最后两个是等价的,如果不理解请自行查阅复指针小节
对于 const修饰的数组 表示这个数组的内容是不能被修改的 ,你用指针 也是不可以的
#include <iostream>
using namespace std;
void f( const int a[]){
a[0]=100;//只读 不能修改
};
int main ()
{
int a[3];//非const可以赋值给const
f(a);
return 0;
}
虽然在c++形式参数中是没有数组的感念,但是如果我们传递一个包含数组的结构的时候一定要注意。这种结构是按值传递的。其中的所有的数据包括数组都是要复制一份的。所以尽量使用指针和引用来操作数值
c风格字符串调用
对于c++字符串来说,下面是等价的。
1、const char *
2、const char []
2、”引号括起来的字符串常量”
示例:
using namespace std;
void f( const char * p){
cout<<p<<endl;
};
int main ()
{
const char a[]="123";
const char b[]={'1','2','3'};
char * str="123";//出现警告const char * 转换为char *
f(a);
f(str);
f("123");
f(b);//虽然没问题,但是很多处理字符串的函数都是识别'\0'为结束标志的
return 0;
}
出现的警告是因为在c++中字符串常量是存在静态存储区的。而静态存储区的常量是不允许更改的,所以编译器提示了善意的警告。而我们使用const char*来防止修改。但是如果你不以为然还是要更改,程序是会崩溃的。还有一点要注意的就是。如果把一个字符指针传递给cout这类函数话,它们会认为这是一个字符串。一直输出倒‘\0’.如果你传递的是一个没有’\0’的数组的话,cout会遍历到数组外面寻找‘\0’才能停下来。
其实我们也可以把字符串传递个类string。假如有下面这样的函数
void f(string s,const char *)
上面的函数我们实际上可以传递两个 char 给它, 因为在string类中定义了char到string转换的功能。
引用调用
向函数传递参数的引用,把实参的地址复制给形式参数。这意味着,修改形式参数会影响实际参数。
因此相应地,在下面的函数 swap() 中,您需要声明函数参数为引用类型,该函数用于交换参数所指向的两个整数变量的值。
// 函数定义
void swap(int &x, int &y)
{
int temp;
temp = x; /* 保存地址 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 x 赋值给 y */
return;
}
现在,让我们通过引用传值来调用函数 swap():
#include <iostream>
using namespace std;
// 函数声明
void swap(int &x, int &y);
int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
cout << "交换前,a 的值:" << a << endl;
cout << "交换前,b 的值:" << b << endl;
/* 调用函数来交换值 */
swap(a, b);
cout << "交换后,a 的值:" << a << endl;
cout << "交换后,b 的值:" << b << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
交换前,a 的值: 100
交换前,b 的值: 200
交换后,a 的值: 200
交换后,b 的值: 100
const的引用作为函数参数
为什么要用const的引用,原因和指针一样,为了使用数据本体而不用复制,增加效率,同时还防止数据被修改
double f(const double & a)
当传递一个变量给f函数时 ,变量的引用会赋值给 a,如果 试图修改a就会报错.
通常情况下我们还可以把表达式传递给函数。
double z=f(x+0.5);
但是又会出现一个小问题。理论上 x+0.5 是一个表达式而不是一个变量,因为 如果是x+0.5是变量话,我们就可以给他赋值如x+0.5=5 ,这显然会报错,c++中表达式是一个左值,是不能出现在赋值语句的右边的。
但是我们在IDE的环境中写代码时又是可以通过编译的(最多给个警告),这就让人很头疼了 ,当然想理解这个问题 我们又要记东西了。
先说一说上面 编译器是怎么让他通过自己的编译规则吧,编译器会把x+0.5计算的值 赋值给一个无名的double变量,再把该变量的引用赋值给a。
编译器什么时候创建临时变量呢? 请拿出你们的笔记本开始记录吧
只有是const的引用 才能创建临时变量 。像上面实例那样(细分为下面两个)
- 实参类型正确,但不是左值(左值是可以被引用数据对象,变量,数组元素,结构成员,引用,指针和解除引用的指针等)
- 实参类型不正确,但是可以转换为正确类型(像上面的实例 x+0.5 是左值 ,类型也不正确但是可以转换为正确类型)
为什么只有常量引用才会被创建临时变量呢。
void f(int & a,int &b){
int c=0;
c=a;
a=b;
b=c;
}
如果实参是两个double的变量且创建了 临时变量,交换的实际是两个临时变量 ,而实参两个double还是没变
总结一下 什么时候使用const的引用最为参数把。
- 避免数据因为被修改而编译错误
- const的变量可以处理const的和非const的数据
- 能正确生成const的临时变量
参数的默认值
当您定义一个函数,您可以为参数列表中后边的每一个参数指定默认值。当调用函数时,如果实际参数的值留空,则使用这个默认值。
这是通过在函数定义中使用赋值运算符来为参数赋值的。调用函数时,如果未传递参数的值,则会使用默认值,如果指定了值,则会忽略默认值,使用传递的值。请看下面的实例:
#include <iostream>
using namespace std;
int sum(int a, int b=20)
{
int result;
result = a + b;
return (result);
}
int main ()
{
// 局部变量声明
int a = 100;
int b = 200;
int result;
// 调用函数来添加值
result = sum(a, b);
cout << "Total value is :" << result << endl;
// 再次调用函数
result = sum(a);
cout << "Total value is :" << result << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Total value is :300
Total value is :120
函数返回值
返回数组
c++函数的返回值可以为数组以外的所有类型(基本类型,指针、引用、复合类型或者对象等)。但是可以是返回“携带着”数组的数据结构,指向数组的指针,数组的引用,包含数组的对象等。
#include<iostream>
struct A{
int a[10];
};
A f1(A);
int *f2(int []);
int [] f2();//error
int main(){
int ar[10];
A a;
f1(a);
f2(ar);
}
A f1(A a){
return a;
}
int *f2(int ar[]){
return ar;
}
返回引用
#include<iostream>
struct A{
int x;
double y;
};
A& f(A& a ){
a.x=100;
a.y=100;
return a;
}
int main(){
A a={1,1};
std::cout<<(f(a).x)<<std::endl;
std::cout<<(f(a).y)<<std::endl;
}
返回结果:
100
100
Tips:
引用变量作为函数返回值,可以把函数变为左值;
#include<iostream>
int & f(int & r){
return r;
}
int main(){
int a=1;
int& b=a;
f(b)=100;
std::cout<<a<<std::endl;
}
输出:
100
如果你不想返回值称为左值的话,只需要把返回类型变为const的
const int & f(int & r)
避免返回函数终止时不存在的内存空间
int& func1()
{
int i;
i = 1;
return i;
}
int main()
{
int& p = func1();
/* p is garbage */
}
上面的代码理论上会报错,因为我们的int &p
引用了一个在程序运行完就消失的变量了。但实际编译器给予了一个警告[Warning] reference to local variable ‘i’ returned [-Wreturn-local-addr]为什么呢?
因为C++中使用临时变量来传值的缘故.例如: 局部的int i
,在return i;
时C++会用一个临时的int来保存i的值,接着i的生存期结束,然后临时int的值被赋给调用处(int& p = func1();
),最后临时复制对象的生存期结束,详情点我点我
但是要注意千万要注意返回栈中的指针和引用
#include<iostream>
#include<cstring>
using std::endl;
using std::cout;
class A{
public :
int i;
A(){
cout<<"create"<<endl;
}
A(A& ){
cout<<"&"<<endl;
}
operator =(A&){
cout<<"="<<endl;
}
};
A *f(){
A a;
return &a;
}
int main(){
A* a=f();
a->i=10;
cout<<a->i<<endl;
f();
cout<<a->i<<endl;
return 0;
}
结果如下:
create
10
create
7405136
由上面的结果我们可以知道,返回局部变量的引用和指针都是非常危险的(行为不确定),就像上面那样第二次调用f()之后
原先的值就不一样了。
编译器不负责深层检测
#include <iostream>
using namespace std;
int* test_1(){
int d=2;
int c = d;
// return &d;
return &c;
}
int* test_2(){
int d[] = {1,2};
return d;
}
int* test_3(){
int d[] = {1,2};
int *t = d;
return t;
}
char* test_4()
{
char str[]="HelloJacky";
return str;
}
char* test_5()
{
char* str=(char*)"HelloJacky";
return str;
}
int* test_6(){
int a = 1;
int *b = &a;
return b;
}
int main(void)
{
int *p = 0;
cout << *test_1() << endl;
cout << *test_2() << endl;
cout << *test_3() << endl;
cout << *test_4() << endl;
cout << *test_5() << endl;
cout << *test_6() << endl;
}
也就是说 test_1, test_2, test_4,编译器都会警告,引用栈空间的地址(一定要避免这种情况)。
那么问题来了,为什么 test_3,test_6 没有警告呢?
因为test1,test2,test4他们都是直接返回指向内存的变量,编译的时候语法分析到就提出警告,test3,test6都是间接的指向内存,编译器没有深层的分析语法,所有,没有提出警告。
指针的示例如下
int* func2()
{
int* p;
p = new int;
*p = 1;
return p;
}
int main()
{
int* p = func2();
/* pointee still exists */
delete p; // get rid of it
}
p的值(地址)被临时保存起来了,在 int* p = func2();
处使用后销毁。对于自动存储区指针是不用你自己删除它的。而在自由存储区需要关注何时使用delete。
上面说了半天,我们也知道了c++的返回值其实是被保存在一个临时变量中被别的代码调用之后销毁。
int func3()
{
return 1;
}
int main()
{
int v = func3();
// do whatever you want with the returned value
}
1被保存在一个临时变量中, int v = func3();
后临时变量会销毁。
Note:
现在很多编译器都做了这方面的优化,使一些大数据返回给调用处时不用被创建两次了。
class big_object
{
public:
big_object(/* constructor arguments */);
~big_object();
big_object(const big_object& rhs);
big_object& operator=(const big_object& rhs);
/* public methods */
private:
/* data members */
};
big_object func4()
{
return big_object(/* constructor arguments */);
}
int main()
{
// no copy is actually made, if your compiler supports RVO
big_object o = func4();
}
切记临时生成的临时变量是const的(不能修改的),所以会出现下面这种情况
int main()
{
// This works! The returned temporary will last as long as the reference exists
const big_object& o = func4();
// This does *not* work! It's not legal C++ because reference is not const.
// big_object& o = func4();
}
函数递归
函数有一个很好玩的地方就是可以使用递归(自己调用自己)然而与c不同,c+++中是不允许的main()调用自己的 。这就会出现一些很好看 很能体现个人智商的代码。
示例:
#include<iostream>
#include<cmath>
void subdivide (char [],int ,int,int);
int main(){
const int len =100;
char ruler[len]={};
int max =len-2;
int min =0;
int level=log10(len-1)/log10(2)+1;
ruler[len-1]='\0';
ruler[min]=ruler[max]='|';
for(int i=1;i<len-2;i++)
ruler[i]=' ';
std::cout<<ruler<<std::endl;
for(int i=1;i<=level;i++){
subdivide(ruler,min,max,i);
std::cout<<ruler<<std::endl;
for(int i=1;i<len-2;i++)
ruler[i]=' ';
}
return 0;
}
void subdivide(char ar[],int low,int high,int level){
if(level==0)
return ;
int mid=(low+high)/2;
ar[mid]='|';
subdivide(ar,low,mid,level-1);
subdivide(ar,mid,high,level-1);
}
输出结果
上面的代码会在终端上面画出一个刻度尺,感兴趣的可以琢磨琢磨
函数重载
函数重载的关键是函数参数的列表——也称函数的特征标,如果函数的参数和类型相同,同时参数的排列也相同,则他们的特征标相同,c++允许定义函数名相同的函数,条件是特征标不同,这就是函数重载
void print(const char *,int);//#1
void print(double,int);//#2
void print(long ,int);//#3
void print(int ,int);//#4
void print(const char *);//#5
对应如下
print("123",1);//use #1
print("123");//use #5
print(1.0,1);//use #2
print(1,1);//use #4
print(1L,1);//use #3
上面的示例都有自己的原型匹配,但是下面这个呢?
unsigned int a=100;
pirnt(a,1);
这个print函数与哪个原型匹配呢?好像都不匹配。但是没有原型匹配并不意味着编译器不使用其中的某个函数来匹配,c++会使用标准类型转换来试着强制匹配。如果上面只有一个#2,那么unsigned int a的值会被赋值给double,但是上面除了#2,还有#3,#4,所以编译器不知道选谁。
函数重载时的函数匹配
如今 我们知道了,c++在找不到一模一样的函数原型时是不会停下脚步的,那么c++有一个什么样的策略来解决找“妈妈”的问题呢?因为这个完整的流程设计到泛型编程,所以现在只讲非模板的情况。
通常匹配最佳到最差的顺序为下
0. 函数原型和实参完全一样。
1. 完全匹配:int和int&之类的
2. 自动转换
4. 用户自己定义的转换,如类中定义的转换函数
例如有下面这些原型
void f(int); //#1
float f(float ,float=3); ///#2
void f(char) //#3
char * f(const char *) //#4
char f(const chat &) //#5
如果调用
f('B)
其中 #4 时一定不行的,剩下的所有如果值只定义了其中的任意一个都是可以实现函数调用的,但是如果同时定义根据上面的匹配原则,就会按照上面的原则从最佳到最差开始匹配。
上面示例中最佳匹配为#3,因为函数新参和实参一样,其次为#5因为完全匹配,再其次为#1因为整型提升,最后为#2因为标准转换,#4时不可能的,因为char类型是不会自动转换为指针的。
这就引出来一个问题,如果每种层次的匹配不止一个怎么办。一般情况下编译器会报错“ambiguous”(二义性),但是完全匹配有一点不一样。
有时候,两个完全匹配是可以完成重载解析的。这有两种情况
第一种为:指向非const数据的指针和引用优先于非const指针和引用
例如下面
struct test_struct{};
test_struct t={};
void f1(test_struct ) //#1
void f1(const test_struct )//#2
void f2(test_struct & ) //#3
void f2(const test_struct & )//#4
首先#1和#2是完全匹配,编译器会出现二义性,但是#3和#4虽然也是完全匹配,但是可以同时存在。因为 t是非const的数据,所以#3(指向非const数据的指针和引用)会优先与#4(非const的指针和引用)。
下面是一些详细的示例:
1、int&和int
void f(int x);
void f(int &x);
如果调用f(x) ,x 与int x 和int &x 都是原型匹配的 ,编译器将无法确定使用那个原型,为避免这样的混乱,编译器在检查特征标时,将上述视为同一特征。
2、const和非const
void f1(char *); //#1
void f1(const char *);//#2
void f2(char *); //#3
void f3(const char *);//#4
const char p1[20]="123";
char p2[20]="123";
f1(p2);//use #1
f1(p1);//use #2
f2(p1);//error no match
f2(p2);//use #3
f3(p1)//use#4
f3(p2);//use#4
通过上面示例我们可以知道对于重载函数f1,编译器会根据实参是否为const来选择相应的函数。而对于非重载函数,函数的自动转换还是在的。
3、返回类型
c++的函数重载是针对函数参数列表的,而对返回类型没有约束
int f(int );
double f(int)
上面一对函数也是函数重载
4、引用参数重载
void f(int & a1)
void f(const int & a2)
void f(int && a3)
先从第三个说起 他的参数是右值引用,第二个可以接受 const左值,非const 左值和右值,第一个只能和非const左值。可以看见a1,a3都与a2匹配。如果重载选择哪一个呢?
答案是最佳匹配。如下
void f1(double &a) //#1
void f1(const double &a)//#2
void f2(double &a) //#3
void f2(const double & a)//#4
void f2(double && a) //#5
double x=1;
const double y=1;
f2(x);//use #3
f2(y);//use #4
f3(x+y);use #5
Tpis:
如果没有定义f2(double && a)就会调用f2(const double x)。
第二种为:是关于模板的,完全匹配会优先调用非模板函数,然后模板的显示具体化,最后模板函数