C++中类的多态

多态基本概念

        多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
        多态性(polymorphism) 提供接口与具体实现之间的另一层隔离,从而将 ”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具 有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时 也能扩展。
        c++支持编译时多态 ( 静态多态 ) 和运行时多态 ( 动态多态 ) ,运算符重载和函数重载 就是编译时多态,而派生类和虚函数实现运行时多态。
        静态多态和动态多态的区别就是函数地址是早绑定( 静态联编 ) 还是晚绑定 ( 动态联 编) 。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就 是静态多态( 编译时多态 ) ,就是说地址是早绑定的。而如果函数的调用地址不能编 译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定( 动态多态 , 运行时多态)
//计算器
class Caculator{
public:
    void setA(int a){
        this->mA = a;
    }
    void setB(int b){
        this->mB = b;
    }
    void setOperator(string oper){
        this->mOperator = oper;
    }
    int getResult(){
        if (this->mOperator == "+"){
            return mA + mB;
        }
        else if (this->mOperator == "-"){
            return mA - mB;
        }
        else if (this->mOperator == "*"){
            return mA * mB;
        }
        else if (this->mOperator == "/"){
            return mA / mB;
        }
}
private:
    int mA;
    int mB;
    string mOperator;
};
    //这种程序不利于扩展,维护困难,如果修改功能或者扩展功能需要在源代码基础上修改
    //面向对象程序设计一个基本原则:开闭原则(对修改关闭,对扩展开放)
    //抽象基类
class AbstractCaculator{
public:
    void setA(int a){
        this->mA = a;
    }
    virtual void setB(int b){
        this->mB = b;
    }
    virtual int getResult() = 0;
protected:
    int mA;
    int mB;
    string mOperator;
};
//加法计算器
class PlusCaculator : public AbstractCaculator{
public:
    virtual int getResult(){
        return mA + mB;
    }
};
//减法计算器
class MinusCaculator : public AbstractCaculator{
public:
    virtual int getResult(){
        return mA - mB;
    }
};
//乘法计算器
class MultipliesCaculator : public AbstractCaculator{
public:
    virtual int getResult(){
        return mA * mB;
    }
};
void DoBussiness(AbstractCaculator* caculator){
    int a = 10;
    int b = 20;
    caculator->setA(a);
    caculator->setB(b);
    cout << "计算结果:" << caculator->getResult() << endl;
    delete caculator;
}

向上类型转换及问题

        对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操 作它。取一个对象的地址( 指针或引用 ) ,并将其作为基类的地址来处理,这种称为 向上类型转换。 也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
class Animal{
public:
    void speak(){
        cout << "动物在唱歌..." << endl;
    }
};
class Dog : public Animal{
public:
    void speak(){
        cout << "小狗在唱歌..." << endl;
    }
};
void DoBussiness(Animal& animal){
    animal.speak();
}
void test(){
    Dog dog;
    DoBussiness(dog);
}
运行结果 : 动物在唱歌
问题抛出 : 我们给 DoBussiness 传入的对象是 dog ,而不是 animal 对象,输出的结果应该是 Dog::speak

问题解决思路

        解决这个问题,我们需要了解下绑定( 捆绑 ,binding) 概念。
        把函数体与函数调用相联系称为绑定( 捆绑, binding) 当绑定在程序运行之前( 由编译器和连接器 ) 完成时,称为早绑定 (early binding).C 语言中只有一种函数调用方式,就是早绑定。 上面的问题就是由于早绑定引起的,因为编译器在只有 Animal 地址时并不知道要 调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个 时候由于 DoBussiness 的参数类型是 Animal&, 编译器确定了应该调用的 speak 是 Animal::speak 的,而不是真正传入的对 Dog::speak
        解决方法就是迟绑定( 迟捆绑 , 动态绑定 , 运行时绑定, late binding), 意味着绑定要根 据对象的实际类型,发生在运行。
        C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合 适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不 知道 Animal 类型的指针或引用指向的实际的对象类型)。

问题解决方案(虚函数,vitual function)

        C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类 (基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆 盖(override) ,或者称为重写。
        对于特定的函数进行动态绑定,c++ 要求在基类中声明这个函数的时候使用 virtual 关键字, 动态绑定也就对 virtual 函数起作用 .为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上 virtual 关键字,定义时候不需要 . 如果一个函数在基类中被声明为 virtual ,那么在所有派生类中它都是 virtual . 在派生类中 virtual 函数的重定义称为重写 (override). Virtual 关键字只能修饰成员函数 .
        构造函数不能为虚函数
注意 :
        仅需要在基类中声明一个函数为 virtual. 调用所有匹配基类声明行为的派生类 函数都将使用虚机制。虽然可以在派生类声明前使用关键字 virtual( 这也是无害的 ) , 但这个样会使得程序显得冗余和杂乱。( 我建议写上 )
class Animal{
public:
    virtual void speak(){
        cout << "动物在唱歌..." << endl;
    }
};
class Dog : public Animal{
public:
    virtual void speak(){
        cout << "小狗在唱歌..." << endl;
    }
};
void DoBussiness(Animal& animal){
    animal.speak();
}
void test(){
    Dog dog;
    DoBussiness(dog);
}

C++如何实现动态绑定

        动态绑定什么时候发生?所有的工作都是由编译器在幕后完成。当我们告诉通过创 建一个 virtual 函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定 机制来实现我们的要求, 不会再执行早绑定。
问题 :C++ 的动态捆绑机制是怎么样的?
        首先,我们看看编译器如何处理虚函数。当编译器发现我们的类中有虚函数的时候, 编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类 中秘密增加一个指针,这个指针就是 vpointer( 缩写 vptr) ,这个指针是指向对象的 虚函数表。在多态调用的时候,根据 vptr 指针,找到虚函数表来实现动态绑定。
验证对象中的虚指针
class A{
public:
    virtual void func1(){}
    virtual void func2(){}
};
//B 类为空,那么大小应该是 1 字节,实际情况是这样吗?
class B : public A{};
void test(){
    cout << "A size:" << sizeof(A) << endl;
    cout << "B size:" << sizeof(B) << endl;
}
        在编译阶段,编译器秘密增加了一个 vptr 指针,但是此时 vptr 指针并没有初始化 指向虚函数表(vtable), 什么时候 vptr 才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构 造函数中,增加一些 vptr 指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化 vptr 指针, 使之指向本对象的虚函数表。
        起初,子类继承基类,子类继承了基类的 vptr 指针,这个 vptr 指针是指向基类虚函数表,当子类调用构造函数,使得子类的 vptr 指针指向了子类的虚函数表。
当子类无重写基类虚函数时 :
过程分析 :
        Animal* animal = new Dog;
        animal->fun1();
        当程序执行到这里,会去 animal 指向的空间中寻找 vptr 指针,通过 vptr 指针找 到 func1 函数,此时由于子类并没有重写也就是覆盖基类的 func1 函数,所以调用 func1 时,仍然调用的是基类的 func1.
执行结果 : 我是基类的 func1
测试结论 : 无重写基类的虚函数,无意义
当子类重写基类虚函数时 :
过程分析 :
        Animal* animal = new Dog;
        animal->fun1();
        当程序执行到这里,会去 animal 指向的空间中寻找 vptr 指针,通过 vptr 指针找 到 func1 函数,由于子类重写基类的 func1 函数,所以调用 func1 时,调用的是子 类的 func1.
执行结果 : 我是子类的 func1
测试结论 : 无重写基类的虚函数,无意义
多态的成立条件:
        有继承
        子类重写父类虚函数函数
        a) 返回值,函数名字,函数参数,必须和父类完全一致 ( 析构函数除外 )
        b) 子类中 virtual 关键字可写可不写,建议写
        类型兼容,父类指针,父类引用 指向 子类对象

抽象基类和纯虚函数(pure virtual function)

        在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进 行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函 数毫无意义的代码。
        做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function), 使得基类称 为抽象类(abstract class).
        纯虚函数使用关键字 virtual ,并在其后面加上 =0 。如果试图去实例化一个抽象类, 编译器则会阻止这种操作。
        当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也 是一个抽象类。
        Virtual void fun() = 0;告诉编译器在 vtable 中为函数保留一个位置,但在这个特定 位置不放地址。
        建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来 操纵一组类,且这个公共接口不需要事先( 或者不需要完全实现 ) 。可以创建一个公共类.
案例 : 模板方法模式
//抽象制作饮品
class AbstractDrinking{
public:
    //烧水
    virtual void Boil() = 0;
    //冲泡
    virtual void Brew() = 0;
    //倒入杯中
    virtual void PourInCup() = 0;
    //加入辅料
    virtual void PutSomething() = 0;
    //规定流程
    void MakeDrink(){
        this->Boil();
        Brew();
        PourInCup();
        PutSomething();
    }
};
//制作咖啡
class Coffee : public AbstractDrinking{
public:
    //烧水
    virtual void Boil(){
        cout << "煮农夫山泉!" << endl;
    }
    //冲泡
    virtual void Brew(){
        cout << "冲泡咖啡!" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "将咖啡倒入杯中!" << endl;
    }
    //加入辅料
    virtual void PutSomething(){
        cout << "加入牛奶!" << endl;
    }
};
//制作茶水
class Tea : public AbstractDrinking{
public:
    //烧水
    virtual void Boil(){
        cout << "煮自来水!" << endl;
    }
    //冲泡
    virtual void Brew(){
        cout << "冲泡茶叶!" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "将茶水倒入杯中!" << endl;
    }
    //加入辅料
    virtual void PutSomething(){
        cout << "加入食盐!" << endl;
    }
};
//业务函数
void DoBussiness(AbstractDrinking* drink){
    drink->MakeDrink();
    delete drink;
}
void test(){
    DoBussiness(new Coffee);
    cout << "--------------" << endl;
    DoBussiness(new Tea);
}

纯虚函数和多继承

        多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。 绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的 概念,c++ 中没有接口的概念,但是可以通过纯虚函数实现接口。
        接口类中只有函数原型定义,没有任何数据定义。 多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功 能实现,子类需要根据功能说明定义功能实现。
注意 : 除了析构函数外,其他声明都是纯虚函数

虚析构函数

        虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
class People{
public:
    People(){
        cout << "构造函数 People!" << endl;
    }
    virtual void showName() = 0;
    virtual ~People(){
        cout << "析构函数 People!" << endl;
    }
};
class Worker : public People{
public:
    Worker(){
        cout << "构造函数 Worker!" << endl;
    pName = new char[10];
    }
    virtual void showName(){
        cout << "打印子类的名字!" << endl;
    }
    ~Worker(){
        cout << "析构函数 Worker!" << endl;
        if (pName != NULL){
            delete pName;
        }
   }
private:
    char* pName;
};
void test(){
    People* people = new Worker;
    people->~People();
}

纯虚析构函数

        纯虚析构函数在 c++ 中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。
那么问题是:如果给虚析构函数提供函数体了,那怎么还能称作纯虚析构函数呢? 纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽 象类,不能创建基类的对象。
//非纯虚析构函数
class A{
public:
    virtual ~A();
};
A::~A(){}
    //纯析构函数
class B{
public:
    virtual ~B() = 0;
};
B::~B(){}
void test(){
    A a; //A 类不是抽象类,可以实例化对象
    B b; //B 类是抽象类,不可以实例化对象
}
        如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之, 则应该为类声明虚析构函数。

重写 重载 重定义

重载,同一作用域的同名函数
        1. 同一个作用域
        2. 参数个数,参数顺序,参数类型不同
        3. 和函数返回值,没有关系
        4. const 也可以作为重载条件 //do(const Teacher& t){} do(Teacher& t)
重定义(隐藏)
        5. 有继承
        6. 子类(派生类)重新定义父类(基类)的同名成员(非 virtual 函数)
重写(覆盖)
        7. 有继承
        8. 子类(派生类)重写父类(基类)的 virtual 函数
        9. 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class A{
public:
    //同一作用域下,func1 函数重载
    void func1(){}
    void func1(int a){}
    void func1(int a,int b){}
    void func2(){}
    virtual void func3(){}
};
class B : public A{
public:
    //重定义基类的 func2,隐藏了基类的 func2 方法
    void func2(){}
    //重写基类的 func3 函数,也可以覆盖基类 func3
    virtual void func3(){}
};

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值