从零学习C++第七章:继承(类的复用)——派生类

7.1 概述

7.1.1 类之间的继承关系——基类与派生类

  • 基类(base class:被继承类
  • 派生类(derived class:继承后得到的类

C++中,继承分为单继承多继承

 

7.1.2 继承的作用

  1. 支持软件复用
  2. 对事物按层次进行分类
    • 一般与特殊的关系(is-a-kind-of)
  3. 对概念进行组合
  4. 支持软件的增量开发

 


7.2 单继承

7.2.1 单继承派生类的定义

  • 单继承(single inheritance):一个派生类只有一个直接基类
    • 定义格式:class <派生类名>:<继承方式><基类名> { <成员说明表> }
      • 继承方式默认private
  • 对派生类的规定:
    1. 派生类包含基类的所有成员(基类的构造函数和赋值操作符重载函数除外)
      • 可以把派生类的对象理解成它包含了基类的一个字对象,该字对象的内存空间位于派生类对象内存空间的前部
    2. 定义派生类时一定要有基类的定义
    3. 基类的友元不是派生类的友元;基类是另一个类的友元,派生类不是该类的友元

 

7.2.2 在派生类访问基类成员

 

7.2.2.1 protected访问控制

  • C++中,派生类不能直接访问基类的私有成员,必须通过基类的非私有成员函数来访问基类的私有成员
    • 类的private成员不能在类外使用(继承与封装的矛盾
  • 一个类有两种不同的用户:类的实例用户(对象)和派生类
  • C++中,引入protected成员访问控制符来缓解继承与数据封装的矛盾
    • 在基类中声明的protected成员可以被派生类使用,但不能对基类的实例用户使用
    • 一个类就存在两个接口
      • 一个接口由类的public成员构成,提供给实例用户使用;
      • 另一个接口由类的public和protected成员构成,提供给派生类使用
    • 般把今后不太可能发生变动的、有可能被派生类使用的、不宜对实例用户公开的成员声明为protected

 

7.2.2.2 标识符作用域的限制

  • 如果派生类中定义了和基类同名的数据成员,该成员名在派生类的作用域中不直接可见(隐藏状态,hidden),必须使用基类名受限方式访问
  • 如果派生类中定义了和基类同名的成员函数,即使参数不同,基类的同名函数在派生类的作用域也是不直接可见的,必须用基类名受限方式访问

 

7.2.3 派生类对基类成员的访问控制

  • 继承方式的含义如下:
    1. public继承方式
      • 基类的public成员,派生类中成为public成员
      • 基类的protected成员,派生类中成为protected成员
      • 基类的private成员,派生类中成为不可直接使用的成员
        • public继承方式的特殊含义:
          • 派生类可以看成基类的子类型(subtype),即对基类对象所能实施的操作也能作用于派生类对象,以及在需要基类对象的地方可以用派生类对象代替
    2. private继承方式
      • 基类的public成员,派生类中成为private成员
      • 基类的protected成员,派生类中成为private成员
      • 基类的private成员,派生类中成为不可直接使用的成员
    3. protected继承方式
      • 基类的public成员,派生类中成为protected成员
      • 基类的protected成员,派生类中成为protected成员
      • 基类的private成员,派生类中成为不可直接使用的成员
    1. 继承方式的调整
      • 可在派生类中对某个继承成员的继承方式进行调整
      • 格式:[ public: | private: | protected: ] <基类名>: :<基类成员名>;
      • 对基类的一个成员函数名的访问控制的调整,将调整基类所有具有该名的重载函数
        • 如果在派生类中定义了与基类同名成员函数,则不能对该函数进行访问控制调整

 

  • 一个派生类的继承方式将影响该派生类的实例用户和该派生类的派生类对从基类继承来的成员的访问限制

 

7.2.4 派生类对象的初始化和赋值操作

  • 派生类对象的初始化有基类和派生类共同完成
    • 创建派生类的对象时,先调用派生的构造函数,在进入其函数体之前,调用基类的构造函数,然后执行函数体
      • 默认调用基类的默认构造函数,可用成员初始化表显式调用非默认构造函数
  • 派生类中包含非基类成员对象
    • 创建该类对象时,先调用派生类的构造函数,在进入其函数体之前,先调用基类的构造函数,然后调用成员对象类的构造函数,最后执行自己的函数体
    • 撤销该类对象时,先调用和执行自己的析构函数,然后调用成员对象类的析构函数,最后执行基类的构造函数
  • 拷贝构造函数
    • 派生类的隐式拷贝构造函数(编译器提供)将会调用基类的拷贝构造函数
    • 派生类自定义的拷贝构造函数默认调用基类的默认构造函数,可用成员初始化表显式指出调用基类的拷贝构造函数
  • 派生类不从基类继承赋值操作
    • 如果派生类没有定义赋值操作符重载函数,系统提供一个隐式的的赋值操作符重载函数:对基类成员调用基类的赋值操作符进行赋值,对派生类成员按逐个成员赋值
    • 派生类中自定义赋值操作符重载函数中需要显式调用基类的赋值操作符来实现基类成员的赋值

 

class A { ... };

class B : public A{
	...
public:
	B& operator = (const B& b){
		if( &b==this ){ //防止自身赋值
			return *this; //调用基类的赋值操作符对基类成员进行赋值
		}
		*(A*)this=b;
		...
		return *this;
	}
}

 为了能对基类数据成员进行赋值,把this指针强制转换为基类类型,这样通过this指针就能得到基类的子对象,而对基类对象进行赋值操作就会调用基类的赋值操作符函数

 

7.2.5 单继承的应用(一般与特殊的关系)

  • 人与学生,员工与经理等

 

7.2.6 类之间的聚集关系

  • 面向对象程序设计中实现代码复用的手段:继承与聚集
    • 继承:一般和特殊(is-a-kind-of)
      • 实现方式:基类与派生类
    • 聚集:部分与整体(is-a-part-of)
      • 实现方式:类的成员对象
  • 从纯代码复用来说,聚集可以避免继承与封装的矛盾
  • 继承机制更容易实现类之间的子类型关系

 


7.3 消息(成员函数调用)的动态绑定

7.3.1 消息的多态性

  • C++将类看作类型,将以public方式继承的派生类看作基类的子类型,使得C++面向对象程序设计中存在三种多态(派生类以public继承)
    1. 对象类型的多态
      • 派生类对象的类型既可以是派生类,也可以是基类,即一个对象可以属于多种类型
    2. 对象标识的多态
      • 基类的指针或引用可以指向或引用基类对象,也可以指向或引用派生类对象,即一个对象标识可以属于多种类型,它可以标识多种对象。
      • 在对象标识符定义时指定的类型称为它的静态类型,在运行时实际标识的对象的类型称为它的动态类型
    3. 消息的多态
      • 一个可以发送到基类对象的消息,也可以发送到派生类对象,从而可能得到不同的解释
  • 2)和3)的多态性带来了成员函数调用绑定问题
    • 如果派生类和基类定义了相同的成员函数,怎样确定调用那个类的成员函数?
      • 解决方法:
        1. 采用静态绑定(C++默认绑定方式,即行参x和p引用和指向的是A,所以func1(a); func2(&a); func1(b); func2(&b);都是调用A::f()
          • 在编译时刻确定
        2. 采用动态绑定(根据行参x和p实际引用的对象类型(动态类型)来确定f属于哪一个类,所以func1(a); func2(&a); 调用的是A,func1(b); func2(&b);调用的是B
          • 在运行时刻确定

 

class A{
public:
	void f();
};

class B: public A{
public:
	void f();
};


void func1 (A& x){  //x可以是A的引用也可以是B的引用
	x.f;  //调用A::f还是B::f
}

void func2(A *p){  //p可以指向A也可以指向B
	p->f();  //调用A::f还是B::f
}


A  a;
func1(a);
func2(&a);
B b;
func1(b);
func2(&b);

7.3.2 虚函数与消息的动态绑定

  • 为了实现动态绑定,可以把基类中同名成员函数声明为虚函数(virtual function

class A{

public:

virtual void f();  //虚函数

};

  • func1和func2中的x.f()和p->f()就会在运行时刻根据x和p实际引用和指向的对象类型来确定调用哪个函数了
  • 虚函数的动态绑定
    • 基类与派生类定义相同型构的成员函数
      • 如果基类中的一个成员函数被定义了虚函数,那么在派生类中定义与之相同型构的所有成员函数是对基类该成员函数的重定义(或称覆盖,override)派生类的成员函数也是虚函数
      • 如果基类中的一个成员函数没有被定义虚函数,那么在派生类中定义与之相同型构的所有成员函数只是隐藏(hide)了基类的同名成员函数
    • 只有通过引用或指针访问对象类的虚函数才能实现动态绑定
    • 虚函数能够实现消息的动态绑定,从而允许派生类对基类的一些成员函数进行重定义
    • 利用虚函数机制,可以通过基类的指针或引用访问派生类中对基类重定义的成员函数
      • 通过类型转换操作(dynamic——cast)实现基类到派生类指针的转换
  • 虚函数的限制
    1. 只有类的成员函数才可以是虚函数
    2. 静态成员函数不能是虚函数
    3. 构造函数不能是虚函数
    4. 析构函数可以(往往)是虚函数
  • 虚函数应用场景:实现动态绑定

 

7.3.3 纯虚函数和抽象类

  • 纯虚函数(pure virtual function:只给出函数声明而没给出实现的虚成员函数
    • 格式为:在函数原型后面加“=0”

virtual int f()=0;

 

  • 包含纯虚函数的类成为抽象类(abstact class,抽象类不能创建对象
    • 作用:为派生类提供一个基本框架和公共的对外接口,派生类应对抽象基类的所有纯虚成员函数进行实现

 

 1、用抽象类实现一个抽象数据类型Stack(栈结构)

思路:栈结构两个功能:进栈(push)、出栈(pop),但为了能接受不同的数据类型(ArrayStack、LinkedStack)的栈类进行操作操作,可给这两个类定义一个抽象的基类Stack,在该抽象基类中定义pop和push两个纯虚函数

//抽象基类
class Stack{
public:
	virtual bool push(int i)=0;//声明纯虚函数push
	virtual bool pop(int& i)=0;//声明纯虚函数pop
};


#include"Stack.h"
class ArrayStack : public Stack{
int elements[100],top;
public:
	ArrayStack();
	bool push(int i);//派生类定义与基类纯虚函数相同型构的成员函数,是对基类成员函数的重定义
	bool pop(int& i);
};
#include"ArrayStack.h"
ArrayStack::ArrayStack(){
	top=-1;
}
bool ArrayStack::push(int i){
	...;
}
bool ArrayStack::pop(int& i){
	...;
}


#include"Stack.h"
class LinkedStack : public Stack{
struct Node{
int content;
	Node* next;
}*first;
public:
	LinkedStack();
	bool push(int i);
	bool pop(int& i);
};
#include"LinkedStack.h"
LinkedStack::LinkedStack(){
	first = nullptr;
}
bool LinkedStack::push(inti){
	...;
}
bool LinkedStack::pop(int&i){
	...;
}


#include"chapter7/Stack.h"
#include"chapter7/ArrayStack.h"
#include"chapter7/LinkedStack.h"

voidf(Stack*p){
	p->push(...);//根据p指向的对象确定push的归属
	p->pop(...);//根据p所指的对象确定pop的归属
}

int main(){
	ArrayStack arrayStack;
	LinkedStack linkedStack;
	f( &arrayStack );//OK
	f( &linkedStack );//OK

	return 0;
}

2、用抽象类解决一种类型描述多种类型数据问题

思路:用抽象类、继承、虚函数的动态绑定实现

 

  • 抽象类的两种描述功能:
    1. 对同一类型的不同实现给出抽象性描述(Stack)
    2. 对不同类型的公共特征进行集中描述(Figure)

 

  • 通过给类定义抽象基类,把该类的数据隐藏,避免非法访问该类的非public对象

 

//I _A.h
class I_A{
public:
	virtual void f ( int ) = 0;
}

//A.cpp
#include"I_A.h"
class A : public I_A{
	int i , j ;
public :
	A ( ) ;
	A ( int x, int y ) ;
	void f ( int x ) ;
}

对于类A的实例对象,不提供类A的定义("A.h"),提供I_A的定义("I_A.h"),这样类A的实例用户就不知道有什么数据成员了

//B.cpp
#include"I_A.h"
void func ( I_A *p ){
	p->f(2); //ok
	... //这里不知道p所指向的对象有哪些数据成员,因此无法访问它的数据成员
}

7.3.4 虚函数动态绑定的一种实现

  • 如果一个类中有虚函数(包括从基类继承来的),那么编译器会为虚函数创建一个虚函数表(vtbl),表中记录了该类中所有虚函数的入口地址。
  • 当创建一个包含虚函数的类的对象时,在所创建对象的内存空间中有一个隐藏指针(vptr)指向该对象所属类的虚函数表

 

  • 当通过基类的引用或指针访问基类的虚成员函数时,他将利用实际引用或指向的对象的虚函数表来动态绑定调用的函数
  • 当通过基类的引用或指针访问基类的非虚成员函数和直接通过对象来访问类的成员函数(包括虚函数)时,则不用虚函数表进行动态绑定,而是采用静态绑定

 


7.4 多继承

  • 多继承(multiple inheritance:一个类可以有两个或两个以上的直接基类
    • 必要性:多继承增强了语言的表达能力,能够自然、方便地描述问题领域中存在于对象类之间的多继承关系
    • 定义:需要给出两个或两个以上的直接基类
      • class <派生类名> : <继承方式> <基类名1>,<继承方式><基类名2>, ... { <成员说明表> };
    • 多继承注意:
      1. 继承方式及访问控制的规定同单继承
      2. 派生类拥有所有基类的所有成员
      3. 基类的声明次序决定:
        • 对基类构造函数/析构函数的调用顺序
        • 对基类数据成员的存储安排

 

  • 若以public继承方式定义的多继承派生类对象的地址赋给任何一个基类指针,这时地址会自动调整

 

  • 命名冲突:多个基类包含同名成员,在派生类中会出现命名冲突(name confliction,出现二义性
    • 解决方法:采用基类名受限访问

 

  • 重复继承:多继承中,如果直接基类有公共的基类,则会出现重复继承(repeated inheritance,这时公共基类的数据成员在多继承的派生类中就有多个拷贝
    • 若类B继承类A,类C继承类A,类D多继承类B和类C,则类A为类D的直接基类(类B和类C)的公共基类
    • 解决方法:将公共基类(类A)定义为虚基类(virtual base class
      • 虚基类注意:
        1. 虚基类的构造函数由最新派生出的类的构造函数调用
        2. 虚基类的构造函数优先于非虚基类的构造函数执行

 


7.5 小结

  • 继承
    • 含义
      • 继承是指定义一个新的类时,首先把一个或多个已有类的功能全部包含进来,然后再给出新功能的定义或对已有类的功能重新定义。在C++中,被继承的类称为基类;从其他类继承的类称为派生类。
    • 种类
      • C++支持单继承和多继承。
    • 继承方式
      • public继承方式实现类之间的子类型关系,protected和private继承用于纯粹的代码复用
    • 访问控制
      • 派生类不能直接访问基类的private成员
      • protected的访问控制使得类有两种接口:与实例用户的接口(public成员)和与派生类的接口(public和protected成员)
    • 多继承
      • 命名冲突:基类名受限访问
      • 重复继承:虚基类
    • 作用
      • 继承为软件复用提供一种手段。
      • 从代码复用的角度来收说,聚集比继承更好,它可以避免继承与封装的矛盾
  • 多态
    • 派生类重新定义基类的成员函数时,为了实现调用成员函数时动态绑定,需在基类中将其定义为虚函数
  • 纯虚函数
    • 只给出函数声明而没给出实现的虚成员函数
  • 抽象类
    • 含义
      • 包含纯虚函数的类
    • 作用
      • 为派生类提供一个基本框架和公共的对外接口,派生类应对抽象基类的所有纯虚成员函数进行实现

参考:《程序设计教程:用C++语言编程》 陈家骏,郑滔

你可能感兴趣的:(c++)