读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值

简介: 从一开始就让我们简化这次的讨论。你有两类你能够继承的函数:虚函数和非虚函数。然而,重新定义一个非虚函数总是错误的(Item 36),所以我们可以安全的把这个条款的讨论限定在继承带默认参数值的虚函数上。

 

从一开始就让我们简化这次的讨论。你有两类你能够继承的函数:虚函数和非虚函数。然而,重新定义一个非虚函数总是错误的(Item 36),所以我们可以安全的把这个条款的讨论限定在继承带默认参数值的虚函数上。

1. 虚函数是动态绑定的,而默认参数是静态绑定的

在这种情况下,这个条款的验证就相当直接了:虚函数是动态绑定的,而默认参数值是静态绑定的。

这是什么?你说你不堪重负的脑袋已经忘记了动态绑定和静态绑定之间的区别?(为了好记,静态绑定也叫做早绑定(early binding),动态绑定也叫做晚绑定(late binding))。让我们看一下:

一个对象的静态类型是你已经在程序文本中声明的类型,考虑如下的类继承体系:

 1 // a class for geometric shapes
 2 class Shape {
 3 public:
 4 enum ShapeColor { Red, Green, Blue };
 5 // all shapes must offer a function to draw themselves
 6 virtual void draw(ShapeColor color = Red) const = 0;
 7 ...
 8 };
 9 
10 class Rectangle: public Shape {
11 public:
12 // notice the different default parameter value — bad!
13 virtual void draw(ShapeColor color = Green) const;
14 ...
15 };
16 class Circle: public Shape {
17 public:
18 virtual void draw(ShapeColor color) const;
19 ...
20 };

 

画成类继承图会是下面这个样子:

 

现在考虑三个指针:

1 Shape *ps;                               // static type = Shape*
2 
3 Shape *pc = new Circle;           // static type = Shape*
4 
5 Shape *pr = new Rectangle;    // static type = Shape*

 

在这个例子中,ps,pc和pr都被声明为指向shape的指针,所以它们用Shape作为它们的静态类型。注意无论shape指针真正指向的是什么对象,静态类型都是Shape*。

 

一个对象的动态类型由指针当前指向的对象类型来决定。也就是,它的动态类型表明了它的行为会是怎样的。看上面的例子,pc的动态类型是Circle*,pr的动态类型是Rectangle*。对于ps,它实际上没有动态类型,因为它还没有引用任何对象。

 

正如字面意思所表示的,在程序运行时动态类型是可以改变的,特别是通过赋值:

1 ps = pc;      // ps’s dynamic type is now Circle* 
2 ps = pr;      // ps’s dynamic type is now Rectangle*

虚函数是动态绑定的,意味着哪个函数被调用是由发出调用的对象的动态类型来决定的:

1 pc->draw(Shape::Red);          // calls Circle::draw(Shape::Red)
2 
3 pr->draw(Shape::Red);          // calls Rectangle::draw(Shape::Red)

 

这些都是旧知识了,我知道你肯定了解虚函数。当你考虑带默认参数值的虚函数时,麻烦出现了,因为虚函数是动态绑定的,但是默认参数是静态绑定的。这意味着你可能会终止一个虚函数的调用,因为函数定义在派生类中却使用了基类中的默认参数:

 

1  pr->draw();                           // calls Rectangle::draw(Shape::Red)!

 

在这种情况中,pr的动态类型是Rectangle*,所以Rectangle的虚函数被调用,这也是你所期望的。在Rectangle::draw中,默认参数值是Green。然而因为pr的静态类型是Shape*,这个函数调用的默认参数值是来自于Shape类而不是Rectangle类!最后的结果是这个调用由一个奇怪的也几乎是你意料不到的组合组成:也即是Shape类和Rectangle类中的draw声明混合而成。

 

Ps,pc和pr都为指针不是造成这个问题的原因。如果它们是引用也同样会出现这个问题。唯一重要的事情是draw是一个虚函数,并且默认参数中的一个在派生类中被重新定义了

 

2. C++为什么不对参数进行动态绑定?

为什么C++坚持用一种反常的方式来运行?答案和运行时效率相关。如果一个默认参数是动态绑定的,编译器就需要用一种方法在运行时为虚函数参数确定一个合适的默认值,比起当前在编译期决定这些参数的机制,它更慢更加复杂。做出的决定是更多的考虑了速度和实现的简单性,结果是你可以享受高效的执行速度,但是如果你没有注意到这个条款的建议,你就会很迷惑。

3. 个例讨论——为基类和派生类提供相同的默认参数

 

这都很好,但是看看如果这么做会发生什么:遵守这个条款的规定并且为基类和派生类函数同时提供默认参数:

 1 class Shape {
 2 
 3 public:
 4 
 5 enum ShapeColor { Red, Green, Blue };
 6 
 7 virtual void draw(ShapeColor color = Red) const = 0;
 8 
 9 ...
10 
11 };
12 
13 class Rectangle: public Shape {
14 public:
15 virtual void draw(ShapeColor color = Red) const;
16 ...
17 };

 

代码重复的问题出现了。更糟糕的是,与代码重复问题便随而来的代码依赖问题:如果Shape中的默认参数被修改了,所有重复这个参数的派生类都需要被修改。否则重新定义继承而来的默认参数值的问题会再度出现。该怎么做?

 

当你让虚函数按照你的方式来运行时遇到了麻烦,考虑替代设计方法是很明智的,Item 35中介绍了替换虚函数的不同方法。其中的一个是非虚接口用法(NVI idiom):用基类中的public非虚函数调用一个private虚函数,private虚函数可以在派生类中重新被定义。现在,我们用非虚函数指定默认参数,而用虚函数来做实际的工作:

 1 class Shape {
 2 public:
 3 enum ShapeColor { Red, Green, Blue };
 4 
 5 void draw(ShapeColor color = Red) const   // now non-virtual
 6 
 7 {                                                                 
 8 
 9  
10 
11 doDraw(color);                            // calls a virtual
12 
13 }                                                  
14 
15 ...                                                 
16 
17 private:                                       
18 
19 
20 virtual void doDraw(ShapeColor color) const = 0; // the actual work is
21 }; // done in this func
22 class Rectangle: public Shape {
23 public:
24 ...
25 private:
26 
27 virtual void doDraw(ShapeColor color) const; // note lack of a
28 
29 ...                                                                       // default param val.
30 
31 };               

                                            

因为非虚函数应该永远不会在派生类中被重定义(Item 36),这个设计保证draw的color默认参数应该永远是Red。

 

4. 总结

永远不要重新定义一个继承而来的默认参数值,因为默认参数值是静态绑定的,而虚函数——你应该重新定义的唯一的函数——是动态绑定的。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
4月前
|
安全 Java 编译器
C++进阶(1)——继承
本文系统讲解C++继承机制,涵盖继承定义、访问限定符、派生类默认成员函数、菱形虚拟继承原理及组合与继承对比,深入剖析其在代码复用与面向对象设计中的应用。
|
8月前
|
存储 安全 Java
c++--继承
c++作为面向对象的语言三大特点其中之一就是继承,那么继承到底有何奥妙呢?继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用,继承就是类方法的复用。
170 0
|
11月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
605 6
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
195 16
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
404 5
|
编译器 C++ 开发者
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
288 1
【C++】继承
|
安全 编译器 程序员
C++的忠实粉丝-继承的热情(1)
C++的忠实粉丝-继承的热情(1)
|
12月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
10月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
399 12
|
8月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
212 0