美文网首页
第6章:函数

第6章:函数

作者: MrDecoder | 来源:发表于2018-10-08 22:20 被阅读25次
    • #1.函数基础
      • 1.1 局部对象
      • 1.2 函数声明
      • 1.3 分离式编译
    • #2.参数传递
      • 2.1 传值参数
      • 2.2 传引用参数
      • 2.3 const形参和实参
      • 2.4 数组形参
      • 2.5 main:处理命令行选项
      • 2.6 含有可变形参的函数
    • #3.返回类型和return语句
      • 3.1 无返回值函数
      • 3.2 有返回值函数
      • 3.3 返回数组指针
    • #4.函数重载
      • 4.1 重载与作用域
    • #5.特殊用途语言特性
      • 5.1 默认实参
      • 5.2 内联函数和constexpr函数
      • 5.3 调试帮助
    • #6.函数匹配
      • 6.1 实参类型转换
    • #7.函数指针

    函数是命名了的代码块,通过调用函数执行相应的代码。函数可以有0个或多个参数,而且会产生一个结果。可以重载函数,也就是说,同一个名字可以对应几个不同的函数。

    #1. 函数基础

    一个典型的函数定义包括以下部分:返回类型、函数名字、由0个或多个形参组成的列表以及函数体。其中,形参以逗号隔开,形参的列表位于一对圆括号之内。函数执行的操作在语句块中说明,该语句块称为函数体

    我们通过调用运算符来执行函数。调用运算符的形式是一对圆括号,它作用于一个表达式,该表达式是函数或者指向函数的指针;圆括号之内是一个用逗号隔开的实参列表,我们用实参初始化函数的形参。调用表达式的类型就是函数的返回类型。

    编写函数

    编写一个求数的阶乘的程序。

    int fact(int val) {
        int ret = 1; //局部变量,用于保存计算结果
        while (val > 1)
        {
            ret *= val--;
        }
        return ret;
    }
    
    调用函数
    int main() {
        int j = fact(5); //j等于120,即fact(5)的结果
        cout << "5! is "<< j << endl; 
    }
    

    函数的调用完成两项工作:

    • 一是用实参初始化函数对应的形参
    • 二是将控制权转移给被调用函数。此时,主调函数的执行被中断,被调函数开始执行。
    形参和实参
    • 实参是形参的初始值。实参与形参存在对应的关系,但是没有规定实参的求值顺序。编译器能以任意可行的顺序对实参求值。
    • 实参的类型必须与对应的形参类型匹配。
    函数的形参列表

    函数的参数列表可以为空,但是不能省略。要想定义一个不带形参的函数,最常用的办法是书写一个空的形参列表。不过为了与C语言兼容,也可以使用关键字void表示函数没有形参:

    void f1(){} //隐式的定义空形参列表
    void f2(void){} //显示的定义空形参列表
    
    函数返回类型

    大多数类型都能用作函数的返回类型。一种特殊的返回类型是void,它表示函数不返回任何值。函数的返回值不能是数组类型或者函数类型,但是可以是指向数组或函数的指针。

    1.1 局部对象

    在C++语言中,名字有作用域,对象有生命周期。

    • 名字的作用域是程序文本中的一部分,名字在其中可见。
    • 对象的生命周期是程序执行过程中该对象存在的一段时间。

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

    自动对象

    对于普通局部变量对应的对象来说,当函数的控制路径经过变量定义语句时创建该对象,当到达所在块的末尾时销毁它。我们把只存在块执行期间的对象称为自动对象

    形参是一种自动对象。函数开始时为形参申请存储空间,因为形参定义在函数体作用域内,所以一旦函数终止,形参也就被销毁。

    局部静态对象

    如果想要局部变量的生命周期贯穿函数调用及之后的时间。可以将局部变量定义成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;
    }
    

    1.2 函数声明

    1. 和其他名字一样,函数的名字也必须在使用之前声明,函数只能定义一次,但可以声明多次。
    2. 函数的声明和函数的定义非常类似,唯一的区别是函数声明无须函数体,用一个分号替代即可。
    3. 因为函数的声明不包含函数体,所以也就无需形参的名字。
    void print(vector<int>::const_iterator beg,vector<int>::const_iterator end);
    

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

    在头文件中进行函数声明

    函数应该在头文件中声明而在源文件中定义。编译器负责验证函数的定义和声明是否匹配。

    ==含有函数声明的头文件应该被包含到定义函数的源文件中。==

    1.3 分离式编译

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

    编译和链接多个源文件

    大多数编译器提供了分离式编译每个文件的机制,这一过程通常会产生一个后缀名是.obj的文件,编译器负责把对象文件链接在一起生成可执行文件.exe。


    #2. 参数传递

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

    2.1 传值参数

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

    int n = 0; //int类型的初始变量
    int i = n; //i是n的值的副本
    i = 42; //i的值改变;n的值不变
    

    传值参数的机理完全一样,函数对形参做的所有操作都不会影响实参。

    指针形参

    指针的行为和其他非引用类型一样。当执行指针拷贝操作时,拷贝的是指针的值。拷贝之后,两个指针是不同的指针。

    //该函数接受一个指针,然后将指针所指的值置为0
    void reset(int *ip) {
        *ip = 0; //改变ip所指向对象的值
        ip = 0; //只改变了ip的局部拷贝,实参未被改变。
    }
    

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

    2.2 传引用参数

    对于引用的操作实际上是作用在引用所引的对象上:

    //该函数接受一个int对象的引用,然后将对象的值置为0
    void reset(int &i) { //i是传递给reset函数的对象的另一个名字
        i = 0; //改变i所引对象的值
    }
    
    使用引用避免拷贝

    拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本不支持拷贝操作。当某种类型不支持拷贝操作时,函数只能通过引用形参访问该类型的对象。

    举个例子:假设要编写一个用于比较两个字符串长度的函数,因为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(s.size()) i = 0; i != s.size(); i++) {
            if (s[i] == c) {
                if (ret == s.size()) {
                    ret = i; //记录c第一次出现的位置
                }
                occurs++; //将出现的次数加1
            }
        }
        return ret;
    }
    

    2.3 const形参和实参

    当形参是const时,顶层const作用于对象本身:

    const int ci = 42; //不能改变ci,const是顶层的
    int i = ci; //正确:当拷贝时,忽略了它的顶层const
    

    和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层const。

    void fcn(const int i/*fcn能够读取i,但是不能向i写值*/){} 
    
    指针或引用形参与const

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

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

    把函数不会改变的形参定义成(普通的)引用是一种比较常见的错误,这样做会给函数调用者一种误导,即函数可以修改它的实参的值。此外,使用引用而非常量引用也会极大地限制函数所能接受的实参类型。

    //不良设计:第一个形参的类型应该是const string&
    string::size_type find_char(string &s,char c,string::size_type &occurs);
    
    bool is_sentence(const string &s) {
        string size_type ctr = 0;
        return find_char(s,'.',ctr) == s.size() - 1 && ctr == 1;//错误:不能将常量引用赋值给非常量引用
    }
    

    2.4 数组形参

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

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

    管理数组实参的第一种方法是要求数组本身包含一个结束标记,使用这种方法的典型示例就是C风格字符串。

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

    管理数组实参的第二种技术是传递指向数组首元素和尾后元素的指针。

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

    第三种管理数组实参的方法是专门定义一个表示数组大小的形参,在C程序和过去的C++程序中常常使用这种方法。

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

    当函数不需要对数组元素进行写操作的时候,数组形参应该是指向const的指针。

    数组引用形参

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

    void print(int (&arr)[10]) { //形参是数组的引用,维度是类型的一部分
        for (auto elem : arr) {
            cout << elem << endl;
        }
    }
    
    传递多维数组

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

    //matrix指向数组的首元素,该数组的元素是由10个整数构成的数组。
    void print(int (*matrix)[10],int rowsize);
    
    *matrix两端的括号必不可少:
    int *matrix[10]; //10个指针构成的数组
    int (*matrix)[10]; //指向含有10个整数的数组的指针
    

    2.5 main:处理命令行选项

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

    第二个形参argv是一个数组,它的元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。

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

    2.6 含有可变形参的函数

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

    • 如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;
    • 如果实参的类型不同,我们可以编写一种特殊的函数,也就是所谓的可变参数模板。
    initializer_list形参

    如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用initializer_list类型的形参。

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

    省略符形参是为了便于C++程序访问某些特殊的C代码而设置的,这些代码使用了名为varargs的C标准库功能。通常,省略符形参不应用于其他目的。省略符形参只能出现在形参列表的最后一个位置:

    void foo(param_list,...);
    void foo(...);
    

    #3. 返回类型和return语句

    return语句终止正在执行的函数并将控制权返回到调用该函数的地方。return语句有两种形式:

    return;
    return expression;
    

    3.1 无返回值函数

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

    3.2 有返回值函数

    return语句的第二种形式提供了函数的结果。只要函数的返回类型不是void,则该函数内的每条return语句必须返回一个值。return语句的返回类型必须与函数的返回类型相同,或者能隐式地转换成函数的返回类型。

    //因为含有不正确的返回值,所以这段代码无法通过编译
    bool str_subrange(const string &str1,const string &str2) {
        if(str1.size() == str2.size()) {
            return str1.size() == str2.size(); //正确:返回布尔类型
        }
        auto size = (str1.size() < str2.size()) ? str1.size : str2.size();
        for(decltype(size) i = 0;i != size;++i) {
            if(str1[i] != str2[i]) {
                return; //错误#1:没有返回值,编译器将报告这一错误
            }
        }
        //错误#2:控制流可能尚未返回任何值就结束了函数的执行
        //编译器可能检查不出这一错误
    }
    
    值是如何被返回的

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

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

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

    //严重错误:函数试图返回局部对象的引用
    const string &mainip() {
        string ret;
        if (!ret.empty()) {
            return ret; //返回局部对象的引用
        }else {
            return "Empty"; //"Empty"是一个局部临时量
        }
    }
    
    返回类类型的函数和调用运算符

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

    //调用string对象的size成员,该string对象是由shorterString函数返回的
    auto sz = shortString(s1,s2).size();
    
    引用返回左值

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

    char &get_val(string &str, string::size_type ix) {
        return str[ix];
    }
    
    int main(void) {
        string s("helloworld");
        cout << s << endl;
        get_val(s, 0) = 'A';
        cout << s[0] << endl;
        return 0;
    }
    
    列表初始化返回值

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

    vector<string> process() {
        string expected;
        string actual;
        if (expected.empty()) {
            return {};
        } else if (expected == actual) {
            return {"functionX","okay"};
        } else {
            return { "functionX",expected,actual };
        }
    }
    
    主函数main的返回值

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

    递归

    如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数。

    int factorial(int val) {
        if (val > 1){
            return factorial(val - 1)*val;
        }
        return 1;
    }
    

    ==main函数不能调用它自己==

    3.3 返回数组指针

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

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

    其中arrT是含有10个整数的数组的别名。因为我们无法返回数组,所以将返回类型定义成数组的指针。

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

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

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

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

    Type (*function(parameter_list)) [dimension]
    

    类似于其他的数组声明,Type表示元素的类型,dimension表示数组的大小。(*function(parameter_list))两端的括号必须存在,如果没有这对括号,函数返回的是指针的数组。举个例子:下面这个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类型
    使用尾置返回类型

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

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

    如果我们知道函数返回的指针将指向哪个数组,就可以使用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; //返回一个数组指针
    }
    

    arrPtr使用关键字decltype表示它的返回类型是个指针,并且该指针所指的对象与odd的类型一致。


    #4. 函数重载

    如果同一个作用域内的几个函数名字相同但形参列表不同,我们称之为重载函数

    调用重载的函数

    定义了一组重载函数后,我们需要以合理的实参调用它们。函数匹配是指一个过程, 在这个过程中我们把函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定

    当调用重载函数时有三种可能的结果:

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

    4.1 重载与作用域

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

    string read();
    void print(const string&);
    void print(double);
    
    void main() {
        bool read = false; //隐藏外层的read
        string s = read(); //错误:read是一个布尔值,而非函数
        void print(int); //新的作用域:隐藏之前的print
        print("value"); //错误:print(const string&)被隐藏掉了
        print(1); //正确:调用print(int);print(double)被隐藏掉了
    }
    

    #5. 特殊用途语言特性

    默认实参、内联函数和constexpr函数。

    5.1 默认实参

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

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

    一旦一个形参被赋予了默认值,它后面所有的形参都必须有默认值。

    使用默认实参调用函数

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

    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,'#');
    

    函数调用时实参按其位置解析,默认实参负责填补函数调用缺少的尾部实参。

    默认实参声明

    对于函数的声明来说,通常的习惯是将其放在头文件中,并且一个函数只声明一次,但是多次声明同一个函数也是合法的。不过有一点需要注意,在给定的作用域中一个形参只能被赋予一次默认实参。

    //表示高度和宽度的形参没有默认值
    string screen(sz,sz,char = ' ');
    

    我们不能修改一个已经存在的默认值:

    string screen(sz,sz,char = '*'); //错误:重复声明
    

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

    默认实参初始值

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

    //wd、def和ht的声明必须出现在函数之外
    sz wd = 80;
    char def = ' ';
    sz ht();
    string screen(sz = ht(), sz = wd,char = def);
    

    5.2 内联函数和constexpr函数

    调用函数一般比求等价表达式的值要慢些,一次函数调用包含着一系列的工作:调用前先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

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

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

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

    一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个75行的函数也不大可能在调用点内联的展开。

    constexpr函数

    constexpr函数是指能用于常量表达式的函数。定义constexpr函数的方法与其他函数类似,不过要遵循几项规定:函数的返回类型及所有形参的类型都必须是字面值类型,而且函数中必须有且仅有一条return语句:

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

    5.3 调试帮助

    C++程序员有时会用到一种类似于头文件保护的技术,以便有选择地执行调试代码。

    assert预处理宏

    assert是一种预处理宏。所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:

    assert(expr);
    

    对表达式进行求值,如果表达式为假,assert输出信息并终止程序运行。如果表达式为真,assert什么都不做。

    NDEBUG预处理变量

    assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。


    #6. 函数匹配

    确定候选函数和可行函数

    函数匹配的第一步是选定本次调用对应的重载函数集,集合中的函数称为候选函数。候选函数具有两个特征:一是与被调用的函数同名,二是其声明在调用点可见。

    第二步考察本次调用提供的实参,然后从候选函数中选出能被这组实参调用的函数,这些函数称为可行函数。可行函数也有两个特征:一是其形参数量和本次调用的实参数量相等,二是每个实参的类型与对应的形参类型相同,或者能转换成形参的类型。

    寻找最佳匹配(如果有的话)

    函数匹配的第三步是从可行函数中选择与本次调用最匹配的函数。在这一过程中,逐一检查函数提供的实参,寻找形参类型与实参类型最匹配的那个可行函数。

    6.1 实参类型转换

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

    1. 精确匹配,包括以下情况:
      • 实参类型和形参类型相同。
      • 实参从数组类型或函数类型转换成对应的指针。
      • 向实参添加顶层const或者从实参中删除顶层const。
    2. 通过const转换实现的匹配。
    3. 通过类型提升实现匹配。
    4. 通过算术类型转换或指针转换。
    5. 通过类类型转换实现匹配。

    #7. 函数指针

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

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

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

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

    ==*pf两端的括号必不可少,去掉则表示pf是一个返回值为bool指针的函数。==

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

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

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

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

    bool b1 = pf("hello","goodbye");
    bool b2 = (*pf)("hello","goodbye");
    
    重载函数指针

    当我们使用重载函数时,上下文必须清晰地界定到底选用哪个函数。如果定义了指向重载函数的指针:

    void ff(int *);
    void ff(unsigned int);
    void (*pf1)(unsigned int) = ff; //pf1指向ff(unsigned)
    void (*pf2)(int) = 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 &));
    
    返回指向函数的指针

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

    using F = int(int *, int); //F是函数类型
    using PF = int(*)(int *,int); //PF是指针类型
    

    使用类型别名将F定义成函数类型,将PF定义成指向函数类型的指针。

    PF f1(int);//PF是指向函数的指针
    F f1(int);//错误:函数不允许返回函数
    F *f2(int);//正确:显示的指定返回类型是指向函数的指针
    

    我们也能用下面形式直接声明f1:

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

    按照由内向外的顺序阅读这条声明语句:f1有形参列表,所以f1是个函数;f1前有*,所以f1返回一个指针:指针类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int。

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

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

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

    当decltype作用于函数时,它返回函数的类型而非指针。

    相关文章

      网友评论

          本文标题:第6章:函数

          本文链接:https://www.haomeiwen.com/subject/mprsaftx.html