C++ Primer 第六章 函数

第六章 函数

6.1 函数基础

6.1.1 函数定义

  • 一个函数的定义包含以下几部分
    • 返回类型
    • 函数名字
    • 0个或多个形参组成的列表
    • 函数体
  • 我们使用调用运算符(call operator)来执行函数,调用运算符的形式是一对圆括号,他作用于一个表达式,该表达式的值是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参.调用表达式的类型就是函数的返回类型

6.1.2 函数调用

  • 函数调用完成两步工作
    1. 用实参(隐式)定义并初始化它的形参
    2. 将控制权转移给被调函数

6.1.3 形参和实参

  • 实参是形参的初始值,第一个实参初始化第一个形参,第二个实参初始化第二个形参....
  • 实参和形参存在对应关系,但是没有规定实参的求值顺序(运算符的运算顺序没有规定)
  • 实参的数量必须和形参相同,类型必须匹配

6.1.4 形参列表

  • 形参列表可以为空,但是不能省略
    • void f1() {} //隐式定义空形参列表
    • void f2(void) {} //显式定义空形参列表
  • 形参名是可选的,但是我们无法使用未命名的形参,有个别形参不会被用到,可以采用不命名的方式以表示在函数中不会使用它

6.1.5 函数的返回类型

  • 大多数类型都能作为函数的返回类型
  • 函数的返回类型不能是数组类型或者函数类型,可以是指向数组的指针

6.1.6 局部静态对象

  • 为了令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义成static类型从而获得这样的对象
  • 局部静态对象在程序执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁

6.1.7 函数声明

  • 和变量一样,函数的名字也必须在使用之前声明,函数只能定义一次,但可以声明多次.
  • 如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义
  • 因为函数的声明不包含函数体,所以也不需要形参的名字,事实上函数的声明经常省略形参的名字,但是写上形参的名字有助于使用者理解函数的功能
  • 函数声明也被称作函数原型
  • 建议函数在头文件中声明而在源文件中定义

6.1.8 分离式编译

  • 随着程序越来越复杂,我们希望把函数存在一个文件里,使用这些函数的代码存在其他源文件中,为了允许编写程序时按照逻辑关系划分,C++支持分离式编译,允许我们把程序分割到几个文件中去,每个文件独立编译
  • e.g. 假设fact函数定义位于fact.cc,声明位于fact.h,显然fact.cc需要包含fact.h,我们需要在factmain.cc中创建main函数,调用fact函数,为了生成可执行文件,我们需要告诉编译器我们用到的代码在哪里
    • CC factmain.cc fact.cc # generates factmain.exe or a.out
    • CC factmain.cc fact.cc -o main # generates main or main.exe
    • 其中,CC是编译器的名字,$是系统提示符,#是命令行下的注释语句,接下来运行可执行文件,就会执行我们定义的main函数
    • 如果我们修改了其中一个源文件,只需要重新编译改动了的文件,大多数编译器提供了分离式编译每个文件的机制,这一过程会产生一个后缀名为.obj(windows)或者.o(unix)的文件,后缀名的含义是该文件包含对象代码(object code)
    • 接下来编译器负责把对象文件链接在一起形成可执行文件,编译的过程如下
    • CC -c factmain.cc #generates factmain.o
    • CC -c fact.cc #generates fact.o
    • CC factmain.o fact.o #generates factmain.exe or a.out
    • CC factmain.o fact.o main #generates main or main.exe

6.2 参数传递

  • 每次调用函数会重新创建它的形参,并且用传入的实参对形参进行初始化
  • 如果形参是引用类型,则绑定到实参上,否则将实参的值拷贝给形参
    • 当形参是引用类型时,我们说实参被引用传递或者函数被传引用调用
    • 实参被拷贝时,我们说实参被值传递或者传值调用

6.2.1 指针形参

  • 指针的行为和其他非引用类型一样,执行指针拷贝操作时,拷贝的是指针的值,拷贝之后两个指针是不同的指针,因为指针使我们可以间接地访问它所指的对象,所以我们可以通过指针修改它所指的对象的值。
  • tips,C语言中常常使用指针类型的形参修改函数外部的对象,C++中建议使用引用类型的形参替代指针

6.2.2 传引用参数

  • 对引用的操作实际上是作用在引用所引的对象上
  • 使用引用避免拷贝,因为拷贝大的类类型对象或者容器对象比较低效,有些类类型甚至不支持拷贝,这种情况下函数只能通过引用形参访问该类型的对象
//e.g. 我们准备编写一个函数比较两个长字符串的长度,需要避免直接拷贝他们,这时候使用引用形参会比较明知,并且把他们定义为常量的引用
bool strcmp(const string &s1, const str &s2) {
  return s1.size() < s2.size();
}
  • 如上,当函数无需修改形参的值的时候最好使用常量引用

6.2.3 引用参数返回额外信息

  • 一个函数只能返回一个值,但是当函数需要返回多个值的时候,可以采用引用形参,将需要返回的值通过引用传递进入函数

6.2.4 const形参和实参

  • 用实参初始化形参时会忽略掉顶层const,当形参有顶层const时,传给它常量对象或者非常量对象都可以
void fun(const int i); //fun可以读取i但是不能向i写值
void fun(int i); //错误:重复定义
  • 因为顶层const被忽略了,所以上面两个函数不构成重载,在使用的时候看来都是void fun(int i);

6.2.5 指针或引用形参参与cosnt

  • 和变量初始化一样,我们可以使用非常量初始化一个底层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); //错误:不能把int&绑定到const对象上
reset(42); //错误:不能把int&绑定到字面值上
reset(ctr);//错误:类型不匹配
  • 如上,想要调用引用版本的reset,只能使用int类型对象,不能使用字面值,求值结果、需要转换的对象或者const int
  • 想要调用指针版本的reset,只能使用int*
  • 但是如果函数的引用形参是常量引用,我们就可以采用字面值初始化常量引用

6.2.6 常量引用

  • 把函数不会改变的形参定义为普通引用是常见的错误,会让调用者觉得这个函数可以修改它实参的值
  • 使用非常量引用也会限制函数所能接受的实参类型,我们不能把const对象、字面值或者需要转换类型的对象传递给普通的引用传参
//e.g. 错误示范
//我们本该将函数的第一个形参定义为const string&
string::size_type find_char(string &s, char c, string::size_type &occ);

//这种情况下,下面的这种调用就变得不可行了,因为我们第一个参数传入了字面量值
find_char("hello world", "o", ctr); 

//更加难以察觉的是下面这种情况,const对象也是不能作为实参传入的
bool fun(const string &s) {
  return find_char(s, '.', 0) == s.size() - 1;
}
//我们或许可以修改fun的传参,但是没有从根本上解决问题,正确的方法还是修改find_char的形参

6.2.7 数组形参

数组有两个特殊性质会对我们定义和使用作用在数组的函数有影响

  • 不允许拷贝数组
  • 使用数组时(通常)会将其转换为指针

因此,我们无法用值传递的方式使用数组参数,当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针

//这三个print实际上是等价的,每个函数都是一个const int*的形参,编译器在处理函数调用的时候,只检查函数的传参是不是const int*类型
void print(const int*);
void print(const int []); //函数的意图是作用于一个数组
void print(const int [10]);  //表示我们期望数组有多少元素,但是实际不一定

当然,以数组作为形参的函数也必须保证使用的时候数组不会越界
因为传递的只有首元素的指针,调用者还需要提供一些额外的信息告诉函数确切的数组的尺寸,有三种常用的方法

  1. 使用标记指定数组长度:例如C风格字符串中的结尾往往有一个空字符表示字符串的停止,但是这种方法在int*这种所有取值都合法的数据下就不太有效
  2. 使用标准库规范:传递指向数组首元素和尾后元素的指针
//e.g.
void print(const int *beg, const int *end) {
  while(beg != end) cout << *beg++ << endl;
}
int j[2] = {0, 1};
print(begine(j), end(j));
  1. 显示传递一个表示数组大小的形参

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

6.2.8 数组引用形参

  • C++允许将变量定义为对数组的引用,同样,形参也可以是数组的引用
//arr是具有10个整数的整型数组的引用
//括号必不可少,否则int &arr[10]变成了引用的数组
void print(int (&arr)[10]) {
  for(auto elem:arr) {
    cout << elem << endl;
  }
}
//但是这一用法无形中限制了print函数的可用性
int i = 0, j[2] = {0, 1};
print(&i);//错误,实参不是含有10个整数的数组
print(j); //错误,实参不是含有10个整数的数组

6.2.9 传递多维数组

  • C++中实际上没有真正的多维数组,所谓的多维数组其实是数组的数组
  • 当多维数组传递给函数时,真正传递的是指向数组首元素的指针,因为处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。第二维(以及后面所有的维度)的大小都是数组类型的一部分,不能省略
//matrix指向数组首元素,数组的元素是10个整数构成的数组
void print(int (*matrix)[10], int rowSize);
//matrix的括号必不可少,否则变成10个指针构成的数组

//上述声明等价于
void print(int matrix[][10], int rowSize);

6.2.10 main:处理命令行选项

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

  • 第一个形参argc表示数组中字符串的数量
  • 第二个形参是数组,当实参传给main函数之后,argv的第一个元素指向程序的名字或者一个空字符串,接下来的元素依次是传递命令行提供的实参。最后一个指针之后的元素值保证为0
  • tips:当使用argv中的实参时,一定要记得可选的实参从argv[1]开始,argv[0]用于保存程序的名字而非用户输入

6.2.11 含有可变形参的函数

有时我们无法提前预知应该向函数传递几个实参,为了能编写处理不同数量实参的函数,C++11提供了两种主要方法

  • 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型
  • 如果实参的类型不同,可以编写一种特殊的函数,也就是所谓的可变参数模板
  • C++还有一种特殊形参类型(省略符),可以用它传递可变数量的实参,不过这种功能一般只用于与C函数交互的接口程序

initializer_list形参
如果函数的实参数量未知但是全部实参的类型都相同,可以使用initializer_list类型的形参,这是一种标准库类型,用于表示某种特定类型的值的数组

initializer_list<T> lst; //默认初始化,T类型元素的空列表
initializer_list<T> lst{a,b,c..}; //lst的元素数量和初始值一样多,lst的元素是对应初始值的副本,列表中的元素是const
lst2(lst); 
lst2 = lst;//拷贝或者赋值一个initializer_list对象不会拷贝列表中的元素,拷贝后原始列表和副本共享元素
lst.size(); //列表中元素数量
lst.begin(); 
lst.end(); //列表的首指针和尾后指针
  • initializer_list也是一种模板类型,定义对象的时候必须说明列表中所含的元素的类型
  • initializer_list对象中的元素永远是常量值,我们无法改变对象中元素的值
//e.g.
void error_msg(ErrCode e, initializer_list<string> il) {
  cout << e.msg() << endl;
  for (auto beg = il.begin(); beg != il.end(); ++beg) {
      cout << *beg << " ";
  }
  cout << endl;
}
//使用
if (excepted != actual) 
    error_msg(ErrCode(42),{"functionX", excepted, actual});
else
    error_msg(ErrCode(0),{"functionX","okey"});

省略符形参

  • 省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库的功能
  • 特别值得注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝
//省略符形参只能出现在形参列表的最后一个位置,他的形式只有两种
void fun(parm_list, ...); //指定了部分形参类型,形参后的逗号是可选的
void fun(...);

6.3 返回类型和return语句

6.3.1 不能返回局部对象的引用或者指针

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

如上,两条return都将返回一个未定义的值,试图使用manip的返回值会引发未定义的行为,这两条语句都指向了不再可用的内存空间

6.3.2 返回引用左值

函数的返回类型决定函数调用是否是左值

  • 调用一个返回引用的函数得到左值,其他返回类型得到右值
    可以像使用其他左值那样来使用返回引用的函数的调用,特别是我们能为返回类型是非常量引用的函数的结果赋值
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;
}
  • 当然,如果返回类型是常量引用,我们不能给调用的结果赋值

6.3.3 列表初始化返回值

C++11 新标准规定,函数可以返回花括号包围的值的列表,类似于返回其他结果,此处的列表也用来表示函数返回的临时量进行初始化。

vector<string> process() {
    //...
    if (excepted.empty()) {
        return {};
    } else if (excepted == actual) {
        return {"functionX","okey"};
    } else {
        return {"functionX", excepted, actual};
    }
}

6.3.4 main的返回值

  • 对于其他函数来说,如果函数的返回类型不是void,那么他必须返回一个值,但是main函数例外,如果main函数没有return语句直接结束,编译器会隐式的插入一条返回0的return语句
  • main函数的返回值可以看作是状态指示器,返回0表示执行成功,其他返回值表示执行失败,其中非0值的具体含义由机器而定。
  • 为了使返回值与机器无关,cstdlib头文件定义了两个预处理变量,我们可以用这两个变量表示成功与失败
int main() {
    if(some_failure) return EXIT_FAILESS;
    else return EXIT_SUCCESS;
}
  • 因为他们是预处理变量,所以既不能在前面加std::,也不能再using声明中出现
  • main函数不能调用他自己

6.3.5 返回数组指针

  • 因为数组不能被拷贝,所以函数不能返回数组,但是函数可以返回数组的指针或引用。
  • 从语法上来说,想要定义一个返回数组的指针或引用比较繁琐,但是有方法可以简化这一任务,比如类型别名
typedef int arrT[10]; //arrT是一个类型别名,表示含有10个整数的数组
using arrT = int[10]; //arrT的等价声明
arrT* func(int i);    //返回一个指向10个整数的数组的指针

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

  • 如果想在省声明func时不用类型别名,必须记住被定义的名字后面数组的维度
int arr[10];        //arr是含有10个整形的数组
int *p1[10];        //p1是含有10个指针的数组
int (*p2)[10] = &arr; //p2是一个指针,指向含有10个整数的数组
  • 如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后
  • 函数的形参列表也跟在函数名字后面且形参列表应该先与数组的维度,因此返回数组指针的函数形式如下所示
    • Type (*function (parameter_list))[dimension]
    • Type表示元素类型,dimension表示数组的大小,(function(parameter_list))*两端的括号必须存在,否则函数的返回类型将会是指针的数组
//e.g.
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类型

6.3.7 使用尾置返回类型

  • 任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效,比如返回类型是数组的指针或者数组的引用。
  • 尾置返回类型跟在形参列表后面并以一个->符号开头,为了表示函数真正的返回类型跟在形参列表之后,在本该出现在返回类型的地方放置一个auto
  • auto func(int i) -> int (*i)[10];
  • 我们可以清楚的看到func含税返回的是一个指针,并且指向了含有10个整数的数组

6.3.8 使用decltype

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

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
//返回一个指针,指向含有5个整数的数组
decltype(odd) *arrPtr(int i) {
    return (i & 1)? &odd : &even;
}
  • tips:decltype并不负责把数组类型转换成对应的指针,所以decltype的结果是个数组

6.4 函数重载

  • 重载函数:同一作用域内的几个函数名字相同但形参列表不同,当调用这些函数时,编译器会根据传递的实参类型推断想要的函数
  • main函数不能重载
  • 不允许两个函数除了返回类型之外的其他所有要素都相同

6.4.1 重载和const形参

顶层const不影响传入函数的对象,一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来

int solve(int);
int solve(const int ); //重复声明int solve(int)

int solve(int*);
int solve(int* const); //重复声明int solve(int*)

6.4.2 const_cast和重载

const_cast可以改变底层const,可以很好的和重载函数搭配

//e.g. 我们现在需要一个比较字符串长度的函数,并且返回较短的那个的引用
const string &shorterString(const string &s1, const string &s2) {
    return s1.size() < s2.size() ? s1 : s2;
} 
//为了保证可以调用shorterString("hello","hi"); 这样的形式,我们将string设置为const引用,但是因此也返回了const string, 但它原本可能是一个非常量
//对于非常量的字符串,我们需要重载它
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.4.3 调用重载的函数

函数匹配是指一个过程,在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,也叫做函数确定
调用重载函数可能有三中可能的结果

  • 编译器找到一个与实参最佳匹配的函数,并调用函数代码
  • 找不到任何一个函数与实参匹配,发出无匹配的错误信息
  • 有多于一个函数可以匹配,但是每一个都不是最佳选择,此时也会发生错误,称为二义性调用

6.4.4 重载与作用域

首先:将函数声明置于局部域不是一个明智的选择

  • 如果我们在内部作用域中声明名字,他将隐藏外层作用域中声明的同名实体。在不同的作用域中无法重载函数名
string read();
void print(const string&);
void print(double);
void func(int val) {
    bool read = false;
    string s = read(); //错误,read是bool
    void print(int);
    print("value: "); //错误,print(const string&)被隐藏了
    print(val); //正确
    print(3.14); //正确,但调用的是print(int)
}

6.5 特殊用途语言特性

6.5.1 默认实参

  • 默认实参作为初始值出现在形参列表中
  • 一旦某个形参被赋予了默认值,它后面的所有形参都必须有默认值(只能省略尾部实参)
//e.g. 默认实参
string screen(size_t weight = 24, size_t width = 80, char background = ' ');
string window;
window = screen();
window = screen(66);
window = screen(66,256);
window = screen(66,256,'#');

window = screen(, ,'#'); //错误,只能省略尾部实参
window = screen('#'); //正确,但调用的是screen('#',80,' ');
  • 给定作用域中一个形参只能被赋予一次默认实参,也就是函数的后续声明只能为之前没有默认值的形参添加默认实参,而且该函数右侧的所有形参都必须有默认值。
//e.g.
string screen(size_t, size_t, char = ' '); //声明最后一个为默认实参
string screen(size_t, size_t, char = '*'); //错误:重复声明
string screen(size_t, size_t = 60, char);  //正确:添加默认实参

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

//wd,def,ht声明必须在函数之外
size_t wd = 80;
char def = ' ';
size_t ht();
string screen(sz = ht(), sz = wd, char = def);
string window = screen();//调用screen(ht(), 80, ' ');
//表达式求值的过程发生在函数调用时
void f2() {
     def = '*';       //改变def
     size_t wd = 100; //隐藏外部wd
     window = screen(); //依然调用screen(ht(), 80, ' ');
}

6.5.2 内联函数

在大多数机器上,一次函数调用包含着一系列工作:调用前保存寄存器,返回时恢复,可能需要拷贝实参,程序转向一个新的位置继续执行

  • 可以通过内联函数避免函数调用的开销
  • 把函数指定为内联函数,就是在他的每个调用点上“内联地”展开

当我们把shorterString(s1,s2)指定为内联函数,cout << shorterString(s1,s2) << endl会被编译器展开为cout << (s1.size() < s2.size() ? s1 : s2) << endl;,从而消除运行函数时候的开销

  • 改写的方法是在函数名前加上inline
  • 内联说明是向编译器发出一个请求,编译器可以选择忽略这个请求
  • 内联机制适用于优化规模较小,流程直接,频繁调用的函数

6.5.3 constexpr函数

constexpr函数是指能用于常量表达式的函数,定义constexpr函数的方法与其他函数类似,但是需要遵守几项约定

  • 函数的返回类型及所有形参的类型都得是字面值类型
  • 函数体必须有且只有一条return语句
constexpr int new_size() {
    return 42;
}
constexpr int foo = new_size();
  • 编译器在初始化的时候能验证new_size返回的是常量表达式,因此可以用来初始化constexpr类型的foo
  • 初始化的时候,编译器把对constexpr函数的调用替换成结果值。为了能在编译的时候随时展开,constexpr函数被隐式的指定为内联函数
  • constexpr函数体可以包含其他语句,只要这些语句在运行时不执行任何操作就行
  • 允许constexpr返回值不是一个常量
constexpr size_t scale(size_t cnt) {
  return new_size() * cnt;
}
int arr[scale(2)]; //正确,scale(2)是一个常量表达式
int i = 2;
int a2[scale(i)]; //错误,i不是常量表达式
  • constexpr不一定返回常量表达式
  • 内联函数和constexpr函数允许在程序中被定义多次,但是多个定义必须完全一致

6.5.4 调试帮助

程序可以包含一些用于调试的代码,但是这些代码只在开发程序的时候使用,当程序完成发布的时候要屏蔽掉这些代码,这种方法用到两项预处理功能assertNDEBUG

  • assert预处理宏
    • 所谓预处理宏其实是一个预处理变量,行为类似于内联函数
    • assert宏使用一个表达式作为条件: assert(expr)
      • 如果表达式为假(0),assert输出信息并终止程序运行
      • 如果表达式为真,则什么也不做
    • assert常用于检查不能发生的条件
  • NDEBUG预处理变量
    • assert行为依赖于名为NDEBUG的预处理变量的状态
      • 如果定义了NDEBUG,则assert什么也不做
      • 默认状态下没有定义,assert将执行运行时检查
    • 可以使用define语句定义NDEBUG从而关闭调试状态
    • 很多编译器也提供了命令行选项让我们定义预处理变量
      • CC -D NDEBUG main.c等价于在main.c文件的一开始写#define DEBUG
    • 定义NDEFBUG避免各种检查的开销,assert只能作为调试程序的辅助手段,不能代替真正运行时候的逻辑检查,也不能代替程序应该包含的错误检查
    • 除了assert,NDEBUG也可以用来编写自己的条件调试代码,如果NDEBUG未定义,将执行#ifndef和#endif之间的代码,如果定义了,这些代码会被忽略
void print() {
#ifndef NDEBUG 
    //__func__是编译器定义的局部静态变量,用来存放函数的名字
    cout << __func__ << endl;
#endif
}

如上,除了__func__之外,还有4个编译器定义的对于程序调试有用的名字

  • FILE 存放文件名的字符串字面值
  • LINE 存放当前行号的整形字面值
  • TIME 存放文件编译时间的字符串字面值
  • DATA 存放文件编译日期的字符串字面值

6.6 函数匹配

大多数情况下我们容易确定某次调用应该选用哪个重载函数,但是当几个重载函数的形参数量相同并且某些形参的类型可以由其它类型转换得来的时候,这项工作就不那么容易了

void f();
void f(int);
void f(int, int);
void f(double, double = 3.14);
f(5.6); //调用void f(double, double);

6.6.1 确定候选函数和可行函数

第一步:选定本次调用对应的重载函数集,集合中的函数称为候选函数,候选函数有两个特征

  • 与被调用的函数同名
  • 声明在调用点可见

第二步:考察调用提供的实参,从候选函数选出能被这组实参调用的函数,这些函数称为可行函数,可行函数也有两个特征

  • 形参数量与本次调用提供的实参数量相等
  • 每个实参的类型与对应的形参类型相同,或者能转换成形参的类型

6.6.2 寻找最佳匹配

第三步:从可行的函数中选择与本次调用最匹配的函数,逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的可行函数。基本思想是:实参类型与形参类型越接近,他们匹配的越好

6.6.3 含有多个形参的函数匹配

当实参的数量含有两个或者更多时,函数匹配就比较复杂了。编译器选择那些形参数量满足要求且实参类型和形参类型能够匹配的函数,如果检查了所有实参后没有任何一个函数脱颖而出,则该调用是错误的,编译器将报告二义性调用信息

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

6.6.4 实参类型转换

为了确定最佳匹配,编译器将实参类型到形参类型的转换划分为几个等级,具体排序如下

  1. 精确匹配
    • 实参类型和形参类型相同
    • 实参从数组类型或函数类型转换为对应指针类型
    • 向实参添加顶层const或者从实参中删除顶层const
  2. 通过const转换实现的匹配
  3. 通过类型提升实现的匹配 (bool -> int)
  4. 通过算术类型匹配或指针转换实现的匹配
  5. 通过类类型转换实现的匹配

所有算术类型转换的级别都一样,从int向unsigned的转换不比向double的转换级别高

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

6.6.5 函数匹配和const实参

如果重载函数的区别在于引用类型的实参是否引用了const或者指针类型的形参是否指向const,则调用发生的时候编译器通过实参是否是常量决定选择哪个函数

Record lookup(Account&);
Record lookup(const Account&);
const Account a;
Account b;
lookup(a); //调用lookup(const Account&);
lookup(b); //调用lookup(Account&);

6.7 函数指针

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

bool lengthCmp(const string &,const string &);
//pf指向一个函数,函数的参数是两个const string&,返回值是bool
bool (*pf)(const string &,const string &); //未初始化
//pf两边括号必不可少,否则变为一个返回值为bool*的函数

6.7.1 使用函数指针

pf = lengthCmp; //pf指向名为lengthCmp的函数
pf = &lengthCmp; //等价的语句,取地址符可选

bool b1 = pf("hello","goodbye"); //调用lengthCmp函数
bool b2 = (*pf)("hello","goodbye"); //等价调用
bool b3 = lengthCmp("hello","goodbye"); //等价调用

指向不同函数类型的指针之间不存在转换规则,但是可以给函数指针赋nullptr或者0表示没有指向任何一个函数

size_type sumLength(const string&, const string&);
bool cstringCmp(const char*, const char*);
pf = 0;
pf = sumLength; //错误:返回类型不匹配
pf = cstringCmp;//错误:形参类型不匹配
pf = lengthCmp; //正确

重载函数的指针

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

函数指针形参

  • 和数组类似,虽然不能定义函数类型的形参,但是形参可以是指向函数的指针,此时形参看起来是函数类型,实际上是当成形参使用
//第三个形参是函数类型,它会自动转换为指向函数的指针
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 &));
//使用
useBigger(s1, s2, lengthCmp);//lengthCmp自动转换为指向函数的指针

可以通过decltype让我们简化使用函数指针的代码

typedef bool Func(const string&, const string&);
typedef decltype(lengthCmp) Func2; //等价的类型
//useBigger等价声明
void useBigger(const string&, const string&, Func);
void useBigger(const string&, const string&, Func2);

虽然不能返回函数,但是可以返回指向函数的指针,和往常一样,最简单的方法是使用类型别名

using F = int(int*, int);  //F是函数类型
using PF = int(*)(int*, int);  //PF是指针类型
PF f1(int);   //正确,PF是指向函数的指针,f1返回指向函数的指针
F f1(int);    //错误,F是函数类型
F *f1(int);   //正确,显示的指定返回类型为指向函数的指针
posted @ 2020-07-08 21:42  Hugh_Locke  阅读(245)  评论(0编辑  收藏  举报