天天看点

C++primer 学习笔记——第六章 函数

一、函数基础

函数:返回类型+函数名字+形参列表+函数体

通过调用运算符来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针。

形参与实参

实参是形参的初始值。

实参的类型和数量必须与对应的形参类型和数量匹配。即使某个形参不被函数使用,也必须为它提供一个实参。

函数的形参列表

函数的形参列表可以为空,但是不能省略。

void f1()  {/*...*/}    //隐式地定义空形参列表
void f2(void)  {/*...*/} //显式地定义空形参列表
           

形参列表中的形参通常用逗号隔开,每个形参都是含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来:

int f3(int v1,v2) {/*...*/}  //错误
int f4(int v1,int v2) {/*...*/}  //正确
           

任意两个形参都不能同名,而且函数最外层作用于中的局部变量也不能使用与函数形参一样的名字。

函数返回类型

函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

1.局部对象

形参和函数内部定义的变量统称为局部变量,仅在函数的作用域内可见,同时局部变量还会隐藏在外层作用域中同名的其他所有声明中

在所有函数体之外定义的对象存在于程序的整个执行过程中。

自动对象:我们把只存在于块执行期间的对象称为自动对象。

局部静态对象:

某些时候,有必要令局部变量的生命周期贯穿函数调用以及之后的时间。可以将局部变量定义成static类型从而获得这样的对象。局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,直到程序终止时销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

如果局部静态变量没有显式的初始值,它将执行值初始化,内置类型的局部静态变量初始化为0。

2.函数声明

函数的名字必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。唯一的例外:如果一个函数永远也不会被用到,那么它可以只有声明没有定义。

函数声明无需函数体,只要一个分号代替就可以。

因为函数的声明不包含函数体,所以也就无须形参的名字。函数声明也称作函数原型。

在头文件中进行函数声明

函数也应该在头文件中声明而在源文件中定义。

定义函数的源文件应该把含有函数声明的头文件包含进来,编译器负责验证函数的定义和声明是否匹配。

3.分离式编译

分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

二、参数传递

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。

和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。

当形参是引用类型时,我们说它对应的实参被引用传递或者函数被传引用调用。

当实参的值被拷贝给形参时,形参和实参是两个独立的对象。我们说这样的实参被值传递或者函数被传值调用。

1、传值函数

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值。

指针形参

当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。因为指针使我们可以间接地访问它所指的对象,所以通过指针可以修改它所指对象的值:

void reset(int *ip)
{
   *ip=0;          //改变指针ip所指对象的值
   ip=0;           //只改变了ip的局部拷贝,实参未被改变
}

int i=42;
reset(&i);           //改变i的值而非i的地址
cout<<"i="<<i<<endl; //输出i=0
           

熟悉C的程序员常常使用指针类型的形参访问函数外部的对象。在C++语言中,建议使用引用类型的形参代替指针。

2、传引用参数

通过使用引用形参,允许函数改变一个或多个实参的值。

使用引用避免拷贝

拷贝的类型性对象或者容器比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过形参访问该类型的对象。

如果函数无须改变引用形参的值,最好将其声明为常量引用。

//比较两个string对象的长度
bool isShorter(const string &s1,const string &s2)
{
    return s1.size()<s2.size();
}
           

使用引用形参返回额外信息

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径:

//返回s中c第一次出现的位置索引
//引用形参occurs负责统计c出现的总次数
string::size_type find_char(const string &s,char c,string::size_type &occurs)
{
    auto ret=s.size();
    occurs=0;
    for(decltype(ret) i=0;i!=size();++i){
        if(s[i]=c){
            if(ret==size())
                ret=i;
            ++occurs;
        }
    } 
    return ret;    //出现次数通过occurs隐式地返回
}
           

3、const形参和实参(十分重要)

当用实参初始化形参时会忽略掉顶层const,当形参有顶层const时,传递给它常量对象或者非常量对象都是可以的:

void fcn(const int i){/*fcn能够读取i,但是不能向i写值*/}
           

调用fcn函数时,既可以传入const int也可以传入int。

允许我们定义若干具有相同名字的函数,不过前提是不同函数的形参列表应该有明显的区别。因为顶层const被忽略掉了,所以在下面的代码中传入两个fcn函数的形参可以完全一样。因此第二个fcn是错误的,尽管形式上有差别,但实际上它的形参和第一个fcn的形参没什么不同:

void fcn(const int i){/*fcn能够读取i,但是不能向i写值*/}
void fcn(int i){/*...*/}//错误:重复定义了fcn(int)
           

指针或引用形参与const

我们可以使用非常量初始化一个底层const对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化;

不能用字面值常量初始化一个非常量引用,但是可以用字面值初始化常量引用;

这些规则同样适用于参数传递:

int i=0;
const int ci=i;
string::size_type ctr=0;
reset(&i);    //调用形参类型是int*的reset函数
reset(&ci);   //错误:不能用指向const int对象的指针初始化int*
reset(i);     //调用形参类型时int&的reset函数
reset(ci);    //错误:不能把普通引用绑定到const对象ci上
reset(42);    //错误:不能把普通引用绑定到字面值上
reset(ctr);   //错误:类型不匹配,ctr是无符号类型
find_char("Hello world!",'o',ctr);   //可以使用字面值初始化常量引用
           

尽量使用常量引用

将函数不会改变的形参定义为普通引用是一种比较常见的错误,一是会带给函数的调用者一种误导,即函数可以修改它的实参的值。二是使用引用而非常量引用也会极大的限制函数所能接受的实参类型——不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参。

4、数组形参

数组的两个性质:

一是不允许拷贝数组,所以我们无法以值传递的形式使用数组参数

二是使用数组时一般将其转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针

//尽管形式不同,但是这三个print函数是等价的
//每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]);

int i=0,j[2]={0,1};
print(&i);  //正确:&i的类型是int*
print(j);   //正确:j转换成int*并指向j[0]
           

如果我们传递给print函数的是一个数组,则实参自动地转换成指向数组首元素的指针,数组的大小对函数的调用并没有影响。

数组有确切大小,但指针没有,所以我们要通过一些手段来管理(数组的)指针。管理指针的三种常用的技术:

①使用标记指定数组长度

这种方法要求数组本身包含一个结束标志,适用于那些有明显结束标记且该标记不会与普通数据混淆的情况。

②使用标准库规范

这种技术传递指向数组首元素和尾后元素的指针

void print(const int *beg,const int *end)
{
    while(beg!=end)
        cout<<*beg++<<endl;  //输出当前元素并将指针向前移动一个位置
}
           

③显式传递一个表示数组大小的实参

第三种管理数组形参的方法是专门定义一个表示数组大小的形参

//const int ia[]等价于const int* ia
//size表示数组的大小,将它显式的传给函数用于控制对ia元素的访问
void print(const int ia[],size_t size)
{
    for(size_t i=0;i!=size;++i){
        cout<<ia[i]<<endl;
    } 
}
           

数组形参和const

当函数不需要对数组元素执行写操作时,数组形参应该是指向const的指针。只有当函数确实要改变元素值的时候,才把形参定义成指向非常量的指针。

数组引用形参

形参也可以是数组的引用,此时,引用形参绑定到对应的实参上,也就是绑定到数组上:

void print(int (&arr)[10])
{
    for(auto elem:arr)
        cout<<elem<<endl;
}
           

&arr两端的括号必不可少:

f(int &arr[10]) //错误:将arr声明成了引用的数组(10个元素都是引用)
f(int (&arr)[10])  //正确:arr是具有10个整数的整形数组的引用
           

这一用法也无形限制了print函数的可用性,我们只能将函数作用于大小为10的函数:

int i=0,j[2]={0,1};
int k[10]={0,1,2,3,4,5,6,7,8,9};
print(&i); //错误:实参不是含有10个整数的数组
print(j);  //错误:实参不是含有10个整数的数组
print(k);  //正确:实参是含有10个整数的数组
           

传递多维数组

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。

因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组第二维(以及后面所有维度)的大小都是数组类型的一部分,不能省略:

//matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int(*matrix)[10],int rowSize){/*...*/}

//等价定义
//matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针
void print(int matrix[][10],int rowSize){/*...*/}
           

再一次强调,*matrix两端的括号不可省略:

int *matrix[10];     //10个指针构成的数组
int (*matrix)[10];   //指向含有10个整数的数组的指针
           

5、main:处理命令行选项

 C++的main函数可以没有输入参数,也可以有输入参数,而且只能有两个参数,习惯上coding如下:

int main(int argc, char* argv[]) 或者 int main(int argc, char** argv)
           

 其中,argc = argument count :表示传入main函数的数组元素个数,为int类型,而 argv = argument vector :表示传入main函数的指针数组,为char**类型。第一个数组元素argv[0]是程序名称,并且包含程序所在的完整路径。argc至少为1,即argv数组至少包含程序名。

#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
    for(int i=0;i<argc;i++)
        cout<<argv[i]<<endl;
    return 0;
}
           

 一般编译器默认使用argc和argv两个名称作为main函数的参数,但这两个参数如此命名并不是必须的,你可以使用任何符合C++语言命名规范的变量名,但要保证第一个参数类型为int型,第二个参数为char**型,如下图所示。

#include <iostream>
using namespace std;
int main(int count, char* input_parameters[])
{
    for(int i=0;i<count;i++)
        cout<<input_parameters[i]<<endl;
    return 0;
}
           

 由于main函数不能被其他函数调用,因此不可能在程序内部取得实际值。main函数的参数值是从操作系统命令行上获取的。在window系统中,假如编译链接成的可执行文件为my_project.exe,则在命令提示符(快捷键windows+R,输入cmd)中,键入如下命令(可执行文件 参数 参数 参数 ...):

my_project.exe jisongxie 1996
           

 将会传递三个参数给main函数,第一个argv[0]是前面提到的文件名,第二个argv[1]是"jisongxie",第三个argv[2]是“1996”。同理,可以传入更多的参数。在ubuntu系统中,可以通过终端进行相同的操作。

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入。

  传入的参数数组类型为char *字符串类型,可以通过atoi,atof函数进行类型的转换。

  1、atoi,即ascii to integer,把字符串转换成int

  2、atof,即ascii to float,把字符串转换成double

  3、atol,即ascii to long int,把字符串转换成long int

  4、atoll,即ascii to long long int,把字符串转换成long long int

  例如上述输入的1996,可以得到如下:

 int year = atoi(argv[2]);  // year = 1996
           

因此,通过上述的命令行输入以及程序里面的类型转换,可以通过命令行窗口传入值(字符串和数字)到程序中运行。

6、含有可变形参的函数

编写实参数量不同的函数有三种方法:initializer_list形参、可变参数模板(第16章)、省略符形参

initializer_list形参

如果函数的实参数量未知但是全部实参的类型相同,我们可以使用initializer_list类型的形参。initializer_list是一种标准库类型,用于标识某种特定类型的值得数组。该类型定义在同名的头文件中。

initializer_list提供的操作

initializer_list<T> lst;
initializer_list<T> lst{a,b,c...};
lst2(lst);
lst2=lst;
lst.size();
lst.begin();
lst.end();

和vector一样,initializer_list也是一种模板类型:

initializer_list<string> ls;
initializer_list<int> li;
           

和vector不同的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中元素的值。

含有initializer_list形参的函数也可以同时拥有其他形参:

void error_msg(ErrorCode e,initializer_list<string> il)
{
    cout<<e.msg()<<":";
    for(const auto &elem:il) 
        cout<<elem<<" ";
    cout<<endl;
}
           

如果想向initializer_list形参中传递一个值得序列,则必须把序列放在一对花括号内:

if(expected!=actual)
    error_msg(ErrCode(42),{"functionX",expected,actual});
else
    error_msg(ErrCode(0),{"functionX","okay"});
           

省略符形参

省略符形参应该仅仅用于C和C++通用的类型。

void foo(parm_list,...);
void foo(...);
           

三、返回类型和return语句

return有两种形式:

return;

return expression;

1、无返回值函数

没有返回值得return语句只能用在返回类型是void的函数中。返回void的函数不要求非得有return语句,因为在这类函数的最后一句后面会隐式地执行return。

如果void函数如果想在它的中间位置提前退出,可以使用return语句。return的这种做法有点类似于我们用break语句。

强行令void函数返回其他类型的表达式将产生编译错误。

2、有返回值函数

return语句返回值的类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。

在含有return语句的循环后面应该也有一条return语句,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。

值是如何被返回的

返回一个值的方式和初始化一个变量和形参的方式完全一样:返回的值用于初始化调用点的一个初始值,该临时量就是函数调用的结果。

如果返回类型为非引用类型,则意味着返回值将被拷贝到调用点:

//如果ctr的值大于1,返回Word的复数形式
string make_plural(size_t ctr,const string &word
                              const string &ending)
{
  return (ctr>1)? word+ending : word;
}
           

如果函数返回引用,则该引用仅仅是它所引用对象的一个别名:

const string &shorterString(const string &s1,const string &s2)
{
    return s1.size()<=s2.size()? s1 : s2;
}
           

不要返回局部对象的引用或指针

函数完成后,它所占的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域,所以返回局部对象的引用是错误的;同样,返回局部对象的指针也是错误的。一旦函数完成,局部对象被释放,指针将指向一个不存在的对象。

//严重错误:这个函数试图返回局部对象的引用
const string &manip
{
    string ret;
    if(!ret.empty())
        return ret;    //错误:返回局部变量的引用
    else
        return "Empty";//错误:“Empty”是一个局部变量
}
           

返回类类型的函数和调用运算符

调用运算符的优先级与点运算符和箭头运算符相同,并且也符合左结合律

auto sz=shorterString(s1,s2).size();//得到较短string对象的长度
           

引用返回左值

调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别的是,我们能为返回类型是非常量引用的函数的结果赋值:

char &get_val(string &str,string::size_type ix)
{
    return str[ix];  //假定索引值是有效的
}

int main()
{
    string s("a value");
    cout<<s<<endl;
    get_value(s,0)='A';
    cout<<s<<endl;    //输出A value
}
           

如果返回类型是常量引用,我们不能给调用的结果赋值。

列表初始化返回值

函数可以返回花括号包围的值的列表,用来初始化函数返回的临时量;如果列表为空,临时量执行值初始化:

vector<string> process()
{ 
    //...
    //expected和actual是string对象
    if(expected.empty())
        return {};                   //返回一个空vector对象
    else if(expected==actual)
        return{"functionX","okay"};  //返回列表初始化的vector对象
    else 
        return{"functionX",expected,actual};
}
           

如果函数返回的是内置类型,则花括号包围的列表最多包含一个值,而且该值所占空间不应该大于目标类型的空间。如果函数返回的是类类型,由类本身定义初始值如何使用。

主函数main的返回值

返回0表示执行成功,返回其他值表示执行失败,其中非0的值得含义由机器而定。

为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以使用这两个变量分别表示成功与失败:

int main()
{
    if(some_failure)
        return EXIT_FAILURE;
    else
        return EXIT_SUCCESS;
}
           

递归

一个函数直接或者间接地调用它自身,称该函数为递归函数:

//计算val的阶乘
int factorial(int val)
{
    if(val>1)
        return factorial(val-1)*val;
    return 1;
}
           

main函数不能调用它自己

3、返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回数组的指针或引用。其中最直接的方法是使用类型别名:

typedef int arrT[10];
using arrT=int[10];
arrT* func(int i); //func返回一个指向含有10个整数的数组的指针
           

声明一个返回数组指针的函数

返回数组指针的函数形式如下所示:

Type(*function(parameter_list))[dimension]

例子:int (*func(int i))[10]

func(int i)表示调用func函数时需要一个int类型的实参。

(*func(int i))意味着我们可以对函数调用的结果执行解引用操作

(*func(int i))[10]表示解引用将得到一个大小是10的数组

int (*func(int i))[10]表示数组中的元素是int类型

使用尾置返回类型

auto func(int  i)->int(*)[10];

//func接受一个int的实参,返回一个指针,该指针指向含有10个整数的数组

使用decltype

如果我们知道函数返回的指针将指向哪个数组,就可以使用decltype关键字声明返回类型:

int odd[]={1,3,5,7,9};
int even[]={0,2,4,6,8};
//返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
    return(i%2)?&odd:&even; 
}
           

decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组,想要表示arrPtr返回指针还必须在函数声明时加一个*符号。

四、函数重载

main函数不能重载。

定义重载函数

对于重载函数,他们应该在形参数量或形参类型上有所不同:

Record lookup(const Account&);
Record lookup(const Phone&);
Record lookup(const Name&);
Account acct;
Phone phone;
Record R1=lookup(acct);
Record R2=lookup(phone);
           

不允许两个函数除了返回类型之外其他所有的要素都相同:

Record lookup(const Account&);
bool lookup(const Account&);  //错误:与上一个函数相比只有返回类型不同
           

判断两个形参的类型是否相异

//每对声明的是同一个函数
Record lookup(const Account &acct);
Record lookup(const Account&);  //省略了形参的名字

typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&);  //Telno和phone的类型相同
           

重载和const形参

一个拥有顶层const的形参无法与一个没有顶层const的形参分开来:

Record lookup(Phone);
Record lookup(const Phone);//重复定义

Record lookup(Phone*);
Record lookup(phone* const); //重复定义
           

如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:

//定义了4个独立的重载函数
Record lookup(Account&);
Record lookup(const Account&);

Record lookup(Account*);
Record lookup(const Account*);
           

const_cast和重载

当实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点:

string &shorterString(string &s1,string &s2)
{
    auto &r=shorterString(const_cast<const string&>(s1),
                          const_cast<const string&>(s2));
    return const_cast<string&>(r);
}
           

1、重载与作用域

一般来说,将函数声明置于局部作用域内不是一个明智的选择。因为如果在内层作用域内声明名字,它将隐藏外层作用域中声明的同名实体:

string read();
void print(const string &);
void print(double);
void fooBar(int ival)
{
    bool read=false;  //外层的read()被隐藏
    string s=read();  //错误:read是一个布尔值,而不是函数
    void print(int);  //隐藏之前的print函数
    print("Value");   //错误:print(const string &)被隐藏
    print(ival);      //正确:当前print(int)可见
    print(3.14);      //正确:调用print(int);print(double)被隐藏
}
           

五、特殊用途语言特性

1、默认实参

某些函数的形参,在函数的很多次调用中都被赋予一个相同的值,此时我们把这个反复出现的值称为函数的默认实参。调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

typedef string::size_type sz;
string screen(sz ht=24,sz wid=80,char backgrnd=' ');
           

需要注意的是,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值。

使用默认实参函数

如果想要使用默认实参,只要在调用函数的时候省略该实参就可以了:

string window;
window=screen();                 //等价于screen(24,80,' ')
window=screen(66);               //等价于screen(66,80,' ')
window=screen(66,256);           //等价于screen(66,256,' ')
window=screen(66,256,'#');       //等价于screen(66,256,'#')
           

函数调用时实参按照其位置解析,默认实参负责填补函数调用缺少的尾部实参(靠右侧的位置)

默认实参声明

通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中。

函数的后续声明只能为之前那些没有默认值的形参添加默认实参,并且该形参右侧的所有形参都必须有默认值。

2、内联函数和constexpr函数

内联函数可以避免函数调用的开销

将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开:

cout<<shorterString(s1,s2)<<endl;

//编译过程类似于下面的形式
cout<<(s1.size()<s2.size()?s1:s2)<<endl;
           

constexpr函数

constexpr函数是指能用于常量表达式的函数,函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句:

constexpr int new_sz() {return 42;}
constexpr int foo=new_sz(); //正确:foo是一个常量表达式
           

constexpr函数不一定返回常量表达式

通常把内联函数和constexpr函数放在头文件中

六、函数匹配

确定候选函数和可行函数

如果函数含有默认实参,则我们在调用该函数时传入的实参数量可能少于它实际使用的实参数量。

如果没找到可行函数,编译器将报告无匹配函数的错误

寻找最佳匹配

含有多个形参的函数匹配

调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理

1、实参类型转换

需要类型提升和算术类型转换的匹配

小整形一般都会提升到int类型或更大的整数类型:

void ff(int);
void ff(short);
ff('a');  //char提升为int;调用ff(int)
           

所有算术类型转换的级别都是一样的

void manip(long);
void manip(float);
manip(3.14);  //错误:二义性调用
           

函数匹配和const实参

如果实参是指向常量的指针,调用形参是const*的函数;如果实参是指向非常量的指针,调用形参是普通指针的函数。引用类型的形参也一样。

七、函数指针

函数指针指向的是函数而非对象。和其他类型一样,函数指针指向某种特定类型。函数的类型由它的返回类型和形参类型共同决定,与函数名无关。例如:

bool lengthCompare(const string&,const string&);
           

该函数的类型是bool(const string&,const string&)。想要声明一个可以指向该函数的指针,只需要用指针替换函数名即可:

//pf指向一个函数
bool (*pf)(const string&,const string&);  //未初始化
           

*pf两端的括号必不可少:

//声明一个名为pf的函数,该函数返回bool*
bool *pf(const string&,const string&);
           

使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针:

pf=lengthCompare;    //pf指向名为lengthCompare的函数
pf=&lengthCompare;   //等价的赋值语句:取地址符是可选的
           

我们还可以直接使用指向函数的指针调用该函数,无须提前解引用指针:

bool b1=pf("hello","goodbye");            //调用lengthCompare函数
bool b2=(*pf)("hello","goodbye");         //一个等价的调用
bool b3=lengthCompare("hello","goodbye"); //另一个等价的调用
           

在指向不同函数类型的指针间不存在转换规则:

string::size_type sumLength(const string&,const string&);
bool cstringCompare(const char*,const char*);
pf=0;    //正确:pf不指向任何函数
pf=sumLength;    //错误:返回类型不同
pf=cstringCompare;     //错误:形参类型不匹配
pf=lengthCompare;    //正确:函数和指针的类型精确匹配
           

重载函数的指针

重载函数指针的类型必须与重载函数中的某一个精确匹配(形参类型+返回类型)

函数指针形参

和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用:

//第三个形参是函数类型,它会自动地转换成指向函数的指针
void useBigger(const string &s1,const string &s2,
               bool pf(const string&,const string&));
//等价的声明:显式地将形参定义成指向函数的指针
void useBigger(const string &s1,const string &s2,
               bool (*pf)(const string&,const string&));
           

我们可以直接把函数作为实参使用,此时它会自动地转换成指针:

//自动地将函数lengthCompare转换成指向函数的指针
useBigger(s1,s2,lengthCompare);
           

直接使用函数指针类型显得冗长而繁琐,可以用类型别名和decltype来简化代码。

返回指向函数的指针

和数组类似,虽然我们不能返回一个函数,但是能返回指向函数类型的指针。然而我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理:

//使用类型别名
using F=int(int*,int);  //F是函数类型,不是指针
using PF=int(*)(int*,int);  //PF是指针类型

PF f1(int);  //正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int);   //错误:F是函数类型,f1不能返回一个函数
F *f1(int);  //正确:显式的指定返回类型是指向函数的指针
           

也可以直接声明f1:

int (*f1(int))(int*,int);
           

f1(int)说明f1是个函数

*f1(int)说明f1返回一个指针

(*f1(int))(int*,int),指针的类型本身也包含形参列表,因此指针指向函数

int (*f1(int))(int*,int),该函数返回的类型是int

将auto和decltype用于函数指针类型

如果我们明确知道返回的函数是哪一个,就能使用decltype简化书写函数指针返回类型的过程:

string::size_type sumLength(const string&,const string&);
string::size_type largerLength(const string&,const string&);
//返回指向指向sumLength或者largerLength的指针
decltype(sumLength) *getFun(const string &);
           

我们将decltype作用域某个函数时,它返回函数类型而非指针类型。因此,我们显式地加上*以表示我们需要返回指针,而非函数本身。

继续阅读