第七章:函数(二)

函数重载与重载解析

函数重载是指使用相同的函数名定义多个函数,每个函数具有不同的参数列表

int fun(int x){
    return x + 1;
}

double fun(double x){
    return x + 1;
}

注意,不能基于不同的返回类型进行重载

编译器在根据实参和函数名称选择正确的函数版本完成调用时需要进行很多工作,具体的请参考Calling Functions: A Tutorial,这里我们主要关注以下两个最重要的步骤:

  • 名称查找:根据函数名称找到候选的函数,关于名称查找我们需要注意以下部分

    1. 名称查找分为限定名称查找和非限定名称查找,这个的限定与非限定主要相对于命名空间来说
      	void fun(){
      
      	}
      	namespace MyNS{
      	    void fun(){
      	
      	    }
      	}
      	
      	int main(int argc, char * argv[]) {
      	
      	// 限定名称查找,只会在指定的名称空间查找 
      	   ::fun();
      	   MyNS::fun();
         // 非限定名称查找
      		fun();
      	
      	}
      
    2. 非限定名称查找会进行域的从调用语句所在的域到外层域的逐级查找,同时内层域的名称会将外层域的相同名称隐藏掉
    3. 针对2存在特例,最典型就是实参依赖查找,比如
      namespace MyNS{
          struct Str{};
      
          void fun1(Str x){
      
          }
          void fun2(){
      
          }
      }
      int main(int argc, char * argv[]) {
      
        MyNS::Str val;
        fun1(val);					// 合法的代码
        fun2();						// 不合法的代码,未定义错误
      
      }
      

      实参依赖查找只针对自定义类型有效

    4. 名称查找通常只会在已声明的名称集合中进行,比如
      void fun(int){
          std::cout << "int" << std::endl;
      }
      
      void go(){
          fun(1.0);				// 这里只能看到fun(int),所以只会调用fun(int)
      }
      
      void fun(double){
          std::cout << "double" << std::endl;
      }
      

      也存在例外,最典型的就是模版函数,主要原因是模版函数只有在调用时会进行实例化,而在调用时已经可以看到模版函数声明后面的名称,关于这个后续会进行叙述

  • 重载解析:在名称查找的基础上进一步选择合适的调用参数,详细的请参考重载决议,这里我们关注其中最重要的两个步骤:

    1. 过滤不能被调用的版本,主要包括

      • 参数个数不对
      • 无法将实参转换为形参
      • 实参不满足形参的限定条件
    2. 在剩余版本中查找形参与调用表达式实参最匹配的版本,通常来说匹配级别越低越好(存在很多特殊规则,具体参考重载决议

      • 级别1:完美匹配 或 平凡转换(比如加一个 const )
      • 级别2:类型提升(short->intfloat->double)或类型提升加平凡转换
      • 级别3 :标准转换(int->double) 或 标准转换加平凡转换
      • 级别4 :自定义转换、自定义转换加平凡转换或自定义转换加标准转换
      • 级别5 :形参为省略号的版本

      同一级别中的几种如果同时出现属于函数重定义错误

      函数包含多个形参时,所选函数的所有形参的匹配级别都要优于或等于其它函数,若不满足会出现警告或者错误

函数相关的其他内容

关于函数,我们还有一些内容需要叙述

递归函数

递归函数是指在函数体中调用自身,,通常用于描述复杂的迭代过程,比如二分查找

一些特殊函数

这里我们讨论一些特殊的函数,包括

  • 内联函数:使用inline关键字建议编译器将函数体的内容“复制”到调用函数的行

    注意这里的“复制”不是简单的将函数体内的代码复制到调用的行,还需要维护函数体内的代码具有单独的域,自动对象的自动销毁等特性,具体的参考inline

    注意inline仅仅是建议,而不是强制必须进行内联优化,很多情况下函数体无法进行内联(比如递归函数)或者内联对性能的提升极小但极大增加了可执行文件的大小

    inline函数的定义必须能被调用函数的翻译单元看到,否则会报无定义的错误

    inline函数需要满足翻译单元级的一次定义原则,但不同翻译单元内的相同的inline函数的函数体要完全相同,否则当编译器不进行函数内联优化时会出现重定义的错误

  • constexpr函数(C++11起):使用constexpr关键字说明函数既可以在编译期调用也可以在运行期调用,具体的可以参考constexpr

    constexpr int fun(){
        return 3;
    }
    
    int main(int argc, char * argv[]) {
    
        int x = fun();
    
    }
    

    constexpr给编译期提供了优化的空间,可以选择在编译期直接返回求值结果而不需要在运行期调用,对于无法在编译期求值的情况下,与普通函数行为一致

  • consteval函数(C++20起):使用consteval关键词说明函数只能在编译期被调用,具体的可以参考consteval

    consteval可以使得编译器有更多的优化空间,可以进行编译期求值后直接返回求值结果而不需要在运行期调用,相比constexpr限制更多,相关变量都必须是编译期常量,对于无法在编译期求值的情况下,直接报错

函数指针

函数本身也可以被视为一种对象,它也是拥有类型的,比如

int fun(int val){			// 类型为int(int)
}

同理,我们也可以声明函数指针

int add(int,int){
    return 0;
}

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

    int (*p)(int,int) = &add;
    p(1,2);

}

函数指针可以用来构造一些以函数作为参数的高阶函数

关于函数指针我们需要注意以下几点:

  • 将函数作为参数时会退化为函数指针
  • 将函数作为返回值时会退化为函数指针
  • 将函数作为纯右值时函数会退化为函数指针
  • 小心Most_vexing_parse
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值