Effective C++(20) 继承与面向对象设计
本文主要参考《Effective C++ 3rd》中的第六章部分章节的内容。
关注的问题集中在继承、派生、virtual函数等。如:
- virtual? non-virtual? pure virtual?
- 缺省参数值与virtual函数有什么交互影响?
- 继承如何影响C++的名称查找规则?
- 什么情况下有比virtual更好的选择?
这些都是我们将要从这一章里学到的内容。
1 确定你的public继承可以塑模出is-a关系
谨记public继承的含义:
如果class D以public形式继承class B,则每一个类型D的对象同时也是一个类型B的对象,反之不成立。
即,B比D表现出更一般化的概念,而D比B表现出更特殊的概念。
如:
class Person { ... }; class Student : public Person { ... };
这个体系告诉我们:每个学生都是人,但并非每个人都是学生。
从C++的角度来看,任何函数,如果期望获得一个类型为Person(或指向Person对象的指针或引用),也都愿意接受一个Student对象(或指针或引用)。
需要留意的一点是:
以我们在生活中的直觉为基础来塑模is-a关系有时是错误的,可以说犯了“经验主义错误”。
如:
class Square应该以public形式继承class Rectangle吗?
即正方形是一个(is-a)矩形吗?
至少我们在学校里是这么学到的:正方形是一个矩形,但是矩形不一定是正方形。
那么我们来写一些这个继承
class Rectangle { public: virtual void setHeight(int newHeight); virtual void setWidth(int newWidth); virtual int height() const; virtual int width() const; ...... }; void makeBigger (Rectangle& r) { int oldHeght = r.height(); r.setWidth(r.width() + ); assert( r.heght() == oldHeght ); // 判断r的高度是否改变,永为真。 }
在这个矩形的基础上派生出一个正方形
class Square : public Rectangle { ... }; Square s;
...
assert( s.width() == s.height() );
makeBigger(s);
assert( s.widht() == s.height() );
显然makeBigger只改变矩形的宽度,而不改变矩形的长度。这和s是个正方形矛盾。
public所包含的含义为:能够使用在base class对象身上的每件事,应该同样可以使用在derived class对象身上。
由此可见,其他领域或者生活中,我们习得的直觉,在软件领域并不总是正确的。
因此,除了is-a关系,我们还要更多地思考和在适当的场合使用has-a和is-implemented-in-terms-of(根据某物实现出)
小结:
“public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,每一个derived class对象也都是一个base class对象。
2 避免遮掩继承而来的名称
关键字:作用域。
先看一个简单的例子:
int x; void someFunc() {
double x;
std::cin >> x;
}
这个读取数据的语句使用的是局部变量x,而不是全局变量x。因为内层作用域的名称会遮掩外围作用域的名称。
加入继承机制,有如下的代码:
class Base { private: int x; public: virtual void mf1() = ; virtual void mf2() ; void mf3(); .... }; class Derived : public Base {
public: virtual void mf1(); void mf4();
....
};
mf4函数中有如下实现:
void Derived::mf4() {
...
mf2();
...
}
编译器的查找作用域顺序:
local作用域--->class Derived覆盖的作用域
---> class覆盖的作用域(本例到这停止)
---> Base的那个namespace作用域
---> global作用域。
现在来为上面的两个类加几个成员函数:
class Base { private: int x; pubic: virtual void mf1() = ; virtual void mf1( int ); virtual void mf2() ; void mf3(); viod mf3( double ); .... }; class Derived : public Base { public: virtual void mf1(); void mf3(); void mf4(); .... };
这样做会有什么效果呢?
Derived d; int x; ...... d.mf1();
d.mf1(x); //error
d.mf2();
d.mf3();
d.mf3(x); //error
由此可见,基于作用的名称遮掩规则,并没有因为重载函数而特殊处理,那些名字相同的重载函数同样被遮掩掉了。
如果我们想在子类中继承那些重载函数,并重写其中的一部分(像本例中的mf1和mf3),那么可以使用using语句
让Base class内名为mf1和mf3的所有东西(所有重载函数)在Derived作用域内都是可见的。
class Base { private: int x; public: Base() {}; virtual void mf1() = ; virtual void mf1( int m ) { std::cout << "Base mf1 int: "<< m << std::endl; } ; virtual void mf2() { std::cout << "Base mf2 " << std::endl; }; void mf3() { std::cout << "Base mf3" << std::endl;}; void mf3( double m ) { std::cout << "Base mf3 double:" << m << std::endl; }; }; class Derived : public Base { public: using Base::mf1; // 让Base class内名为mf1和mf3的所有东西(所有重载函数) using Base::mf3; // 在Derived作用域内都是可见的。 virtual void mf1() { std::cout << "Derived mf1" << std::endl; }; void mf3() { std::cout << "Derived mf3" << std::endl; }; void mf4() { std::cout << "Derived mf4" << std::endl; }; };
调用:
Derived* d = new Derived(); d->mf1();
d->mf1();
d->mf2();
d->mf3();
d->mf3();
d->mf4();
运行截图:
上面这种技术告诉我们如何继承所有重载函数,实现is-a的关系。
有时候我们并不想继承base classes的所有函数,而是用public继承和名字遮掩规则又不符合public继承所包含的is-a关系。
因此,这里介绍一种转交函数技术,很简单,看一个例子就懂了。
class Base {
public:
virtual void mf1() = ;
virtual void mf1(int );
....
}; class Derived : private Base { public: virtual void mf1() { Base::mf1(); } // 转交函数
......
};
小结:
derived classes内的名称会遮掩base classes内的所有相同名称的重载函数,在public继承下这个机制并不希望发挥作用。
可使用using声明式或转交函数来调用被遮掩的重载函数。
3 区分接口继承和实现继承
选择继承的集中情况:
- a:希望derived classes只继承成员函数的接口
- b:希望derived classes同时继承函数的接口和实现,又希望能够重写它们所继承的实现
- c:希望derived classes同时继承函数的藉口和实现,并且不允许重写任何东西
看一个几何图形例子:
class Shape { public: virtual void draw() const = ; virtual void error(const std::string& msg); int objectID() const; }; class Rectangle: public Shape { ...... }; class Ellipse: public Shape { ...... };
首先考虑纯虚函数draw
pure virtual函数有两个最突出的特征:
- 它们必须被任何“继承了它们”的子类重新声明
- 它们在抽象class中通常没有定义
综合上面两个特征:声明一个纯虚函数的目的是为了让derived class只继承函数接口
满足了本节开头的情景a。
考虑虚函数error。
虚函数的目的是让derived classes继承该函数的接口和缺省实现。满足了情景b。
最后,考虑non-virtual函数objectID。
声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
对应了情景c。
纯虚函数、虚函数和非虚函数使得你可以精确地指定你想要derived classes继承的东西。
小结:
接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口
pure virtual函数只具体指定接口继承。
virtual 函数具体指定接口继承及缺省实现继承。
non-virtual函数具体指定接口继承以及强制性实现继承。
4 考虑virtual函数之外的选择
考虑为游戏内的人物设计一个继承体系。
class GameCharacter {
public:
virtual int healthValue() const; // 返回人物的健康指数。
......
};
有时候,常规的面向对象设计方法往往看起来是那么的自然,以至于我们从未考虑其他的一些解法。
这一节就让我们跳出常规设计的思维,考虑一些不那么常规的设计方法。
方法1:借由non-virtual interface手法实现Template Method模式
class GameCharacter { public: int healthValue() const {
...
int retVal = doHealthValue();
...
return retVal;
} .... private:
virtual int doHealthValue() const {
...
} };
让客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。
这个non-virtual函数(healthValue)称为virtual函数的包装器(wrapper)。
从程序执行的角度来看,derived classes重新定义了virtual函数,从而赋予它们“如何实现功能”的控制能力,base classes保留控制“函数何时被调用”的权利。
方法2:借由Function Pointer实现Strategy模式
代码如下:
class GameCharacter:; int defaultHealthCalc(const GameCharacter& gc); class GameCharacter { public: typedef int ( *HealthCalcFunc ) ( const GameCharacter& ); explicit GameCharacter( HealthCalcFunc hcf = defaultHealthCalc ) : healthFunc(hcf) { } int healthValue() const {
return healthFunc(*this);
} ... private:
HealthCalcFunc healthFunc;
};
还有其他的一些方法,在此并不一一讨论,详见《Effective C++》
5 绝不重新定义继承而来的non-virtual函数
在子类中重定义继承而来的non-virtual函数,会导致子类你的设计出现矛盾。
比如在class Base有一个non-virtual函数setBigger,而所有继承Base的子类都可以执行变大的动作,那么这个动作就是一个不变性(共性)。
而在class Derived : public Base子类中,重写了setBigger函数,那么class Derived便无法反映出“不变性凌驾于特性”的性质。
从另一方面说,如果setBigger操作真的需要在子类中重定义,那么就不应该把它设定为一个共性(non-virtual)。
因此,重新定义继承来的non-virtual函数可能并不会对你的程序的运行造成太大的困扰,但是正如上面提到的,这是设计上的矛盾,或者说缺陷。
6 绝不重新定义继承而来的缺省参数值
本小节的讨论局限于“继承一个带有缺省参数值的virtual函数”。
理由:virtual函数动态绑定,缺省参数值静态绑定。
class Shape { public:
Shape() {}; enum ShapeColor { Red = "red", Green = "green" , Blue = "blue"}; virtual void draw(ShapeColor color = Red) const =
{
std::cout << "This shape is " << color << std::endl;
}
}; class Rectangle : public Shape {
public:
Rectangle() {};
virtual void draw ( ShapeColor color = Green ) const;
}; class Circle : public Shape {
public:
virtual void draw(ShapeColor color) const;
};
先考虑如下指针
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
ps、pc、pr的静态类型都是Shape*
所谓动态类型就是“目前所指对象的类型”。也就是说动态类型可以表现出一个对象将会有什么行为。
在本例中,ps没有动态类型,pc的动态类型为Circle*,pr的动态类型为Rectangle*。
动态类型可以在程序执行过程中改变(通常是经由赋值动作)。如
ps = pc; // ps的动态类型现在是Circle
ps = pr; // ps的动态类型现在是Rectangle
上面是对动态绑定和静态绑定的简单复习。
现在,考虑带有缺省参数值的virtual函数。
在上面的例子中,Shape中的draw函数的color默认参数是Red,而子类中的draw函数的color默认参数是Green。
Shape* shape = new Rectangle();
shape->draw();
根据动态绑定规则,上述代码的输出应该为:This shape is 1
但是运行代码之后会发现,结果并不是我们想的那样。
我们来分析一下导致这种结果的原因:
shape的动态类型为Rectangle*,调用draw时,根据动态绑定,调用的应该为Rectangle的版本
Rectangle版本的draw的默认参数应该为Green(1)
而结果是Red(0)
为了更清楚的看一下究竟调用的是哪一个draw,我们多加一点打印信息。
ok,结果很清楚了,函数调用的版本是Rectangle*,但是默认参数调用的是Shape*中定义的。
所以结论就是:这个函数调用由class Shape和class Rectangle class的draw声明式各出一半力。
那么C++为什么有这么奇怪的设定呢?简单的说,就是:性能。运行期对参数动态绑定缺省值很慢很复杂,所以考虑到性能问题,并没有支持默认参数的动态绑定。
小结:
所以正如本节的标题,禁止重载一个继承而来的默认参数值,因为缺省参数值都是静态绑定,而使用这些默认参数的virtual函数却是动态绑定。
参考资料:
《Effective C++ 3rd》
Effective C++(20) 继承与面向对象设计的更多相关文章
- Effective C++: 06继承与面向对象设计
32:确定你的public继承塑模出is-a关系 以C++进行面向对象编程,最重要的一个规则是:public继承表示的是"is-a"(是一种)的关系. 如果令class D以pub ...
- Effective C++ 6.继承与面向对象设计
//条款32:确定你的public继承塑模出is-a关系 // 1.public继承意味着is-a的关系,适用在基类上的方法都能用于派生类上. //条款33:避免遮掩继承而来的名称 // 1.在pub ...
- 【Effective C++】继承与面向对象设计
关于OOP 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private,也可以是virtual或non-virtual. 2,成员函数的各个选项:virtu ...
- EffectiveC++ 第6章 继承与面向对象设计
我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的"可能比较准确"的「翻译」. Chapter 6 继承与面向对象设计 Inheritance and ...
- Effective C++ —— 继承与面向对象设计(六)
条款32 : 确定你的public继承塑模出is-a关系 以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系.请务必牢记.当 ...
- Effective C++ -- 继承和面向对象设计
32.确保你的public继承了模is-a关系 public继承意味着is-a关系(里氏替换原则),一切适用于基类也适用于派生类. 矩形继承正方形问题: 可实施与矩形的操作无法实施与正方形 在编程领域 ...
- Effective C++ ——继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系 以public继承的类,其父类的所有的性质都应该使用与子类,任何需要父类的地方都应该能用子类来代替,任何子类类型的对象也同时是父类的: class ...
- Effective C++笔记:继承与面向对象设计
关于OOP 博客地址:http://www.cnblogs.com/ronny 转载请注明出处! 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private ...
- Effective C++笔记(六):继承与面向对象设计
参考:http://www.cnblogs.com/ronny/p/3756494.html 条款32:确定你的public继承塑模出is-a关系 “public继承”意味着is-a.适用于base ...
随机推荐
- Android的网络编程
1.3主要接口 Android平台有三种网络接口可以使用,他们分别是:java.net.*(标准Java接口).Org.apache接口和Android.net.*(Android网络接口).下面分别 ...
- JavaScript数据结构-5.队列
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- InterView之C/CPP cal
cal #define DOUBLE(x) x+x ,i = 5*DOUBLE(5): i 是多少? i 为30. 5 * 5 + 5 下面关于"联合"的题目的输出? A // / ...
- PHP之mb_strpos使用
mb_strpos (PHP 4 >= 4.0.6, PHP 5, PHP 7) mb_strpos - Find position of first occurrence of string ...
- logstash 启动报找不主类或无法加载 java
logstash 启动报无法找到主类解决方案 Zparkle 关注 2018.03.08 22:04* 字数 2051 阅读 1评论 0喜欢 0 当logstash启动时,首先要注意正确配置java ...
- [中英对照]How PCI Express Works | PCIe工作原理
How PCI Express Works | PCIe工作原理 PCI Express is a high-speed serial connection that operates more li ...
- Leetcode 483. Smallest Good Base
For an integer n, we call k>=2 a good base of n, if all digits of n base k are 1. Now given a str ...
- (一)JNI基本概念
1. 基本概念: 首先,注意:C和C++在调用JNI时候方法是不一样的 注意看下面两个的区别: C++ #include <jni.h> #include <string> e ...
- Node.js事件驱动模型
一.传统线程网络模型 在了解Node.js事件驱动模型之前,我们先了解一下传统的线程网络模型,请求进入web服务器(IIS.Apache)之后,会在线程池中分配一个线程来线性同步完成请求处理,直到请求 ...
- CakePHP调用model类和foreach循环
1. 引入Model类 2.调用model类(Guarantee)下的getCity()方法 3.写sql语句 并返回获得值 4.foreach循环取得的城市