《Effective C++》学习笔记——条款32

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

六、继承与面向对象设计

six、Inheritance and Object-Oriented Design

面向对象编程(OOP)几乎流行了两个年代,即使你过去只用C编程,现在也没办法逃脱这个趋势。

本章主要看 C++的OOP与你可能习惯的OOP的不同:

> "继承" 可以使单一继承或多重继承

> 每一个继承连接(link)可以是public,protected or private,也可以是 virtual or non-virtual

> 成员函数的各个选项:virtual?non-virtual?pure virtual?以及成员函数和其他语言特性的交互影响:缺省参数值与virtual函数有什么交互影响。

> 还有一些其他的,比如:继承如何影响C++的名称查找规则?设计选项有哪些?如果class的行为需要修改,virtual函数是最佳选择吗?

除了这些,本章还会解释C++各种不同特性的真正意义。比如:

> "public继承" 意味 "is-a"

> virtual函数 意味 "接口必须被继承";non-virtual函数 意味 "接口和实现都必须被继承"

等等

条款 32:确定你的public继承塑模出is-a关系

Rule 32:Make sure public inheritance models "is-a"

1.public继承 与 is-a

以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味"is-a"的关系

如果你令 class D("Derived")以public形式继承 class B("Base"),你便是告诉C++编译器(亦或是看你代码的人)——每一个类型为D的对象同时也是一个类型为B的对象,反之则不成立。

例如:

class Person  {  ...  };
class Student : public Person  {  ...  };

上面的代码说明——每个学生都是人,但并非每个人都是学生。这就是is-a。

在C++领域中,任何函数如果期望获得一个类型为Person(或pointer-to-Person或reference-to-Person)的实参,都也愿意接受一个Student对象(或pointer-to-Student或reference-to-Student):

void eat(const Person& p);    // 任何人都会吃
void study(const Student& s);    // 只有学生才到校学习

Person p;    // p是人
Student s;    // s是学生
eat(p);    // ok,p是人,可以执行吃这个动作
eat(s);    // ok,s是学生,根据is-a,可以执行吃这个动作
study(s);    // ok,s是学生,可以执行到校学习这个动作
study(p);    // no! p是人,并非每个人都可以执行到校学习这个动作

>这个论点只对public继承才成立<
对于private继承,会完全不同,详见条款39,至于protected继承,作者也没搞太明白。。。

2.产生的各种问题

>1 public继承和is-a之间的等价关系听起来非常简单,但有时候可能被误导,比如:企鹅是一种鸟,鸟可以飞,但如果我们用这样的形式来描述这种关系:

class Bird  {
public:
  virtual void fly();    // 鸟可以飞
  ...
};
class Penguin : public Bird  {    // 企鹅是一种鸟
  ...
};

Boom!发生了什么事情?我们的三观何在。。。

显然,这样是错误的,我们不得不承认有数种鸟不会飞。

但如何反应我们的意思呢?

① 就像下面这样的继承体系,更能准确反映出我们的意思:

class Bird  {
  ...    // 没有声明fly函数
};
class FlyingBird : public Bird  {
public:
  virtual void fly();
  ...
};
class Penguin : public Bird  {
  ...
};

但是,此时这件事就结束了吗?No no no,因为对某些软件系统而言,可能不需要区分会飞的鸟和不会飞的鸟。假如,你的程序对鸟喙和鸟翅更加感兴趣,完全不在乎飞行,那么刚开始的"双class继承体系"或许就可以满足了。

> 这实际上是说明一个事实——世界上并不存在一个"适用于所有软件"的完美设计;所谓的最佳设计,取决于系统希望做什么事。 <

② 对于更加准确反映我们的想法,还有一种方法。为企鹅重新定义fly函数,令它产生一个运行期错误。

void error(const std::string& msg);
class Penguin : public Bird  {
public:
  virtual void fly()  {  error("Attempt to make a penguin fly!");  }
  ...
};

这里所说的某些东西可能和你所想的不同,这里并不是说“企鹅不会飞”,而是说“企鹅会飞,但尝试那么做是一种错误”!

③两者的差异,从错误被侦测出来的时间点来看:“企鹅不会飞”这一限制可由编译期强制实施,但若违反了“企鹅会飞,但尝试那么做是错误的”这条规则,只有运行期才能检测出来。

>2 class Square 应该以 public继承 class Rectangle 吗?

当然,学校是这么说的——正方形是一种特殊的长方形

但是,看看下面这段代码:

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 oldHeight = r.height();
  r.setWidth(r.width()+10);    // 为r的宽度加10
  assert(r.height()==oldHeight);    // 判断r的高度是否未曾改变
}

显然,上面的 assert结果永远为真。因为makeBigger只改变了r的宽度,r的高度从未被改变。

然后:

class Square : public Rectangle  {  ...  };
Square s;
...
assert(s.width()==s.height());    // 这对于所有的正方形一定为真
makeBigger(s);                    // 由于public继承,s是一种(is-a)矩形

assert(s.width()==s.height());    // 对所有的正方形应该仍然为真

这也很明显,第二个assert结果也应该永远为真。因为根据定义,正方形的高度和宽度相同。

But,我们如何调节下面这些assert判断式?

? 调用makeBigger之前,s的高度和宽度相同

? 在makeBigger函数内,s的宽度改变,但高度不变

? makeBigger返回之后,s的高度再度和其宽度相同。(注意,s是以by reference方式传给makeBigger,所以makeBigger修改的是s自身,不是s的副本)

其实,本例的根本困难是,某些可施行与矩形身上的事(例如宽度可独立于其高度被外界修改)却不可实行于正方形身上(宽度总应该与高度一样)。

但是,public继承主张,能够实行于base class对象身上的每件事情,每件事情也同时可以实行于derived class身上。

还有最最重要的一点:☆ 代码通过编译并不代表可以正常的运作 ☆

3.最后

is-a并非是唯一存在于class之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据实物实现出)。

将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见,所以你应该确定你确实了解这些个"class相互关系"之间的差异,并知道如何在C++中最好的塑造它们。

★请记住★

? "public继承"意味着is-a。适用于base class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。

***************************************转载请注明出处:http://blog.csdn.net/lttree********************************************

时间: 2024-11-10 12:03:02

《Effective C++》学习笔记——条款32的相关文章

Effective C++学习笔记 条款07:为多态基类声明virtual析构函数

一.C++明确指出:当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义——实际执行时通常发生的是对象的derived成分没有被销毁!(注:使用基类引用派生类的方式使用多态,由于引用只是对原对象的一个引用或者叫做别名,其并没有分配内存,对其引用对象内存的销毁,将由原对象自己负责,所以使用引用的时候,如果析构函数不为virtual,那么也不会发生derived成员没有销毁的情况) 例如: class b

effective c++学习笔记条款23-25

条款23:宁可用非成员,非友元函数来替代成员函数 1.非成员函数提供了更好的封装性,这个函数内不能访问类的私有成员,封装的越严密我们对类的数据就可以弹性越大的操纵,因为可见这些数据的客户越少,反之数据影响的客户也就越少. 2.c++比较自然的做法-(关系到标准库numplace的组织结构),可以把不同便捷函数放到不同Namespace去,让客户来决定要用的非成员函数功能,这是类不能提供的. 条款24:若所有参数皆需类型转换,请为此采用非成员函数. 1.如果你需要为某个函数的所有参数(包括被thi

effective c++学习笔记条款11-13

条款11: 1.令赋值运算符返回一个&,因为STL,string都是这样做的,除非你有足够好的理由不这样做. 2.处理自我赋值的方法----(1).在没有成功获取对象数据时不要删除自己的数据,避免发生异常后原对象指针是一个悬浮指针 (2).判断自我赋值的检查操作会耗费不少时间,可以用swap交换数据技术来优化---(1)形参为赋值而来,(2)形参为静态引用,多加一个函数内拷贝操作.

effective c++学习笔记条款8-10

条款7:为多态基类声明虚析构函数 1.一个基类指针接受一个派生类对象的地址时,对该指针delete,仅仅释放基类部分 2.给所有类都带上虚析构函数是个馊主意,会带有vptr指向一个函数指针数组,扩大不必要的对象大小,除非补偿vptr,否则没有移植性. 3.string类和STL不含有虚析构函数,然而一些用户 却将他们作为基类,运用   delete指向派生类的基类指针,导致错误[c++11添加了禁止派生性质],他们不适合当基类. 4,手头上没有合适的纯虚函数,但你确实需要一个抽象类,把析构函数声

effective c++学习笔记条款20-22

条款20:用引用传递代替值传递 1.尽量以引用传递来代替传值传递,前者比较高效,并且可以避免切割问题 2.以上规则不适用于内置类型,以及STL的迭代器,和函数对象 条款21:必须返回对象时,别妄想返回对象的引用 1.绝对不要返回指针和引用指向一个局部对象或者静态局部对象而有可能需要多个这样的对象,条款4已经为在单线程环境合理返回&指向一个局部静态提供了一份设计实例.(保护初始化顺序) 条款22:将成员变量声明为private 1.切记将成员变量声明为private.这可赋予客户访问数据的一致性,

effective c++学习笔记条款4-7

条款4:确定对象被使用前已经初始化 一. 变量在不同情况下可能会初始化,也可能不会初始化. 注意初始化和赋值的区别. 1.在类中内置类型不会发生隐式初始化,自定义有默认构造函数的能被默认初始化 所以在构造类时务必初始化内置类型,最好给自定义的对象显示初始化避免在函数体中赋值浪费资源. 2.内置类型在函数体内不会初始化,在函数体外自动初始化为0. 二. 1.const和引用类型必须初始化,不可能赋值 三 1.当类实在是有较多构造函数,并且总是要对一些成员数据重复初始化,可以考虑将那些“赋值和初始化

effective c++学习笔记条款17-19

条款17:以独立语句将New对象放置入智能指针. 1.以独立语句将newed对象放置入智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露. void name(shared_ptr<管理对象类型>(new 管理对象类型),其它函数)),New被分配内存不一定马上放入管理对象,因为有其它函数干扰,这不是独立语句. 条款18:让接口容易被正确使用,不易被误用. 1.好的接口很容易被正确使用,不容易被误用.你应该在你的所有接口中努力达成这些性质. 2.“促进正确使用”的办法包括接

effective c++学习笔记条款35-37

#include<iostream> using namespace std; class A { public: void asd() { pri(); } private: /*virtual*/ void pri() { cout << "基类函数" << endl; } }; class B :public A { private: void pri() /*override*/ { cout << "派生类函数&quo

effective c++学习笔记条款29-31

条款29:为异常安全而努力是值得的[回顾] 1.异常安全函数即使发生异常也不会泄露资源或允许任何数据结构败坏,这样的函数分为3种可能的保证:基本型,强烈型,不抛异常型 2.“强烈保证”往往能通过copying and swap 来实现出来,但并非所有函数都可实现或者具备现实意义. 3.函数提供的“异常安全保证”通常最高只等于其所调用的各个函数的“异常安全中”的最弱者.