本文主要是介绍《Effective C++》《继承与面向对象设计——33、避免遮掩继承而来的名称》,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1、term53:Avoid hiding inherited names
- 前言:
- 1.1、同名全局变量在局部作用域中被隐藏
- 1.2、继承中的隐藏
- 1.3、进一步论证——继承中的函数的隐藏
- 1.4、如何将隐藏的行为进行覆盖
- 1.4.1 通过using声明增加对基类成员函数的使用
- 1.4.2 使用forwarding函数
- 2、面试相关
- 2.1什么是名称遮掩(Name Hiding)?
- 2.2 在派生类中定义一个与基类同名的成员函数会发生什么?
- 2.3 如何避免名称遮掩?
- 2.4 如果派生类中的函数遮掩了基类中的函数,但你又想在派生类中调用基类版本的函数,你应该怎么做?
- 2.5 什么是虚函数和重写(Override)?
- 2.6 如何在派生类中扩展而不是替换基类的功能?
- 2.7 为什么使用虚函数通常比非虚函数更安全?
- 2.8你如何在代码审查中识别名称遮掩的问题?
- 2.9 如果基类有一个保护成员,派生类不小心定义了一个同名的公有成员,会发生什么?
- 2.10在继承中,你更倾向于使用公有继承、保护继承还是私有继承?为什么?
- 3、总结
- 4、参考
1、term53:Avoid hiding inherited names
前言:
在C++中,继承是一个强大的特性,它允许一个类(派生类或子类)继承另一个类(基类或父类)的成员变量和成员函数。然而,在继承的过程中,有时会出现基类成员被派生类成员 “隐藏” 的情况。这主要发生在派生类定义了与基类同名的成员时。
隐藏的概念
当派生类定义了一个与基类同名的成员(无论是数据成员还是成员函数),基类的该成员在派生类中就会被隐藏。这意味着,当通过派生类的对象访问这个同名成员时,只能访问到派生类自己的成员,而无法直接访问到基类的同名成员。
隐藏的例子
下面是一个简单的例子来说明继承中的隐藏:
class Base {
public: void show() { std::cout << "Base::show()" << std::endl; }
}; class Derived : public Base {
public: void show() { // 与基类中的show函数同名 std::cout << "Derived::show()" << std::endl; } int value; // 与基类可能存在的同名数据成员
}; int main() { Derived d; d.show(); // 输出:Derived::show() // d.Base::show(); // 如果需要访问基类的show函数,需要这样显式指定 return 0;
}
在上面的例子中,Derived 类继承了 Base 类,并定义了一个与 Base 类中同名的 show 函数。当通过 Derived 类的对象 d 调用 show 函数时,调用的是 Derived 类中定义的 show 函数,而不是 Base 类中的。如果想要访问基类的 show 函数,需要显式地使用作用域解析运算符 :: 来指定。
注意点
- (1)隐藏不是重写(Override):重写是发生在派生类中的函数与基类中的虚函数具有相同的签名,并且派生类意图替换基类的虚函数实现。隐藏不涉及虚函数,只是简单的同名成员覆盖。
- (2)数据成员的隐藏:如果派生类定义了一个与基类同名的数据成员,那么基类的数据成员在派生类中也会被隐藏。通过派生类的对象,只能访问到派生类的该数据成员。
- (3)通过基类指针或引用访问:如果有一个指向派生类对象的基类指针或引用,那么通过这个指针或引用访问的将是基类的成员,而不是派生类中被隐藏的成员。
如何避免隐藏
为了避免意外的隐藏,应该仔细考虑在派生类中定义的成员是否与基类中的成员同名。如果确实需要覆盖基类的成员函数,应该确保基类中的函数是虚函数,并使用相同的签名在派生类中定义。对于数据成员,如果可能的话,最好避免使用与基类同名的数据成员,或者使用不同的名称来避免混淆。
1.1、同名全局变量在局部作用域中被隐藏
名称其实和继承无关,而是和作用域(scopes)有关。如下面这段代码:
#include <iostream>
using namespace std;int x = 10; //全局变量void someFunc()
{double x = 5.0; //局部变量cout << x; //输出局部变量
}int main(){someFunc();return 0;
}
当全局和局部存在相同的变量时,在局部作用域中,全局作用域的变量名会被隐藏,优先使用局部的变量。
C++的名称遮掩规则所做的唯一事情是:遮掩名称。至于名称是否是同一类型,并不重要。
1.2、继承中的隐藏
现在进入继承。我们知道当我们处在一个派生类成员函数内部时,并且指向了一些基类的东西(例如,一个成员函数,一个typedef或者一个数据成员),编译器能够找到我们所指向的东西,因为派生类继承了声明在基类中的这些东西。实际的工作方式是派生类的作用域被嵌套在基类作用域内部。举个例子:
class Base
{
private:int x;
public:virtual void mf1() = 0;virtual void mf2();void mf3();...
};class Derived :public Base
{
public:virtual void mf1(); //重写(覆盖)void mf4();...
};
void Derived::mf4()
{...mf2();...
}
当编译器看到这里使用了名字mf2,它们必须理解mf2指向的是什么。它们会在作用域中寻找名字为mf2的一个声明。
在Derived的fm4()函数中调用了fm2()函数,对于fm2()函数的查找顺序如下:
- ① 先在fm4()函数中查找,如果没有进行②;
- ② 然后在Derived类中查找,如果没有进行③;
- ③ 然后在基类Base中查找(查找到了就调用基类中的Base);
- ④ 假设在Base中还没有查找到,那么就在Base所在的namespace中查找;如果还有没继续在全局作用域查找;
1.3、进一步论证——继承中的函数的隐藏
考虑前面的例子,这次我们除了要重载mf1和mf3之外,还在Derived中添加一个mf3版本。(正如条款36中解释的,Derived中mf3的声明——一个继承而来的非virtual函数——会让这个设计看起来很可疑,但是为了理解继承下的名字可见性,我们忽略这个问题。)
class Base
{
private:int x;
public:virtual void mf1() = 0;virtual void mf1(int);virtual void mf2();void mf3();void mf3(double);...
};class Derived :public Base
{
public:virtual void mf1(); //基类中的所有mf1()都被隐藏void mf3(); //基类中的所有fm3()都被隐藏void mf4();...
};
现在使用下面代码进行调用:
Derived d;
int x;
...
d.mf1(); //正确
d.mf1(x); //错误,被隐藏了
d.mf2(); //正确
d.mf3(); //正确
d.mf3(x); //错误,被隐藏了
可以看到,对于有相同名字的基类和派生类中的函数,即使参数类型不同,上面的隐藏规则也同样适用,并且它和函数的虚与非虚没有关系。在这个条款开始也是同样的方式,函数someFunc中的double x隐藏了全局作用域的int x,在这里Derived中的函数mf3隐藏了基类中名字为mf3的函数,即使参数类型不一样。
1.4、如何将隐藏的行为进行覆盖
1.4.1 通过using声明增加对基类成员函数的使用
有时隐藏可能会违反基类与派生类之间的is-a关系。因此我们可以使用using声明表达式取消这种隐藏,在派生类中导入基类的函数行为。如下面例子:
#include <iostream>
using namespace std;
class Base {
private: int x;
public: // 纯虚函数,需要在派生类中提供实现 virtual void mf1() = 0; // 重载版本的mf1函数 virtual void mf1(int val) { std::cout << " Base::mf1(int): " << val << std::endl; } // 另一个虚函数 virtual void mf2() { std::cout << " Base::mf2()" << std::endl; } // 非虚函数 void mf3() { std::cout << " Base::mf3()" << std::endl; } // 重载版本的mf3函数 void mf3(double val) { std::cout << " Base::mf3(double): " << val << std::endl; } // ... 其他成员函数和数据 ...
}; class Derived : public Base {
public: using Base::mf1; // 使得Base的所有mf1版本在Derived中可见 using Base::mf3; // 使得Base的所有mf3版本在Derived中可见 // 重写纯虚函数mf1() virtual void mf1() override { std::cout << "Derived::mf1()" << std::endl; } // 隐藏了Base中的非虚函数mf3(),但是mf3(double)没有隐藏,因为使用了using声明 void mf3() { // 注意:这里使用override是因为mf3在Base中是虚函数 std::cout << "Derived::mf3()" << std::endl; } // 新增的成员函数 void mf4() { std::cout << "Derived::mf4()" << std::endl; } // ... 其他成员函数和数据 ...
}; int main() { Derived d; d.mf1(); // 调用Derived中重写的mf1 d.mf1(10); // 调用Base中重载的mf1(int) d.mf3(); // 调用Derived中重写的mf3 d.mf3(3.14); // 调用Base中重载的mf3(double) d.mf2(); // 调用Base中的mf2 d.mf4(); // 调用Derived中新增的mf4 return 0;
}
输出内容如下:
Derived::mf1()Base::mf1(int): 10
Derived::mf3()Base::mf3(double): 3.14Base::mf2()
Derived::mf4()
如果你的继承基类并加上重载函数,你想对其中的一些函数进行重新定义或者覆盖,你需要为每个即将被隐藏掉的名字包含一个using声明,如果你不这样做,你想继承的一些名字就会被隐藏。
1.4.2 使用forwarding函数
有时候你并不想继承基类的所有函数。在public继承下,这绝对不可能发生,因为它违反了基类和派生类之间public继承的”is-a”关系。(这也是为什么上面的using声明放在派生类的public部分:基类中的public名字在public继承的派生类中应该也是public的)。然而在private继承中(见条款39),它也是有意义的。举个例子,假设Derived私有继承自基类Base,Derived类想继承基类函数mf1的唯一版本是不带参数的版本。Using声明在这里就不工作了,因为一个using声明会使得所有继承而来的函数的名字在派生类中是可见的。这里可以使用不同的技术,也就是简单的forwarding函数:
#include <iostream>
using namespace std;
class Base {
private: int x;
public: // 纯虚函数,需要在派生类中提供实现 //virtual void mf1() = 0; virtual void mf1() { std::cout << " Base::mf1() " << std::endl; } // 重载版本的mf1函数 virtual void mf1(int val) { std::cout << " Base::mf1(int): " << val << std::endl; } // ... 其他成员函数和数据 ...
}; class Derived : private Base {
public: virtual void mf1() { Base::mf1();} // ... 其他成员函数和数据 ...
}; int main() { Derived d; d.mf1(); // true 调用Derived中重写的mf1 //d.mf1(10); // false Base::mf1(int) 被遮掩住了return 0;
}
输出内容:
Base::mf1()
inline转交函数的另一个用途是为那些不支持using声明式(注:这并非正确行为)的老旧编译器另开了一条新路,将继承而得的名称汇入派生类作用域内。当继承同模板结合起来的时候,一个完全不同的“继承而来的名字被隐藏”问题就会出现,详情见 条款43。
2、面试相关
以下是一些与避免遮掩继承而来的名称相关的C++面试问题:
2.1什么是名称遮掩(Name Hiding)?
解释名称遮掩的概念,以及它如何在继承中发生。
(1)名称遮掩(Name Hiding)在C++中是一个重要的概念,它涉及到在派生类中定义的成员与基类中的同名成员之间的交互。当一个派生类引入了一个新的成员(无论是变量、函数还是类型别名),而这个成员的名字与基类中的某个成员名字相同时,就会发生名称遮掩。
具体来说,当派生类中的成员与基类中的成员具有相同的名称时,派生类中的成员会“遮掩”或“隐藏”基类中的同名成员。这意味着,在派生类的上下文中,如果试图访问这个被遮掩的名称,将默认解析为派生类中的成员,而不是基类中的成员。
这种遮掩行为可能会导致一些非直观或意外的结果,特别是当程序员意图调用基类版本的成员时。由于派生类的成员遮掩了基类的同名成员,如果没有明确的指定,编译器将不会考虑基类的成员。
名称遮掩通常发生在以下情况:
(1)成员函数遮掩:当派生类定义了一个与基类成员函数同名的新函数时,基类中的该函数在派生类的作用域内将被遮掩。
class Base {
public: void func() { /* Base implementation */ }
}; class Derived : public Base {
public: void func() { /* Derived implementation */ } // 遮掩了Base::func()
};
在上面的例子中,Derived::func()遮掩了Base::func()。在Derived类的上下文中调用func()将执行Derived版本的函数。
(2)数据成员遮掩:当派生类定义了一个与基类数据成员同名的数据成员时,也会发生名称遮掩。
class Base {
public: int value;
}; class Derived : public Base {
public: double value; // 遮掩了Base::value
};
在Derived类中,value指的是double类型的成员,而不是基类中的int类型成员。
为了避免名称遮掩带来的问题,程序员应该谨慎选择成员名称,并尽量避免在派生类和基类中使用相同的名称。如果确实需要访问被遮掩的基类成员,可以使用作用域解析运算符(::)来明确指定要访问的是基类中的成员。
例如:
class Derived : public Base {
public: void useBaseFunc() { Base::func(); // 显式调用基类中的func() }
};
在useBaseFunc()函数中,通过Base::func()可以明确地调用基类中的func()函数,即使它在派生类中被遮掩了。
2.2 在派生类中定义一个与基类同名的成员函数会发生什么?
讨论这种情况下会发生什么,以及它如何影响程序的行为。
2.3 如何避免名称遮掩?
讨论避免名称遮掩的最佳实践,如使用作用域解析运算符(::)来明确指定要调用的基类函数。
2.4 如果派生类中的函数遮掩了基类中的函数,但你又想在派生类中调用基类版本的函数,你应该怎么做?
解释如何使用作用域解析运算符来调用基类中被遮掩的函数。
2.5 什么是虚函数和重写(Override)?
讨论虚函数的概念,以及如何在派生类中重写基类的虚函数。这有助于理解如何在不遮掩基类函数的情况下扩展类的功能。
2.6 如何在派生类中扩展而不是替换基类的功能?
讨论如何在派生类中调用基类的函数,并在其基础上添加新的功能,而不是完全替换它。
2.7 为什么使用虚函数通常比非虚函数更安全?
解释虚函数如何在运行时确定要调用的函数版本,从而避免名称遮掩可能导致的问题。
2.8你如何在代码审查中识别名称遮掩的问题?
讨论代码审查过程中如何识别和解决名称遮掩问题的方法。
在代码审查过程中,识别和解决名称遮掩问题是非常重要的,因为这有助于维护代码的可读性、可维护性和正确性。以下是一些建议的方法,用于在代码审查中识别和解决名称遮掩问题:
- 仔细阅读代码和文档
- 理解继承关系:首先,审查者需要理解代码中的继承关系,包括哪些类是从其他类继承而来的,以及它们是如何相互关联的。
- 查看类定义:审查基类和派生类的成员列表,特别是那些具有相同名称的成员。
- 阅读注释和文档:如果有相关的注释和文档,它们可能会提供关于成员名称选择的理由和意图,从而帮助识别可能的名称遮掩问题。
- 使用静态分析工具
- 集成开发环境(IDE):许多IDE都提供了静态分析工具,可以自动检测名称遮掩等潜在问题。
- 静态代码分析工具:使用专门的静态代码分析工具来扫描代码库,这些工具可以报告名称遮掩等常见的编程错误。
- 检查函数调用和成员访问
- 检查派生类成员:在派生类中查找对基类成员的引用。如果使用了与基类成员相同的名称,但没有使用作用域解析运算符(
::
),则可能发生名称遮掩。 - 检查作用域:注意代码中的作用域范围,特别是在嵌套类或函数内部。名称遮掩可能在这些情况下更加隐蔽。
- 讨论和沟通
- 与团队成员交流:与代码的作者或其他团队成员讨论名称选择的原因,了解他们是否意识到名称遮掩的问题。
- 提出改进建议:如果发现了名称遮掩问题,提出明确的改进建议,例如重命名派生类成员、使用作用域解析运算符或考虑其他设计选择。
- 制定编码规范
- 避免使用相同名称:在编码规范中明确规定,尽量避免在基类和派生类中使用相同的成员名称。
- 使用描述性名称:鼓励使用描述性强的成员名称,以减少名称冲突和遮掩的可能性。
- 审查重构代码
- 重构机会:如果在代码审查过程中发现大量的名称遮掩问题,这可能是一个重构代码的好机会。通过重构,可以消除名称遮掩问题,并改进代码的整体结构和可读性。
通过结合这些方法,代码审查者可以更有效地识别和解决名称遮掩问题,从而提高代码的质量和可维护性。
2.9 如果基类有一个保护成员,派生类不小心定义了一个同名的公有成员,会发生什么?
讨论这种情况下的行为,以及它如何影响类的封装性和访问控制。
2.10在继承中,你更倾向于使用公有继承、保护继承还是私有继承?为什么?
讨论不同继承类型的优缺点,以及它们如何影响名称遮掩和类的设计。
在继承中,我倾向于使用公有继承(public inheritance)、保护继承(protected inheritance)还是私有继承(private inheritance)取决于具体的设计需求和目的。每种继承方式都有其特定的用途和优缺点,下面我会逐一解释并说明我的倾向。
- 公有继承(Public Inheritance)
公有继承是最常用的一种继承方式。当派生类公有继承基类时,基类的公有成员在派生类中仍然是公有的,基类的保护成员在派生类中变为保护成员,而基类的私有成员在派生类中是不可访问的。
我倾向于在以下情况下使用公有继承:
- 当派生类需要作为基类的一个特定类型的对象使用时。
- 当派生类需要扩展基类的接口,并允许客户端代码通过派生类访问基类的公有成员时。
公有继承保持了基类接口的完整性,并允许派生类添加新的功能或特性。它适用于“is-a”关系,即派生类确实是基类的一种特殊类型。
- 保护继承(Protected Inheritance)
保护继承相对较少使用。当派生类保护继承基类时,基类的公有和保护成员在派生类中都变成保护的,而基类的私有成员仍然是不可访问的。
我倾向于在以下情况下使用保护继承:
- 当派生类需要访问基类的受保护成员和私有成员,但又不希望这些成员在派生类的外部可见时。
- 当实现一种更复杂的继承层次结构,并且需要在多个层次之间共享某些数据或功能时。
保护继承提供了一种在派生类中封装基类实现细节的方式,同时允许派生类扩展和定制这些实现。然而,它通常比公有继承更加复杂,并且需要谨慎使用以避免引入不必要的复杂性。
- 私有继承(Private Inheritance)
私有继承是最不常用的一种继承方式。当派生类私有继承基类时,基类的所有成员(包括公有、保护和私有成员)在派生类中都变成私有的。
我倾向于在以下情况下使用私有继承:
- 当派生类需要利用基类的实现细节,但这些细节不应该暴露给派生类的外部用户时。
- 当实现一种“has-a”关系,即派生类包含一个基类的对象作为其实现的一部分,但不需要继承基类的接口时。
私有继承允许派生类访问和利用基类的内部实现,同时隐藏了这些实现细节。它适用于那些更关注实现复用而不是接口扩展的场景。然而,由于它隐藏了基类的接口,因此在使用时需要格外小心,以避免违反封装原则。
综上所述,我的倾向取决于具体的设计需求。在大多数情况下,我会选择公有继承,因为它保持了基类接口的完整性,并允许派生类扩展和定制这些接口。然而,在某些特殊情况下,我也可能会考虑使用保护继承或私有继承来满足特定的设计需求。
3、总结
天堂有路你不走,地狱无门你自来。
4、参考
4.1 《Effective C++》
4.2 Effective C++条款33:避免遮掩继承而来的名称(Avoid hiding inherited names)
这篇关于《Effective C++》《继承与面向对象设计——33、避免遮掩继承而来的名称》的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!