C++ Primer(第五版)第五章-函数读书笔记。

比较有意思的:

  • 数组传参时退化为指针

  • 数组类型;返回数组指针的多种方式;

  • 函数指针;返回函数指针的多种方式;

  1. 函数调用过程

    函数使用调用运算符,即$()$来调用。

    调用是,第一步便是使用实参(arguments)去隐式初始化函数的形参(params)。形参中是每个参数,必须有类型,但可以没有变量名!(模板时可能只需要类型名)但不管有没有变量名,参数都要传递过去。

         void f(int , int * , int &) ; // 声明, 只需类型,不用加变量名
    

    return要执行两步,第一是用返回的值初始化接收值(如果有),其次跳回主调函数。当然,从汇编角度就更好理解了吧?

    是先弹栈,再压返回值?

  2. 局部自动变量与局部静态变量

    局部变量,包括形参和函数内部定义的变量。 所以除了全局变量,就是局部变量了吧。

    局部静态变量,就是使用static关键字修饰的 。与之相对的,称为自动变量。

    静态变量虽然是局部的,但是却只会初始化一次,且与整个程序生命周期相同。库函数中的strtok函数,就使用了静态变量来记住上次分割的位置。

    书上的例子,记录调用次数:

     size_t count_calls()
     {
         static size_t ctf  = 0 ;
         return ++ctf ;
     }
    

    自动变量就是常见的普通变量了,“随控制路径”创建变量,超出作用域后就销毁。

    二者初始化也有区别——局部自动变量,如果不显式初始化,就会执行默认初始化。对内置类型的局部变量,就是随机初始化(或者说不被初始化,仅申请空间)。静态变量则会执行值初始化,即初始化为0 ;

    全局(自动)变量,也会执行值初始化,即初始化为0 .

  3. 函数声明

    声明定义到头文件,定义(实现)在源文件。定义时需要引入头文件。头文件有必要加编译保护头。

    可声明多次,但只能定义一次。

  4. 分离式编译

     cc -c including.cc # 生成including.o / including.obj
     cc -c main.cc # 生成 main.o / main.obj
     cc main.o including.o -o main # 生成main / main.exe
    
  5. 参数传递

    记住,实参到形参传递的过程 等价于 用实参初始化形参

     void f(int param_a , const string & param_b) ;
     int argument_a = 2 ;
     f(argument_a , "ni hao a") ;
    
     <==>
    
     int param_a = argument_a ;
     const string & param_b = "ni hao a" ;
    

    最佳实践: 使用引用避免拷贝。

     void f(const string & str , const vector<int> & vec) ;
    

    注意: 形参中的顶层const将被忽略。

    顶层const是指: 1. cont int a ; 2. int * const p = &a ;

     故
     void f(const int a) ;
     与
     void f(int a)
     在编译器看来是一样的!所以,这样“定义”将报错!
    

    在形参中定义顶层const,保证了在函数中不能修改该形参!这与实参能否被修改是没有关系的!

  6. 传引用、常量引用

    传递引用参数的好处:

    1. 避免拷贝

    2. 有时只能传递引用——IO类不支持拷贝(值传递)

    非常量引用的限制:不能将const对象、字面值、需要类型转换的对象作为引用传递

    经过测试,临时变量在GCC下也不能作为引用参数传递。 报的错误为:no known conversion for argument 1 from ‘[class_name]’ to ‘‘[class_name]&’ . 这个错误信息似乎在暗示引用参数传递存在隐式的转换,即类型到类型的引用的转换。或者一种猜测,临时变量被认为是const的,因为当我们形参设为const引用时,该语句就合乎语法了。

    常量引用:

    如果函数不会改变参数的值,推荐传递常量引用。

     bool is_in_sentence(const string &str , char target_char) ;
    

    原因:

    1. 避免给调用者错觉。

    2. 定义为常量引用能够接受的参数更广。

      如果定义为普通引用,则 const对象 , 字面值 , 需要类型转换的对象 都不能够作为参数传递。

  7. 数组形参

    数组两个性质:

    1. 不允许拷贝(赋值)

      即 int a[2] = {1,2} ; int b[] = a ; // ERROR !

    2. 常常退化为指针

    上述两个性质决定了:如果以传值方式定义数组形参,那么 数组就会退化为 指向数组头元素的指针

    所以,在以值传递形式定义数组形参时,等价于定义一个指针类型。

     void f(int a[]) ;
     void f(int *a) ;
     void f(int a[10]) ; 
    

    上述都是等价的。注意,尽管第三个尽管定义了数组维度,但是一点用都没有… 因为会被转换为指针!

    书上接着讲了在函数中如何获取数组的长度(知道数组结尾呢)?方法有3:

    1. 使用标识量

      即字符数组,使用’\0’作为字符数组的结尾。

    2. 传递头指针和尾指针

      可以使用begin和end函数。

       void print_array(int * array_beg , int * array_end) ;
      
    3. 传递数组长度

      这个在以前的C++中、以及C中最直观、常用

      言外之意,估计现在用begin、end方法更多吧。因为这样就保证了和迭代器的一致性接口了。的确更美观了。

    引用方式传递的数组

    例子:

     void f(int (&a)[10])
    

    一定要注意!

    1. 对数组的引用的写法: int (&a)[10] , 如果写作int &a[10] 是非法的!因为此表示a是10个引用元素构成的数组——而我们知道,引用时不能构成数组的。我们定义的是对数组的引用!

    2. 数组的维度不可缺少。

      如果缺少,将报错:

       “错误:形参‘a’包含了指向具有未知边界数组‘int []’的引用”
      

      这是因为传递引用时,数组实参初始化形参时保证了数组未退化!

      所以要遵循数组引用的规则:

       int a[4] = {1,2,3,4} ;
       int (&b)[4] = a ;
      

      即要保证维度确定且一致! 这个需要注意!!

    书上说,以后学习了模板,可以解决这个问题!就是可以传递不定长的数组。我想本质上,就是为相应的调用生成一个确切长度的函数吧。这也是模板干的事情嘛…所以本质是没有变的。

    关于数组的扩展

    感觉对数组一直没有搞懂。

    做了以下测试:

     int a[4] = {1,2,3,4}
     int b[] = a ;
     int b[4] = a ;
    

    第一个赋值语句报错:

    错误:数组必须为一个由花括号包围的初始值设定所初始化

    第二个赋值语句报两个错误:

    错误:初始值设定无法决定‘b’的大小

    错误:数组必须为一个由花括号包围的初始值设定所初始化

    由此得到结论,如编译器报错所言,数组是不能直接用数组赋值的!

    我们平时常用的是这样的:

     int *b = a ;
    

    所以常常造成了数组可以赋值的错觉。

    其实,上面真正的意义是——将数组a退化为指向数组第一个元素的指针,然后赋值给指针b。所以其本质是指针的赋值

    数组的退化

    上面也提到了,如果我们使用值传递的方式传递数组参数,那么数组将会退化为指针。

    经过测试,我认为数组只能退化一次——即多维数组,只能是第一维发生退化。所以,在向函数传递多维数组时,只有第一维可以忽略维度大小,后面的都不能忽略——第一维退化为指针了,维度信息自然就损失了,所以编译器不能也不会检测;不过后面的维度都保持了数组的类型不退化,而数组是需要明确指定维度大小的,所以编译器要求必须指定维度。

    数组与指针

    上面说了数组的退化,这里就是要明确: 数组是数组,指针是指针。数组可以退化为指针,但是数组不是指针。

    二维数组作为参数传递

    考虑如下:

     void f(int **a) ;
    

    输入变量是指针的指针,它与普通二维数组没有关系!——可以认为,只有在C风格字符串数组时它才是有意义的。

     void f(char **a) ;
    
     char *a[] = {"str1" , "str2"} ;
     f(a) ; 
    

    因为一维数组退化为指针,变为指向数组首元素的指针,而首元素又是字符串数组,也是一个指针,所以满足char **的定义。而且,由于是硬编码字符串,且连续,所以内存中可以认为是连续的,可以在函数中使用 a[0] , a[1]来访问。如果我们我们将其他整型指针放到一个数组中,传递给函数时退化为一个int**的变量, 但是不能使用a[0][0]来访问——这可能照成未知的结果!因为基于a的指针偏移,可能指向的不是其真正的内存空间!

    上面对应的等价定义就是:

     void f(int *a[]) ;
    
     void f(int **a) ;
    

    正确的二维数组传递方式,有两种:

     void f(int (*a)[10]) ; // (1)
    
     void f(int a[][10]) ; // (2)
    

    比较上面的定义与前面int **的差别,(1)中多了括号,表示其与变量是一个整体——a是指向一个int[10]类型的指针,即维度为10的整型数组的指针。(2)中指定了第二维的维度,因为第一维退化,其实其就(1)就是等价的。想想,还是(2)更加直观和好理解啊。(1)需要做数组向指针退化的逆向过程,即a值指向int[10]的指针,其实对应实参,其可能是int[10]的数组——数组的数组,就是二维数组咯。

    还有一种,就是对于二维数组,直接传递一个头元素指针即可。

     void f(int *a) ;
     int a[2][2] = { {1,2} , {3,4} } ;
     f(a) ;
    

    上述是可行的。我们要知道,多维数组在内存中,是一行一行按序依次存储的。所以,我们可以根据行列去访问它, 具体就是 a + row * col_size + col 。

    总结以上,有关数组的知识:

    1. 数组是复合类型,元素的类型、元素的个数(维度)共同决定数组的类型。

      int[4] , int[5] 是两种类型!虽然他们都是数组

    2. 数组值传递会退化为指向头元素的指针。多维数组只有第一维会退化。

    3. 数组与指针不是一回事!

      虽然数组可以退化为指针,指针可以用下标访问符[]去访问元素。但,这也些都是编译器提供的隐式操作。

      数组退化可以认为是语言的局限,或者是兼容C语言的负面影响;指针用下标访问,可以认为是在数组退化的条件下,提供给程序员的便利。

      它们都是在具体上下文下才有联系。脱离上下文去谈数组与指针的联系,就是耍流氓。

    4. 多维数组值传递,有两种定义方式: T [][d1][d2] / T (*)[d1][d2]

  8. 不定参数 / 可变参数

    C系的可变参数,如printf系列的实现,使用了...

    在C++中不推荐使用该方法,因为该方法: 1. 仅支持C与C++交集的数据类型(即C++仅是继承了C的语法,不愿意扩展到C++上);2. 无类型检查

    C++系,如果可变参数的类型相同,则使用initializer_list类型;如果类型不同,则使用后面介绍的可变参数模板

    关于initializer_list :

    1. 标准库中的类,无需额外的include。

    2. 与vector使用类似

       initializer_list<int> il{1,2,3,4} ;
       initializer_list<int> il_empty ;
       initializer_list<int> il_copy = il ;
       initializer_list<int> il_cc(il) ;
       for(auto ite = il.begin() ; ite != il.end() ; ++il ){} ;
       cout << il.size() << endl ;
       // no more
      
    3. 内容只读

       il[2] = 4 ; // Error
       (il.begin()) = 4 ; // Error
              
       void processing_multi_var(const initializer_list<int> &il)
       {
           for(auto & i : il)
           {
               i = 10 ; // Error
               cout << i << endl ; 
           }
       }
      

      上述函数报错:错误:向只读形参‘i’赋值

    用途: 书上举了例子,用于传递不定量的错误信息字符串。

  9. 函数返回值

    1. 如何返回

      存在一定的疑问。

      一种可能:首先有return处的值初始化一个函数定义域内的满足函数返回类型的临时变量。该临时变量再被压栈方式返回给调用点处。

      另一种: return出的值直接返回到调用处,由调用处做类型转换等。

      第一种似乎更有可能。

      主要是书上有这么一个例子:

       const string & ret()
       {
           return "Empty" ;
       }
      

      书上说这个是错误的!因为返回了局部变量的引用。可知,函数必须先在函数定义域内构造一个string类型的临时变量,并用字面值”Empty”去初始化它,然后再返回该临时string的引用。所以第一种似乎更有可能。

    2. 注意事项

      不能返回临时变量的指针;不能返回临时变量的引用。

      因为函数运行完毕后对应的内存空间销毁,指针无效,引用无效。

    3. C++0x支持返回列表

       vector<int> return_list()
       {
           return {1,3,4} ;
       }
      

      如上,返回一个列表(按照1的理解,其实用列表初始化了vector临时变量再返回该vector,故本质仍然是返回一个单变量对象)。

      如果返回类型是内建基本类型,列表中只能有一个元素。

       int return_int()
       {
           return {1} ;
       }
      

      将其认为是列表初始化好了。

    4. 关于main函数

      1. main函数是不支持递归调用的!

      2. main函数即使声明为返回整型,也可以不写return语句。

        编译器会隐式插入一条: return 0 ;

      返回宏定义的状态码(在cstdlib中定义):

       return EXIT_FAILURE ;
      
       return EXIT_SUCCESS ;
      
  10. 返回数组的指针或者引用

    因为数组不支持拷贝,所以不能返回数组本身;

    可以返回一个退化的指针。

    但是如果想要就是返回一个数组的指针或者引用,有4中方法可以做到。

    首先说明,如下的函数定义时错误的:

    int (*)[5] return_array_pnt()
    {
        int a[5] = {} ;
        return &a ;
    }
    

    为了构造返回数组指针的情况,返回了局部数组的指针。编译器会给出警告: 警告:返回了局部变量的‘a’的地址 [-Wreturn-local-addr] 这里仅作为演示。

    会报两个莫名的错误:

    expected unqualified-id before ‘)’ token
    expected initializer before ‘return_array_pnt’
    

    总的还是格式不对吧(规约出错了吧)。

    接下来介绍这4中正确的方法:

    1. 使用别名 /*** * Method 1 typedef int intArr5[5] ; ****/ using intArr5 = int[5] ; intArr5 *return_array_pnt(){}

      两种定义别名的方法,typedef是C的传统,using应该是C++ 11新标准。

      我觉得using语义更加清晰,不容易记错。

    2. 使用尾置返回类型

      标准的C++ 11 新标准,非常直观:

       auto return_array_pnt() -> int (*)[5]
      

      使用auto-> 关键字。注意后面的*的括号,前面关于数组的指针和退化的指针有提及。

    3. 使用如下奇妙的格式

       int ( *return_array_pnt() )[5]
      

      之前从未见过。不是C++ 11 的新标准,不知道c支不支持。

      经测试,使用gcc 4.8.3 20140911 (Red Hat 4.8.3-9) (GCC) 成功编译C语言版本。

      这个是 int (* name)[d] 的拓广,也很好理解。不过就是很少看到用在函数上。看到很兴奋,不知道还有多少没有见过的东西~

    4. 使用decltype 关键字

      非常trick的方法。

       int b[5] ;
       decltype(b)  *return_array_pnt() 
      

      因为decltype返回了数组的类型,所以这个本质与方法1——别名没有什么区别。

      应该在模板编程时很有用。

    以上都是返回数组的指针。返回数组的引用类似,如

    auto get_array_ref() -> int (&)[5] ;
    int ( &get_array_ref() )[5] ;
    

    一定要注意括号!特别是尾置返回类型,括号是必须的,否则就是引用的数组,通不过编译。

    此外,获取数组的引用,一般都是返回非局部变量的,如传入参数的引用(传入参数也是引用),应用场景应该还是比较小的。

  11. 函数重载

    重载,只发生在相同作用域,有相同的函数名、不同的参数的情况下。

    1. 如果作用域不同,那么最近的作用域内的名字将会覆盖上级作用域内的所有同名函数

       void print(string info) ;
       void print(initializer_list<string> &info) ;
      
       int main()
       {
           void print(int infoID) ;
      
           print(2) ;
       }
      

      如上,mian函数中(局部作用域)的print函数就覆盖了main函数上层(全局作用域)的两个print函数;

      注意,C++中也是支持在局部作用域中定义函数的!

    2. 参数不同,包括参数类型不同或者参数个数不同

      这里要注意的就是const的事情。

      前面说过,顶层const对应编译器来说与没有const是相同的。

      所以

       void copy_str(const string ori_str) ;
       void copy_str(string ori_str) ;
      

      是等价的,不构成重载;

      但是,不影响底层const:

       void copy_str(const string & ori_str) ;
       void copy_str(string & ori_str) ;
      

      注意到引用的区别——换成指针也是类似的。cost的指针是顶层的,指针指向的是const,就是底层的const,能够作为重载的区分。

    3. 函数的返回值不会影响编译器对重载函数的判断

  12. 内联函数和constexpr函数

    内联函数

    格式:使用inline关键字,仅当声明与定义同时存在时才有效;一般放入.h头文件中。

    编译过程:编译器在编译时在调用内联函数的地方展开函数,而不是生成调用函数的代码。

    适用范围:规模较小、流程直接、频繁调用的函数。

    优点:消除了定义函数时函数调用开销;消除了不定义函数时代码的繁琐。

    注意:inline只是向编译器发出内联请求,编译器可以忽略该请求!如,编译器一般不支持递归函数的内联;会放弃规模较大的函数的内联请求。

    内联函数与宏定义的异同

    1. 都是展开代码!二进制文件中无源代码中的调用逻辑。

      • 宏在预处理阶段展开,内联函数在编译过程中展开。

      • 宏本质就是字符串替换——是基于字符串的,处于源代码层次,不涉及编译过程、语言特性 ; 内联函数本质仍然是函数,需要满足语法规则,是语言特性。

      • 比较弱的一点,宏肯定会展开替换,但内联不一定。

    constexpr函数

    constexpr int new_sz(){ return 42 ;}
    

    如上,一般constexpr函数就是这样的(唯一)类似格式。

    明确constexpr函数的本质:在编译过程展开函数,且(必须)得到一个常量表达式。

    首先,constexpr可以认为是内联函数的应用——编译器隐含的将constexpr定义为内联的,这样编译器才能够在编译过程中展开它!因此它也需要定义和声明写在一起,最好放到头文件中。

    其次,内联函数中,实际执行的代码只能是return语句!不能有变量定义、操作等语句,最多含有别名声明等!因为我们要明白,函数展开仅能返回一个常量表达式,不能包含多余的语句。

    以下语句:

    constexpr size_t scale(size_t cnt){ return 2*cnt ;}
    
    int arrValid[ scale(2) ] ; // right 
    int i = 2 ;
    int arrInvalid[ scale(i) ] ; // wrong
    

    第一个调用正确,因为展开后就是 2*2的常量,第二个展开是2*i,i不是常量,表达式不是常量,所以报错。(当然,一般说来g++不会报错!但这只是编译器扩展,不是被标准所支持的!!)

    注意到上面定义函数时不会检查返回值是否是常量!所以,其实对于单纯constexpr函数的编译检查,仅仅包括:

    1. 函数返回类型、参数类型都是字面值类型(即int、double、const char * , char 等 非类类型)

    2. 只有return语句(如前所述,也支持别名、using等不含实际操作的语句)

    实测constexpr

    1.
    constexpr size_t get_sz()
    {
        int i = 55 ;
        return 32 ;
    }
    报错:
    body of constexpr function ‘constexpr size_t get_sz()’ not a return-statement
        
    2.
    constexpr size_t get_sz(size_t i)
    {
        return 32 * i ;
    }
        
    int main()
    {
        int i = 4 ;
        int a[get_sz(i)] = {} ;
    }
    g++下也报错了:
    
    可变大小的对象‘a’不能被初始化
    
    3. 
    对2做调用修改,去掉初始化:
    int main()
    {
        int i = 4 ;
        int a[get_sz(i)] ;
    }
    成功编译!
    后来测试了下
    
    int i = 4 ;
    int a[i] = {} ;
    在g++下也会报错!
    
    看来之前一直存在误解啊!——
    g++也是不支持非常量数组的初始化的,但是支持其声明。所以说,这样程序在后续操作时就很有可能内存错误了!
    我认为这是g++的BUG或者不稳定的地方,还不如向MSVC一样,直接禁止非常量数组的声明。
    
  13. 调试帮助(Assert , 调试预定义变量)

    Assert

    #include <cassert>
    
    assert(expr) ;
    

    这是一个宏!!

    定义在assert.h中,关键如下:

    linux gcc 版本:
    
    # define assert(expr)                 \
    ((expr)                               \
    ? __ASSERT_VOID_CAST (0)              \
    : __assert_fail (__STRING(expr), __FILE__, __LINE__, __ASSERT_FUNCTION))
    
    (__assert_fail 为 extern 的一个函数,可能定义在了链接库中,没有细找)
        
    
    msvc版本:
    
    #ifdef  NDEBUG
    #define assert(exp)     ((void)0)
    #else
    #ifdef  __cplusplus
    extern "C" {
    #endif
    _CRTIMP void __cdecl _assert(void *, void *, unsigned);
    #ifdef  __cplusplus
    }
    #endif
    
    #define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) )
    #endif  /* NDEBUG */
    
    void _assert(int test, char const *test_image,
    char const *file, int line)
    {
           if (!test)
           {
                  printf("Assertion failed: %s, file %s, line %d\n",
                  test_image, file, line);
                  abort();
           }
    }
    

    上述两个版本,写法不同,但都是宏定义~ (写得挺有意思的,gcc看起来更加C-sytle , msvc简直奇技淫巧,不过很酷炫,js中貌似常这么写)

    注意NDEBUG这个控制变量,如果我们编译时,使用

    cc -D NDEBUG test.cpp 
    

    那么生成的代码就不含检查的语句!这就是宏定义的优势!

    所以,assert被认为是调试时的手段

    cc -D NDEBUG test.cpp ,使用 -D选项,等价于在代码中写#define NDEBUG

    调试时常用的常量

    常量名 意义
    __func__ 函数名称
    __FILE__ 文件名(编译时的路径)
    __LINE__ 行号
    __TIME__ 编译时时间
    __DATE__ 编译时日期

    上面的常量,都是编译器为每个函数定义的局部变量,类型是const char *

    assert报错不能使用try…catch捕获

    try{
        assert(cin) ; 
    }
    catch(exception e)
    {
        cout << e.what() ;
    }
    

    上述代码是无意义的!!

    从上面assert的实现可知,其不是throw出来的异常,而是直接调用abort函数暴力退出的。try并没有什么用。

    我认为这是C++ 完全兼容 C语言造成分裂的一个例子。(Python中,assert测试错误抛出AssertionError,可以用try捕获)

    但是,我觉得只要消除这种理解歧义,就不会有什么不妥。assert初衷本就不是用在release version的!其更多是用于调试,在发布时使用-D NDEBUG取消该断言行为。而try则是为了在运行过程中处理异常,以便增强函数的鲁棒性!二者的目的、初衷不是相同的,混用其实是乱用…

    如果需要在运行时判断变量应该满足的属性,应该使用 if + try .

  14. 函数匹配

    包含两个过程:

    1. 获得候选函数集

      根据函数名、作用域

    2. 获取可行集

      根据参数匹配程度

    重载函数情况下,包含一个选择顺序:

    1. 精确匹配

      参数类型一致、参数个数相同或者在有省略参数的情况下保持个数合法。

      数组退化被认为是精确匹配的。

    2. 有自动类型转换的匹配

      计数转换次数,转换次数越少的优先匹配;

      存在相同的,则报二义性错误退出。

  15. 函数指针

    函数类型,是由它的返回类型和参数列表确定的。

    比如:
    int (int , int) ;
    int sum(int , int) ; // sum 就是类型为 int (int , int)的函数类型。
    

    函数指针指向的是函数类型。

    比如:
    int (*)(int , int) ;
    int (*sum_p)(int , int) ; // sum_p就是类型为 int(*)(int , int)函数指针类型。
    

    函数指针的定义非常模糊,既可以用取地址符也可以不用;既可以用解引用符,也可以不用。

    int sum(int a , int b){
        return a + b ;
    }
    
    int main(int argc ,  char *argv[] )
    {
        int (*plus_p)(int , int) = &sum ;
        int (*plus_e)(int , int) = sum ; // 右端可以有取地址或无取地址,但左边必须是指针形式!
            
        cout << (*plus_p)(1,1) << endl ; // 解引用优先级低于函数调用,括号是必须的!
        cout << plus_p(1,1) << endl ;
            
        cout << (*plus_e)(1,1) << endl ;
        cout << plus_e(1,1) << endl ;
        return 0 ;
    }
    

    返回函数指针

    与返回数组指针完全一致。

    首先,不能返回函数实体,就像不能返回数组一样。

    using func = int (int , int) ;
    func get_func()
    {
        return sum ;
    }
    

    上述代码报错:错误:‘get_func’声明为返回一个函数的函数

    接着说返回函数指针的几种方式(与返回数组指针类似):

    // 使用 using
    using funcP = int (*) (int , int) ;
    funcP get_sump()
    {
        return &sum ;
    }
    
    //使用 typdef
    typedef int (*funcP_typedef) (int , int) ;
    funcP_typedef get_sump_typedef()
    {
        return &sum ; 
    }
        
    //使用 尾置返回类型
    auto get_sump_rear() -> int (*) (int , int)
    {
        return &sum ;
    }
        
    //直接定义
    // 不加括号就错了,编译器认为返回的是
    // int* (int , int)的函数实体,报错:错误:‘get_sum_directly’声明为返回一个函数的函数
    int (*get_sum_directly())(int , int)
    {
        return &sum ;
    }
    

    关于函数指针的博客嗯,让我们彻底搞懂C/C++函数指针吧