[Cpp基础] [04] 名字查找与函数重载

Last Updated on 2020年7月22日

名字

  • 在C++中,有各种各样的名字,变量名和函数名(可调用对象名)是最重要的两个
  • 命名空间:形如namespace XX{}的作用域.
    • 我们的代码默认都位于全局空间内,该空间直接通过::访问
    • 可以定义匿名的namespace,其效果和inline命名空间类似,匿名的namespace是文件私有的,在文件内部共享,在文件之间独立.在C++中,该特性设计用于替代C风格的全局static型变量.
  • 作用域:用{}分割的各种区域都是作用域,常见的如class,namespace,函数体,代码块等.作用域是可以嵌套的.
  • 嵌套的命名空间需要注意名字隐藏的问题,例如,对于namespace A{#include <vector>}
    • 相当于在::A内定义了一个std命名空间,::A::std会将外部的::std名字隐藏
    • 当在A内使用std::vector<>时,实际使用的是::A::std::vector<>,由于::A::std内不存在vector的定义,所以会导致链接错误.
    • 命名空间内不使用#include是一种很好的风格.

名字查找

  • 当我们使用一个名字时,编译器就会向上查找名字的声明语句.变量名的查找和函数名的查找有一定区别.
    • 变量名查找:最终仅会确定唯一的对象.
    • 函数名查找:最终会确定候选函数集合,主要是为后续的函数重载服务.
  • 函数名和变量名都是名字,彼此可以相互hide.
  • 函数体内的名字搜索是顺着域说明符向外的.例如,int A::B::f (){return i;},查找i时, 先从f()的局部开始,再到B->A->::的顺序

变量名字查找流程

  • 从当前作用域开始,逐层向上查找名字.
  • 找到名字后就完成查找,否则失败.
  • 注:名字查找成功后,编译器材开始进行类型检查,即判断找到的名字是否符合使用处的规则.

函数名字查找流程

  • 函数名字fun查找的实际是确定搜索域,搜索域内的同名可调用对象都被加入候选函数集.
  • 搜索域是一个常规搜索域和若干实参类型搜索域的并集.
    • 常规搜索域确定:从当前作用域开始,逐层向上查找名字声明,名字首次出现的作用域就是常规搜索域
    • 对于函数调用的每个实参arg_i,其类型decltype(arg_i)及其所有基类所在的作用域都加入实参类型搜索域
  • 搜索域内所有(无论先后)名为fun的可调用对象都将纳入重载候选列表.重载候选列表确定后,才开始重载匹配.

重载匹配规则

假设已经确定了候选函数集,那么,在候选函数集中选择函数时,依照以下规律进行重载匹配.

  • 函数重载的基础是传入参数的类型,和返回类型无关.对于非引用的形参,const不作为重载的依据.
  • 首先根据实参类型排除不能产生调用的函数,得到一个可行函数列表.
  • 可行函数中选择最优匹配,具体的判定流程.
      1. 类型转换越少越好,最少的那个就是最优匹配.
      1. 如果几个函数一样好:
      • 有多个普通函数 -> 匹配失败
      • 一个普通函数+模板 -> 普通的函数优先于模板
      • 多个匹配 -> 特化的模板优先于通用的模板. (模板特化的比较规则不太常用,在使用时应当慎重)
      1. 匹配失败.
  • 注意:
    • 仅函数/数组向指针的转换是精确匹配,只要类型不同,都被视作类型转换,例如:派生类指针到基类指针,非const对象绑定到到const &,,都是类型转换
    • 类型转换彼此没有优先级,从int 到uint并不比从int 到double要优秀.
  • 注意: 很多系统中,整数最多提升到unsigned int,这意味着以long为形参的函数很可能不会被重载系统匹配到.

using XX::name.

  • 常规作用:将某个名字引入当前作用域.
    • 引入的名字位于当前作用域,可以隐藏外层作用域的名字,且不能与当前作用域已有的名字冲突.
    • 对于函数,引入的名字将参与当前作用域的重载.
  • 在类定义中,将基类名字引入派生类时有不同的特性
    • using仅可用于引入基类中的名字
    • 这种场合下引入的函数名可以部分被派生类重写/override,而非重定义冲突.

using namespace XX

  • using namespace X:对当前作用域而言,相当于将名字空间X内的名字全部引入某个外层作用域.
    • using namepsace X所在语句逐层向外,当某个作用域包含X时,将名字插入这个作用域.
    • using namespace引入的名字在外层作用域,所以不会和当前作用域的名字产生冲突,我们甚至可以在后续代码中隐藏引入的名子.(using引入的名字也不会和插入位置的名字重定义,但是需要在使用时手动消除歧义.)
    • using namespace X引入的函数名若被查找到,是在逻辑上的插入位置查找到的,而非当前作用域.因此,当前作用域定义的函数不会和using namespace引入的函数形成重载.
  • using namespace的有效区间仍仅限于所在的作用域.
  • 在类定义内不能使用using namespace
namespace Top{

namespace A{
int i=0;
}//namespace A

double i=1;

namespace B{
void fun(){
    using namespace A;//将A内的名字引入了Top作用域,此时不会产生重定义错误.
    ++i;//错误,因为使用了i而触发二义性错误
    ++Top::A::i;//正确,使用Top::A::i;
    ++Top::i;//正确,使用Top::i
    void i();//正确,隐藏之前所有的i;
}
float i=2;//定义B内的i.
}// namespace B

}// namespace Top