C++基础 | 多态

多态

多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻 系统 升级,维护,调试的工作量和复杂度.

多态的定义

C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象 对同一消息会作出不同的响应。

多态的三个必要条件:
1.要有继承
2.要有虚函数重写
3.父类指针或引用指向子类对象

在基类的要调用的方法前加个virtual,而且是基类指针指向子类对象,子类中有重写方法,就可以实现多态。指向哪个子类,就调用哪个子类的方法。
如果没加virtual的话,编译器为了安全起见,只会调用基类的方法。

#include <iostream>
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、静态联编(sta5c binding),是程序的匹配、连接在编译阶段实现,也称为
早期匹配。重载函数使用静态联编。
3、动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑
定)。switch 语句和 if 语句是动态联编的例子。 多态也是

a. C++与C相同,是静态编译型语言
b. 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象; 所以编译器认为父类指针指向的是父类对象。
c. 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是 子类对象,从程序安全的角度,编译器假设父类指针只指向父类对象,因此编 译的结果为调用父类的成员函数。这种特性就是静态联编。
d. 多态的发生是动态联编,实在程序执行的时候判断具体父类指针应该调用 的方法。

虚析构函数

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开 始,沿着继承路径逐个调用基类的构造函数。
析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象。

#include <iostream>
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指针)。


20988794-6ae9366aeb7e7d34.png
屏幕快照 2020-02-03 下午11.32.21.png

说明:

  1. 通过虚函数表指针VPTR调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就 确定了调用的函数。在效率上,虚函数的效率要低很多。
    2.出于效率考虑,没有必要将所有成员函数都声明为虚函数.
    3.C++编译器,执行run函数,不需要区分是子类对象还是父类对象,而是 直接通过p的VPTR指针所指向的对象函数执行即可。
vptr的分布初始化

构造函数中能否调用虚函数,实现多态?
不能。
对象在创建的时,由编译器对VPTR指针进行初始化
只有当对象的构造完全结束后VPTR的指向才最终确定
父类对象的VPTR指向父类虚函数表
子类对象的VPTR指向子类虚函数表

#include <iostream>
using namespace std;
class Parent
{
public:
    Parent(int a=0)
    {
        this->a = a;
        print();
        }
    virtual void print()
    {
    cout<<"我是爹"<<endl; 
    }
private:
    int a;
};
class Child : public Parent
{
public:
    Child(int a = 0, int b=0):Parent(a)
    {
    this->b = b;
    print();
    }
   virtual  void print()
    {
    cout<<"我是儿子"<<endl;
    }
private:
    int b;
};
void HowToPlay(Parent *base)
{
    base->print(); //有多态发  
}
int main(void)
{
    Child c1; //定义一个子类对象
    HowToPlay(&c1);
    return 0; 
}

在上述代码中,声明c1时,触发c1的构造函数,但此时要先构造父类,所以此时child类的print()还没构造,vptr指向父类,等父类构造完后才指向子类child,这叫vptr的分布初始化

父类指针和子类指针的步长
#include <iostream>
using namespace std;
class Parent
{
public:
    Parent(int a=0)
    {
        this->a = a; 
    }
    virtual void print()
    {
        cout<<"我是爹"<<a<<endl; 
    }
private:
    int a;
};
class Child : public Parent
{
public:
    Child(int b = 0):Parent(0)
    {
        this->b = b; 
    }
    virtual void print()
    {
        cout<<"我是儿子"<<b<<endl; 
    }
    int b;
    
};
int main() {
    Parent *pP = NULL;
    Child  *pC = NULL;
    Child  array[] = {Child(1), Child(2), Child(3)};
    Parent A;
    Child B;
    pP = array;
    pC = array;
    pP->print();
    pC->print();
    pP++;   //pP+ sizeof(Parent) 
    pC++;   //pC+ sizeof(Child)
    // pP->print();   会报错
    pC->print();
    cout<<sizeof(A)<<"    "<<sizeof(B)<<endl;
    return 0; 
}

p++是根据指针的类型进行跨度的累加

另外上面计算类的大小时,运算出来是 16 和16。
在parent类中,有一个4字节的int和8字节的vptr,之所以不是12是因为内存对齐,如果空着一个4字节给下一个对象,那这就意味着读取一个类要两次,这是计组的知识。把virtual去掉后就可以验证了。

有关多态的理解

多态的实现效果:
多态:同样的调用语句有多种不同的表现形态;

多态实现的三个条件:
有继承、有virtual重写、有父类指针(引用)指向子类对象。

多态的c++实现:
virtual关键字,告诉编译器这个函数要支持多态;不是根据指针类型判断 如何调用;而是要根据指针所指向的实际对象类型来判断如何调用

多态的理论基础:
动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。

多态的重要意义:
设计模式的基础 是框架的基石。

多态的原理探究:
虚函数表和vptr指针。

纯虚函数和抽象类

纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本
纯虚函数为个派生类提供一个公共界面(接口的封装和设计、软件的模 块功能划分)

纯虚函数的语法:
virtual 类型 函数名(参数表) = 0;

一个具有纯虚函数的基类称为抽象类。

#include <iostream>
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<<endl;
    }
private:
    int a;
    int b; 
    };

class Tri : public Figure
{
public:
    Tri(int a, int b)
    {
        this->a = a;
        this->b = b; }
    virtual void getArea()
    {
        cout<<"三角形的面积: "<<a*b/2<<endl; 
    }
private:
    int a;
    int b; 
};

void area_func(Figure *base)
{
    base->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 <iostream>
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"<<endl;
    }
    int add(int a, int b)
    {
        return a + b; 
    }
    int sub(int a, int b)
    {
        return a - b; 
    }
};
int main() {
    Child c;
    c.print();
    cout<<c.add(3, 5)<<endl;
    cout<<c.sub(4, 6)<<endl;
    Interface1* i1 = &c;
    Interface2* i2 = &c;
    cout<<i1->add(7, 8)<<endl;
    cout<<i2->add(7, 8)<<endl;
    return 0; 
}

另外很重要的一点:考虑到子类会额外开辟东西,为了防止内存泄漏,要在抽象类写虚析构函数,即在抽象类中,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 <iostream>
#include "animal.h"
using namespace std;
Animal::Animal()
{
    cout<<"Animal::Animal()"<<endl;
}
Animal::~Animal()
{
    cout<<"Animal::~Animal()"<<endl;
}


//dog.h
#ifndef DOG_H
#define DOG_H
#include "animal.h"
class Dog:public Animal
{
    public:
        Dog();
        ~Dog();
        virtual void voice();
};
#endif // DOG_H


//dog.cpp
#include "dog.h"
#include <iostream> using namespace std;
Dog::Dog()
{
    cout<<"Dog::Dog()"<<endl;
}
Dog::~Dog()
{
        cout<<"Dog::~Dog()"<<endl;
}
void Dog:: voice()
{
    cout<<"wang wang"<<endl;
}


//cat.h
#ifndef CAT_H
#define CAT_H
#include "animal.h"
class Cat:public Animal
{
    public:
            Cat();
            ~Cat();
            virtual void voice();
};
#endif // CAT_H


//cat.cpp
#include "cat.h"
#include <iostream>
using namespace std;
Cat::Cat() {
    cout<<"Cat::Cat()"<<endl;
}
Cat::~Cat()
{
    cout<<"Cat::~Cat()"<<endl;
}
void Cat::voice()
{
    cout<<"miao miao "<<endl;
}

//main.cpp
int main() {
// Animal ani; 抽象基类,不能实例化。
    Animal * pa = new Dog;
    pa->voice();
    delete pa;
    cout<<"---------------"<<endl;
    pa = new Cat;
    pa->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 <stdio.h>
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 的代码作了修改,也不必改动库的代码,就可以正常实现调用,便于程序的维护和升级。

回调函数的本质:
提前做了一个协议的约定(把函数的参数、函数返回值提前约定)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值