Effective C 条款33避免遮掩继承而来的名字在 C 的继承体系中你是否遇到过明明基类有这个方法为什么编译器说找不到的困惑这很可能是名字遮掩name hiding在作祟。本条款将揭开这个隐秘陷阱的面纱。一、问题引入消失的基类成员先看一个令人困惑的例子#includeiostream#includestringclassBase{public:voidmf1(){std::coutBase::mf1()\n;}voidmf1(intx){std::coutBase::mf1(int): x\n;}voidmf2(){std::coutBase::mf2()\n;}voidmf3(){std::coutBase::mf3()\n;}voidmf3(doublex){std::coutBase::mf3(double): x\n;}};classDerived:publicBase{public:voidmf1(){// 注意这里只声明了 mf1()没有参数版本std::coutDerived::mf1()\n;}voidmf3(){// 同样只声明了 mf3()std::coutDerived::mf3()\n;}};intmain(){Derived d;d.mf1();// OK: 调用 Derived::mf1()// d.mf1(10); // 编译错误Base::mf1(int) 被遮掩了d.mf2();// OK: 调用 Base::mf2()d.mf3();// OK: 调用 Derived::mf3()// d.mf3(3.14); // 编译错误Base::mf3(double) 也被遮掩了}奇怪的现象Derived明明 public 继承了Base但Base::mf1(int)和Base::mf3(double)却无法通过Derived对象访问二、名字遮掩的原理2.1 C 的名字查找规则要理解这个问题我们需要了解 C 的**名字查找name lookup**机制C 的名字查找规则只隐藏名字hiding names与类型无关。当编译器遇到一个名字时它会按照以下顺序查找局部作用域当前函数体内包含作用域当前类的成员下一个包含作用域基类的成员命名空间作用域关键规则一旦在某个作用域中找到了匹配的名字查找就会停止不会再继续搜索外层作用域中的同名标识符。intx10;// 全局变量voidsomeFunc(){doublex3.14;// 局部变量名字与全局变量相同std::coutx;// 输出 3.14全局的 int x 被遮掩了// 编译器在局部作用域找到了 x就不再查找全局的 x}2.2 继承中的名字遮掩在继承体系中这个规则同样适用classBase{public:voidmf1();// 版本1voidmf1(int);// 版本2重载voidmf1(double);// 版本3重载};classDerived:publicBase{public:voidmf1();// 派生类中声明了同名函数// 结果Base 中所有名为 mf1 的函数都被遮掩了// 包括 mf1()、mf1(int)、mf1(double)};现象说明名字相同即遮掩不需要参数列表相同甚至不需要是函数全部重载版本被遮掩派生类中一个同名函数会遮掩基类中所有同名函数与 virtual 无关即使是 non-virtual 函数也会发生遮掩与访问权限无关private 成员也会参与名字查找并导致遮掩2.3 变量和类型也会被遮掩classBase{public:intx10;enumColor{Red,Green,Blue};typedefintInteger;voidfunc(int){}};classDerived:publicBase{public:doublex3.14;// 遮掩了 Base::xenumColor{Cyan};// 遮掩了 Base::ColortypedefdoubleInteger;// 遮掩了 Base::Integervoidfunc(double){}// 遮掩了 Base::func(int)};intmain(){Derived d;std::coutd.x;// 输出 3.14Base::x 被遮掩// d.func(10); // 错误Base::func(int) 被遮掩}三、为什么 C 要这样设计你可能会问为什么 C 不设计成只遮掩参数完全相同的函数保留重载版本原因防止意外的行为变化。// 假设你开发了一个库classLibraryBase{public:voidprocess(intx){/* ... */}};// 用户继承你的类classUserDerived:publicLibraryBase{public:voidprocess(conststd::strings){/* ... */}// 用户只关心处理字符串};// 后来库升级LibraryBase 新增了一个重载classLibraryBase{public:voidprocess(intx){/* ... */}voidprocess(doublex){/* 新增 */}// 如果 C 不遮掩// 这会突然在 UserDerived 中可用};如果 C 不采用名字级别的遮掩那么库的作者新增一个重载函数可能会意外地改变派生类的行为。这种设计确保了派生类对继承来的名字有完全的控制权。四、解决方案让被遮掩的名字重见天日4.1 方案一using 声明式推荐用于 public 继承using声明式可以将基类中的名字引入到派生类的作用域中classBase{public:voidmf1(){std::coutBase::mf1()\n;}voidmf1(intx){std::coutBase::mf1(int): x\n;}voidmf1(doublex){std::coutBase::mf1(double): x\n;}voidmf2(){std::coutBase::mf2()\n;}};classDerived:publicBase{public:// 使用 using 声明将 Base 中所有名为 mf1 的成员引入 Derived 作用域usingBase::mf1;usingBase::mf2;voidmf1(){// 现在这是重载不是遮掩std::coutDerived::mf1()\n;}voidmf3(){std::coutDerived::mf3()\n;}};intmain(){Derived d;d.mf1();// OK: 调用 Derived::mf1()d.mf1(10);// OK: 调用 Base::mf1(int)d.mf1(3.14);// OK: 调用 Base::mf1(double)d.mf2();// OK: 调用 Base::mf2()return0;}using 声明的作用特性说明引入所有重载using Base::mf1引入 Base 中所有名为 mf1 的函数保持访问权限在 public 区域 using引入的就是 public 成员可配合重载派生类可以声明自己的重载版本与引入的版本共存适用于变量/类型也可以用于引入基类的成员变量、嵌套类型等4.2 方案二转交函数Forwarding Functions如果你只想暴露基类的部分重载版本或者需要在 private 继承中暴露特定接口可以使用转交函数classBase{public:voidmf1(){std::coutBase::mf1()\n;}voidmf1(intx){std::coutBase::mf1(int): x\n;}voidmf1(doublex){std::coutBase::mf1(double): x\n;}};classDerived:publicBase{public:voidmf1(){// 派生类自己的版本std::coutDerived::mf1()\n;}// 转交函数只暴露 Base::mf1(int)不暴露 mf1(double)voidmf1(intx){std::cout[Derived forwarding] ;Base::mf1(x);// 显式调用基类版本}// 注意Base::mf1(double) 仍然被遮掩无法通过 Derived 访问};intmain(){Derived d;d.mf1();// OK: Derived::mf1()d.mf1(10);// OK: 通过转交函数调用 Base::mf1(int)// d.mf1(3.14); // 错误Base::mf1(double) 仍然被遮掩}转交函数的优势场景使用转交函数选择性暴露只暴露基类的部分重载版本添加前置/后置逻辑在调用基类前后添加日志、校验等private 继承将基类接口包装后暴露为 public改变参数对参数进行转换后再转发给基类4.3 两种方案的对比特性using 声明转交函数代码量少一行多每个函数都要写灵活性引入所有重载可精确控制引入哪些可扩展性基类新增重载自动可用需要手动添加转交函数可添加逻辑否是适用场景public 继承需要全部重载private 继承或需要包装五、实际应用场景场景1GUI 框架中的事件处理classWidget{public:// 多种事件处理重载virtualvoidonEvent(constMouseEvente){std::coutWidget 处理鼠标事件\n;}virtualvoidonEvent(constKeyEvente){std::coutWidget 处理键盘事件\n;}virtualvoidonEvent(constResizeEvente){std::coutWidget 处理尺寸变化事件\n;}};classButton:publicWidget{public:// 如果不使用 using下面这个声明会遮掩 Widget 中所有 onEvent 重载voidonEvent(constMouseEvente)override{std::coutButton 处理鼠标点击\n;// 用户可能还想处理键盘事件比如空格键触发按钮// 但 Widget::onEvent(KeyEvent) 已经被遮掩了}};// 正确的做法classButton:publicWidget{public:usingWidget::onEvent;// 引入基类的所有事件处理voidonEvent(constMouseEvente)override{std::coutButton 处理鼠标点击\n;Widget::onEvent(e);// 可选调用基类默认处理}};// 使用intmain(){Button btn;btn.onEvent(MouseEvent{});// Button::onEvent(MouseEvent)btn.onEvent(KeyEvent{});// Widget::onEvent(KeyEvent) - 仍然可用btn.onEvent(ResizeEvent{});// Widget::onEvent(ResizeEvent) - 仍然可用}场景2自定义容器的接口暴露#includevector#includealgorithm// 自定义一个有序数组继承自 std::vector仅为示例实际不推荐 public 继承 STL 容器templatetypenameTclassSortedVector:privatestd::vectorT{public:// 使用转交函数暴露需要的接口usingtypenamestd::vectorT::size_type;usingtypenamestd::vectorT::iterator;usingtypenamestd::vectorT::const_iterator;// 转交函数暴露 size()size_typesize()const{returnstd::vectorT::size();}// 转交函数暴露迭代器iteratorbegin(){returnstd::vectorT::begin();}iteratorend(){returnstd::vectorT::end();}const_iteratorbegin()const{returnstd::vectorT::begin();}const_iteratorend()const{returnstd::vectorT::end();}// 自定义插入时保持有序voidinsert(constTvalue){autoitstd::lower_bound(std::vectorT::begin(),std::vectorT::end(),value);std::vectorT::insert(it,value);}// 注意我们不暴露 push_back因为它会破坏有序性// 也不暴露 operator[]因为直接修改元素也会破坏有序性// 但暴露 at() 用于只读访问constTat(size_type index)const{returnstd::vectorT::at(index);}};场景3游戏开发中的技能系统classSkill{public:virtual~Skill()default;// 多种使用方式virtualvoidcast(){std::cout释放技能\n;}virtualvoidcast(Entitytarget){std::cout对目标释放技能\n;}virtualvoidcast(constVec3position){std::cout对位置释放技能\n;}virtualvoidcast(Entitytarget,constVec3position){std::cout对目标在指定位置释放技能\n;}};classFireball:publicSkill{public:usingSkill::cast;// 引入所有 cast 重载voidcast()override{std::cout释放火球术\n;createFireEffect();}voidcast(Entitytarget)override{std::cout向 target.getName() 发射火球\n;applyDamage(target,50);createFireEffect();}// cast(Vec3) 和 cast(Entity, Vec3) 继承自 Skill仍然可用private:voidcreateFireEffect(){std::cout创建火焰特效\n;}voidapplyDamage(Entitytarget,intdamage){target.takeDamage(damage);}};六、重载、重写、隐藏的区别这是 C 继承中最容易混淆的三个概念我们来彻底理清classBase{public:voidfunc(int){}// 1. 基类版本virtualvoidvfunc(int){}// 2. 虚函数};classDerived:publicBase{public:voidfunc(int){}// 3. 隐藏hide基类的 func(int)voidfunc(double){}// 4. 重载overloadDerived::func(int)// 同时也隐藏了 Base::func(int)voidvfunc(int)override{}// 5. 重写overrideBase::vfunc(int)};概念英文发生条件作用域是否要求 virtual重载Overload同名函数参数不同同一作用域否重写Override函数签名完全相同基类与派生类是基类必须有 virtual隐藏Hide同名即可参数可不同基类与派生类否// 详细对比示例classBase{public:voidfunc(int){std::coutBase::func(int)\n;}virtualvoidvfunc(int){std::coutBase::vfunc(int)\n;}};classDerived:publicBase{public:voidfunc(int){std::coutDerived::func(int)\n;}// 隐藏voidfunc(double){std::coutDerived::func(double)\n;}// 重载 隐藏voidvfunc(int)override{std::coutDerived::vfunc(int)\n;}// 重写};intmain(){Derived d;Basebd;d.func(10);// Derived::func(int) - 静态绑定d.func(3.14);// Derived::func(double) - 重载解析b.func(10);// Base::func(int) - 不是虚函数静态绑定b.vfunc(10);// Derived::vfunc(int) - 虚函数动态绑定// d.func(10) 调用的是 Derived::func(int)Base::func(int) 被隐藏了// 如果想调用基类版本d.Base::func(10);// 显式调用基类版本}七、private 继承中的特殊考量在 private 继承中基类的 public 成员在派生类中变成 private。如果你希望暴露某些接口转交函数特别有用classTimer{public:voidstart(){/* ... */}voidstop(){/* ... */}doubleelapsed()const{/* ... */}};classAnimation:privateTimer{public:// 使用转交函数选择性暴露 Timer 的接口voidstart(){Timer::start();}doubleelapsed()const{returnTimer::elapsed();}// 注意不暴露 stop()Animation 自己控制停止时机voidupdate(){if(elapsed()duration_){// 动画结束Timer::stop();}}private:doubleduration_;};八、总结要点说明名字遮掩规则派生类中的名字会遮掩基类中所有同名名字与参数列表无关原因C 的名字查找在找到第一个匹配后就停止影响基类的重载函数版本可能被意外隐藏using 声明将基类中某个名字的所有重载引入派生类作用域转交函数精确控制暴露哪些基类接口可添加额外逻辑请记住derived classes 内的名称会遮掩 base classes 内的名称。在 public 继承下从来没有人希望如此。为了让被遮掩的名称再见天日可使用 using 声明式或转交函数forwarding functions。using 声明适用于需要暴露基类所有重载版本的场景。转交函数适用于需要选择性暴露或添加额外处理的场景。名字遮掩是 C 中一个隐蔽但重要的陷阱。在 public 继承中我们期望派生类扩展基类的行为而不是限制它。养成使用using声明的习惯可以让你的继承体系更加健壮和透明。参考《Effective C》第三版Scott Meyers 著相关条款条款32确定 public 继承塑模出 is-a 关系、条款34区分接口继承和实现继承、条款39明智而审慎地使用 private 继承