c++ 继承与多态, 虚基类,虚函数,纯虚函数

继承与多态

继承与派生

c++通过类派生来支持继承,被继承的类型称为基类(baseclass)或超类(superclass),而产生的类为派生类(derived class)或子类(subclass)。基类和派生类的集合称作类的层次结构(hierarchy).

定义:

class 派生类名 : 访问限定符 基类名1<,访问限定符 基类名2, ... , 访问限定符 基类名n>{

//成员表  新增的或替代父类的成员
}

同一个派生类可以同时继承多个基类,称为多重继承(multi-inheritance)

单继承(single-inheritance): 一个派生类只继承一个基类

先讨论单继承:

编写派生类的4个步骤:

  1. 吸收基类的成员除构造函数,析构函数,运算符重载函数,友元函数外所有的数据成员和函数成员全都成为派生类的成员
  2. 改造基类成员,当有的基类成员在新的应用中不合适时,可以进行改造,如果派生类声明一个和某个基类成员同名的成员,派生类中的成员会屏蔽基类同名的成员,类似函数中的局部变量屏蔽全局变量如果是成员函数,参数表和返回值完全一样时,称为同名覆盖(override),否则为重载
  3. 发展新成员,增加新的数据成员和函数成员,更适合新的应用
  4. 重写构造函数和析构函数
继承方式

继承方式分为三种,公有方式public,保护方式protected,私有方式private,如不显示的给出继承方式关键字,则默认为私有继承

在一层继承关系中,private和protected在行为上完全相同,但是当有两层或多层继承时,新保护派生的派生类可访问底层基类的公有和保护成员,而私有继承不可访问。

简单理解:会将基类中的访问限定符使用继承限定符进行进一步的访问限定

  • public 继承会保留原访问形式
  • priotected 会将public 进一步限定为protected
  • private 会将public 和protected进一步限定为private
派生方式 基类中的访问限定符 在派生类中对基类的访问限定 在派生类对象外访问派生类对象的基类成员
公有派生 public public public (可直接访问) 可直接访问
公有派生 public protected protected(可直接访问) 不可直接访问
公有派生 public private 不可直接访问 不可直接访问
私有派生 private public private(可直接访问) 不可直接访问
私有派生 private protected private(可直接访问) 不可直接访问
私有派生 private private 不可直接访问 不可直接访问
构造和析构函数

派生类的构造函数的定义:

派生类名:: 派生类 (参数总表): 基类名1(参数名表)<, 基类名2(参数名表), ..., 基类名2(参数名表)><,成员对象名1(成员对象参数名表)1, ... >){
// 新增成员的初始化}

在类体的声明中不需要写":"后面的部分。
派生类构造函数各部分的执行次序:

  1. 调用基类构造函数,按他们在派生类定义中的先后顺序依次调用, 若未显示声明则默认调用基类中的无参构造函数
  2. 调用新增成员对象的构造函数,按他们在类定义中排列的先后顺序依次调用
  3. 派生类的构造函数体中的初始化操作

在析构函数中只要处理好新增的成员就好。对于新增的成员对象和基类中的成员,系统会调用成员对象和基类的析构函数。析构函数各部分的执行次序与构造函数相反。

在实际中,成员对象的使用或者聚合是一种完善的封装,推荐将数据,数据的操作,资源的动态分配与释放封装在一个完善的子系统中,就像string。

虚基类

对于多继承(环状继承),A->D, B->D, C->(A,B),例如:

class D{......};
class B: public D{......};
class A: public D{......};
class C: public B, public A{.....};

这个继承会使D创建两个对象,要解决上面问题就要用虚继承方式:
将class D这个共同基类设置为虚基类,这样从不同路径中继承来的同名数据成员在内存中就合并为1个。
格式:class 类名: virtual 继承方式 父类名
其中,virtual关键字支队紧随其后的基类名起作用。

class D{......};
class B: virtual public D{......};
class A: virtual public D{......};
class C: public B, public A{.....};

在虚基类对象的创建中,步骤如下:

  1. 虚基类的构造函数
  2. 非虚基类的构造函数,按照声明顺序
  3. 成员对象的构造函数
  4. 派生类自己的构造函数
    实例:
#include <iostream>

using namespace std;
//基类

class D
{
public:
    D(){cout<<"D()"<<endl;}
    ~D(){cout<<"~D()"<<endl;}
protected:
    int d;
};

class B:virtual public D
{
public:
    B(){cout<<"B()"<<endl;}
    ~B(){cout<<"~B()"<<endl;}
protected:
    int b;
};

class A:virtual public D
{
public:
    A(){cout<<"A()"<<endl;}
    ~A(){cout<<"~A()"<<endl;}
protected:
    int a;
};

class C:public B, public A
{
public:
    C(){cout<<"C()"<<endl;}
    ~C(){cout<<"~C()"<<endl;}
protected:
    int c;
};

int main()
{
    cout << "Hello World!" << endl;
    C c;   //D, B, A ,C
    cout<<sizeof(c)<<endl; //一共有4个int 值,字节为24 .
    system("pause");
    return 0;
}

派生类的应用讨论

  1. 赋值兼容规则
    对于公有派生,派生类所有的访问限定和基类一样,其接口也全盘接受。这样只要基类能解决的问题,公有派生类都可以解决。在任何需要基类对象的地方都可以用公有派生类的对象来代替,这一规则称为赋值兼容规则。包括以下情况:
    1. 派生类的对象可以赋值给基类的对象,这是把派生类从对象基类中继承来的成员赋值给基类对象。 反过来不行
    2. 可以将一个派生类对象的地址赋值给其基类的指针变量,但只能访问派生类中有基类继承而来的成员,不能访问派生类中的新成员。 反过来也不行
    3. 派生类对象可以初始化基类对象的引用。

赋值兼容规则下的自定义赋值构造函数:
调用基类的赋值构造函数,在对新增成员完成赋值

Person:: Person(Person &ps){
IdPerson = ps.IdPerson;
Sex=ps.Sex
}

Student:: Student(Student &Std): Person(Std){
NoStudent=Std.NoStudent;
}

同样的,对于重载的复制赋值操作符,也实在定义函数体中先调用基类的复制赋值操作符

Person& Person:: operator= (Person &ps){
IdPerson = ps.IdPerson;
Sex=ps.Sex
}

Student& Student:: operator= (Student &Std){
this->Person::operator=(Std);
NoStudent=Std.NoStudent;
}

注意:一定要将资源的动态分配和释放封装在对象中,这样按语义进行赋值是完全可以的。否则回引起资源的污染和重复析构,这涉及到指针的深复制和浅复制的问题。

多态性与虚函数

多态性:

  • 静态的多态性: 函数的重载和运算符的重载
  • 运行时的多态性:以虚函数为基础,考虑在不同层次的类(继承)中同名的成员函数之间的关系问题

运行时的动态性:在程序执行前,无法根据函数名和参数来确定调用哪一个函数,必须在执行过程中根据执行的具体情况来动态的确定

虚函数的定义:
virtual 返回类型 函数名(参数列表){...}

虚函数在该类中派生的所有类中都保持虚函数的特性,在派生类中重新定义该虚函数时,可以不加关键字virtual,但重新定义时不仅要同名,而且参数列表和返回值类型全部与基类中的虚函数一样。

虚函数:与同名覆盖不同的是在基类中多了一个virtual关键字
同名覆盖:都相同
重载:参数列表不同

通过对象访问时虚函数的行为与同名覆盖完全相同,不同的是当使用基类的指针或引用访问时(基类指针指向派生类对象), 此时调用虚函数,执行的是派生类中的定义。


class father
{
public:
    virtual void foo()
    {
        cout<<"father::foo() is called"<<endl;
    }
};
class son:public father
{
public:
    void foo()
    {
        cout<<"son::foo() is called"<<endl;
    }
};
int main(void)
{
    father *a = new son();
    father->foo();   // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
    return 0;
}

纯虚函数
  1. 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在虚函数原型后加"=0"
    格式:
    virtual 返回类型 函数名(参数表)=0

2、引入原因
  1、为了方便使用多态特性,需要在基类中定义虚函数。
  2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
  
  为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类它不能生成对象。使派生类仅仅只是继承函数的接口。
  
声明了纯虚函数的类是一个抽象类。用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中没有定义。

class A{
    public:
          virtual void f() = 0;
          void g(){ this->f(); }
          A(){}
};
class B:public A{
      public:
          void f(){ cout<<"B:f()"<<endl;}
};
int main(){
    B b;
    b.g();
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章