06_继承与派生

Posted eokey

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了06_继承与派生相关的知识,希望对你有一定的参考价值。

一:继承的概念

  面向对象程序设计有 4 个主要特点:抽象、封装、继承和多态性。我们已经讲解了类和对象,了解了面向对象程序设计的两个重要特征一数据抽象与封装,已经能够设计出基于对象的程序,这是面向对象程序设计的基础。
  要较好地进行面向对象程序设计,还必须了解面向对象程序设计另外两个重要特征——继承性和多态性。继承性是面向对象程序设计最重要的特征,可以说,如果没有掌握继承性,就等于没有掌握类和对象的精华,就是没有掌握面向对象程序设计的真谛。

(1)类之间的关系

has-A, uses-A 和 is-A
has-A 包含关系,用以描述一个类由多个“部件类”构成。实现 has-A 关系用类成员表示,即一个类中的数据成员是另一种已经定义的类。
uses-A 一个类部分地使用另一个类。通过类之间成员函数的相互联系,定义友员或对象参数传递实现。
is-A 机制称为“继承”。关系具有传递性,不具有对称性。

(2)继承关系举例
万事万物中皆有继承,是重要的现象
两个案例: 1)植物继承图; 2程序员继承图
技术图片

 

 

 (3)继承相关概念

技术图片

 

 

 (4)派生类的定义

技术图片

 

 

 (5)继承的重要说明

1、子类拥有父类的所有成员变量和成员函数
2、子类可以拥有父类没有的方法和属性
3、子类就是一种特殊的父类
4、子类对象可以当作父类对象使用

二:派生类的访问控制
  派生类继承了基类的全部成员变量和成员方法(除了构造和析构之外的成员方法),但是这些成员的访问属性,在派生过程中是可以调整的。

(1)单个类的访问控制
1、类成员访问级别(publicprivateprotected
2、思考:类成员的访问级别只有 public private 是否足够?

(2)不同的继承方式会改变继承成员的访问属性
1) C++中的继承方式会影响子类的对外访问属性
  public 继承:父类成员在子类中保持原有访问级别
  private 继承:父类成员在子类中变为 private 成员
  protected 继承:父类中 public 成员会变成 protected
    父类中 protected 成员仍然为 protected
    父类中 private 成员仍然为 private

2) private 成员在子类中依然存在,但是却无法访问到。不论种方式继承基类,派生类都不能直接使用基类的私有成员 。
3) C++中子类对外访问属性表

技术图片

 

 

 4)继承中的访问控制
技术图片

 

 

 (3)“三看”原则
C++中的继承方式(public、 private、 protected)会影响子类的对外访问属性
判断某一句话,能否被访问
1)看调用语句,这句话写在子类的内部、外部
2)看子类如何从父类继承(public、 private、 protected)
3)看父类中的访问级别(public、 private、 protected)

(4)派生类类成员访问级别设置的原则
思考:如何恰当的使用 public, protected 和 private 为成员声明访问级别?
1、需要被外界访问的成员直接设置为 public
2、只能在当前类中访问的成员设置为 private
3、只能在当前类和子类中访问的成员设置为 protected, protected 成员的访问权限介于public 和 private 之间。

(5)综合训练
练习:
public 继承不会改变父类对外访问属性;
private 继承会改变父类对外访问属性为 private;
protected 继承会部分改变父类对外访问属性。
结论:一般情况下 class B : public A

//类的继承方式对子类对外访问属性影响
#include <cstdlib>
#include <iostream>
using namespace std;
class A
{
private:
  int a;
protected:
  int b;
public:
  int c;
  A()
  {
    a = 0;
    b = 0;
    c = 0;
  }
  void set(int a, int b, int c)
  {
    this->a = a;
    this->b = b;
    this->c = c;
  }
};
class B : public A
{
public:
  void print()
  {
    //cout<<"a = "<<a; //err
    cout<<"b = "<<b;
    cout<<"c = "<<endl;
  }
};
class C : protected A
{
public:
  void print()
  {
    //cout<<"a = "<<a; //err
    cout<<"b = "<<b;
    cout<<"c = "<<endl;
  }
};
class D : private A
{
public:
  void print()
  {
    //cout<<"a = "<<a; //err
    cout<<"b = "<<b<<endl;
    cout<<"c = "<<c<<endl;
  }
};
int main_01(int argc, char *argv[])
{
  A aa;
  B bb;
  C cc;
  D dd;
  aa.c = 100; //ok
  bb.c = 100; //ok
  //cc.c = 100; //err 类的外部是什么含义
  //dd.c = 100; //err
  aa.set(1, 2, 3);
  bb.set(10, 20, 30);
  //cc.set(40, 50, 60); //ee
  //dd.set(70, 80, 90); //ee
  bb.print();
  cc.print();
  dd.print();
  system("pause");
  return 0;
}

三:继承中的构造和析构
(1)类型兼容性原则
  类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员。这样,公有派生类实际就具备了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。类型兼容规则中所指的替代包括以下情况:
  子类对象可以当作父类对象使用
  子类对象可以直接赋值给父类对象
  子类对象可以直接初始化父类对象
  父类指针可以直接指向子类对象
  父类引用可以直接引用子类对象
在替代之后,派生类对象就可以作为基类的对象使用,但是只能使用从基类继承的成员。类型兼容规则是多态性的重要基础之一。

总结: 子类就是特殊的父类 (base *p = &child;)
#include <cstdlib>
#include <iostream>
using namespace std;
/*
子类对象可以当作父类对象使用
子类对象可以直接赋值给父类对象
子类对象可以直接初始化父类对象
父类指针可以直接指向子类对象
父类引用可以直接引用子类对象
*/
//子类就是特殊的父类
class Parent03
{
protected:
  const char* name;
public:
  Parent03()
  {
    name = "Parent03";
  }
  void print()
  {
    cout<<"Name: "<<name<<endl;
  }
};
class Child03 : public Parent03
{
protected:
  int i;
public:
  Child03(int i)
  {
    this->name = "Child2";
    this->i = i;
  }
};
int main()
{
  Child03 child03(1000);
  //分别定义父类对象 父类指针 父类引用 child
  Parent03 parent = child03;
  Parent03* pp = &child03;
  Parent03& rp = child03;
  parent.print();
  pp->print();
  rp.print();
  system("pause");
  return 0;
}

(2)继承中的对象模型
  类在 C++编译器的内部可以理解为结构体
  子类是由父类成员叠加子类新成员得到的

技术图片

 

 

 技术图片

 

 

 继承中构造和析构

问题: 如何初始化父类成员?父类与子类的构造函数有什么关系
在子类对象构造时,需要调用父类构造函数对其继承得来的成员进行初始化
在子类对象析构时,需要调用父类析构函数对其继承得来的成员进行清理
#include <cstdlib>
#include <iostream>
using namespace std;
class Parent04
{
public:
  Parent04(const char* s)
  {
    cout<<"Parent04()"<<" "<<s<<endl;
  }
  ~Parent04()
  {
    cout<<"~Parent04()"<<endl;
  }
};
class Child04 : public Parent04
{
public:
  Child04() : Parent04("Parameter from Child!")
  {
    cout<<"Child04()"<<endl;
  }
  ~Child04()
  {
    cout<<"~Child04()"<<endl;
  }
};

void run04()
{
  Child04 child;
}

int main_04(int argc, char *argv[])
{
  run04();
  system("pause");
  return 0;
}

(3)继承中的构造析构调用原则
1、子类对象在创建时会首先调用父类的构造函数
2、父类构造函数执行结束后,执行子类的构造函数
3、当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
4、析构函数调用的先后顺序与构造函数相反

(4)继承与组合混搭情况下,构造和析构调用原则

原则: 先构造父类,再构造成员变量、最后构造自己
先析构自己,在析构成员变量、最后析构父类
//先构造的对象,后释放
//子类对象如何初始化父类成员
//继承中的构造和析构
//继承和组合混搭情况下,构造函数、析构函数调用顺序研究
#include <iostream>
using namespace std;
class Object
{
public:
  Object(const char* s)
  {
    cout<<"Object()"<<" "<<s<<endl;
  }
  ~Object()
  {
    cout<<"~Object()"<<endl;
  }
};
class Parent : public Object
{
public:
  Parent(const char* s) : Object(s)
  {
    cout<<"Parent()"<<" "<<s<<endl;
  }
  ~Parent()
  {
    cout<<"~Parent()"<<endl;
  }
};
class Child : public Parent
{
protected:
  Object o1;
  Object o2;
public:
  Child() : o2("o2"), o1("o1"), Parent("Parameter from Child!")
  {
    cout<<"Child()"<<endl;
  }
  ~Child()
  {
    cout<<"~Child()"<<endl;
  }
};
void run05()
{
  Child child;
}
int main05(int argc, char *argv[])
{
  cout<<"demo05_extend_construct_destory.cpp"<<endl;
  run05();
  system("pause");
  return 0;
}

(5)继承中的同名成员变量处理方法
1、当子类成员变量与父类成员变量同名时
2、子类依然从父类继承同名成员
3、在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符)
4、同名成员存储在内存中的不同位置

技术图片

 

 

 技术图片

 

 

 总结:同名成员变量和成员函数通过作用域分辨符进行区分

(6)派生类中的 static 关键字
继承和 static 关键字在一起会产生什么现象哪?
理论知识
  ? 基类定义的静态成员,将被所有派生类共享
  ? 根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问性质 (遵守派生类的访问控制)

  ? 派生类中访问静态成员,用以下形式显式说明:

类名 :: 成员
或通过对象访问 对象名 . 成员

 技术图片

 

 

 技术图片

 

 

 技术图片

 

 

 总结:
1> static 函数也遵守 3 个访问原则
2> static 易犯错误(不但要初始化,更重要的显示的告诉编译器分配内存)
3> 构造函数默认为 private

四:多继承

(1)多继承应用

多继承概念
? 一个类有多个直接基类的继承关系称为多继承
? 多继承声明语法
class 派生类名 : 访问控制 基类名 1 , 访问控制 基类名 2 , … , 访问控制 基类名 n
{

   数据成员和成员函数声明
};
? 类 C 可以根据访问控制同时继承类 A 和类 B 的成员,并添加自己的成员

技术图片

 

 

 多继承的派生类构造和访问
? 多个基类的派生类构造函数可以用初始式调用基类构造函数初始化数据成员
? 执行顺序与单继承构造函数情况类似。多个直接基类构造函数执行顺序取决于定义派生类时指定的各个继承基类的顺序。
? 一个派生类对象拥有多个直接或间接基类的成员。不同名成员访问不会出现二义性。如果不同的基类有同名成员,派生类对象访问时应该加以识别。

多继承简单应用
技术图片

 

 

 技术图片

 

 

 (2)虚继承

如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。

技术图片

 

 技术图片

 

 总结:
? 如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性
? 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象
? 要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类。
? 虚继承声明使用关键字 virtual

技术图片

 

 技术图片

 

 五:继承总结
? 继承是面向对象程序设计实现软件重用的重要方法。程序员可以在已有基类的基础上定义新的派生类。
? 单继承的派生类只有一个基类。多继承的派生类有多个基类。
? 派生类对基类成员的访问由继承方式和成员性质决定。
? 创建派生类对象时,先调用基类构造函数初始化派生类中的基类成员。调用析构函数的次序和调用构造函数的次序相反。
? C++提供虚继承机制,防止类继承关系中成员访问的二义性。
? 多继承提供了软件重用的强大功能,也增加了程序的复杂性。

以上是关于06_继承与派生的主要内容,如果未能解决你的问题,请参考以下文章

面向对象——继承派生组合以及接口

Python类与对象最全总结大全(类实例属性方法继承派生多态内建函数)

绑定与非绑定方法 继承 继承与抽象 查找属性关系 派生与覆盖 访问父类的内容

面向对象 继承 派生

C++_继承(菱形继承与虚基表)

浅析C++继承与派生