多态
多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻 系统 升级,维护,调试的工作量和复杂度.
多态的定义
C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象 对同一消息会作出不同的响应。
多态的三个必要条件:
1.要有继承
2.要有虚函数重写
3.父类指针或引用指向子类对象
在基类的要调用的方法前加个virtual,而且是基类指针指向子类对象,子类中有重写方法,就可以实现多态。指向哪个子类,就调用哪个子类的方法。
如果没加virtual的话,编译器为了安全起见,只会调用基类的方法。
#include
using namespace std;
class HeroFighter
{
public:
virtual int ackPower()
{
return 10;
}
};
class AdvHeroFighter : public HeroFighter
{
public:
int ackPower() // 标识修饰一个成员方法是一个虚函数
{
return HeroFighter::ackPower()*2;
}
};
class enemyFighter
{
public:
int destoryPower()
{
return 15;
}
};
//如果把这个结构放在动态库
void objPK(HeroFighter *hf, enemyFighter *enemyF) {
if (hf->ackPower() >enemyF->destoryPower())
{
printf("英雄打败敌 。。。胜利\n"); }
else
{
printf("英雄。。。牺牲\n");
}
}
int main() {
HeroFighter hf;
enemyFighter ef;
objPK(&hf, &ef);
AdvHeroFighter advhf;
objPK(&advhf, &ef);
return 0;
}
静态联编和动态联编
1、联编是指一个程序模块、代码之间互相关联的过程。
2、静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为
早期匹配。重载函数使用静态联编。
3、动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑
定)。switch 语句和 if 语句是动态联编的例子。 多态也是
a. C++与C相同,是静态编译型语言
b. 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象; 所以编译器认为父类指针指向的是父类对象。
c. 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是 子类对象,从程序安全的角度,编译器假设父类指针只指向父类对象,因此编 译的结果为调用父类的成员函数。这种特性就是静态联编。
d. 多态的发生是动态联编,实在程序执行的时候判断具体父类指针应该调用 的方法。
虚析构函数
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开 始,沿着继承路径逐个调用基类的构造函数。
析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象。
虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露。
#include
using namespace std;
class A
{
public:
A() {
}
virtual ~A()
{
delete [] p;
printf("~A()\n");
}
private:
char *p;
};
class B : public A
{
public:
B() {
}
~B() {
delete [] p;
printf("~B()\n");
}
private:
char *p;
};
class C : public B
{
public:
C() {
}
~C() {
delete [] p;
printf("~C()\n");
}
private:
char *p;
};
//通过父类指针 把 所有的子类对象的析构函数 都执一遍
//通过父类指针 释放所有的子类资源
void howtodelete(A *base)
{
delete base;
}
int main() {
C *myC = new C;
//delete myC;
//直接通过子类对象释放资源 不需要写virtual
howtodelete(myC);//通过 类的指针调 释放子类的资源
return 0;
}
重载、重写、重定义
重载(添加):
a 相同的范围(在同一个类中)
b 函数名字相同
c 参数不同
d virtual关键字可有可无
重写(覆盖) 是指派生类函数覆盖基类函数,特征是:
a 不同的范围,分别位于基类和派生类中
b 函数的名字相同
c 参数相同
d 基类函数必须有virtual关键字
重定义(隐藏) 是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
a 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无 virtual,基类的函数被隐藏。
b 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没 有vitual关键字,此时,基类的函数被隐藏。
多态的实现原理
虚函数表和vptr指针:
当类中声明虚函数时,编译器会在类中生成一个虚函数表;
虚函数表是一个存储类成员函数指针的数据结构;
虚函数表是由编译器自动生成与维护的;
virtual成员函数会被编译器放入虚函数表中;
存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。
说明:
- 通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就 确定了调用的函数。在效率上,虚函数的效率要低很多。
2.出于效率考虑,没有必要将所有成员函数都声明为虚函数.
3.C++编译器,执行run函数,不需要区分是子类对象还是父类对象,而是 直接通过p的VPTR指针所指向的对象函数执行即可。
vptr的分布初始化
构造函数中能否调用虚函数,实现多态?
不能。
对象在创建的时,由编译器对VPTR指针进行初始化
只有当对象的构造完全结束后VPTR的指向才最终确定
父类对象的VPTR指向父类虚函数表
子类对象的VPTR指向子类虚函数表
#include
using namespace std;
class Parent
{
public:
Parent(int a=0)
{
this->a = a;
print();
}
virtual void print()
{
cout<<"我是爹"<b = b;
print();
}
virtual void print()
{
cout<<"我是儿子"<print(); //有多态发
}
int main(void)
{
Child c1; //定义一个子类对象
HowToPlay(&c1);
return 0;
}
在上述代码中,声明c1时,触发c1的构造函数,但此时要先构造父类,所以此时child类的print()还没构造,vptr指向父类,等父类构造完后才指向子类child,这叫vptr的分布初始化
父类指针和子类指针的步长
#include
using namespace std;
class Parent
{
public:
Parent(int a=0)
{
this->a = a;
}
virtual void print()
{
cout<<"我是爹"<b = b;
}
virtual void print()
{
cout<<"我是儿子"<print();
pC->print();
pP++; //pP+ sizeof(Parent)
pC++; //pC+ sizeof(Child)
// pP->print(); 会报错
pC->print();
cout<
p++是根据指针的类型进行跨度的累加
另外上面计算类的大小时,运算出来是 16 和16。
在parent类中,有一个4字节的int和8字节的vptr,之所以不是12是因为内存对齐,如果空着一个4字节给下一个对象,那这就意味着读取一个类要两次,这是计组的知识。把virtual去掉后就可以验证了。
有关多态的理解
多态的实现效果:
多态:同样的调用语句有多种不同的表现形态;
多态实现的三个条件:
有继承、有virtual重写、有父类指针(引用)指向子类对象。
多态的c++实现:
virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断 如何调用;而是要根据指针所指向的实际对象类型来判断如何调用
多态的理论基础:
动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。
多态的重要意义:
设计模式的基础 是框架的基石。
多态的原理探究:
虚函数表和vptr指针。
纯虚函数和抽象类
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本
纯虚函数为个派生类提供一个公共界面(接口的封装和设计、软件的模 块功能划分)
纯虚函数的语法:
virtual 类型 函数名(参数表) = 0;
一个具有纯虚函数的基类称为抽象类。
#include
using namespace std;
//// 向抽象类编程(面向一套预先定义好的接口编程)
class Figure //抽象类
{
public:
//阅读一个统一的界面(接口),让子类使用,让子类必须去实现
virtual void getArea() = 0 ; //纯虚函数
};
class Circle : public Figure
{
public:
Circle(int a, int b)
{
this->a = a;
this->b = b; }
virtual void getArea()
{
cout<<"圆形的面积: "<<3.14*a*a<a = a;
this->b = b; }
virtual void getArea()
{
cout<<"三角形的面积: "<getArea(); //会发生多态
}
int main() {
//Figure f; //抽象类不能被实例化
Figure *base1 = new Circle(10,20);
Figure *base2 =new Tri(20,30);
// 向抽象类编程(面向一套预先定义好的接口编程)
area_func(base1);
area_func(base2);
return 0;
}
在上面程序,定义两个Figure类的指针指向Circle类和Tri类,是为了实现低耦合,这样main函数只需处理抽象类Figure,不需要去知道Figure和Circle和Tri类之间的关系。
main是高层业务逻辑层
抽象类是抽象层,可实例化的类为实现层
高层业务逻辑层向抽象层靠拢,实现层向抽象层靠拢,这叫做依赖倒转原则
写架构就是设计抽象类,就是做既能实现要求的方法又能拓展未来的抽象类
1,含有纯虚函数的类,称为抽象基类,不可实列化。即不能创建对象,存在的意义 就是被继承,提供族类的公共接口。
2,纯虚函数只有声明,没有实现,被“初始化”为 0。
3,如果一个类中声明了纯虚函数,而在派生类中没有对该函数定义,则该虚函数在 派生类中仍然为纯虚函数,派生类仍然为纯虚基类。
纯虚函数和多继承
绝大多数面向对象语言都不支持多继承,绝大多数面向对象语言都支持接 口的概念
C++中没有接口的概念,C++中可以使用纯虚函数实现接口 接口类中只有函数原型定义,没有任何数据的定义.
#include
using namespace std;
/*
C++中没有接口的概念 C++中可以使用纯虚函数实现接口,接口类中只有函数原型定义,没有任何数据的定义。
*/
class Interface1
{
public:
virtual void print() = 0;
virtual int add(int a, int b) = 0;
};
class Interface2
{
public:
virtual void print() = 0;
virtual int add(int a, int b) = 0; //如果这里没有int b,那child就要重写virtual int add(int a)
virtual int sub(int a, int b) = 0;
};
class Child : public Interface1, public Interface2
{
public:
void print()
{
cout<<"Child::print"<add(7, 8)<add(7, 8)<
另外很重要的一点:考虑到子类会额外开辟东西,为了防止内存泄漏,要在抽象类写虚析构函数,即在抽象类中,virtual ~A();
面向抽象类编程案例
//animal.h
#ifndef ANIMAL_H
#define ANIMAL_H
class Animal
{
public:
Animal();
virtual ~Animal();
virtual void voice() = 0;
};
#endif // ANIMAL_H
//animal.cpp
#include
#include "animal.h"
using namespace std;
Animal::Animal()
{
cout<<"Animal::Animal()"< using namespace std;
Dog::Dog()
{
cout<<"Dog::Dog()"<
using namespace std;
Cat::Cat() {
cout<<"Cat::Cat()"<voice();
delete pa;
cout<<"---------------"<voice();
delete pa;
return 0;
}
数组指针和数组类型
int array[10];
array是int类型的常指针 int * const
int *const Array=&array[0]. ==== array
数组指针定义方法:
方法一:直接定义一个数组类型
typedef int (ARRAY_INT_10)[10];
ARRAY_INT_10 a; //a为长度为10的int数组
数组指针:ARRAY_INT_10 *array_10_p=&array;
array_10_p指向的是40个字节的内存
*array_10_p是array指向的内存地址。
(*array_10_p)[0]才是int
方法二:定义一个数组指针类型
typedef int (*ARRAY_INT_10)[10];
数组指针:ARRAY_INT_10 array_10_p=&array;
方法三:直接定义
int (*p)[10]=&array; //定义了一个int[10] 指针指向常量指针array的地址
函数指针
int add(int a,int b){ return a+b;}
方法一:定义一个函数类型
typedef int(FUNC)(int,int);
FUNC *p=add; //函数指针指向add函数。
方法二:定义一个函数指针
typedef int(*FUNC)(int,int);
FUNC p= add;//不建议用这种方法,会乱
方法三:直接定义
type (*pointer)(parameter list);
pointer为函数指针变量名
type为指向函数的返回值类型
parameter list为指向函数的参数类型列表
int (*p)(int,int)=add;
调用函数:p(100,200) 或(*p)(100,200) 两种是等价的
函数指针做函数参数
当函数指针 做为函数的参数,传递给一个被调用函数,被调用函数就可 以通过这个指针调用外部的函数,这就形成了回调。
#include
int add(int a, int b);
int libfun(int (*pDis)(int a, int b));
int main(void)
{
int (*pfun)(int a, int b);//定义一个函数指针pfun 指向 int ()(int, int)函数类型 pfun = add;
libfun(pfun);
return 0;
}
int add(int a, int b)
{
return a + b;
}
int libfun(int (*pDis)(int a, int b))
{
int a, b;
a = 1;
b = 2;
add(1,3); //直接调用add函数
printf("%d", pDis(a, b)); //通过函数指针做函数参数,间接调用add函数
return 0;
}
回调函数的优点:
1 函数的调用 和 函数的实现 有效的分离
2 类似C++的多态,可扩展
现在这几个函数是在同一个文件当中
int libfun(int (*pDis)(int a, int b)) 是一个库中的函数,就只有使用回调了,通过函数指针参数将外部函数地址传入来实现调用。
函数 add 的代码作了修改,也不必改动库的代码,就可以正常实现调用,便于程序的维护和升级。
回调函数的本质:
提前做了一个协议的约定(把函数的参数、函数返回值提前约定)