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

    暂时就写这么点了。。。

  • 相关阅读:
    SHELL脚本扩展
    Linux服务器核心参数配置
    JavaIO系统
    SHELL脚本进阶
    计算机是怎么跑起来的?
    3年,从双非到百度的学习经验总结
    桥接模式:探索JDBC底层实现
    并发编程(三):从AQS到CountDownLatch与ReentrantLock
    并发编程(四):ThreadLocal从源码分析总结到内存泄漏
    Zookeeper:分布式程序的基石
  • 原文地址:https://www.cnblogs.com/jiulin/p/4528566.html
Copyright © 2020-2023  润新知