zoukankan      html  css  js  c++  java
  • C++读书笔记 关于继承

    最近在看 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] = '';
        strncpy(lastname, ln, LIM - 1);
        lastname[LIM - 1] = '';
        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] = '';
    }
    
    Painter::Painter(const char *ct, const Artist & art): Artist(art)
    {
        strncpy(category, ct, LIM - 1);
        category[LIM - 1] = '';
    }

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

    画家类的构造函数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进行初始化,那么则要定义相关的复制构造函数,重载赋值操作符=。

    暂时就写这么点了。。。

  • 相关阅读:
    OSX安装nginx和rtmp模块(rtmp直播服务器搭建)
    用runtime来重写Coder和deCode方法 归档解档的时候使用
    Homebrew安装卸载
    Cannot create a new pixel buffer adaptor with an asset writer input that has already started writing'
    OSX下面用ffmpeg抓取桌面以及摄像头推流进行直播
    让nginx支持HLS
    iOS 字典转json字符串
    iOS 七牛多张图片上传
    iOS9UICollectionView自定义布局modifying attributes returned by UICollectionViewFlowLayout without copying them
    Xcode6 iOS7模拟器和Xcode7 iOS8模拟器离线下载
  • 原文地址:https://www.cnblogs.com/jiulin/p/4528566.html
Copyright © 2011-2022 走看看