C++基础篇--成员函数同名隐藏(overwrite)

部署运行你感兴趣的模型镜像
  C++中成员函数间的各种关系比较复杂,之前介绍过重载和虚函数覆盖,今天再梳理另一种容易让人迷惑的机制--函数隐藏。先看两个例子:

1.成员函数public继承

class Base{

public:

  void fun1(int a){  cout << "Base::fun1(int a)" << endl;  }

};

class Drv: public Base {

public:

  void fun2(int a) {  cout << "Drv::fun2(int a)" << endl;  }

};

void main()

{

  Drv dr;

  dr.fun1(1);   //继承自基类,输出Base::fun1(int a)

  dr.fun2(2);   //派生类自定义,输出Drv::fun2(int a)

}

    关于public/protected/private继承及接口/实现继承后文讨论,上例是为说明public继承后,派生类Drv可直接调用基类Base的成员函数,换句话基类普通成员函数(非virtual)在public派生类中默认可见。再看:

2成员函数同名:

class Base

{

public:

  void fun1(int a){ cout<<"Base fun1(int a)"<<endl; }   //

};

class Drv:public Base

{

public:

  void fun1(char* x){ cout<<"Drv fun1(char *x)"<endl; }   //

};

void main(){

  Drv dr;

  char x =0;

  dr.fun1(1);   //

  dr.fun1(&x); //

}

    据已有知识分析:Drv public继承Base,因而基类成员函数void fun1(int a)①在Drv类域中可见;Drv自身又定义了void fun1(char *x)②,即Drv类域内同时有fun1(int a)fun1(char *x)。有问题?木有吧,一个类域里包含同名不同参的函数,不就是C++函数重载(overload)么?

    而实际编译却报错,提示Drv中没有fun1(int)型函数,为什么?Base①处void fun1(int a)不是被Drv自然继承而可见么?这引出C++继承时的一个额外限制:派生类成员函数某些情况下会屏蔽与其同名的基类成员函数,称为函数隐藏(overwrite),规则如下:

  1)不论基类函数是否为virtual,只要有派生类函数与其同名但不同参,该基类函数在派生类中将被隐藏。(别与重载混淆)

  2)基类函数非virtual,有派生类函数与此基类函数同名同参,此时该基类函数在派生类中将被隐藏。(别与覆盖混淆)

    上例中由于Drv②处定义了与基类函数同名不同参的fun1(char *x),根据规则1)Base类中fun1(int a)Drv中被隐藏,Drv域中只有fun1(char *x)而没有fun1(int a),因此处调用dr.fun1(1)错。

    从命名空间和作用域的视角去理解:隐藏就是内部作用域的成员屏蔽同名的"外部作用域成员,如C局部变量屏蔽同名的全局变量。同样基类和派生类也有各自作用域且基类域(外层)包含派生类域(内层),因此如果派生类没有与基类同名成员(变量或函数),则基类成员在派生类里可见(继承);如果派生类里有同名成员,则基类成员在派生类不可见(隐藏)。

    可能感觉不对,既然同名隐藏,为什么上面的函数隐藏规则那么啰嗦呢,隐藏和覆盖/重载又有什么关联?这其实是同一个问题。

世界为什么是现在这个样子L

    事物初始几步的定义往往决定了后面千百步,初期为解决某问题引入的方法常导致后面产生更多问题需要解决。环环相扣,无数的妥协和平衡后,软件就变成现在这样。

    当初制定C++隐藏规则时,估计会有两个问题摆在面前(完全臆测:)):

    1)成员变量同名隐藏没什么可说的,但由于类的成员函数支持重载,隐藏规则必须在“隐藏同名同参的基类函数”还是“隐藏所有同名不同参的基类重载函数”间选择。似乎同名同参才隐藏更合情理,即只隐藏完全相同的基类函数,基类里其他同名不同参的函数可以继承下来,和派生类里同名新函数继续重载。最终C++却决定一杆子打翻一船,别说完全同名,同姓也不行。例2Drv里定义了fun1就代表它会隐藏基类里所有叫fun1的,不管fun1(int)还是fun1(double)全部一扫空,世界清静了,我的地盘里只有我能姓fun1

    这么做是有考量的,C++允许一个派生类从多个基类继承(多重继承),当程序规模较大,类继承层次较深时,来自不同基类的函数层层继承积累,派生类中同名重载函数可能很多,程序员又未必了解这些来自基类的重载函数。当他设计了与基类函数同名不同参的派生类函数,调用该函数时又不小心写错参数类型或顺序时,问题就大了:C++编译器会认为你本来就想调另一个基类继承的同名重载函数,于是热情的牵线搭桥,而程序员见编译正常还以为调用了正确函数,一运行结果不对,悲催了。特别是基于框架和类库编程时,这个trap更容易出现

    隐藏机制可以预防这点:派生类里定义某函数fun1,所有基类fun1的重载函数版本全被屏蔽,这时如果在派生类里调fun1时写错参数,编译器就会报错,而不会再乱拉郎配。所以《Effetive C++》里说:隐藏背后原因是为防止在程序库或应用框架内建立新的derived class时从疏远的base classes继承重载函数

    当然这种株连九族的做法反过来会导致例2中这种误伤,没关系,起码这种错误会被编译器主动抛出,有弥补机会。而且基类函数只是被隐藏,而不是不存在,上帝关一扇门,一定会为你留一扇窗,等会儿带你看窗

    2)决定隐藏所有基类重载函数后问题又来了,都被隐藏,多态怎么实现?最后办法就是给基函数增加virtual关键字,从隐藏机制里划出一部分例外逻辑,用于虚函数覆盖机制。

    这就是函数隐藏规则不整齐的原因,重新归纳:如果基类函数为virtual,实现同名同参的派生类函数就是覆盖;除此之外,一旦派生类与基类函数同名(无论是否同参),所有同名基类函数都被隐藏。

如何访问被隐藏的成员

    函数被隐藏不代表其不存在,只是藏起来而已,C++有两种方法可以重新启用被隐藏的函数:

    1)using关键字,自定义命名空间一节提到using可将一个作用域中的名字引入另一个作用域中;它的另一个用法是”using Base::fun”,这样派生类中如果定义同名但不同参的函数,基类函数将不会被隐藏,两个函数并存在派生类域中形成新的重载,如:

class Base

{

public:

  void fun1(int a){ cout<<"Base fun1(int a)"<endl; }

};

class Drv:publicBase

{

public:

  using Base::fun1;   //这句使基类中名为fun1的系列函数不再被株连(只可能被隐藏同名同参的一个)

  void fun1(char *x){cout<<"Drv fun1(char *x) !"<<endl; }

};

void main(){

  Drv dr;

  dr.fun(1);

}

    运行输出: Base fun1(int a)

    派生类中using Base::fun1显式包含基类中函数名为fun1的所有重载版本,这样派生类定义的fun1只能隐藏和它同名同参的基类函数,不能株连其他重载版本。

    2)用域限定符::来定位继承自Base却被派生类隐藏的成员,如上例将dr.fun1(1);改为dr.Base::fun1(1);即可,这种方法能调用所有被隐藏的基类成员变量和函数

    这两种方法中2)更通用,派生类中被同名同参函数隐藏的基类函数,以及被隐藏的基类成员变量,只能用::定位启用,另外被覆盖的基类virtual函数也可以用这种方式显式调用。1)只是保护重载系列,同名同参仍然会被隐藏。

总结:

     不同于重载和覆盖的正面功能,同名隐藏在编程中应尽量避免:1)由于缺少类似virtual这种明显的语法特征,很多人容易忽略C++“隐藏”机制的存在,当派生类与基类函数同名时就和重载、覆盖等机制混淆,既降低可读性,又易产生bug。2)从面向对象思想的角度,隐藏也应尽量避免。基类里使用普通函数就代表这一功能在类的传承链中应固定不变,所有派生类都直接继承使用,隐藏则表明后来对这种继承链条的否定,体现了一种不变性部分的突变,使得逻辑上不协调。换句话,既然要隐藏,当初基类函数中就应该使用virtual,以代表类功能中可变部分的功能。

 

您可能感兴趣的与本文相关的镜像

Langchain-Chatchat

Langchain-Chatchat

AI应用
Langchain

Langchain-Chatchat 是一个基于 ChatGLM 等大语言模型和 Langchain 应用框架实现的开源项目,旨在构建一个可以离线部署的本地知识库问答系统。它通过检索增强生成 (RAG) 的方法,让用户能够以自然语言与本地文件、数据库或搜索引擎进行交互,并支持多种大模型和向量数据库的集成,以及提供 WebUI 和 API 服务

<think>首先,用户的问题是基于之前的引用内容:"我想确认基类成员函数与其公有派生类成员函数之间是否可以形成重载且互不冲突"。用户引用了[^1]、[^2]、[^3]、[^4],我需要参考这些引用。 回顾引用内容: - 引用[^1]:讨论了C++继承的基本概念,如派生类、基类、继承方式、支配规则等。 - 引用[^2]:展示了C++继承的代码示例,包括派生类公有继承基类。它还提到了重载(C++重载)。 - 引用[^3]:讨论了构造函数和析构函数,包括重载(构造函数可以重载)。 - 引用[^4]:解析了类的私有成员访问权限。 用户的问题焦点是:基类成员函数与公有派生类成员函数是否能形成重载且互不冲突。 核心概念是函数重载和继承。 在C++中,函数重载是指在同一个作用域内定义多个函数,这些函数具有相同的名称但不同的参数列表(参数类型、数量或顺序不同)。 在继承的上下文中: - 基类和派生类是不同的作用域。 - 派生类继承基类的成员函数- 如果派生类定义了与基类同名函数(不管参数列表),它会覆盖(hide)基类的函数,而不是重载。 具体到用户的问题: - 用户问的是基类成员函数与公有派生类成员函数是否能形成重载。 - 在派生类中定义的函数,如果与基类函数同名,但不是虚函数,它会隐藏基类的同名函数,而不是重载。 - 重载通常发生在同一个作用域内;在不同作用域(如基类和派生类),同名函数隐藏,而不是重载。 从引用[^3]中:"可以重载,(重载?新概念,后面学到什么是重载。)",它提到构造函数可以重载,但这是在类的上下文内。 引用[^2]中提到了"C++重载",但不是在继承的上下文中。 关键点是:在派生类中定义同名函数是否会重载基类函数。 标准C++行为: - 如果派生类定义了一个与基类成员函数同名函数(即使参数不同),它会隐藏基类的所有同名函数(包括那些可能被重载的函数)。这是因为派生类的作用域嵌套在基类作用域中。 - 要让基类和派生类的函数共存而不冲突,用户需要使用作用域解析运算符或使用using声明来引入基类函数。 从引用[^1]中提到了支配规则:"类 X中的名字 N 支配类 Y 中同名的名字 N ,是指类 X以类 Y 为它的一个基类,这称为支配规则。" 这暗示了在派生类中定义的成员会隐藏基类的同名成员。 引用[^1]也说:"解决二义性的方法有使用作用域运算符和引进虚基类。" 这表明在继承中可能有二义性。 所以,回答用户的问题: 基类成员函数与公有派生类成员函数不能直接形成重载,因为它们在不同作用域。在派生类中定义同名函数隐藏基类的函数。用户需要使用using声明或作用域解析来访问基类函数。 互不冲突的意思:如果参数不同,它们可以被区分,但默认情况下,派生类函数隐藏基类函数,所以调用时可能会出错。 例如: ```cpp class Base { public: void func() {} void func(int) {} // 重载 }; class Derived : public Base { public: void func(double) {} // 这会隐藏 Base::func() 和 Base::func(int) }; ``` 在Derived中调用func()时,只会调用Derived的func,如果不指定作用域,会编译错误。 要使它们共存,在Derived中添加using声明: ```cpp class Derived : public Base { public: using Base::func; // 这将基类的func引入作用域 void func(double) {} }; ``` 然后,在Derived中,func可以被重载。 所以,本质上,它们不能自动形成重载;需要通过using声明来使它们在同一作用域下重载。 用户指定了"公有派生类",所以继承方式是公有,但这不影响函数隐藏机制;隐藏发生无论继承方式如何。 回答结构: 1. 清晰解释概念。 2. 使用代码示例说明。 3. 参考引用的内容。 4. 添加相关问题。 系统指令: - 行内数学表达式用$...$格式(但这里可能不需要数学表达式)。 - 独立公式用$$...$$(如果有公式)。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 引用的段落末尾添加引用标识,如[^1]。 在回答中,我可以引用[^1]、[^2]等。 最终回答: - 解释:基类成员函数与派生类成员函数不能直接形成重载,因为它们在派生类作用域中会隐藏基类函数。需要使用using声明来解决冲突。 - 提供代码示例。 - 结束语。 相关问题生成: </think>### 基类成员函数与公有派生类成员函数是否能形成重载且互不冲突? 在C++继承机制中,基类成员函数与其公有派生类成员函数之间**不能直接形成重载(overload)关系**,且默认情况下会出现隐藏(hiding)问题,可能导致冲突。下面我将逐步解释其原因、机制和解决方法,确保内容可靠且基于C++标准。 #### 1. **重载的本质与作用域规则** - 函数重载要求在**同一个作用域**内定义多个同名函数,但参数列表不同(参数类型、数量或顺序)。重载函数通过参数区分,编译器根据调用时的参数匹配正确版本。 -继承体系中,基类和派生类属于**不同作用域**: - 基类的成员函数位于基类作用域。 - 派生类的成员函数位于派生类作用域(嵌套在基类作用域内)。 - 当派生类定义了与基类同名成员函数(无论参数是否相同),它会**隐藏(hide)**基类中的所有同名函数,包括基类中可能存在的重载版本。这是因为派生类作用域优先于基类作用域,符合C++的支配规则[^1]。 #### 2. **隐藏问题与默认冲突** - 在公有派生类中定义同名函数时,基类函数会被隐藏,导致以下问题: - 直接在派生类对象上调用该函数时,编译器只查找派生类作用域,忽略基类版本。 - 即使派生类函数的参数与基类不同,也无法自动形成重载,因为作用域隔离。 - 示例代码演示隐藏现象: ```cpp #include <iostream> using namespace std; class Base { public: void func() { cout << "Base::func()" << endl; } void func(int x) { cout << "Base::func(int)" << endl; } // 基类重载函数 }; class Derived : public Base { // 公有派生类 public: void func(double d) { cout << "Derived::func(double)" << endl; } // 派生类同名函数 }; int main() { Derived d; d.func(3.14); // 正确:调用Derived::func(double) d.func(); // 错误:Base::func()隐藏 d.func(42); // 错误:Base::func(int)隐藏 return 0; } ``` - 上述代码中: - `d.func(3.14)` 调用成功(匹配派生类函数)。 - `d.func()` 和 `d.func(42)` 编译失败,因为基类的 `func` 函数隐藏(即使参数不同)。 #### 3. **如何实现“互不冲突”的重载** - 要使基类成员函数与派生类成员函数在同一派生类中形成重载关系(即互不冲突),需要使用 **`using` 声明**将基类函数引入派生类作用域: - `using Base::func;` 显式引入基类所有同名函数- 这样,派生类作用域会包含基类和派生类的同名函数,编译器根据参数列表区分重载版本。 - 修改后的示例(解决冲突): ```cpp class Derived : public Base { public: using Base::func; // 引入基类func的所有重载版本 void func(double d) { cout << "Derived::func(double)" << endl; } // 形成重载 }; int main() { Derived d; d.func(); // 正确:调用Base::func() d.func(42); // 正确:调用Base::func(int) d.func(3.14); // 正确:调用Derived::func(double) return 0; } ``` - 现在,所有函数调用均成功,基类和派生类函数在同一作用域形成重载关系,互不冲突。 - **关键点**: - 公有继承方式确保基类公有成员在派生类中保持公有访问权限(如引用[^1]所述)[^1],但作用域问题仍需手动解决。 - 如果基类函数是虚函数(`virtual`),派生类覆盖(override)时会动态绑定,但这与重载无关(覆盖要求函数签名相同)。 #### 4. **为什么不能自动重载?** - 设计哲学:C++强调作用域隔离,避免意外覆盖(如派生类无意中改变基类行为)。 - 支配规则:派生类作用域中的名称优先于基类名称,导致隐藏(如引用[^1]所述)[^1]。 - 默认冲突:派生类新增函数可能隐藏基类重载版本,需通过 `using` 或作用域运算符(如 `d.Base::func()`)访问(如引用[^1]解决二义性的方法)[^1]。 #### 总结 - **不能直接形成重载**:基类和派生类成员函数因作用域不同,无法自动形成重载关系。 - **互不冲突的实现**:通过 `using Base::func;` 声明,强制将基类函数引入派生类作用域,使其与派生类函数在同一作用域下形成重载。 - 可靠依据:此机制符合C++标准,并参考了继承中的支配规则和解决二义性的方法[^1]。 相关问题: 1. 在C++中,如何区分函数重载、覆盖(override)和隐藏(hiding)的不同? 2. 虚函数继承体系中如何影响成员函数的调用行为? 3. 使用 `using` 声明引入基类成员时,访问权限如何变化?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值