c++ 继承和多态性、虚拟基类、虚拟函数、纯虚拟函数
继承与多态
继承与派生
c++通过类派生来支持继承,被继承的类型称为基类(baseclass)或超类(superclass),而产生的类为派生类(derived class)或子类(subclass)。基类和派生类的集合称作类的层次结构(hierarchy).
定义:
class 派生类名 : 访问限定符 基类名1<,访问限定符 基类名2, ... , 访问限定符 基类名n>{
//成员表 新增的或替代父类的成员
}
同一个派生类可以同时继承多个基类,称为多重继承(multi-inheritance)
单继承(single-inheritance): 一个派生类只继承一个基类
先讨论单继承:
编写派生类的4个步骤:
- 吸收基类的成员, 除构造函数,析构函数,运算符重载函数,友元函数外所有的数据成员和函数成员全都成为派生类的成员
- 改造基类成员,当有的基类成员在新的应用中不合适时,可以进行改造,如果派生类声明一个和某个基类成员同名的成员,派生类中的成员会屏蔽基类同名的成员,类似函数中的局部变量屏蔽全局变量。 如果是成员函数,参数表和返回值完全一样时,称为同名覆盖(override),否则为重载
- 发展新成员,增加新的数据成员和函数成员,更适合新的应用
- 重写构造函数和析构函数
继承方式
继承方式分为三种,公有方式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, ... >){
// 新增成员的初始化}
在类体的声明中不需要写":"后面的部分。
派生类构造函数各部分的执行次序:
- 调用基类构造函数,按他们在派生类定义中的先后顺序依次调用, 若未显示声明则默认调用基类中的无参构造函数
- 调用新增成员对象的构造函数,按他们在类定义中排列的先后顺序依次调用
- 派生类的构造函数体中的初始化操作
在析构函数中只要处理好新增的成员就好。对于新增的成员对象和基类中的成员,系统会调用成员对象和基类的析构函数。析构函数各部分的执行次序与构造函数相反。
在实际中,成员对象的使用或者聚合是一种完善的封装,推荐将数据,数据的操作,资源的动态分配与释放封装在一个完善的子系统中,就像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{.....};
在虚基类对象的创建中,步骤如下:
- 虚基类的构造函数
- 非虚基类的构造函数,按照声明顺序
- 成员对象的构造函数
- 派生类自己的构造函数
实例:
#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;
}
派生类的应用讨论
- 赋值兼容规则
对于公有派生,派生类所有的访问限定和基类一样,其接口也全盘接受。这样只要基类能解决的问题,公有派生类都可以解决。在任何需要基类对象的地方都可以用公有派生类的对象来代替,这一规则称为赋值兼容规则。包括以下情况:- 派生类的对象可以赋值给基类的对象,这是把派生类从对象基类中继承来的成员赋值给基类对象。 反过来不行
- 可以将一个派生类对象的地址赋值给其基类的指针变量,但只能访问派生类中有基类继承而来的成员,不能访问派生类中的新成员。 反过来也不行
- 派生类对象可以初始化基类对象的引用。
赋值兼容规则下的自定义赋值构造函数:
调用基类的赋值构造函数,在对新增成员完成赋值
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;
}
纯虚函数
- 纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在虚函数原型后加"=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;
}
上一篇: C++ 类内存大小知识汇总
下一篇: C++ 函数参数传递方法与编程模式对比