C++ Primer 学习(第六章)

1.函数的实参类型必须与对应的形参类型相匹配或者实参的类型能够隐式地转换成形参的类型。例如定义函数:

int fact(int val)
{
  int ret=1;
  while(val>1)
     ret*=val--;
  return ret;
}

那么调用fact(3.14)也是合法的,因为double类型的可以转换成int,等价调用fact(3)。

2.函数形参列表中的形参通常用逗号隔开,其中每个形参都是含有一个声明符的声明。即使两个形参的类型一样,也必须把两个类型都写出来,但形参名是可选的,不过由于在函数中我们无法使用未命名的形参,所以形参一般都应该有个名字。例如:

int f1(int v1, v2) {/*...*/}   //错误
int f2(int v1,int v2) {/*...*/} //正确
int f3(int ,int ) {/*...*/}  //正确,不过形参没有名字,在函数体里就无法使用该形参,所以一般形参都应该有个名字

3.形参和函数体内部定义的变量统称为局部变量。它们对函数而言是"局部"的,仅在函数的作用域内可见,同时局部对象还会隐藏在外层作用域中同名的其他所有声明。局部对象当到达块末尾时就会被销毁。当块执行结束后,块中创建的局部对象的值就变成未定义的了。

将局部变量定义成static类型,这样的对象称为局部静态变量。局部静态变量在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。例如下面的函数统计自己被调用了多少次:

size_t count_calls()
{
  static size_t ctr=0;//调用结束后,这个值仍然有效,ctr只在第一次调用时被初始化
  return ++ctr;
}
int main()
{
  for(size_t i=0;i!=10;++i)
     cout<<count_calls()<<endl;
  return 0;
}

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

4.函数只能定义一次,但可以声明多次。

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

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

6.const形参和实参

当用实参初始化形参时会忽略掉顶层const。换句话说,形参的顶层const被忽略掉了当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。例如:

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

调用fcn函数时,既可以传入const int也可以传入int,因为用实参初始化形参时会忽略掉顶层const。因为这一特性,所以注意下面函数的定义:

void fcn(const int i) {/*...*/}
void fcn(int i) {/*...*/} //错误:重复定义了fcn(int)

因为顶层const被忽略掉了,所以上述代码尽管形式上有差异,但实际上第2行函数的形参和第1行函数的形参没什么不同。

7.指针或引用形参与const

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

int i=42;
const int *cp=&i;//正确,这是一个底层const,因为cp的值可以改变,只是不能通过cp指针改变其所指的值i,i本身是个变量,所以说可以使用非常量初始化一个底层const对象
const int &r=i;//正确,凡是声明为引用的const都是底层const
const int &r2=42;//正确
int *p=cp;//错误,指针p是非常量,指针cp是底层const属性,不能用底层const去初始化非常量
int &r3=r;//错误,不能用底层const引用去初始化非常量引用
int &r4=42;//错误,不能用字面值初始化一个非常量引用

不过我们可以使用一个顶层const对象去初始化非常量,在初始化的时候其顶层const被忽略掉了。 

int a=1,b = 2;
int *const q = &b;//q为常量指针,顶层const
int *q1 = q;//顶层const去初始化非常量,顶层const被忽略了,q1是一个普通指针
q1=&a;//正确,q1是一个普通指针,q1指向a
*q1=5;//现在a变为5
    

理解了上述代码,对于形参的初始化规则与上述变量的初始化规则其实是相同的。

在这里先定义两个函数形参类型分别为int *和int&的函数:

void reset(int *ip) {/*...*/}
void reset(int &i)  {/*...*/}

下述代码在上述两个函数已经定义的基础上写出,注意下述代码的合法性:

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);//错误,类型不匹配,普通引用只能用同类型的对象初始化

8.如果函数无须改变引用形参的值,最好将其声明为常量引用。把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这么做会给函数的调用者一种误导,误认为函数可以修改它的实参的值。另外,使用普通引用而不是常量引用也会极大地限制函数所能接受的实参类型。因为一个普通的引用必须用同类型的对象初始化,所以不能把const对象、字面值或者需要类型转换的对象传递给普通的引用形参

9.数组形参

因为不能拷贝数组,所以我们无法以值传递的方式使用数组参数。因为数组会被转换成指针,所以当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。尽管不能以值传递的方式传递数组,但是我们可以把形参写成类似数组的形式。例如:

void printf(const int*);
void printf(const int[]);//可以看出,函数的意图是作用于一个数组
void printf(const int[10]);//这里的维度表示我们期望数组含有多少元素,实际上不一定

尽管上述函数形式不同,但这三个printf函数是等价的,每个函数都有一个const int*类型的形参。当编译器处理对printf函数的调用时,只检查传入的参数是否是const int*类型:

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

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

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

10.数组引用形参

C++语言允许将变量定义成数组的引用,基于同样的道理,形参也可以是数组的引用。例如:

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

需要注意的是,&arr两端的括号必不可少。

f(int &arr[10])//错误,将arr声明成了引用的数组,不存在引用的数组
f(int (&arr)[10])//正确,arr是具有10个整数的整型数组的引用

另外需要注意的是,数组引用实参中,维度也是类型的一部分,这与一般的数组实参不一样,数组实参并不关注数组的维度大小。例如:

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

结合第9条和第10条的知识点,注意下述函数定义的错误:

void printf(const int ia[10])
{
  for(size_t i=0;i!=10;++i)
     cout<<ia[i]<<endl;
}

 因为不能以值传递的方式传递数组,所以上述定义是错误的。应该改为对含有10个整数数组的引用,代码如下:

void printf(const int (&ia)[10]) {/*...*/}

11.传递多维数组

和所有数组一样,当将多维数组传递给函数时,真正传递的是指向数组首元素的指针。因为我们处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。看下述函数定义:

void print(int (*matrix)[10],int rowsize) {/*...*/}

上述语句将matrix声明成指向含有10个整数的数组的指针。这样就可以传递一个多维数组,一维数组指针指向的是数组中的元素,而matrix则指向的是含有10个整数的数组。需要注意的是,*matrix两端的括号必不可少:

int *matrix[10];//一个数组,该数组含有10个整型指针
int (*matrix)[10];//一个指针,指向含有10个整数的数组

与上述代码等价的函数定义如下:

void print(int matrix[][10],int rowsize){/*...*/}

上述函数定义,matrix的声明看起来是一个二维数组,实际上形参是指向含有10个整数的数组的指针。

12.返回类型和return语句

return语句有两种形式:

return ;
return expression;

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

一个返回类型是void的函数也能使用return语句的第二种形式,不过此时return语句的expression必须是另一个返回void的函数

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

在含有return语句的循环后面应该也有一条return语句,因为有时候在循环里面执行不到return语句,导致函数没有返回值,如果没有的话该程序就是错误的。很多编译器都无法发现此类错误。

我们允许main函数没有return语句直接结束。如果控制到达了main函数的结尾处而且没有return语句,编译器将隐式地插入一条返回0的return语句。

13.函数返回不能返回局部对象的引用或指针。因为函数完成后,它所占用的存储空间也随之被释放掉。因此,函数终止意味着局部变量的引用或者指向局部变量的指针将指向不在有效的内存区域,这显然是不合法的。例如:

const string &manip()
{
  string ret;
  //以某种方式改变一下ret
  if(!ret.empty())
     return ret;//错误,返回局部变量的引用
  else
     return "Empty";//错误,"Empty"是一个局部临时量
}

在第二条return语句中,字符串字面值转换成一个局部临时string对象,对于manip来说,该对象和ret一样都是局部的。当函数结束时临时对象占用的空间也就随之释放了。

14.引用返回左值

调用一个返回引用的函数得到的是左值,其他返回类型得到的是右值。我们能为返回类型是非常量引用的函数的结果赋值。但是如果返回类型是常量引用,我们不能给调用的结果赋值。例如:

char &get_val(string &str,string::size_type ix)//该函数返回类型是非常量引用
{
  return str[ix];   
}
int main()
{
  string s("a value");
  cout<<s<<endl;
  get_val(a,0)='A';//将s[0]的值改为A
  cout<<s<<endl;
  return 0;
}

15.返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。不过,函数可以返回指向数组的指针或引用。

将函数的返回类型定义成数组指针有4种方法:

  • 使用类型别名
typedef int arrT[10];//arrT是一个类型别名,它表示的类型是含有10个整数的数组
using arrT=int[10];//等价声明
arrT* func(int i);//func返回一个指向含有10个整数的数组的指针
  • 直接声明一个返回数组指针的函数

  形式如下:

Type (*function(parameter_list)) [dimension]

其中,Type表示元素的类型,dimnsion表示数组的大小。(*function(parameter_list))两端的括号必须存在,加了括号表征这是一个指针,如果没有这个括号,函数的返回类型将变为指针的数组,而函数显然是不能返回数组的。

以下是具体的例子:

int (*func(int i)) [10];//func返回一个指向含有10个整数的数组的指针
  • 使用尾置返回类型

任何函数的定义都可以使用尾置返回,但这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。尾置返回类型跟在形参列表后面并一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们需要在本该出现返回类型的地方放置一个auto。

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

因为我们把函数的返回类型放在了形参列表之后,所以可以清楚地看到func函数返回的是一个指针,并且该指针指向了含有10个整数的数组。

  • 使用decltype

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

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

需要注意一点,decltype并不会把数组转换成对应的指针,所以decltype的结果是一个数组,在本例中,是一个含有5个整数的数组。所以要想表示func返回的是指针还必须在函数声明时加一个*符号

16.函数重载

main函数不能重载。

对于重载函数来说,它们应该在形参数量或形参类型上有所不同。不允许两个函数除了返回类型外其他所有的要素都相同

Record lookup(const Account&);
bool lookup(const Account&);//错误,与第一个函数相比只有返回类型不同
Record lookup(const Account &acct);//错误,与第一个函数相比只是多了一个形参名,没有本质的不同

17.重载和const形参

因为顶层const在实参初始化形参时会被忽略掉,所以顶层const不影响传入函数的对象,因而一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。以下的两组函数声明,均属于重复声明。

Record lookup(Phone);
Record lookup(const Phone);//重复声明了Record lookup(Phone)

Record lookup(Phone*);
Record lookup(Phone* const);//重复声明了Record lookup(Phone*)

另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现重载,此时的const是底层的。如下两组函数声明都是独立的,没有重复声明。

Record lookup(Account&);
Record lookup(const Account&);//底层const,新函数

Record lookup(Account*);
Record lookup(const Account*);//新函数,作用于指向常量的指针,属于底层const

因为const不能转换成其他类型,所以我们只能把const对象(或指向const的指针)传递给const形参。相反地,因为非常量可以转换成const,所以上面4个函数都能作用于非常量对象或者指向非常量对象的指针。不过,当我们传递一个非常量对象或者指向非常量对象的指针时,编译器会优先考虑非常量版本。

下面结合自己在VS上编程的实际经验理解为什么顶层const就算重复声明:

合法代码1:

int fcn(const int i)
{
	return 2;
}
int main()
{
	int a = 1;
	cout<<fcn(a)<<endl;//输出2
	return 0;
}

合法代码2:

int fcn(int i)
{
	return 2;
}
int main()
{
	const int a = 1;
	cout<<fcn(a)<<endl;/输出2
	return 0;
}

通过合法代码1,可以看出当用实参初始化形参时会忽略掉顶层const。换句话说,就是形参的顶层const被忽略掉了。所以常量对象和非常对象都可以直接传入形参,地位同等,只不过在fcn函数中不可以修改i,只能读i。在fcn函数中,所有操作都是对形参i的操作,无法对实参a产生影响。

通过合法代码2,可以看出顶层const对象也可以去初始化非常量。 因为除了形参时引用类型之外,其他类型的形参的初始化方式都是值传递,即将实参的值拷贝后赋给形参,此时形参和实参是两个独立的对象。所以常量对象和非常对象都可以直接传入形参,地位同等。在fcn函数中,所有操作都是对形参i的操作,无法对实参a产生影响。

合法代码3:

int fcn(int *const i)
{
	return 2;
}
int main()
{
	int a = 1;
	int *p = &a;
	cout<<fcn(p)<<endl;//输出2
	return 0;
}

通过合法代码3, 因为形参的顶层const被忽略掉了,所以常量指针和非常量指针均可传入形参,地位同等。在fcn函数中,可以通过指针改变a的值,但是指针p和指针i本身均不可变。

合法代码4:

int fcn(int *i)
{
	return 2;
}
int main()
{
	int a = 1;
	int *const p = &a;
	cout<<fcn(p)<<endl;//输出2
	return 0;
}

 通过合法代码4,可以看出顶层const对象也可以去初始化非常量。由于形参和实参是两个独立的对象,虽然普通指针i和常量指针p都指向变量a,在fcn函数中,可以通过指针i改变a的值或者改变指针i本身的值,但是无法改变指针p的值,因为形参和实参是两个独立的对象。

所以综合上述4个代码,因为形参为顶层const的函数能够同等地接受普通实参和顶层const形参,普通形参函数也能够同等的接受普通实参和顶层const实参,所以当同时定义两个函数时,当调用函数时,编译器并不知道应该调用哪个函数,所以这两个函数算重复声明。

而底层const形参函数与普通形参函数不算重复声明的原因是,底层const实参不能初始化非常量,虽然非常量能够初始化底层const,但是很显然,当调用函数时,编译器很容易知道应该调用哪个,只需根据其指向的是常量对象还是非常量对象即可判别。

所以综合以上理解,我觉得根本原因是顶层const可以初始化非常量,并且在初始化的时候丢失了顶层const属性,而底层const不可以初始化非常量,因而造成了函数对实参的接受能力不同

18.重载与作用域

在C++中,调用函数的时候,一旦在当前作用域找到了所需的名字,编译器就会忽略掉外层作用域的名实体。剩下的工作就是检查函数调用是否有效了。在C++语言中,名字查找发生在类型检查之前。例如:

string read();
void print(const string&);
void print(double);
int main()
{
  bool read=false;//新作用域,隐藏了外层的read
  string s =read();//错误,先检查名字,因为在内层作用域检查到了同名实体,但read是一个布尔值,而非函数
  void print(int);//新作用域,隐藏了之前的print
  print("value:");//错误,print(const string&)被隐藏了
  print(3.14);//正确,调用 print(int),相当于 print(3)
}

因此,在局部作用域声明函数或者与函数名相同的实体不是一个好习惯,其会隐藏外部作用域的同名实体,因此应该将重载函数声明放在同一个作用域中。

19.默认实参

我们可以为一个或多个形参定义默认值,不过需要注意的是,一旦某个形参被赋予了默认值,其后所有形参都必须有默认值,其前面的形参可以没有默认值

在调用含有默认实参的函数时,可以包含该实参,也可以省略该实参。

当函数调用时,默认实参负责填补函数调用缺少的尾部实参(靠右侧位置)。例如:

定义一个含有默认实参的函数:

typedef string::size_type sz;
string screen(sz ht=24,sz wid=80,char background=' ');//为3个形参都提供了默认实参

如果想要覆盖background的值,必须为ht和wid提供实参,例如:

window=screen( , , '?');//错误,只能省略尾部实参,必须为ht和wid提供实参
window=screen('?');//正确,但与本意不符,本意是修改background的值,但实际上char类型的'?'会转化为string::size_type类型,'?'变为其对应的ASCII码值,实际上修改的是ht的值
window=screen(66,256);//正确,等价调用screen(66,256,' ')

基于上述原因,当设计含有默认实参的函数时,其中一项任务是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面。 

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

20.默认实参声明

在给定的作用域中一个形参只能被赋予一次默认实参。也就是说,函数的后续声明只能为之前没有默认值的形参添加默认实参,而且该形参右侧的所有形参必须都有默认值。例如:

string screen(sz,sz,char=' ');
string screen(sz,sz,char='*');//错误:重复声明
string screen(sz=24,sz=80,char);//正确,添加默认实参

21.默认实参初始值

局部变量不能作为默认实参。注意理解下述代码以及关注代码后面的注释。 例如:

sz wd=80;
char def=' ';
sz ht();
string screen(sz=ht(),sz=wd,char=def);//声明screen函数
string window=screen();//调用,相当于调用screen(ht(),80,' ')
void f2()
{
  def='*';//def是全局变量,其改变了默认实参的值
  sz wd=100;//wd属于在函数内部定义的变量,属于局部变量,其隐藏了外层的wd,其并没有改变默认值
  window=screen();//相当于调用screen(ht(),80,'*')
}

22.内联函数和constexpr函数

对于一些规模较小的函数,一般来说,调用函数比求等价表达式的值要慢一些,因为调用函数需要保存寄存器等操作。此时可以使用内联函数去避免函数调用时的开销。内联函数只需在函数的返回类型前面加上关键字inline。一般内联机制适用于优化规模较小、流程直接、频繁调用的函数。

constexpr函数是指能作用于常量表达式的函数。constexpr函数需要遵循几项约定:函数的返回类型及所有形参的类型都是字面值类型,而且函数体类必须有且仅有一条return语句。例如:

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

我们允许constexpr函数的返回值并非一个常量,也就是说,constexpr函数不一定返回常量表达式。例如:

//如果arg是常量表达式,则scale(arg)也是常量表达式
constexpr size_t scale(size_t cnt)
{
  return new_sz()*cnt;
}
//当scale的实参是常量表达式时,它的返回值也是常量表达式
int arr[scale(2)];//正确,scale(2)是常量表达式
int i=2;
int arr2[scale(i)];//错误,scale(i)不是常量表达式

需要注意的是,constexpr函数应该足够简单以使得编译时就可以计算出结果,constexpr函数是隐式地内联函数,这样编译器会把对constexpr函数的调用替换为其结果值。 

23.assert预处理宏

形式:assert(expr);

首先对expr求值,如果表达式为假(即0),assert输出信息并终止程序的执行。如果表达式为真(即非0),assert什么事也不用做。

assert宏只在程序的开发阶段使用,常用于检查"不能发生"的条件。例如我们需要要求某个输入文本内所有给定单词长度都要大于某个阈值。

assert(word.size()>threshold);

24.函数匹配

含多个形参的函数匹配原则:如果有且仅有一个函数满足下列条件,则匹配成功:

  • 该函数的每个实参的匹配都不劣于其他可行函数提供的匹配
  • 至少有一个实参的匹配优于其他可行函数提供的匹配

如果在检查了所有实参之后没有任何一个函数脱颖而出,则该调用会因为二义性调用而报错。

以一组例子来解释一下:

void f();
void f(int);
void f(int,int);
void f(double,double=3.14);

下面具体调用:

f(2.56,42);//错误,二义性调用

显然第3个函数和第4个函数是可行函数,对实参2.56来说,第4个函数其匹配都不劣于其他可行函数,对实参42来说,第3个函数其匹配不劣于其他可行函数。很明显两个函数均满足匹配规则2,都不满足匹配规则1,因此会因为二义性而报错。

f(42);//正确,调用f(int)

显然,可行函数为2和4,但是2是精确匹配,4的实参42需要强制类型转换,可行函数2遵循两条匹配规则,故正确。

f(42,0);//正确,调用f(int,int)

显然,可行函数为3和4,但是3为精确匹配,而4的实参42需要强制类型转换,可行函数3遵循两条匹配规则,故正确。

f(2.56,3.14);//正确,调用f(double,double=3.14)

显然,可行函数为3和4,但是4为精确匹配,而3的两个实参均需要强制类型转换,可行函数4遵循两条匹配规则,故正确。

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

假设有两个函数,一个接受int、另一个接受short,则只有当调用提供的是short类型的值时才会选择short版本的函数。有时候,即使实参是一个很小的整数值,也会直接提升成int类型,此时使用short版本反而会导致类型转换

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

 所有算术类型转换的级别都一样,所以要注意这种情况下的二义性。

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

上述调用错误的原因在于,字面值3.14的类型为double,它既能转换成long也能转换成float,因为算术类型转换的级别都一样,故认为二义性调用,报错。

26.函数指针(注:本节从26开始,都是关于函数指针的,后续的代码都可能会使用之前已经定义的函数等)

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

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

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

//pf指向一个函数,该函数的形参是两个const string的引用,返回值为bool类型
bool (*pf)(const string &,const string &);//未初始化

如何理解,这与之前介绍的复杂的数组声明很类似,首先,括号内的是主语,表明这是一个指针,后面的都是修饰;右侧形参列表表示这是一个指向函数的指针,并且函数的形参是两个const string的引用;在观察最左侧,发现所指的函数的返回类型为布尔值。需要注意的是,*pf两端的括号必不可少,如果不写括号,则pf是一个返回值为bool指针的函数:

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

27 .使用函数指针与重载函数的指针

当我们把函数名作为一个使用时,该函数自动地转换成指针。例如,按照如下形式我们可以将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;//错误,形参类型不匹配
pf=lengthCompare;//正确,函数和指针的类型精确匹配

类似的,当我们使用重载函数时,函数指针的指针类型必须与重载函数的某一个精确匹配。例如:

void ff(int*);
void ff(unsigned int);
void (*pf1)(unsigned int)=ff;//正确,pf1指向函数ff(unsigned int)
void (*pf2)(int)=ff;//错误,没有任何一个ff与该形参列表匹配
double (*pf3)(int*)=ff;//错误,ff与pf3返回类型不匹配

28.函数指针形参

函数的形参不能是函数类型的,但是可以是指向函数的指针。此时,形参看起来是函数类型,实际上却是当成指针使用

//第三个形参看起来是函数类型,它会自动地转换成指向函数的指针
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;//等价的类型

上述代码是定义类型别名,需要注意的是,decltype返回函数类型,此时不会将函数类型自动转换成指针类型。 因为decltype的结果为函数类型,所以只有在结果前面加上*才能得到指针。

在定义了类型别名后,下面就可以简化声明含有函数指针形参的函数。例如:

//useBigger的等价声明
void useBigger(const string&,const string&,Func);
void useBigger(const string&,const string&,FuncP2);

在第一个函数声明中,编译器自动将Func表示的函数类型转换成指针

29.返回指向函数的指针

和数组类似,函数不能返回一个函数,但是能返回指向函数的指针。不过需要注意的是,我们必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理,这点与使用函数指针和声明函数指针形参处是不同的

同返回数组指针类似,返回函数指针也有四种声明方式:

  • 采用类型别名声明返回指向函数的指针
//采用类型别名声明返回指向函数的指针
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);//正确,显式地指定返回类型是指向函数的指针

 必须时刻注意,和函数类型的形参不一样,返回类型不会自动地转换成指针 

  •  直接声明返回指向函数的指针
    int (*f1(int)) (int*,int);

     上述代码表示返回类型是一个指针,该指针指向形参为(int*,int)的int型函数

  •  

    采用尾置返回类型

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

    ->后面的表示返回的函数类型

  •  

    采用decltype

    string::size_type sumLength(const string&,const string&);
    string::size_type largerLength(const string&,const string&);
    decltype(sumLength) *gecFcn(const string&);

     

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值