《C++primer学习笔记》——第6章函数

此章知识点不多,都是较为基础的或是C语言中学过的,便不多赘述

一、函数基础[P182]

T I P 1 \color{#FF0000}{TIP1} TIP1:函数的返回类型不能是数组类型或函数类型,但可以是指向数组或函数的指针。

1.局部对象

自动对象

对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达定义所在的块末尾时销毁它。我们把只存在于块执行期间的对象称为自动对象(automatic object)。当块的执行结束后,块中创建的自动对象的值就变为未定义的了。
形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域之内,所以一旦函数终止,形参也就被销毁。

局部静态对象

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

size_t count_calls()
{
	static size_t ctr = 0;
	return ++ctr;
}
int main()
{
for(size_t i = 0;i!=10;++i)
	cout <<count_calls()<<endl;
}

这段程序将输出从1到10的数字
在控制流第一次经过ctr的定义之前,ctr被创建并初始化为0.每次调用将ctr加1并返回新值。每次执行count_calls函数时候,变量ctr的值都已经存在并且等于函数上一次退出ctr的值。因此,第二次调用ctr的值是1,第三次调用ctr的值是2。

2.函数声明

和其他名字一样,函数的名字也必须在使用之前声明。类似于变量,函数只能定义一次,但可以声明多次。

void print(vector<int>::const_iterator beg , vector<int>::const_iterator end);

函数的三要素(返回类型、函数名、形参类型)描述了函数的接口,说明了调用该函数所需的全部信息。函数声明也称为函数原型(function prototype)

在头文件中进行函数声明

由于我们建议在头文件中声明变量,在源文件中定义变量。与之类似,函数也应该在头文件中声明而在源文件中定义。看起来把函数的声明直接放在使用该函数的声明直接放在使用该函数的源文件中是合法的,也比较容易被人接受,但这么做可能会很繁琐而且容易出错。相反,如果把函数声明放在头文件中,就能确保同一函数的所有声明保持一致。而且一旦我们想改变函数的接口,只需要改变一条声明即可。

3.分离式编译

随着程序越来越复杂,我们希望把程序的各个部分分别存储在不同文件中。C++语言支持所谓的分离式编译。分离式编译允许我们把程序分割到几个文件中去,每个文件独立编译。

编译和链接多个源文件

如fact函数定位于一个名为fact.cc的文件中,它的声明位于Chapter6.h的头文件中。显然与其他所有用到fact函数的文件一样,fact.cc应该包含Chapter6.h头文件中。另外,我们在名为factMain.cc的文件中创建main函数,main函数调用fact函数。要生成可执行文件,必须要素编译器我们用到的代码在哪里。

二、参数传递[P187]

每次调用函数时都会重新创建它的形参,并用传入的实参对形参进行初始化。
T I P 2 \color{#FF0000}{TIP2} TIP2:和其他变量一样,形参的类型决定了形参和实参交互的方式。如果形参是引用类型,它将绑定到对应的实参上;否则,将实参的值拷贝后赋给形参。
当形参是引用类型时,我们说它对应的实参被 引用传递(passed by reference) 。和其他引用一样,引用形参也是它绑定的对象的别名;也就是说,引用形参是它对应的实参的别名。
当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被 值传递(passed by value)

1.passed by value

当初始化一个非引用类型的变量时,初始值被拷贝给变量。此时,对变量的改动不会影响初始值,如果想改变请使用指针(略)
T I P 3 \color{#FF0000}{TIP3} TIP3:熟悉C的程序员常常使用指针类型的形参访问函数外部的对象,在C++语言中,建议使用引用类型的形参代替指针。

2.passed by reference

void reset(int &i)
{
 i = 0;
}

使用引用避免拷贝

拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型(包括IO类型在内)根本就不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。
如下,编写一个比较两个string对象的长度。因为string对象可能会非常长,所以应该尽量避免直接拷贝它们,这时候使用引用形参是比较明智的选择。又因为比较长度无须改变string对象的内容,所以把形参定义成对常量的引用:
e x a m p l e 1 \color{#0000FF}{example1} example1:比较两个string对象的长度

bool isShorter(const string &s1, const string &s2)
{
return s1.size()<s2.size();
}

T I P 4 \color{#FF0000}{TIP4} TIP4:如果函数无须改变引用形参的值,最好将其声明为常量引用

使用引用返回额外的信息

一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效的途径。举个例子我们定义一个名为find_char的函数,它返回在string对象中某个指定字符第一次出现的位置。同时,我们也希望函数能返回该字符出现的总次数。
e x a m p l e 2 \color{#0000FF}{example2} example2**:返回s中c第一次出现的位置统计,引用形参occurs负责统计c出现的总次数

string::size_type find_char(const string &s , char c, string::size_type &occurs)
{
	auto ret = s.size();
	occuts = 0;
	for(decltype(ret) i =0; i!= s.size() ; i++)
	{
		if(s[i]==c)
			if(ret == s.size())
				ret = i;
			++occurs;
	}
	return ret;
}

3.const形参和实参

指针或引用形参与const

形参的初始化方式和变量的初始化方式是一样的,所以回顾一下通用的初始化规则:

int i =42;
const int *cp = &i;//正确,但是cp不能改变i,cp是一个const int型指针	
const int &r = i;//正确:但是R不能改变i

将同样的初始化规则应用到参数传递上可得如下形式:

int i =0;
const int ci = i;
reset(&i);//调用形参类型是int*的reset函数
reset(&ci);//错误:不能使用指向const int的对象的指针初始化int*

4.数组形参和实参

数组的两个特殊性质对我们定义和使用作用在数组上的函数有影响,这两个性质分别是:不允许拷贝数组以及使用数组时(通常)会将其转换成指针。因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。
尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式:

//尽管形式不同,但这三个print函数是等价的
//每个函数都有一个const int*类型的形参
void print(const int*);
void print(const int[]);//可以看出来,函数的意图是作用于一个数组
void print(const int[10]);//这里的维度表示我们期望数组含有多少元素,实际不一定

T I P 5 \color{#FF0000}{TIP5} TIP5:和其他使用数组的代码一样,以数组作为形参的函数也必须确保使用数组时不会越界。
因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术。
使用标记指定数组长度:即要求数组本身包含一个结束标记,如使用C风格字符串,并且在最后一个字符后面跟着一个空字符,遇到空字符停止

void print(const char *cp)
{
	if(cp)//若cp不是一个空指针
		while(*cp)//只要指针所指的字符不是空字符
			cout<<*cp++;//输出当前字符并将指针向前移动一个位置
}

这种方法的问题在于像int这样所有取值都是合法值的数据就不太有效了。
使用标准库规范:这种方法是传递指向数组首元素和尾后元素的指针,这种方法受到了标准库技术的启发。

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

int j[2] = {0,1,2};
//j转换成指向它首元素的指针
//第二个实参是指向j的尾后元素的指针
print(begin(j),end(j));//在这里,我们使用标准库begin和end函数提供所需的指针。

显示传递一个表示数组大小的形参:这种方法在C程序中常常使用。使用该方法,可以将print函数重写为如下形式:

//const int ia[]等价于const int* ia
void print(const int ia[],size_t size)
{
	for(size_t i = 0;i!=size;++i)
		cout<<ia[i]<<endl;
}

int j[2] = {0,1,2};
//j转换成指向它首元素的指针
//第二个实参是指向j的尾后元素的指针
print(j,end(j)-begin(j));//在这里,我们使用标准库begin和end函数提供所需的指针。

只要传递给函数的size值不超过数组实际的大小,函数就是安全的。

数组引用形参

C++允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。此时,引用形参绑定到对应的实参上,也就是绑定到数组上

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

注意,&arr两侧的括号必不可少,这代表arr是具有10个整数的整形数组的引用。

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

在16节我们会介绍应该如何编写这个函数,使其可以给引用类型的形参传递任意大小的数组。

传递多维数组

我们曾经介绍过,C++实际上没有真正的多维数组,所谓的多维数组其实是数组的数组。和所有数组一样,当多维数组传递给函数时,真正传递的是指向数组首元素的指针,此处是一个指向数组的指针。

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

等价定义

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

5.main:处理命令行选项

有时我们需要给main传递实参,一种常见的情况是用户通过设置一组选项来确定函数要执行的操作。例如main函数位于可执行文件prog之内,我们可以向程序传递下面的选项:

prog -d -o ofile data0

这些命令选项通过两个形参传递给main函数:

int main(int argc,char *argv[]){...}

第二个形参argv是一个数组,它的元素是指向C风格字符串的指针,第一个形参argc表示数组中字符串的数量,当实参传递给main函数后,argv的第一个元素指向程序的名字或者一个空字符,接下来的元素一次传递命令行提供的实参。最后一个指针之后的元素值保证为0。以上提供的命令行为例,argc应该等于5,argv应该包含如下字符串:

argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = "0";

T I P 6 \color{#FF0000}{TIP6} TIP6:当使用argv中的实参时,一定要记得可选的实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入。

6.含有可变形参的函数

有时我们无法提前预知应该向函数传递几个实参,为了编写能处理不同数量实参的函数,C++11新标准提供了两种主要的方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板(在16.4节详细介绍)。C++还有一种特殊的类型参数(省略符)可以传递可变数量的参数,这种功能一般只能用于与C函数交互的接口程序。

initializer_list形参

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

语句 说明
initializer_list<T> lst; 默认初始化:T类型元素的空列表
initializer_list<T> lst{a,b,c,…} lst的元素对应初始值的副本;列表中的元素是const
lst2(lst) 拷贝或赋值一个initializer_list对象不会拷贝列表中的元素;拷贝后,原始列表和副本共享元素
lst.size() 求元素数量
lst.begin() 返回指向lst的首元素的指针
lst.end() 返回指向lst尾后指针

和vector元素一样,initializer_list也是一种模板类型,定义initializer_list对象时,必须说明列表中所含元素的类型:

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

和vector不同的是,initializer_list对象中的元素永远是常量值,我们无法改变initializer_list对象中的元素的值。
e x a m p l e 3 \color{#0000FF}{example3} example3:输出错误信息的函数

void error_msg(initializer_list<string> il)
{
for(auto beg = il.begin();beg !=il.end();++beg)
	cout<<*beg<<" ";
	cout<<endl;
}

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

if(except!=actual)
	error_message({"functionX",excepted,actual});
else
	error_message({"functionX","OK"});

注意,含有initializer_list形参的函数也可以同时拥有其他形参,这里便不再展开。

省略符形参

省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不能作用其他目的。

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

一共这两种形式,第一种指定了foo函数的部分形参的类型,对应于这些形参的实参将会执行正常的类型检查。第二种是全部省略。
没太懂,暂时搁置

三、返回类型和return语句[P199]

return语句终止当前正在执行的函数并将控制权返回到调用该函数的地方

1.无返回值函数

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

2.有返回值函数

只要返回类型不是void,函数内的每条return必须返回一个值。return语句返回值的类型必须与函数的返回类型相同,或者能隐式的转换成函数的返回类型。
T I P 7 \color{#FF0000}{TIP7} TIP7:在含有return语句的循环后面也应该有一条return语句,如果没有的话该程序就是有错误的。很多编译器都无法发现此类错误

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

函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用将指向不再有效的内存区域:

const string& manip()
{
	string ret;
	//执行一波操作改变ret
	if(!ret.empty())
		return ret;//错误!返回局部变量的引用
	else
		return "Empty";//错误!“EMPTY”是一个局部临时量
}

引用返回左值

我们能为返回类型是非常量引用的函数的结果赋值。

char &get_val(string &str, string::size_type ix)
{
	return str[ix];
}
int main()
{
	string s("a value");
	cout<<s<<endl;
	get_val(s,0) = 'A';//将s[0]的值改为A
	cout<<s<<endl;
	return 0;
}

如果左侧不是一个返回非常量的引用的函数,显然我们不能给它赋值

列表初始化返回值

C++11新标准规定,函数可以返回花括号包围的值的列表,如:

vector<string > process()
{
	return {"aa","bbb","ccc"};
}

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

主函数main的返回值

前面说到函数的返回类型如果不是void那么必须返回一个值。但是main函数除外,如果控制达到了main函数的结尾处而没有return语句,编译器将隐式的插入一条返回0的return语句。
main函数的返回值可以看作是状态指示器。返回0表示执行成功,返回其他值表示执行失败。非0值具体含义依机器而定。为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量(NULL也是一个定义在cstdlib中的预处理变量)来表示成功和失败:

int main()
{
	if(some_failure)
		return EXIT_FAILURE;//定义在cstdlib头文件中
	else
		return EXIT_SUCCESS;
}

因为他们是预处理变量,所以既不能在前面加上std::,也不能在using声明中出现

3.返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。但是函数可以返回数组的指针或引用。虽然从语法上来说,想要定义一个返回数组的指针或者引用函数比较繁琐,但是可以通过使用类型别名来简化这一任务:

typedef int arrT[10];//arrT是一个类型别名,他表示的类型是含有10个整数的数组
或者
using arrT = int [10];//与上方等价

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

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

想要在声明func时不使用类型别名,我们必须牢记被定义的名字后面数组的维度:

int arr[10];//arr是一个含有10个整数的数组
int *p1[10];//p1是一个含有10个指针的数组
int (*p2)[10] = &arr;//p2是一个指针,它指向含有10个整数的数组

同理,我们如果不使用类型别名,声明func应该如下:

int (*func(int i))[10];

应当按以下顺序逐层理解:
func(int i)表示调用func函数时需要一个int类型的实参。
(*func(int i))意味着我们可以对函数调用的结果执行解引用操作。
(*func(int i))[10]表示解引用的func的调用将得到一个大小是10的数组。
int (*func(int i))[10] 表示数组中的元素是int类型。
【PS:麻烦还不易读…还是类型别名方便】

使用尾置返回类型

在C++11新标准中还有一种可以简化上述func声明的方法,就是使用尾置返回类型(trailing return type)。尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表后,我们在本应该出现返回类型的地方防止一个auto:

//func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10]

因为我们把函数返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针

使用decltype

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

int odd[]={1,3,5,7,9};
int even[]={0,2,4,6,8};
decltype(odd) *arrPtr(int)
{
	return blahblahblah...
}

arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指的对象与odd类型一致(含有5个整数的数组的指针)
T I P 8 \color{#FF0000}{TIP8} TIP8:需要注意decltype并不负责把数组类型转化成对应的指针,所以decltype的结果是个数组,所以必须加一个*的符号

四、函数重载[P206]

如果同一作用域内的几个函数名字相同但形参列表不同,我们称之为重载(overloaded)函数,编译器会根据传递的实参类型推断想要的是哪个函数。
T I P 9 \color{#FF0000}{TIP9} TIP9:main函数不能重载
T I P 10 \color{#FF0000}{TIP10} TIP10:不允许两个函数除了返回类型外其他要素都相同

Record lookup(const Account&);
bool lookup(const Account&);

第二个将会报错。

重载与const形参

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

Record lookup(Phone);
Record lookup(const Phone);//错误,重复声明

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

Record lookup(Account&);
Record lookup(const Account&);//新函数,作用于常量引用

const_cast和重载

回忆一下const_cast这种显式转换的功能:我们通过const_cast来实现“去掉/加上const性质”,一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。
const_cast在重载函数中最有用,对于如下代码:

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

这个函数的输入输出都是const型,如果我们想让非const型也可以调用,就应该写如下重载函数:

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

这里就实现了非const类型调用该函数实现相应的功能,主要是这个操作是安全的,保障不会改变形参的值。

调用重载函数

编译器首先将调用的实参与重载集合中每一个函数的形参进行比较,然后根据比较的结果决定到底调用哪个函数。但有一种情况需要考虑:当两个重载函数参数数量相同且参数类型可以互相转换时。我们在第6节会进行介绍。

重载与作用域

总结就是一句话:如果内层作用域内有满足的函数,根本就不去检查作用域外还有没有其他可以匹配的函数,直接对找到的内层作用域中的函数进行调用。

五、特殊用途语言特性[P211]

1.默认实参

某些函数有这样一种形参,在函数的很多次调用中它们都被赋予了一个相同的值,此时,我们把这个反复出现的值称为默认实参

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

T I P 11 \color{#FF0000}{TIP11} TIP11:需要注意,一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值,当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面

使用默认实参调用函数

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

string window;
window = screen();
window = screen(66);
window = screen(55,66,"#");
window = screen(,,#);//省略前面的加逗号即可

默认实参声明

string screen(sz,sz,char = ' ');//正确
string screen(sz,sz,char = '*');//错误,我们不能修改一个已经存在的默认值
string screen(sz = 24,sz = 80,char);//正确,可以按照如下形式添加默认实参

默认实参初始值

局部变量不能作为默认实参。另外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:

sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(),sz = wd, char = def);

2.内联函数和constexpr函数

内联函数

调用函数是有一定开销的,内联函数可以避免函数调用的开销。将函数指定为内联函数(inline),通常就是将它在每个调用点上“内联地”展开。比如调用如下函数时:

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

在编译过程中展开成类似于下面的形式

cout<< (s1.size()<s2.size() ? s1:s2)<<endl;

从而消除了shorterString函数运行时候申请寄存器等等的系统开销。
在shorterString函数的返回类型前面加上关键字inline,这样就可以将它声明成内联函数了:

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

T I P 12 \color{#FF0000}{TIP12} TIP12:内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求
一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数。

constexpr函数

constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵守几项约定:函数的返回类型及所有形参的类型都得是字面值类型,并且函数体中必有return

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

我们把new_sz定义成无参数的constexpr函数。因为编译器能在程序编译时验证new_sz函数返回的是常量表达式,所以可以用new_sz函数初始化constexpr类型的变量foo。执行该初始化任务时候,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐式地指定为内联函数。
constexpr函数体内也可以包含其他语句,只要这些语句在运行时不执行任何操作就行,比如空语句、类型别名、using声明。

constexpr size_t scale(size_t cnt){return new_sz()*cnt;}

我们允许constexpr函数的返回值并非一个常量。

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

3.调试帮助

C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。基本思想是:程序可以包含一些用于调试的代码,这些代码只在开发程序时使用。当应用程序编写完成准备发布时,要先屏蔽掉这些代码。这种方法用到两项预处理功能:assert和NDEBUG。

assert宏处理

(暂略)

NDEBUG预处理变量

(暂略)

六、函数指针[P221]

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

//比较两个string对象的长度
bool lengthCompare(const string &,const string &);

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

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

pf前面有个*,因此pf是指针;右侧是形参列表,表示pf指向的是函数;再观察左侧,发现函数的返回类型是布尔值。因此,pf就是一个指向函数的指针,其中该指针的参数是两个const string的引用,返回值是bool类型。
T I P 13 \color{#FF0000}{TIP13} TIP13:*pf两端的括号必不可少。如果不写这对括号,则pf是一个返回值为bool指针的函数

使用函数指针

当我们把函数名作为一个值使用时,该函数自动地转换成指针。例如,我们将lengthCompare的地址赋给pf

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

此外,我们还能直接使用指向函数的指针调用该函数,无需提前解引用指针。

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

在指向不同函数类型的指针间不存在转换规则。但是和往常一样,我们可以为函数指针赋一个nullptr或者值为0的整型常量表达式,表示该指针没有指向任何一个函数。

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

重载函数的指针

指针类型必须与重载函数中的某一个精准的匹配

函数指针形参

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

//第三个形参是函数类型,它会自动地转换成指向函数的指针
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能让我们简化了使用了函数指针的代码:

//Func和Func2是函数类型
typedef bool Func(const string&,const string&);
typedef decltype(lengthCompare) Func2;//等价的类型
//FuncP 和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string&,const string&);
typedef decltype(lengthCompare) *FuncP2;//等价的类型

我们使用typedef定义自己的类型Func和Func2是函数类型,而FuncP和FuncP2是指针类型。需要注意的是,decltype返回函数类型,但是并不会自动转换为指针,所以必须前面加上*才可以。重新尚明useBigger:

void useBigger(const string&,const string&, Func);
void useBigger(const string&,const string&, FuncP2);

这两个声明语句声明的是同一个函数,在第一条语句中,编译器自动的将Func表示的函数类型转换成指针。

返回指向函数的指针

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

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);

还可以使用尾置返回类型的方式声明一个返回函数指针的函数:

auto f1(int) ->int(*)(int*,int);

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

如果我们知道返回的函数是哪一个,就用decltype简化书写,假如有两个函数的返回类型都是string::size_type,并且各有两个const string&类型的形参,此时我们可以编写第三个函数,接受string类型的参数,返回一个指针,该指针指向前两个函数中的一个:

string::size_type smallLength(const string&,const string&);
string::size_type largeLength(const string&,const string&);
decltype(sumLength) *getFcn(const string&);
上一篇:《C++ Primer》笔记 第12章 动态内存


下一篇:c primer plus 7编程练习