C++函数(二):C++函数返回类型和函数匹配

199 阅读9分钟

函数返回类型

  • 返回一个值的方式和初始化一个变量或形参的方式完全一样:返回的值用于初始化调用点的一个临时量,该临时量就是函数调用的结果
  • 必须注意当函数返回局部变量时的初始化规则:当函数完成后,它所占用的存储空间被释放,意味着局部变量的引用将指向一块无效的内存区域

     所以,不要返回局部对象的引用或指针

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

    调用返回一个引用的函数,返回的是左值,其余的返回值类型得到右值

        作为左值,可以给调用结果赋值

        但如果返回的是常量引用,就不能给调用结果赋值

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

返回数组指针

  • 函数不能直接返回数组,但是可以返回数组的指针或者引用

  • 想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。然而,函数的形参列表也跟在函数名字后面且形参列表应该先于数组的维度。因此,返回数组指针的函数形式如下所示:

     type (*func(params)) [size]
    

    例如:

     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类型。
  • 也可以使用尾置返回类型简化上述func声明,任何函数定义都可以使用尾置返回,但是对返回复杂类型最有用

    • 使用->符号,为了表示函数真正的返回类型跟在形参列表之后,在本应该出现返回类型的地方放置一个auto

       auto func(int i) -> int(*)[10];
      
  • 如果知道函数返回的指针指向哪个数组,可以使用decltype声明返回类型

    • decltype不负责把数组类型转换成指针,所以必须函数声明中加上一个*符号

       int odd[] = {1,3,5,7,9};
       int even[] = {0,2,4,6,8};
       ​
       //返回一个指针,指向含有5个整数的数组
       decltype(odd) *arrPtr(int i) {
        return (i % 2 == 0) ? &odd : &even;
       }
      
  • 最后一种简化,使用typedef或者using定义别名

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

函数重载

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

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

重载和const形参

  • 顶层const形参和没有顶层const形参的函数不能算重载,会报重复声明错误

  • 形参如果是引用或者指针,则区分常量对象还是非常量对象可以实现函数重载,此时const是底层的

    • const形参可以接收const实参,也可以接收非const实参,所以尽量定义const形参函数
    • 当同时存在const和非const形参函数时,在调用时编译器会优先选择非常量版本的函数

const_cast和重载

const_cast在重载函数场景最有用

如都是const引用的形参,重载一个非const引用的形参函数

 //形参和返回类型都是const string&
 ​
 const string &shorterString(const string &s1, const string &s2)
 {
   return s1.size() <= s2.size() ? s1 : s2;
 }
 ​
 //利用上述方法,重载一个使用非const形参,且返回非const得返回值函数,就可以使用const_cast
 string &shortString(string &s1, string &s2)
 {
   //先强制转换成const类型
   auto &r = shorterString(const_cast<const string &>(s1), 
                           const_cast<const string &>(s2));
   //在强制转换回普通string&,显然是安全的
   return const_cast<string &>(r);
 }
 ​

函数匹配

大多数情况,很容易确定调用的是哪一个重载函数,然而有时就不那么容易了

例如:

 void f();
 void f(int);
 void f(int,int);
 void f(double,double = 3.14);
 ​
 f(5.6);
  • 第一步:确定候选函数

    候选函数具备两个特征:

    1. 与被调用函数同名
    2. 函数声明在调用点可见

    所以上述4个f都是候选函数

  • 第二步:确定可行函数

    从候选函数中确定可行函数,需要具有两个特征:

    1. 形参数量与本次调用提供的实参数量相同
    2. 每个实参的类型与对应的形参类型相同或能转换成形参类型

    如果没有找到可行函数,编译器会报错

    所以排除空参,两个int类型形参的函数

  • 第三步:寻找最佳匹配

    从可行函数中寻找最佳匹配,这一过程会逐一检查调用提供的实参,寻找与形参类型最匹配的可行函数。

    基本思想是实参类型与形参类型越接近,匹配就越好

    所以,此时最佳匹配就是两个double形参的函数,因为5.6可以是double类型,不需要转换,且第二个形参有默认值,传一个实参也可以调用

二义性调用

即当有多个函数可以最佳匹配时,会发生二义性调用错误

当有多个参数时,但是多个函数中每个形参和实参都可以匹配上,此时就会发生二义性调用

函数匹配和const实参

如前面所述,当使用引用类型或者指针类型时,const和非const形参是可以重载的,即底层const

此时函数调用就通过const来区分了:

  1. 实参是const,那么就会调用const形参的函数;
  2. const实参,就会调用非const形参的函数。因为两个函数都是可行的,但是非const实参初始化const实参是需要类型转换的,那么精准匹配上非const更加匹配

函数指针

函数指针指向的是函数;函数的类型由返回类型和形参类型共同决定

形式:

type (*func) (params);

*func两端的括号必不可少

例如: bool (*pf) (const string &, const string &)

使用函数指针

把函数名作为一个值使用时,该函数自动地转换成指针

 bool lengthCompare(const string &s1,const string &s2);
 pf = lengthCompare;
 pf = &lengthCompare; //和上述等价,&是可选的

调用:

 //使用使用指向函数的指针调用该函数,无需解引用
 pf("hello", "goodbye");

重载函数指针

当使用重载函数时,必须明确界定到底使用那个函数

 void ff(int*);
 void ff(unsigned int);
 ​
 void (*pf1) (unsigned int) = ff; //pf1指向ff(unisgned)
 ​

函数指针形参

  • 和数组类似,不能定义函数类型形参,但是形参可以指向函数的指针

     //第三个参数会自动转换成函数指针
     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,lengthCompare);
    
  • 使用别名简化函数类型声明

     //定义函数类型
     typedef bool Func (const string&,const string&);
     //定义函数指针类型
     typedef bool (*FuncP) (const string&,const string&);
     //和上述定义等价
     typedef decltype(lengthCompare) *FuncP2;
    

注意使用decltype是不会自动转化成指针的,必须手动加上*

函数指针返回值

  • 和数组类似,函数不能作为返回值,但是可以返回函数指针

  • 当使用别名定义了函数指针时,将别名用作函数的返回类型,此时必须显式地将返回类型指定为指针,是不会自动转换成指针的

     using F = int(int,int); //F是函数类型
     using PF = int() (int*,int); //PF是指针类型
     ​
     PFf1(int); //正确
     F f1(int); //错误,F是函数类型,不能返回一个函数
     F * f1(int); //正确
    

    完整f1的声明:

     int (f1(int)) (int,int);
     ​
     //尾置返回类型:
     ​
     auto f1(int) -> int () (int,int);
     ​
    

其他特性

默认实参

  • 某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,把这个反复出现的值称为函数的默认实参

  • 函数调用时,可以包含实参,也可以省略该实参

  • 注意:一旦某个形参被赋予了默认值,它后面所有形参都必须有默认值

  • 形式:

     string screen(int hz = 24,int wid = 80);
     ​
     //为每一个形参都提供了默认实参
     ​
     //调用函数
     screen();  //都使用默认实参
     screen(66); //hz = 66,wid使用默认实参
     screen(66,256); //hz = 66, wid = 256
     ​
    
  • 通常,应该在函数声明中指定默认实参,并将该声明放在合适的头文件中

内联函数

  • 使用inline关键字,将函数声明成内联函数
  • 内联函数,也就是将函数内联展开,直接将内联函数体添加到调用函数中,不会产生函数调用开销
  • 内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求

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

constexpr函数

  • constexpr函数是指能用于常量表达式的函数

  • 遵循的约定:

    1. 函数的返回类型及所有形参类型都是字面值类型
    2. 函数体中必须有且只有一条return语句
 constexpr int new_sz() { return 42; }
 constexpr int foo = new_sz();
  • constexpr函数不一定返回常量表达式

    允许constexpr函数的返回值并非一个常量

     constexpr size_t scale(size_t cnt) { return new_sz() * cnt; }
     ​
     //调用scale时,如果实参是常量表达式,那么scale返回的也就是常量表达式
     ​
     scale(2); //是常量表达式
     int i = 2;
     scale(i); //不是常量表达式
     ​
    

通常,会将内联函数和constexpr函数放在头文件内

调试帮助的函数

  • assert(expr); 进行assert断言

    如果定义了NDEBUG,就会关掉assert宏的功能

  • func输出当前调试函数的名字

  • FILE存放文件名的字符串字面值

  • LINE存放当前行号的整型字面值

  • TIME存放文件编译时间的字符串字面值

  • DATE存放文件编译日期的字符串字面值