c++ primer 第五版 笔记 第六章

第六章 函数

函数:一个命名的代码块。通过调用函数执行响应的代码。函数可以有0个或者多个参数。通常会产生一个结果。

重载:同一个名字可以对应几个不同的函数

6.1 函数基础

函数的格式:

返回类型  函数名字 (参数列表){
    函数体
}


参数列表,以逗号分隔

函数的调用:

函数或者指向函数的指针(实参列表)

实参列表用逗号隔开

例子:

//返回类型  函数名  (参数列表)
int fact(int val){
    int ret = 1;
    while(val >1)
        ret *= val--;
    return ret;
}
//调用
int main(){
    //函数或者指向函数的指针(实参列表)
    int j = fact(5);
    cout << "5! is " << j << endl;
    return 0;
}

函数执行的步骤:

  1. 隐式的定义并初始化它的形参。因此调用fact函数的时候,首先创建一个val的int变量,然后将它初始化为调用时所用的实参5

  2. 开始执行函数体

  3. 当遇到return语句时,结束函数体的执行。同时,将return语句的值,返回给调用者

形参和实参

实参时形参的初始值。他们依据位置一一对应。虽然位置一一对应,但是却没有规定他们的求值顺序。

实参类型必须和形参类型一一匹配。这个和初始化变量的要求一模一样。

形参列表

形参列表可以为空,这样就只有一个小括号。

同时,为了和c语言兼容,也可以在空形参列表的位置,写上void

形参列表以逗号分隔,每个元素跟变量声明一样,注意,就算多个变量类型,一样,也要分开写。如下:

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

形参名,不能重名

形参属于,函数体内部的局部变量,超出这个范围,访问失败

返回类型

  1. void类型,表示函数不返回任何值。
  2. 函数不能返回数组类型,和函数类型,但是可以返回指向他们的指针

6.1.1 局部对象

作用域:名字可见的范围
生命周期:对象存在的时间

函数的函数体,就是一个语句块,这个语句块构成一个作用域,形参和函数体内部定义的变量都在这个作用域中。

形参和函数体内部定义的变量称为局部变量。之所以称为局部,是因为超出函数体之后,这些将不复存在。

局部对象还会隐藏外层作用域的同名声明。

在函数体内部的对象,分为以下几种:

  1. 自动对象:把只存在于块执行期间的对象,称为自动对象。

自动对象的创建:从变量的定义语句开始。一旦程序的控制路径到达变量的定义语句,则创建该对象。

自动对象的销毁:一旦到达定义语句所在作用域的末尾,则销毁该对象。

形参是一种自动对象,函数开始时,为形参申请存储空间。因为形参的作用域为函数体,因此,函数一旦结束,则形参被销毁。

而形参的初始化,是通过实参:用实参的值,初始化形参。而其他在函数体内部定义的对象,如果没有进行初始化,那么他们执行默认初始化,因此对于内置类型来说,如果没有初始化,那么他们的值就是未定义的。

  1. 局部静态变量:将局部对象用static关键字修饰,就变成了局部静态对象。

局部静态对象的初始化:从定义语句开始。
局部静态对象的销毁:程序终止时,销毁。

例子如下:

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;
    return 0;
}

count_calls内部的ctr使用了static关键字修饰,因此是一个局部静态变量,当第一次调用count_calls函数时,将初始化ctr变量,然后自增ctr。当后续几次调用count_calls时,因为已经初始化了ctr,所以不再初始化,而是,直接自增ctr。这样ctr一直保存有count_calls被调用的次数

6.1.2 函数声明

函数在使用之前必须先声明,函数的声明跟,函数定义一样。唯一的区别是:将函数体用分号代替。注意此处,的分号,并不是代替了空的语句块,而是表示语句的结束。

又因为在声明中没有函数体,所以形参名也就用不上,所以声明中可以省略形参名。但是加上形参名会更好,因为,这样有助于程序的理解。

函数声明也叫做,函数的原型。因为声明包括了函数的三要素:返回类型,函数名,形参类型。

这三要数可以唯一标识一个函数类型。

通常将函数声明放在头文件中,然后在需要用到的地方,include这个头文件即可。这样可以保证,所有需要用到这个函数的声明都是一致的。

6.1.3 分离式编译

随着程序规模的变大,程序员希望将程序,按照逻辑进行划分。c++提供了这种功能,这种功能称为分离式编译.分离式编译可以将程序分开到不同的文件中,每个文件单独编译。这样程序员就可以将不同的逻辑放在不同的文件中,每个文件单独编译。

举个例子(书中例子):

一个fact.cc文件,声明在Chapter6.h中,一个factMain.cc文件,包含程序入口main

使用下面的命令,同时编译两个文件

$ CC factMain.cc fact.cc #产生factMain.exe 或者a.out
$ CC factMian.cc fact.cc -o main #产生main或者main.exe

分离式编译如下:

$ CC -c factMain.cc #编译factMain.cc生成中间文件factMain.o
$ CC -c fact.cc #编译fact.cc,生成中间文件fact.o
$ CC factMain.o fact.o #将中间文件生成可执行文件
$ CC factMian.o fact.o -o main #将中间文件生成可执行文件

不同的编译器,分离式编译使用的步骤有细微差别,可参考编译器手册。

6.2 参数传递

参数传递分为两种:引用传递,和值传递

6.2.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++语言中,建议使用引用类型的形参代替指针类型

6.2.2 引用传递

先来个例子

int n = 0;i = 42;
int &r = n; //r绑定到了n
r = 42; //现在的n为42
r = i;  //现在的n和i值形同
i = r;  //i的值和n相同

引用形参和上面的引用变量类似,可以让函数改变引用所绑定的对象。

举例如下:

void reset(int &i){
    i = 0;//会改变i所绑定对象的值
}

因为i时引用类型,所以当改变i的值,就相当于改变i所绑定对象的值。

int j = 42;
reset(j);   //j使用引用传递,j的值会被改变
cout << "j = "<< j << endl;//输出j = 0

使用引用避免拷贝

  1. 拷贝大类型对象时,比较耗时,因此可以使用引用

  2. 对于有些类型来说,不支持拷贝操作,因此在函数穿参中,必须使用引用累心给,比如(IO类型)

举例如下:

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

因为string对象可能会比较大,为了避免无所谓的复制,此处的函数形参定义为引用类型,同时,又因为不会修改内容,定义了const类型。

使用引用形参还可以返回额外的信息

一个函数只能返回一个值,如果一个逻辑需要返回多个值,可以使用引用形参。将需要返回的结果,放入引用形参中。

举例如下:

下面逻辑,希望返回c第一出现的位置,同时,统计其次数,因为需要返回两个参数,所以将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 != s.size() ;++i){
        if(s[i] == c){
            if(ret == s.size())
                ret = i;
            ++occurs;
        }
    }
    return ret;
}

6.2.3 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被忽略,所以上面两个相当于同一个定义。

尽量使用const引用

定义成普通的引用,常会引入下面的不便:

  1. 让使用者认为,里面的值可以被改变
  2. 不能使用const对象,字面值,或者需要类型转换的对象,进行传参。

因此,常常将形参定义成const 引用

6.2.4 数组形参

当形参时数组时,有如下两个限制:

  1. 不能拷贝数组
  2. 使用数组的时候,会将其转换成指针

举例如下:

//尽管形式不一样,但是这三个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]

如果给实参是一个数组,自动将这个数组转换成一个指向数组首元素的指针。

注意:以数组作为形参的函数,也必须确保使用数组的时候不会越界

正是由于数组以指针的形式被传递出去,所以数组的大小就需要另外的方式传递给函数,下面有几种常见的方式传递给函数:

使用标记表示数组长度

这种方式表示,在数组本身内容中,有表示数组结束的标记,比如c风格字符串,他以空字符作为结尾,这样一旦遇到空字符就可以表示达到了末尾。

void print(const char *cp){
    if(cp)
        while(*cp)
            cout << *cp ++;
}

这种方式的唯一要求是:数组的内容和标记不能混淆。

使用标准库规范

向函数传递数组的首元素和尾后元素的指针。例子如下:

void print(const int* beg,const int *end){
    while(beg!=end)
        cout << *beg++ << endl;
}

调用形式如下:

int j[2] = {0,1};
print(begin(j),end(j));

其中begin和end是标准库提供的支持,这样就可以将一个数组的范围提供给一个函数了。

显示的传递一个表示数组大小的形参

例如如下:

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

调用形式如下:

int j[] = {0,1};
print(j,end(j)-begin(j));

数组引用形参

形参和普通的变量一样,可以定义为数组的引用。此时,引用形参绑定到了对应的实参上面,也就是绑定到了数组上面。

例如如下:

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

注意:arr两边的括号必不可少

f(int &arr[10]) //错误:声明成了数组的引用
f(int (&arr)[10]) //正确

调用如下:

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

传递多维数组

跟普通的数组一样,传递多维数组,实际上,传递是的指向多维数组首元素的指针。因为多维数组,实质上是数组的数组,所以,首元素就是一个指向数组的指针。数组第二维的大小就是数组类型的一部分,不能够胜率:

void print(int (*matrix)[10],int rowSize){}

将上面函数的matrix为,一个指向数组的指针,这个数组有10int类型。

等价定义

void print(int matrix[][10],int rowSize){}

6.2.5 main函数处理命令行参数

没有什么特殊,不再做笔记

6.2.6 含有可变形参的函数

有时候,不知道函数需要多少个形参,此时可以使用可变形参的函数,可变形参的函数有两种方法:

initializer_list形参

如果实参数量未知,但是类型,相同,我们可以使用initializer_list类型的形参。它提供的操作如下:
在这里插入图片描述

注意:initialize_lise对象中的元素是常量值,无法对其进行改变

例子如下:

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

使用的例子如下:

if(expected!=actual)
    error_msg({"functoinX",expected,acuta;});
else
    error_msg({"functionX","okay"});

省略符形参

省略符形参应该仅仅用于c和c++通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝

省略符形参只能出现在形参的最后一个位置,它的形式无外乎如下两种:

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

对于有类型的形参,则跟其他普通的形参一样,会进行相应的类型检查。省略符形参对应的实参无须进行类型检查。在第一种形式中,形参声明后面的逗号是可选。

此种用法还不会,带后续完善加强

6.3 返回类型和return语句

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

return语句有两种形式:

return;
return expression;

6.3.1 无返回值的函数

无返回值的return语句,只能用在返回类型是void的函数,而void函数不一定非要return语句。举例如下:

void swap(int &v1,int &v2)}{
    if( v1 == v2)
        return;
    int tmp = v2;
    v2 =v1;
    v1 = tmp;
    //此处无须显示的return语句
}

6.3.2 有返回值得函数

如果一个函数的返回类型不为空,那么这个函数的每一条return
语句都需要返回一个值。并且这个返回值类型必须与函数的返回类型相同,或者隐式地转换成函数的返回类型。

例如:

bool str_subrange(const string &str1,const string &str2){
    if(str1,size() == str2.size()){
        return str1 == str2;
    }

    auto size = (str1.size() < str2.size())?
        str1.size():str2.size();
    for(decltype(size) i = 0;i!=size;i++){
        if(str1[i] != str2[2]){
            return;//错误,没有返回值
        }
    }

    //错误:控制流可能尚未返回值,就结束了函数的执行
    //编译器可能检查不出这一错误
}

for循环内部的return语句是错误的,因为他没有返回值,编译器能够检查这种错误

for语句之后没有提供return语句。在上面的程序中,如果string对象是另外一个的子集,则函数在执行完for循环之后还将继续执行,显然应该有一个return语句,来专门处理这种情况。编译器可能检查不出这种错误。一旦检查不出来,这种行为是未定义的。

值是如何被返回的

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

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

函数完成之后,所占用的存储空间也随之被释放。因此,局部变量将被销毁,如果返回局部变量的指针,或者引用,那么这个指针指向的对象,或者这个引用绑定的对象是不存在的。

引用返回左值

函数的返回类型决定了函数返回的是左值还是右值。调用一个返回引用的函数得到的是左值,其他返回类型得到的是右值。

列表初始化返回值

举例如下:

vector<string> process(){
    if(expected.empty())
        return {};
    else if(expected == actual)
        return {"functoinX","okay"};
    else
        return {"functionX",expected,actual};
}

上面函数的所有return语句,都返回一个用大括号括起来的,列表。

这就跟使用列表进行对变量初始化一样。所以对于内置类型来说,花括号之内只能有一个值。

main的返回值

前面有介绍过:如果函数的返回类型不是void,则必须返回一个值。

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

main函数的返回值,被认为程序执行状态的指示器。返回0表示成功,非0值表示执行失败,其中非0值的具体含义依机器而定。

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

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

6.3.3 返回数组指针

因为数组不能拷贝,因此函数只能返回数组的指针,或者数组的引用。

格式如下:

type (*function(parameter_list))[dimension]
int (*func(int i))[10];

理解如下:

  1. func是一个函数,它的形参为int
  2. *func(int i)函数的返回类型为一个指针
  3. (*func(int i))[10]这个指针指向一个数组,数组维度为10
  4. 数组的元素为int类型

上面的写法对于新手来说有点不易理解,有几下几种写法可以更加容易理解

尾置返回类型

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

将返回类型放在最后,然后以前函数类型的位置,使用一个auto关键字

使用decltype

int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};

decltype(odd) * arrPtr(int i){
    return (i%2)? &odd:&even;//返回一个指向数组的指针
}

注意:decltype并不负责将数组类型转换成对应的指针类型,所以decltype的结果是一个数组,要想表示指针,还需要加上一个星号*

使用类型别名

typedef int arrT[10];
using arrT = int[10];
arrT * func(int i);

6.4 函数重载

重载函数:同一作用域中的几个函数名相同但是形参列表不同。

void print(const char *cp);
void print(const int *beg,const int*end);
void print(const int ia[],size_t size);

注意:main函数不能重载

对于重载函数来说:他们应该在形参类型,后者形参数量上有所不同。

注意下面的情况:

Record lookup(const Account & acct);
Record lookup(const Account &); //省略掉了名字

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

上面两组都是相同的函数。因为他们的形参类型和形参的数量并没有变化。

在第二组中形参的类型,本质上是同一种类型

再思考下面的情况:

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


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

这两组声明中,第二个都跟第一个是同样的声明。因为,顶层const会被忽略掉,因此,对于含有顶层const的形参,和没有顶层const的形参,无法区别开来。

再思考下面的例子:

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

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

上面两组函数,都是不同的函数,因为编译器可以通过实参是否为常量来决定调用那一个函数。

const_cast和重载

如果有下面的函数

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

如果我们希望,我们传递给这个函数的实参是非常量,并且返回类型也希望是非常量,上面的函数有所局限,所以可以在新增一个函数,这个函数是上面这个函数的一个重载版本:

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. 编译器找到一个最佳匹配的函数,并调用它
  2. 找不到任何与调用匹配的函数,此时编译器发出无匹配的错误信息
  3. 找到多个函数可以匹配,但是每一个匹配都不是最佳匹配,此时编译器发出二义性调用的错误。

6.4.1 重载与作用域

记住:如果在内层作用域中声明的名字,它将隐藏外层作用域中声明的同名实体。

例子如下:

string read();
void print(const string &);
void print(double);
void foorBar(int ival){
    bool read = false;//新作用域,隐藏外层的read
    string s = read();//错误,read是一个布尔值,而非函数
    //不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
    void print(int);
    print("Value: ");//错误:print(const string &) 被隐藏掉了
    print(ival);    //正确:当前print(int);可见
    print(3.14);    //正确:调用print(int); print(double)被隐藏掉
}

因为c++中,名字查找在类型检查之前,所以,当调用print函数时,先找到了局部作用域中声明的函数,然后再进行类型的检查。发现3.14时,将会将其转换成int类型,然后调用局部作用域中的函数声明。

6.5 特殊用途语言特性

6.5.1 默认实参

例子如下:

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

注意:一旦一个形参被赋予了默认值,那么这个形参之后的所有形参都必须有默认值。

在传递参数的时候,带有默认实参的形参,可以不用传递实参,也可以传递实参。

默认实参的声明

在一个作用域中一个形参只能被赋予一次默认实参。例子如下:

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);
string window = screen();//调用screen(ht(),80,' ');

用作默认实参的名字在函数声明所在的作用域解析,而这些名字的求值过程在发生在函数调用时:

void  f2(){
    def = ' *'; //改变了默认值
    sz wd = 100; //隐藏了外层定义的wd,但是没有改变默认值
    window = screen();  //调用screen(ht(),80,'*');
}

6.5.2 内联函数和constexpr函数

对于一些频繁调用的小的函数而言,可以使用内联函数,以此来减少函数调用产生的跳转消耗

例子如下:

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

一般来说,内联机制用于优化规模较小,流程直接,频繁调用的函数。

不过很多编译器都不支持内联递归函数。

constexpr函数

constexpr函数:能用于常量表达式的函数。

constexpr函数:函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条return语句

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

执行改初始化时,编译器把对constexpr函数的调用替换成其结果值。为了能够在编译时随时展开,constexpr函数被隐式地指定为内联函数

constexpr函数体内也可以包含其他语句,只要这些语句运行时不执行任何操作就行。例如,可以包含空语句,类型别名,已经using声明等。

//如果cnt是一个常量表达式,则scale为constexpr函数,否则,不是
constexpr size_t scale(size_t cnt){return new_sz() * cnt;}

int arra[scale(2)];//正确,scale(2)是一个常量表达式
int i = 2;
int a2[scale(i)];//错误:scale(i)不是一个常量表达式

因此从上面可以知道:constexpr函数不一定返回常量表达式

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

和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。毕竟,编译器要想展开函数仅有函数声明是不够的,还需要函数的定义。不过对于某个给定的内联函数或者constexpr函数lai9shuo,它的多个定义必须完全一样。基于这个原因,内联函数和constexpr函数通常定义在头文件中。

6.5.3调试帮助

assert预处理

assert(expr);

如果expr为假,则输出信息并终止程序。如果为真,assert什么也不做

assert定义在cassert头文件中。

在开发阶段,让这个assert生效,而在正式的软件版本中,让assert失效。这依赖NDEBUG预处理变量

如果定义了NDEBUG则assert什么也不做。默认状态下没有定义NDEBUG。

这样只需要在正式版本中,定义NDEBUG即可。就可以忽略掉assert的运行时开销。

除此之外,还可以更具NDEBUG自己定义相应的调试代码。例子如下:

void print(const int ia[],size_t size){
    #ifndef NDEBUG
        cerr << __func__ << ": array size is "<< size << endl;
    #endif
    //...
}

上面函数中,如果没有定义NDEBUG,则执行#ifndef和#endif之间的代码。如果定义了NDEBUG,这些代码将被忽略掉

c++除了定义了__func__之外,还定义了下面:

__FILE__  存放文件名字的字符串字面值
__LINE__  存放当前行号的整形字面值
__TIME__  存放文件编译时间的字符串字面值
__DATE__  存放文件编译日期的字符串字面值

6.6 函数匹配

在重载函数中,函数的匹配有下面的几个过程。

确定候选函数和可行函数

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

候选函数具备两个特征:1.与被调用的函数同名;2.其声明在调用点可见。

第二步:从候选函数中选出能被这组实参调用的函数,这些函数称为可行函数。

可行函数有两个特征:1.形参数量与实参数量相同;2.每个形参类型与实参类型相同,或者实参类型能够转换成形参类型。

寻找最佳匹配

第三步:从可行函数中选择与本次调用最匹配的函数。这一步的过程为:逐一检查函数调用提供的实参,寻找形参类型与实参类型最匹配的可行函数。类型越接近,匹配越完美

例子如下:

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

运用上面的步骤:

  1. 候选函数为:
void f();
void f(int);
void f(int,int);
void f(double,double = 3.14);
  1. 可行函数为:
void f(int);
void f(double,double = 3.14);
  1. 最佳匹配
    类型越接近匹配越完美,因此,最佳匹配为:
void f(double,double = 3.14);

现在思考下面的调用的匹配过程:

f(42,2.56);

1.候选函数:

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

2.可行函数

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

3.最佳匹配,逐一对每个实参进行类型检查
对于第一形参来说,最佳匹配为

void f(int,int);

对于第二个形参来说,最佳匹配为

void f(double,double = 3.14);

无法判断哪一个可行函数最佳,因此,编译器报,二义性错误

6.6.1 实参类型转换

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

1·精确匹配,包括如下情况:

实参类型和形参类型相同

实参从数组类型或者函数类型转换成对应的指针类型

向实参添加顶层const或者从实参中删除顶层const

2.通过const转换实现的匹配

3.通过类型提升实现的匹配

4.通过算术类型或者指针类型转换实现的匹配

5.通过类类型转换实现的匹配

例子:

void ff(int);
void ff(short);

ff('a');    //char提升为int,调用f(int)

注意:所有的算术类型的转换级别都一样。例如,int向unsigned int的转换并不比int向double的类型转换级别高:

void mainp(long);
void mainp(float);

manip(3.14);//二义性错误,因为3.14为double,可同时转换成float和long,并且两者的转换级别相同
Record lookup(Account &);
Record lookup(const Account &);

const Account a;
Account b;

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

在第一次调用中,传入的是const对象,不能将const对象绑定到普通的引用中,所以,最佳匹配为

Record lookup(const Account &);

在第二次调用中,传入的是一个非const对象,而非const对象可以绑定const引用,也可以绑定到非const引用。然后用非常量对象绑定给const引用,需要进行一次类型转换,而非const引用形参的函数,则是精确匹配,所以最佳匹配为这个函数

Record lookup(Account &);

指针也有上面类似的行为

6.7 函数指针

函数类型由返回类型,和形参类型决定。

可以声明一个指针,指向函数,格式如下:

bool (*pf)(const string &,const string &);//为初始化

解说:

  1. pf是一个指针
  2. 这个指针指向一个函数,这个函数的返回类型为bool
  3. 这个函数的形参为两个const string &

注意: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 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 &));

//自动将函数lengthCompre转换成指向该函数的指针

useBigger(s1,s2,lengthCompare);


可以使用类型别名来简化这种写法:

//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; //等价的类型

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


返回指向函数的指针

不能返回函数,但是能够返回指向函数的指针

写法类似下面这样:

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

解析:

  1. f1是一个函数,
  2. 它的返回类型是一个指针,
  3. 这个指针指向一个函数,
  4. 指向的函数的形参为int*,int
  5. 指向的函数的返回类型为int

上面的写法较为繁琐,可以简化为下面的写法:

使用尾置类型

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

可以使用类型别名

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);//正确:显示地指定返回类型为函数指针

使用auto和decltype

string::size_type sumLength(const string &,const string &);
string::size_type largerLength(const string &,const string &);

decltype(sumLength) * getFcn(const string &);

注意:跟decltype作用在数组一样,decltype作用在函数上面时,返回的时函数类型,
因此如果想要声明函数指针,则应该多加一个星号*

上面难免错误,遇到错误的请指出,谢谢

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值