文章目录
前言
c++类的继承与多态
一、类的继承是什么?
提供可以重用的代码,为原始类代码提供新特性,并且保留旧特性。
二、学习内容
1.派生类
- 派生类对象存储了基类的数据成员
- 派生类对象可以使用基类的方法
- 派生类需要自己的构造函数
- 派生类可以根据需要添加额外的数据成员和函数
- 构造函数必须给新成员和继承的成员提供数据(满足基类构造函数的参数要求)
1.1 公有派生
基类的公有成员将成为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法(protected)访问:
基本派生语法:
//基类A
class A
{
private:
int _a;
public:
A(int a){_a = a;}
int read(){return _a;}
}
//A_1从基类A派生
class A_1 : public A//public+冒号指出A_1是A的公有派生类
{
private:
int _b;//新成员b
public:
A_1(int b, int a);//构造函数必须给新成员和继承的成员(b)提供数据
A_1(int b, const A & tp);//继承的成员也可直接传入所要继承的类进行数据传递
int readson(){return _b;}
......
}
1.2 构造函数:访问权限的考虑
- 派生类无法直接访问基类私有成员,必须通过基类方法进行访问
- 派生类构造函数无法设置继承的成员,必须通过基类的公有方法
- 派生类构造函数必须使用基类构造函数
派生类构造函数要点:
- 首先创建基类对象
- 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数(为指定则为默认基类构造函数)
- 派生类构造函数应初始化派生类新增的数据成员
- 派生类对象过期时,先析构派生再析构基类
派生类构造函数语法
对于A_1(int b, int a)这一构造函数
//1.通过成员初始化列表语法定义,先创建基类再创建派生
A_1::A_1(int b, int a) : A(a)//通过A_1构造函数传入的参数先调用基类A构造函数生成A,再传入新成员_b完成A_1派生
{
_b = b;
}
//2.未给定基类初始化列表,则自动调用基类默认构造函数,但传参依旧不能少(一般都应采取第一种方式)
A_1::A_1(int b, int a)
{
_b = b;
}
对于A_1(int b, const A & tp)这一构造函数
//1..直接传入基类参数
A_1(int b, const A & tp):A(tp)//基类中如果没有定义复制构造函数A(tp),则系统根据需要自动生成
{
_b = b;
}
//2.成员初始化列表法
A_1(int b, const A & tp):A(tp), _b(b)
{
}
1.3 使用派生类
创建派生对象语法:
//创建基类对像obj_0
A obj_0(a);
//以初始化列表成员法创建派生对象,A_1::A_1(int b, int a) : A(a)
A_1 obj_1(b,a);
//将已创建的基类对象作为参数创建派生对象A_1(int b, const A & tp):A(tp),即调用默认复制构造函数
A_1 obj_2(b,obj_0);
1.4 派生类与基类关系
- 派生类对象可以使用基类方法,但不能是基类私有部分:
obj_1.read();//read()为A基类的公有函数
- 基类指针可以在不进行显式类型转换的情况下指向派生类对象(反过来不行);基类引用可以再不进行显式类型转换的情况下引用派生类对象(反过来不行):类似ue4中的cast to概念
//以初始化列表成员法创建派生对象,A_1::A_1(int b, int a) : A(a)
A_1 obj_1(b,a);
//设定基类引用和指针
A & rt = obj_1;
A * pt = & obj_1;
//基类指针或引用只能调用基类方法不能调用派生类方法
rt.read();
pt->read();
结合引用单向性,派生类可以作为基类默认复制构造函数的实参传入,将其基类部分传递给要初始化的基类对象进行基类对象初始化:
A_1 obj_1(b,a);
A obj_0_1(obj_1);//后台默认调用复制构造函数A(const A &)
A obj_0_2 = obj_1;//后台默认调用隐式重载赋值运算符A & operator =(const A &) const;
- 基类指针和引用的单向特性可以让基类与派生类都能作为函数的参数传入并调用基类方法,即实现了基类和所有派生类都能作为同一参数传入函数并调用共有的基类公有函数:
//通过引用给函数传参
//全局函数show,定义中只涉及基类方法,形参为对象的引用形式,由于单向特性,引用可以传入基类和派生类的对象
void show(const A & rt)
{
cout<<rt.read()<<endl;
}
//通过定义函数时采用基类引用,使函数能够传入基类和派生类
show(obj_0);//传入基类对象
show(obj_1);//传入派生类对象
//通过指针给函数传参
//全局函数show,形参为基类对象指针形式
void show(const A*pt)
{
cout<<pt->read()<<endl;
}
//调用
show(& obj_0);
show(& obj_1);
2.多态公有继承
2.1多态公有继承(虚函数)
- 通过引用或者指针,实现派生类到基类的转化,并且调用派生类中从基类继承而来的虚函数,实现多态。
- 当设立虚函数时,根据引用或者指针所指对象来选择调用基类还是派生类的对应虚函数具体定义;当未设立虚函数时,只调用引用或指针原本类型的虚函数定义。
- 也可以直接通过生成对象调用基类还是派生类具体虚函数。
- 注意作用域解析运算符::的作用,即在调用虚成员函数时调用当前类的定义还是其基类的定义。
- 虚方法的行为:指向基类指针或者基类引用可以传入派生类对象,达到不同派生类存储在同一个数组的目的。
- 基类的析构函数要设成虚函数,这样才能保证正确调用指针指向对象类型的析构函数,从而保证正确的派生类——基类的析构序列。
#include<iostream>
#include<string>
using namespace std;
class fruit
{
protected:
string _name;
int _heat;
public:
fruit(const string& s, int heat):_name(s), _heat(heat) {};
virtual void showname() { cout << "Please choose fruit to show name!"<< endl; }
void showheat() { cout << "Please choose fruit to show heat!" << endl; }
};
class apple :public fruit
{
public:
apple(const string& s, int heat) :fruit(s , heat) {};
virtual void showname() { cout << "This is apple!" << endl; }
virtual void showname(int a) { cout << "This is apple: " <<a<< endl; }
void showheat() { cout << "Apple's heat is " << _heat << endl; }
};
int main()
{
fruit BasicFruit("no name", 0);
apple Apple("Apple", 20);
//引用相当于强制转化,能够将虚函数在派生类的定义传递回基类,实现多态
fruit& Fruit1 = BasicFruit;
fruit& Fruit2 = Apple;
//对使用了virtual的成员函数,将根据引用指向的对象调用virtual函数
Fruit1.showname();//Fruit1 指向基类,调用基类virtual函数,结果为"Please choose fruit to show name!"
Fruit2.showname();//Fruit2 指向派生,调用派生类virtual函数,结果为"This is apple!"
Apple.showname(1);//showname(int a)无法通过基类引用来调用,因为基类中不存在对应的声明,只能通过派生类对象直接调用
//对未使用virtual的成员函数,通过引用将调用引用类型的成员函数而不是派生类
Fruit1.showheat();//showheat()未设置为virtual,因此只调用引用类型fruit的showheat()
Fruit2.showheat();//showheat()未设置为virtual,因此只调用引用类型fruit的showheat()
BasicFruit*p[4];//利用多态性开辟不同派生类的统一存储区
for(int i = 0 ; i<4 ; i++)
{
p[i] = new apple ("apple" , 20);
}
return 0;
}
2.2静态联编和动态连编
2.2.1 什么是静态和动态连编
c++中由于函数重载和虚函数等多态的存在,使函数调用时使用哪个可执行代码块(联编)变成较为麻烦的事。
静态(早期)联编:编译过程中进行联编,虚方法使其变得复杂。
动态(晚期)连编:在程序运行时选择正确的虚方法(如A * pt = & b,b为A的派生类对象并且基类存在虚函数,则调用pt虚函数时调用的是在派生类中的定义),因为程序运行时才能确定对象类型。
2.2.2 指针和引用类型的兼容性
- 向上强制转换(upcasting):将派生类引用或指针转化为基类引用或指针。upcasting是可以传递的且不需要使用显示类型转换:
- 向下强制转换(downcasting):必须使用显示类型转换,因为is-a关系是不可逆的,派生类新增成员并不属于基类。
2.2.3 虚成员函数和动态联编
-
编译器对非虚方法使用静态联编,即在编译时根据指针类型或引用类型调用函数而不是指向的对象类型。
-
编译器对虚方法使用动态连编,即若对有虚函数重定义则调用对象类型的重定义虚函数。
-
为什么有两种类型联编并且默认为静态联编:动态联编会造成计算机跟踪基类指针或引用对象的处理开销。如果派生类类无需向上强制转化或派生类不重新定义基类的任何方法则静态联编更合理,即虚函数有必要才用。
-
虚函数工作原理:对象中有一个隐藏成员,是一个指针,指向虚函数表,表中存储了为类对象声明的虚函数的地址,若派生类重新定义了虚函数,则其地址改变,否则继承基类中的地址。这样的做法可以减少内存空间的浪费。
-
使用虚函数的内存和执行成本:虚函数表增大,增大量为存储地址的空间;对于每个类编译器都创建虚函数地址表;对于每个函数调用,都需要查找表中地址的过程。
2.3 虚函数使用注意事项
- 构造函数不能为虚函数:创建派生对象时,要先创建基类对象再创建派生对象,具有序列性,两者要区分开。
- 析构函数应当为虚函数,除非类不作为基类:以下述代码为例,B为A的继承,若析构函数不为虚,则默认静态联编(不是虚函数动态联编不起作用),删除pt时删除的是类A的成员,而类B为派生类可能有新成员则删除不掉。若为虚,则进行动态联编,先删除派生类指向内存,再删除基类指向内存。
A* pt = new B;
.....
delete pt;
`
3. 友元函数不能为虚函数
4. 没有重新定义:若派生类没有重新定义函数,将使用该函数的基类版本,若派生类位于派生链中,则将使用最新的虚函数版本,除非基类版本是隐藏的。
5. 重新定义的方法应与原型完全相同,只有返回类型是基类引用或指针,可以修改为派生类的引用或指针,请注意只适用于返回值,称作返回类型协变。
若基类虚函数声明被重载了,则在派生类中要重新定义所有基类版本。