继承

本文深入讲解面向对象程序设计中的继承概念,包括继承权限、访问限定符、派生类的构造与析构、对象模型等内容,并探讨了虚继承解决菱形继承问题的方法。

继承概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程

继承权限&访问限定符

  • 成员限定符与继承关系
  • 继承方式及权限对应表
    这里写图片描述

  • 总结

    • 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。 可以看出保护成员限定符是因继承才出现的
    • public继承是一个接口继承,保持is-a原则,每个父类可用的成员对子类也可用,因为每个子类对象也都是一个父类对象
    • protected/private继承是一个实现继承,基类的部分成员并非完全成为子类接口的一部分,是 has-a 的关系原则,所以非特殊情况下不会使用这两种继承关系,在绝大多数的场景下使用的都是公有继承。私有继承意味着is-implemented-in-terms-of(是根据……实现的)。通常比组合(composition)更低级,但当一个派生类需要访问基类保护成员或需要重定义基类的虚函数时它就是合理的
    • 不管是哪种继承方式,在派生类内部都可以访问基类的公有成员和保护成员,基类的私有成员存在但是在子类中不可见(不能访问)
    • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
    • 在实际运用中一般使用都是public继承,极少场景下才会使用 protetced/private继承
class Person 
{ 
public :    
    void Display ()    
    {        
        cout<<_name <<endl;    
    } 
protected :    
    string _name ; // 姓名 
private :    
    int _age ; // 年龄 
};

//class Student : protected Person 
//class Student : private Person
class Student : public Person
{ 
protected :    
    int _num ; // 学号 
} ;

赋值兼容规则

在public继承权限下,子类和派生类对象之间有:

  • 子类对象可以赋值给父类对象(切割/切片)
  • 父类对象不能赋值给子类对象
  • 父类的指针/引用可以指向子类对象
  • 子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)
class Person
{
public:
    void Display()
    {
        cout <<_name << "-" << _sex << "-" << _age << endl;
    }
private:
    string _name;
    string _sex;
    int _age;
};

class Student :public Person
{
private:
    int _num;
};
void Test()
{
    Person p;
    Student s;
    // 1.子类对象可以赋值给父类对象(切割 /切片)    
    p = s;
    // 2.父类对象不能赋值给子类对象 
    //s = p;    
    // 3.父类的指针/引用可以指向子类对象    
    Person* p1 = &s;
    Person& r1 = s;
    //4.子类的指针/引用不能指向父类对象(可以通过强制类型转换完成)    
    Student* p2 = (Student*)& p;
    Student& r2 = (Student&)p;
    // 这里会发生什么?崩溃(下行转化)    
    //p2->_num = 10;   
    //r2._num = 20
}

继承中的作用域

  • 在继承体系中基类和派生类都有独立的作用域
  • 类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问
    • 同名隐藏
      • 在基类和派生类中,具有相同名称的成员(成员函数||成员变量),如果用派生对象去访问继承体系中的同名成员,只能访问到派生类自己的,基类的成员无法访问。
      • 只能通过加基类作用域的方式去访问相同名称的基类成员 基类::基类成员 访问
  • 注意在实际中在继承体系里面最好不要定义同名的成员

派生类默认成员函数

  • 派生类对象的构造与析构
    • 继承体系下派生类和基类构造函数的调用次序
      • 派生类的构造函数,在进入派生类函数体之前,先要在初始化列表中完成派生类中成员的初始化
      • 先初始化基类中成员(调用基类的构造函数),在初始化派生类自己的
    • 继承体系下派生类和基类析构函数的调用次序
      • 如果要销毁派生类的对象,则需要调用派生类析构函数来清理派生类对象自己管理的对象
      • 在派生类析构函数的最后需要调用基类的析构函数已完成基类部分资源的销毁
class Base
{
public:
    Base(int b)
    {
        //_b = b;
        cout << "Base::Base()" << endl;
    }
    ~Base()
    {
        cout << "Base:~:Base()" << endl;
    }
    int _b;
};

class Derived :public Base
{
public:
    Derived()
        //:Base(10)
        //, _d(20)
    {
        cout << "Derived::Derived()" << endl;
    }
    ~Derived()
    {
        cout << "Derived:~:BaDerivedse()" << endl;
        //call ~Base()
    }
    int _d;
};

void Test()
{
    Derived d;
}

说明

  • 基类没有缺省构造函数,派生类必须要在初始化列表中显式给出基类名和参数列表
  • 基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省构造函数
  • 基类定义了带有形参表构造函数,派生类就一定定义构造函数

如何实现一个不能被继承的类

静态方法实例化与释放+私有构造函数

class Base
{
public:
    static Base* GetObj()
    {
          return new Base;
    }
    static void ReleaseObj(Base *pb)
    {
        if (pb)
            delete pb;
    }
private:
    Base()
    {   
        cout << "Base()" << endl;
    }
    int _b;
};
class A :public Base
{};

继承有友元&statc

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员 ,因为友元函数不是类的成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例

继承体系下派生类的对象模型

注意:对象模型为对象中非静态成员变量在内存中的布局形式,与成员函数无关,因此以下类中只给出了成员变量

  • 单继承
    一个子类只有一个直接父类时称这个继承关系为单继承
class Dpublic B


- 多继承
一个子类有两个或以上直接父类时称这个继承关系为多继承

class D:public B1,public B2


- 菱形继承

class C1: public B
class C2: public B
class D:public C1,public C2


- 菱形虚拟继承
从上面的菱形继承的对象模型中,我们发现D类中存在两份Base
对象,因此在访问继承于基类的成员变量时,会存在数据冗余及二义性的问题

class Person 
{
public :    
    string _name ; // 姓名 
}; 
class Student : public Person 
{ 
protected :    
    int _num ; //学号 
}; 
class Teacher : public Person 
{ 
protected :    
    int _id ; // 职工编号 
}; 
class Assistant : public Student, public Teacher 
{ 
protected :    
    string _majorCourse ; // 主修课程 
}; 
void Test () 
{    
    // 显示指定访问哪个父类的成员    
    Assistant a ;    
    a.Student ::_name = "xxx";    
    a.Teacher ::_name = "yyy"; 
}

虚拟继承-—解决菱形继承的二义性和数据冗余的问题
虚拟继承在继承权限前加上virtual关键字即可构成虚拟继承 虚继承的特点是,在任何派生类中的virtual基类总用同一个(共享)对象表示

class Person 
{
public :    
    string _name ; // 姓名 
}; 
class Student : virtual public Person 
{ 
public:    
    void setNum(int num){_num = num;}; 
protected :    
    int _num ; //学号 
}; 
class Teacher : virtual public Person 
{ 
public:    
    void setId(int id){_id = id;}; 
protected :    
    int _id ; // 职工编号 
}; 
class Assistant : public Student, public Teacher 
{ 
public:    
    void setMajor(string major){_majorCourse = major;}; 
protected :    
    string _majorCourse ; // 主修课程 
}; 
void Test () 
{    
    Assistant a ;    
    a.setId(10);    
    a.setNum(8);    
    a._name = "xxx";    
    a.setMajor("math");  
}

虚继承和直接继承有什么区别

  • 时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象(偏移量表格),只不过这个调整是运行时间接完成的。
  • 空间

    • 由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。
    • 虚拟继承与普通继承不同的是,虚拟继承可以防止出现菱形继承时,一个派生类中同时出现了两个基类的子对象。也就是说, 为了保证这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针
  • 总结

    • 虚拟继承多了4个字节—>地址—>指向偏移量表格
    • 虚拟继承的派生类的对象模型:基类在下,派生类在上
    • 派生类对象访问基类成员—>通过偏移量表格地址
    • 合并构造函数,为了在构造对象期间将偏移量表格的地址放在对象的前4个字节;并且多传递1个1,检测是否为虚拟继承

常见问题

  1. 派生类从基类那里继承了什么?
    基类的公有成员成为派生类的公有成员。基类的保护成员成为派生类的保护成员。基类的私有成员被继承,但不能直接访问。
  2. 派生类不能从基类那里继承什么?
    不能继承构造函数、析构函数、赋值运算符和友元。
  3. 假设baseDMA::operator=()函数的返回类型为void,而不是baseDMA&,这将有什么后果》如果返回类型为baseDMA,而不是base DMA&,又将有什么后果?
    如果返回值为void,仍可以使用单个赋值,但不能使用连锁赋值。
    如果方法返回一个对象,而不是引用,则该方法的执行速度将有所减慢,这是因为返回语句需要复制对象。
  4. 创建和删除派生类对象时,构造函数和析构函数调用的顺序是怎样的?
    派生类的构造函数,在进入派生类函数体之前,先要在初始化列表中完成派生类中成员的初始化;先初始化基类中成员(调用基类的构造函数),在初始化派生类自己的。
    如果要销毁派生类的对象,则需要调用派生类析构函数来清理派生类对象自己管理的对象;在派生类析构函数的最后需要调用基类的析构函数已完成基类部分资源的销毁。
  5. 如果派生类没有添加任何数据成员,它是够需要构造函数?
    需要。每个类都必须有自己的构造函数。如果派生类没有添加新成员,则构造函数为空,但必须存在。
  6. 如果基类和派生类定义了同名的方法,当派生类对象调用该方法时,被调用的将是哪个方法?
    只调用派生类方法。派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问(同名隐藏)。
    仅当派生类没有重新定义方法或者使用作用域解析运算符时,才会调用基类方法(基类::基类成员) 。然而,应当把所有要重新定义的函数声明为虚函数。
  7. 在什么情况下,派生类应定义赋值运算符?
    如果派生类构造函数使用new或new[]运算符来初始化类的指针成员,则应定义一个赋值运算符。更普遍的说,如果对于派生类成员来说,默认赋值不正确,则应定义赋值运算符。
  8. 可以将派生类对象的地址赋给基类指针吗?可以将基类对象的地址赋给派生类指针吗?
    可以将派生类对象的地址赋给基类指针。
    只有通过显示类型转换,才可以将基类对象的地址赋给派生类指针(向下转化),而使用这样的指针不一定安全。
  9. 可以将派生类对象赋给基类对象吗?可以将基类对象赋给派生类对象吗?
    可以将派生类对象赋给基类对象。对于派生类中新增的数据成员都不会传递给基类对象。此时,程序将使用基类的赋值运算符。
    仅当派生类定义了转换运算符(即包含将基类引用作为唯一参数的构造函数)或使用基类作为的赋值运算符时,才可以将基类对象赋给派生类对象。
  10. 假设定义了一个函数,它将基类对象的引用作为参数。为什么该函数也可以将派生类对象作为参数?
    因为C++允许基类引用指向从该基类派生而来的所有类型。
  11. 假设定义了一个函数,它将基类对象作为参数(即函数按值传递基类对象)。为什么该函数也可以将派生类对象作为参数?
    按值传递对象将调用复制构造函数。由于形参是基类对象,因此将调用基类的复制构造函数。复制构造函数以基类引用为参数,该引用可以指向作为参数传递的派生对象。最终结果是,将生成一个新的基类对象,其成员对应于派生对象的基类部分。
  12. 为什么通常按引用传递对象比按值传递的效率更高?
    按引用(而不是传值)传递对象,这样可以确保函数从虚函数受益。另外,按引用(而不是传值)传递对象可以节省内存和时间,尤其对大型对象。
    按值传递对象的主要优点在于可以保护原始数据,但可以通过将引用作为const类型传递,来达到同样的目的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值