码迷,mamicode.com
首页 > 编程语言 > 详细

C++读书笔记 关于继承

时间:2015-06-17 20:02:06      阅读:153      评论:0      收藏:0      [点我收藏+]

标签:

最近在看 C++,再细读关于面向对象的一些知识点,好记性不如烂笔头,就归类总结一下。

面向对象中,继承是非常重要的基础概念。从已有的类派生出新的类,就可对原有的类进行扩展和修改。

称已有的类为基类,继承出来的类叫做派生类。

 

什么时候使用继承呢? 如果两个对象是is-a关系,则可以用继承。

比如水果与苹果。苹果 (is-a) 水果,这时候 水果是基类,苹果则是水果的派生类。

如果不是(is-a)关系,最好不要用继承。比如下面的关系例子。

has-a         午餐 (has-a) 水果,但是午餐 !(is- a )水果。所以不合适继承

is-like-a    敌人(is-like-a)豺狼,但是敌人 !(is- a )豺狼。所以不适合继承。

is-implemented-as-a  数组(is-implemented-as-a)堆栈,但是 数组 !(is- a )堆栈。所以不适合继承。

uses-a      计算机(uses-a)打印机,但是 计算机 !(is- a )打印机。所以不适合继承。

 

继承实现很简单,但是要管理好继承类之间的关系,却是件比较复杂的事情。

涉及到构造析构函数,虚函数&虚函数表,静态联编&动态联编,抽象基类,动态内存分配等。

如果不了解这些机制,设计的类之间就会出现一些和自己原有意图违背的运行错误,以及内存泄漏。

 

  • 基础实现

继承的基本实现非常简单。语法就是定义类时,定义类class MyClass的后面 接上":  修饰符 基类名"即可。

修饰符有 public, private, protect 3种。这3个修饰符分别表示了C++的3种继承方式:公有继承,私有继承,保护继承。

比如SimpleClass继承了BaseClass, 「class SimpleClass : public BaseClass」并且修饰符为public,表明是公有继承。

 

公有继承也是最常用的方式。

关于修饰符,遵循一个基本规则,不论哪种继承方式,基类的私有成员,私有函数在派生类中都是不可见的。

想访问他们,只能通过基类提供公有或者保护方法去访问。

通过3种继承方式,原有基类的成员访问属性也会发生变化。

公有继承:派生类公有继承基类后,基类的成员和函数的访问限制在派生类中不变。

               基类原来是public,到了派生类里还是public。

私有继承:派生类私有继承基类后,基类的成员和函数的访问限制在派生类全部变成私有。

              所以如果私有继承一个基类,就相当于完全隐藏了基类中的所有成员和函数。

保护继承:派生类私有继承基类后,基类的成员和函数的public访问限制在派生类全部变成protect。

              基类中的private还是继续保持private。 

 

关于protect属性的注意点,类数据成员一般推荐用private,而不用protect或者public。

理由是用protect的话,派生类可以直接访问修改基类的数据成员。

成员函数用protect限定比较有用,使此成员函数只对派生类公开,对公众保持隐秘。

 

  • 构造函数与析构函数

派生类需要有自己的构造函数,如果你没有看到构造函数,那只是编译器悄悄用默认的构造函数了。

派生类一定会调用基类的构造函数! 那么调用基类的那个构造函数呢? 答案很简单,你指定哪个就是哪个,如果你太懒不指定,那就调用默认的构造函数。

那如何指定呢?用成员初始化列表句法即可。

比如有一个艺术家类,记录艺术家的姓名,年龄。艺术家类派生出一个画家类。

画家除了有艺术家的姓名,年龄的基本属性外,还设定一个画作类别属性,表示画家最擅长的画是油画,中国画,水彩画之中的某一种。

 

#ifndef __CJiaJia__Artist__
#define __CJiaJia__Artist__

#include <iostream>
using namespace std; enum {LIM = 20}; class Artist { private: char firstname[LIM]; //所有艺术家都应该有一个姓名 char lastname[LIM]; unsigned int age; //所有艺术家都有自己的年龄 public: Artist (const char *fn = "none", const char *ln = "none", unsigned int age = 5); //构造函数 void showName() const; //显示艺术家的姓名 int getAge() const { return age; }; //获得艺术家的年龄 void setAge(int age) { this->age = age; }; //设定艺术家的年龄 }; class Painter : public Artist { private: char category[LIM]; //显示画家的画作种类。如水墨画,油画,水彩画等 public: Painter(const char *ct, const char *fn, const char *ln, unsigned int age); //构造函数1 Painter(const char *ct, const Artist & art); //构造函数2 char getCategory() const; //取得画家的绘画种类 void setCategory(const char *ct); //设定画家的绘画种类 };

#endif /* defined(__CJiaJia__Artist__) */

#include "Artist.h"

Artist::Artist (const char *fn, const char *ln, unsigned int ag)
{
    strncpy(firstname, fn, LIM - 1);
    firstname[LIM - 1] = \0;
    strncpy(lastname, ln, LIM - 1);
    lastname[LIM - 1] = \0;
    age = ag;
}

void Artist::showName() const
{
    cout << firstname <<" "<< lastname<< endl;
}

Painter::Painter(const char *ct, const char *fn, const char *ln, unsigned int age): Artist( fn, ln, age)
{
    strncpy(category, ct, LIM - 1);
    category[LIM - 1] = \0;
}

Painter::Painter(const char *ct, const Artist & art): Artist(art)
{
    strncpy(category, ct, LIM - 1);
    category[LIM - 1] = \0;
}

艺术家类的构造方法里,指定了艺术家的姓名,年龄。画家类同样拥有姓名,年龄,还特别记录了擅长的画作类别。

画家类的构造函数1里,用初始化成员列表显式调用艺术家类的构造函数 Artist( fn, ln, age )。

Painter::Painter(const char *ct, const char *fn, const char *ln, unsigned int age): Artist( fn, ln, age)

画家类的构造函数2里,将调用Artist类的复制构造函数。在这个例子里Artist类没有显式定义复制构造函数,所以调用其默认的复制构造函数进行浅拷贝即可。

关于复制构造函数,如果Artist类里面有成员变量指针通过new申请了内存,则需要显式定义Artist类的复制构造函数以对此成员进行深拷贝。

Painter::Painter(const char *ct, const Artist & art): Artist(art)
  •  基类和派生类之间的特殊关系

派生类可以使用基类的非私有方法。

Painter chinesePainter("中国画", "潘", "天寿", 74);
chinesePainter.showName();

基类指针可以在不进行显式类型转换的情况下指向派生类对象。基类引用可以在不进行显式类型转换的情况下引用派生类对象。

这叫向上强制转换。画家是艺术家,但是艺术家不是画家。这和现实逻辑是一样的。

Painter chinesePainter("中国画", "潘", "天寿", 74);
Artist & rt = chinesePainter;
Artist * pt = &chinesePainter;
rt.showName();pt->showName();
  • 多态公有继承

同一个方法在派生类和基类中可以有不同的行为,取决于调用该方法的对象。

C++有两种重要的机制实现多态公有继承。

- 在派生类中重新定义基类的方法

- 使用虚方法

比如上面的painter类,声明一个基类的方法。

 void showName() const;  //画家类也定义一个显示姓名的方法,同时显示画家的画作类别

 void Painter::showName() const;

{

    cout << firstname<< " "<< lastname<< "擅长" << category << endl;

}

如果不showName不指定为virtual 方法,那么下面代码运行情况如下:

Artist  chineseArtist("齐", "白石", 93);
Painter chinesePainter("中国画", "潘", "天寿", 74);
Artist & art1_ref = chineseArtist;
Artist & art2_ref = chinesePainter;

//如果showName不是vitrual方法
art1_ref.showName(); //使用 Artist::showName()
art2_ref.showName(); //使用 Artist::showName()
//如果showName是vitrual方法
art1_ref.showName(); //使用 Artist::showName()
art2_ref.showName(); //使用 Painter::showName()

 如果一个方法是virtual虚方法,程序会根据引用或者指针指向的对象的类型来选择方法。

 如果一个方法不是virtual虚方法,程序会根据引用或者指针的类型来选择方法。

 在基类中如果将方法声明为虚方法,在派生类中即使不明确指定该方法为虚方法,也是虚方法。

 派生类方法中若要调用基类的方法,加上域限定修饰符和该方法名调用即可。

 

 按照惯例,基类应该包含一个虚拟析构函数!理由是为了调用相应对象类型的析构函数,然后基类的析构函数会被自动调用。

 如果派生类包含了执行某些操作的析构函数,则基类必须有虚拟析构函数,即使该析构函数不执行任何操作。

  • 静态联编和动态联编  虚函数表

 程序调用函数时,编译器决定使用哪个执行代码块。根据函数调用去执行特定的代码块被称为联编。

在编译过程中,就能完成的联编称为静态联编。

但是由于c++里有虚函数的存在,编译时期不能确定使用哪个函数,编译器必须生成在程序运行时选择正确的虚方法的代码,

这被称为动态联编。

什么时候用动态联编,什么时候用静态联编呢? 非虚方法用静态联编,虚方法用动态联编。

 

大多数时候,动态联编很好,它可以让程序选择为特定类型设计的方法。

但是编译器默认还是使用静态联编。理由在于效率,为了使程序在运行阶段决心决策,编译器必须采取一些方法来跟踪基类指针(虚函数表),增加额外的开销。

大神说C++的指导原则之一就是,不要为不使用的特性付出代价(内存或者处理时间)。

所以如果要在派生类中重新定义基类的方法,则将它设置为虚方法,否者则应该设为非虚方法。

 

编译器处理虚函数的方法,就是给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。

这个数组就称为虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址。

可以看出虚函数定义得越多,数组就越大。调用虚函数时,编译器会到表中去查找函数的地址。

 

  • 防止重定义

 如果在画家类中重新了如下的方法 void showName(int type) const ,那么Artist类的showName() 将被隐藏。

 画家类调用showName()时,就会出现编译错误。

 void showName(int type) const;  //画家类也定义一个显示姓名的方法,同时显示画家的画作类别

 void Painter::showName(int type) const;

{

    cout << firstname<< " "<< lastname<< "擅长" << category << endl;

}

所以总结2条经验规则:

1. 重新定义继承的方法应该和基类的原型完全相同。 例外情况是函数的返回值如果是基类的引用或指针,将其修改为指向派生类的引用或指针是OK的。

    这个特性叫返回类型协变( covariance of return type)。

2.  如果基类中的函数重载了,派生类中又想重新定义它们的实现,那么应在派生类中重新定义所有的基类版本。

     如果偷懒只定义一个版本,另外的重载版本将被隐藏。

  • 抽象基类(ABC)

定义抽象类的原因是,一些虽然是is-a的关系通过继承出来,解决问题的效率却不高。

比如圆和椭圆。圆只需要半径值 就可以描述大小和形状,不需要长半轴a和短半轴b。当然也可以通过将同一个值赋给成员a和b来照顾这种情况,但是导致信息冗余没有必要。

这时候可以把圆和椭圆的共性放到一个ABC中,从该ABC派生出圆和椭圆类。

从语法上来说,至少有一个纯虚函数的类即为抽象类,不能创建抽象类的实例对象。

纯虚函数就是在虚函数结尾加个 =0 ,即表明该函数是纯虚函数。

比如virtual double Area() const = 0;

  • 动态内存分配

如果类成员通过new进行初始化,那么则要定义相关的复制构造函数,重载赋值操作符=。

 

暂时就写这么点了。。。

 

C++读书笔记 关于继承

标签:

原文地址:http://www.cnblogs.com/jiulin/p/4528566.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!