面向对象笔记__多个类

继承与派生的概念

基类与派生类

  • 继承是面向对象程序设计的一个重要特性,是软件复用的一种重要形式
  • 继承允许在原有类的基础上创建新的类,新类可以从一个或多个原有类中继承数据成员和成员函数,并且可以重新定义或增加新的成员,从而形成类的层次。继承具有传递性,不仅支持系统的可重用性,而且还促进系统的可扩充性。
  • 类的对象是各自封闭的,如果没有继承性机制,则类对象中数据、函数就会出现大量重复
  • 一个新类从已有的类那里获得其特性这种现象称为类的派生
  • 派生分为单级派生和多级派生
  • 继承分为单一继承(一个派生类只有一个基类派生)和多重继承(一个派生类有两个及两个以上的基类)
  • 派生类是基类的组合,可以把多重继承看做是多个单一继承的简单组合

派生类的定义

  • 定义派生类的一般形式为:

    class 派生类名:类派生列表{ //类体
    	成员列表
    };
    
  • 类派生列表指定了一个或多个基类,具有如下形式

      访问标号 基类名
    
    • 访问标号表示继承方式,可以是public(公有继承)、protected(保护继承)或private(私有继承),继承方式决定了对继承成员的访问权限。如果未给出访问标号则默认为private
  • 派生类的成员列表描述的是派生类自己新增加的数据成员和成员函数

  • 注意:

    • 如果基类定义了静态成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个静态成员只有一个实例
    • 静态成员遵循常规访问控制:如果静态成员在基类中为私有的,则派生类不能访问它,如果该静态成员在基类是共公有的,则基类可以访问它,派生类也可以访问它
  • 派生类的设计

    • 从基类接收成员:除了构造函数和析构函数,派生类会把基类全部的成员继承过来。这种继承是没有选择的,不能选择接受一部分放弃另一部分
    • 调整基类成员的访问:派生类接收基类成员是不能选择的,但是可以对这些成员作出访问策略
    • 修改基类成员:可以在派生类中声明一个与基类成员同名的成员,则派生类中的新成员会覆盖基类的同名成员,就实现了修改基类成员功能的效果。
    • 在定义派生类时增加新的成员

类成员的访问

  • 如果没有继承,一个类只有两种类型的访问者:类成员和类用户。将类划分为private和public访问级别反映对访问者的访问权限:类用户只能访问公有成员,类成员和友元既可以访问公有成员也可以访问私有成员。有了继承,就有了类的第三种访问者:派生类成员
  • 派生类通常需要访问(一般为私有的)基类成员,为了允许这种访问而仍然禁止外部对基类的一般访问,可以使用protected访问标号。类的protected部分仍然不能被类用户访问,但是可以被派生类访问。
  • 只有基类类成员及其友元可以访问基类的private部分,派生类不能访问基类的私有成员
  • 对基类成员和派生类自身的成员是按不同的原则处理的,需要考虑以下6种情形:
    1. 基类的成员和友元访问基类成员
    2. 派生类的成员和友元访问派生类自己新增的成员
    3. 基类的成员访问派生类新增的成员
      • 基类的成员不能直接访问派生类的成员(因为有基类的时候尚未有派生类),但可以通过虚函数间接访问派生类的成员
    4. 派生类新增的成员访问基类的类成员
    5. 类用户访问派生类的成员
      • 基类的成员不能直接访问派生类的成员(因为有基类的时候尚未有派生类),但可以通过虚函数间接访问派生类的成员
    6. 类用户访问派生类的基类成员
    • 其中4、6是由访问标号决定的(不同的继承方式决定了基类成员在派生类中的访问属性):
      • 公有继承
        属性变化 对象访问 成员访问
        public -> public Y Y
        protected -> protected N Y
        private -> private N N
      • 保护继承
        属性变化 对象访问 成员访问
        public -> protected N Y
        protected -> protected N Y
        private -> protected N N
      • 私有继承
        属性变化 对象访问 成员访问
        public -> private N Y
        protected -> private N Y
        private -> private N N
    • 无论采用何种继承方式得到的派生类,派生类成员及其友元都不能访问基类的私有成员,派生类外部的用户只能访问公有属性的成员
    • 保护继承和私有继承中的属性变化意味着在派生类的派生类中,部分继承到的资源访问收到进一步限制。
  • 赋值兼容规则:赋值兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代
    • 派生类的对象可以赋值给基类对象
    • 派生类的对象可以初始化基类的引用
    • 派生类对象的地址可以赋给指向基类的指针

派生对象的产生

  • 在定义派生类时,派生类并没有把基类的构造函数和析构函数继承下来。因此,对继承的基类成员初始化的工作主要由派生类的构造函数来承担,同时基类的析构函数也要被派生类的析构函数来调用。
  • 派生类构造函数的定义,在执行派生类的构造函数时,使派生类的数据成员和基类的数据成员同时都被初始化。其定义形式如下:
    派生类名(形式参数列表):基类名(基类构造函数实参列表),派生类初始化列表
    {
    派生了初始化函数体
    }
  • 组合关系的派生类的构造函数
    • 假定派生类A和类B的关系是组合关系,类A中有类B的子对象。如果类B有默认构造函数,或者参数全市默认参数的构造函数,或者有无参数的偶早函数,那么类A的构造函数中可以不用显式初始化子对象。编译器总是会自动调用B的构造函数进行初始化
    • 可以在一个类的构造函数中显示地初始化其子对象,×××初始化式只能在构造函数初始化列表中×××,形式为:
      类名(形式参数列表):子对象名(子对象构造函数实参列表),类初始化列表
      {
      类初始化函数体
      }
    • 调用顺序为:
      • 调用基类构造函数
      • 调用子对象构造函数,各个子对象按其声明的次序先后调用;
      • 执行派生类初始化列表
      • 执行派生类初始化函数体
  • 注意
    • 如果在基类和子对象所属类的定义中都没有定义带参数的构造函数,而且也不需要对派生自己的数据成员初始化,那么可以不必显示地定义派生类构造函数。派生类会合成一个默认构造函数,并在调用派生类构造时,会自动先调用基类的默认构造函数和子对象所属类的默认构造函数
    • 如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么,在定义派生类构造函数时可以不显式地调用基类构造函数。在调用派生类构造函数,系统会自动先调用基类的无参数构造函数或默认构造函数
    • 如果在基类或子对象所属类的定义中定义了带参数的构造函数,那么就必须显式地定义派生类构造函数,并在派生类构造函数中显式地调用基类或子对象所属类的构造函数**(依据合成构造函数的性质)**
    • 如果在基类中既定义了无参数的构造函数,又定义了有参的构造函数(构造函数重载),则在定义派生类构造函数时,既可以显式调用基类构造函数,也可以不调用基类构造函数。(依据合成构造函数的性质)

继承体系横向扩展:多重继承

多重继承派生类

  • 多重继承派生类的定义
	class D:public B, public C 
	{
     
		body
	};
  • 多重继承派生类的构造函数
    • 多重继承派生类的构造函数形式与单一继承时的构造函数形式基本相同,只是在派生类的构造函数初始化列表中调用多个基类构造函数。一般形式为:
    •   	派生类名(形式参数列表):基类名1(基类构造函数实参列表),基类名2(基类2构造函数实参列表)
             派生类初始化列表
        	{
               
        		派生类初始化函数体
        	}
        	```
      
      

二义性问题

  • 多重继承时,多个基类可能出现同名的成员。在派生类中如果使用一个表达式的含义能解释为可以访问多个基类的成员,则这种对基类成员的访问就是不确定的,称这种访问具有二义性。C++要求派生类对基类成员的访问必须是无二义性的
    • 解决方法:使用成员名限定可以消除二义性,名字支配规则
class A {
     public:
			void fun() {
     }
};
class B {
     public:
			void fun() {
     }
			void gun() {
     }
};
class C:public A, public B{
     public:
			void gun() {
     }//重写gun()
			void hun() {
      fun();}//出现二义性
};
	使用成员名限定可以消除二义性,例如:
	c.A::fun();
	c.B::fun();
	p->A::fun();
	p->B::fun();
  • 名字支配规则:C++对于在不同的作用域声明的名字,可见性原则是:如果存在两个或多个具有包含关系的作用域,外层声明了一个名字,而内层没有再次声明相同的名字,则外层名字在内层不可见,这时称内层名字隐藏(或覆盖)了外层名字,这种现象称为隐藏规则
    • 在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用域是不同的:基类在外层,派生类在内层
    • 如果派生类声明了一个和基类成员同名的新成员,派生的新成员就覆盖了基类同名成员,直接使用成员名只能访问到派生类的成员
    • 如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数不同,从基类继承的同名函数的所有重载形式也都会被覆盖
    • 如果要访问被覆盖的成员,就需要使用基类名和作用域限定符

重复继承问题

  • 解决方法:虚基类
  • C++提供虚基类(virtual base class)的机制,使得在继承间接共同基类时只保留一份成员
    class A
    class B1:public A{
           
    }
    class B2:public A{
           
    }
    class D:public B1,public B2{
           
    }//此时D继承了A两次
    
  • 虚基类的定义
class 派生类名: virtual 访问标号 虚基类名,...{
     //类体
	成员列表
};

需要注意,为类保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承

  • 虚基类举例
#include 
using namespace std;
class A //声明为基类A
{
     
public://外部接口
	A(int n){
     }
	void fun(){
     }
private:
	int nv;//私有数据成员
};
class B1:virtual public A //声明A为虚基类
{
     
public:
	B1(int a):A(a){
      cout <<"Member of B1"<<endl;}//B1类的构造函数
private:
	int nv1;
};
class B2:virtual public A //声明A为虚基类
{
     
pubic:
	B2(int a):A(a){
     };
private:
	int nv2;
};
class C:public B1,public B2
{
     
public:
	//派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用
	C(int a):A(a), B1(a), B2(a){
     }
	void fund(){
     }
private:
	int bvd;
};
int main()
{
     
	C c1(1);
	c1.fund();
	c1.fun();//不会产生二义性
	return 0;
}

  • 虚基类的初始化
    • 如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接和间接派生)中,都要通过构造函数的初始化表对虚基类进行初始化。
    • 在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化
  • 注意:
    • 派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的默认构造函数
    • 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行

继承体系纵向扩展:多态

多态性概念:

  • 多态是指同样的消息被不同类型的对象接受时导致不同的行为。所谓消息是指对类成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数

  • 面向对象程序设计的真正力量不仅仅是继承,而是允许派生类对象像基类对象一样处理,其核心机制就是多态动态联编**

  • 从广义上说,多态性是指***一段程序能够处理多种类型对象的能力***(同一段程序,处理不同的对象时,发生不同的动作)。在C++中,这种多态性可以通过(前两个一大类,后两个一大类)

    • 重载多态(函数和运算符重载)
      • 重载是多态性的最简单形式,分为函数重载和运算符重载
      • 重定义已有的函数称为函数重载。在C++中既允许重载一般函数,也允许重载类的成员函数。如对构造函数进行重载定义,可以使程序有集中不同的途径对类对象进行初始化
      • C++允许为类重定义已有运算符的语义,使系统预定义的运算符可操作于类对象。如流插入(<<)运算符和流提取(>>)运算符(原先语义是位移运算)
    • 包含多态(继承及虚函数)
      • CPP中采用虚函数实现包含多态。虚函数为C++提供了更为灵活的多态机制,这种多态性在程序运行时才能确定,因此虚函数是多态性的精华,至少含有一个虚函数的类型称为多态类。包含多态在面向对象程序设计中使用十分频繁
      • 派生类继承基类的所有操作,或者说,基类的操作能被用于操作派生类的对象。当基类的操作不能适应派生类时,派生类就需要重载基类的操作
    • 强制多态(强制类型转换)
      • 强制多态也称类型转换
      • C++定义了基本数据类型之间的转换规则,即:
        • char -> short -> int -> unsigned -> long -> unsigned ->long ->float ->double ->long double
      • 同时,可以在表达式中使用3种强制类型转换表达式:
        • static_cast (E)
        • T(E)
        • (T)E
    • 类型参数化多态(模板)
      • 参数化多态即:将类型作为函数或类的参数,避免了为各种不同的数据类型编写不同的函数或类,减轻了设计者负担,提高了程序设计的灵活性
      • 模板是C++实现参数化多态性的工具,分为函数模板和类模板。类模板中的成员函数均为函数模板,因此函数模板是为类模板服务的

包含多态

  • 当函数名称相同,签名不同时,用重载实现多态,当函数名相同,签名也相同时,用包含多态(虚函数)实现多态

  • 联编(binding)又称绑定,就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数分配内存地址,并且对外部访问也分配正确的内存地址

  • 在编译阶段就将函数实现和函数调用绑定起来称为静态联编(static binding)。静态联编在编译阶段就必须了解所有函数或模块执行所需要的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型。C语言中,所有的联编都是静态联编,C++中一般情况下联编也是静态联编。

  • 静态联编举例:

#include 
using namespace std;
class Point {
      //Point类表示平面上的点
	double x, y;//坐标值
public:
	Point(double x1 = 0, double y1 = 0):x(x1), y(y1){
     }//构造函数
	double area() {
     return 0;} //计算面积
};
class Circle:public Point {
      //Circle 类表示圆
	double r; //半径
public:
	Circle(double x, double y, double r1):Point(x, y),r(r1){
     }
	double area() {
      return 3.14*r*r;} //计算面积
};
int main()
{
      
	Point a(2.5, 2.5); Circle c(2.5, 2.5, 1);
	cout << "Point area = " << a.area() << endl; //基类对象
	cout << "Circle area = " << c.area() << endl; //派生类对象
	Point *pc = &c, &rc = c; //基类指针、引用指向或引用派生类对象
	cout << "Circle area = " << pc -> area() << endl;//静态联编基类调用
	cout << "Circle area = " << rc.area() << endl; //静态联编基类调用
	return 0;
}
运行结果:
	Point area = 0
	Circle area = 3.14
	Circle area = 0    //pc指针是Point类,因为是静态联编,所以pc->area()调用的是基类的area()函数 编译的时候将基类函数和基类的指针和引用绑定在一起
	Circle area = 0
  • 动态联编
    • 如果在编译 ‘Point *pc = &c’ 时,只根据兼容性规则检查它的合理性,即检查它是否符合派生类对象的地址可以赋给基类的指针的条件。至于"pc->area()"调用哪个函数,等到程序运行到这里再决定
    • 如果希望"pc->area()"调用Circle::area(),也就是使类Point的指针pc指向派生类函数area的地址,则需要将Point类的area函数设置成虚函数
    • 虚函数的定义形式为:
      virtual double area() {return 0;} //计算面积
  • 动态联编举例:
#include 
using namespace std;
class Point {
      //Point类表示平面上的点
	double x, y;//坐标值
public:
	Point(double x1 = 0, double y1 = 0):x(x1), y(y1){
     }//构造函数
	virtual double area() {
     return 0;} //虚函数
};
class Circle:public Point {
      //Circle 类表示圆
	double r; //半径
public:
	Circle(double x, double y, double r1):Point(x, y),r(r1){
     }
	double area() {
      return 3.14*r*r;} //虚函数
};
int main()
{
      
	Point a(2.5, 2.5); Circle c(2.5, 2.5, 1);
	cout << "Point area = " << a.area() << endl; //基类对象
	cout << "Circle area = " << c.area() << endl; //派生类对象
	Point *pc = &a; //基类指针指向基类对象
	cout << "Circle area = " << pc -> area() << endl;
	pc =&c; //基类指针指向派生类对象
	cout << "Circle area = " << pc->area() << endl; //动态联编
	return 0;
}
运行结果:
	Point area = 0
	Circle area = 3.14
	Circle area = 0
	Circle area = 3.14
  • 虚函数的调用规则是:根据当前对象,优先调用对象本身的虚成员函数。这和名字支配规律类似,不过虚函数是动态联编的,是在运行时(通过虚函数表中的函数地址)”间接“调用实际上欲联编的函数。

  • 注意:

    • 只有用指针或者引用调用函数时候才有动态联编
    • 被virtual关键字修饰的成员函数,就是虚函数虚函数的作用就是实现多态性————以共同的方法,对不同的对象采取不同的策略
    • 需要注意,virtual只在类体中使用
      • 当在派生类中定义了一个同名的成员函数时,只要该成员函数的参数个数、参数类型以及返回类型与基类中同名的虚函数完全一样,则派生类的这个成员函数无论是否使用virtual,它都将会成为一个虚函数
      • 程序员习惯给派生类的同名函数也加上virtual,便于阅读理解
    • 使用虚函数并不一定产生多态性,也不一定使用动态联编。例如,在调用中对虚函数使用成员名限定,可以强制C++对该函数的调用使用静态联编
  • 虚函数实现多态的条件

    1. 类之间的继承关系满足赋值兼容性规则(赋值兼容规则仅针对公有继承)
    2. 改写了同名的虚函数
    3. 根据赋值兼容性规则使用指针(或引用)
    • 满足前两条并不一定产生动态联编,必须同时满足三条才能实现动态联编
  • 类成员函数的指针与多态性

    • 在派生类中,当一个指向基类成员函数的指针指向一个虚函数,并且通过指向对象的基类指针(或引用)访问这个虚函数时,仍将发生多态性。
      #include 
      using namespace std
      class Base {
               
      	public: virtual void print() {
                cout << "Base" << endl;} //虚函数
      };
      class Derived: public Base{
               
      	public: void print() {
               cout <<"derived" << endl; }//虚函数
      };
      void display(Base *p, void(Base::*pf)())
      {
               (p->*pf)();}
      int main()
      {
               
      			Derived  d;
      			Base b;
      			display(&d, &Base::print); //输出Derived
      			display(&b, &Base::print); //输出Base
      			return 0;
      }
      
  • 何时需要虚函数

    • 首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后有无可能被更改功能,如果希望派生类更改其功能的,一般应该将它声明为虚函数
    • 如果成员函数在类被继承后功能不需要修改,或派生类用不到该函数,则不要吧它声明为虚函数。不要仅仅考虑到要作为基类而把类中的所有成员函数都声明为虚函数
    • 应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
  • 虚析构函数

    • 派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数
      • 但是,如果用new运算符建立了派生类对象,且定义了一个基类的指针指向这个对象,那么当用delete运算符撤销对象时,系统只会执行基类的析构函数,而不执行派生类的析构函数,因而也无法对派生类对象进行真正的撤销清理工作
    • 例如:
      Point *pp = new Circle; //基类指针指向派生类
      delete pp;//仅执行基类析构函数
      
    • 如果希望“deletepp”执行Circle的析构函数,那么基类Point的析构函数要声明为虚函数,称为虚析构函数。
    • 如果将基类的析构函数声明为虚函数,由该基类所派生的所有派生类的析构函数也都自动成为虚函数,即使派生类的析构函数与基类的析构函数名字不同
    • 当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统总会采用动态联编,调用正确的析构函数,对该对象进行清理
  • 纯虚函数和抽象类

    • 在许多情况下,不能在基类中为虚函数给出一个有意义的定义,这时可以将它说明为纯虚函数(pure virtual function),将具体定义留给派生类去做。纯虚函数的定义形式为:
      virtual 返回类型 函数名(形式参数列表) = 0; \\在需函数的原型声明后加上“=0”,表示纯需函数根本没有函数体
    • 纯虚函数的作用是在基类中为其派生类保留一个函数的名字,以便派生类根据需要对它进行定义。如果在一个类中声明了纯虚函数,而在其派生类中没有对该函数的定义,则该虚函数在派生类中仍然为纯虚函数
    • 包含有纯虚函数的类称为抽象类(abstract class)。一个抽象类只能作为基类来派生新类,所以又称为抽象基类(abstract base class)。抽象类不能定义对象。
    • 如果在派生类中给出了抽象类的纯虚函数的实现,则该派生类不再是抽象类。否则只要派生类仍然有纯虚函数,则派生类依然是抽象类。抽象类至少含有一个虚函数,而且至少有一个虚函数是纯虚函数。

你可能感兴趣的:(CPP学习,c++)