C++ 多态

发布时间:2024年01月13日

多态概念

多态是什么?一个函数有多种形态。

拿生活中例子来,比如说Animal类(基类)下面有CatDog等等数百数千个派生类,每过一年我们的派生类的age需要+1,那么是不是我们要实现数百数千个函数?显然这样代码冗余严重,实际上我们只需要用Animal *或者 Animal&来接受就行 也就是说形参基类可以接受派生类。

多态分为:静态联编动态联编

通常来说联编就是将模块或者函数合并在一起生成可执行代码的处理过程,同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序彼此关联的过程。

按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。

静态联编

概念

联编工作在编译阶段完成,这种联编过程是在程序运行之前完成的,又称为早期联编。

在编译阶段就会确定程序中的操作调用(如函数调用)与执行该操作代码间的关系。

静态联编对函数的选择是基于指向对象的指针或者引用的关系,优点是效率高 缺点是灵活性差

也就是说 静态联编在编译阶段就能知道函数/模块的入口

体现

在编译完成后,就知道某条语句会执行哪一函数/模块

  1. 隐藏:基类和派生类同名成员属性或函数
  2. 函数重载:编译器在生成符号表的时候,使用函数名字和形参类型作为符号
  3. 运算符重载:对已有运算符重新进行定义,赋予其另外一种功能,以适应不同的数据类型
  4. 泛型重载:后续继续

运算符重载

概述

对已有运算符重新进行定义,赋予其另外一种功能,以适应不同的数据类型

运算符重载不能改变本来寓意,不能改变基础类型寓意。

运算符重载只是一种语法上的方便,也就是它只是另一种函数调用的方式

在c++中,可以定义一个处理类的新运算符。这种定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。差别仅此而已。它像任何其他函数一样也是一个函数,当编译器遇到适当的模就会调用这个函数。

可以重载的运算符
在这里插入图片描述

不可以重载的运算符

  • 成员访问运算符(.)
  • 成员指针访问运算符(->)
  • 域运算符(::)
  • 长度运算符(sizeof)
  • 条件运算符(:?)
  • 预处理运算符(#)

运算符重载实例

号的重载

#include <iostream>
#include "cstring"
class Test{
public:
    int data;
    char *ptr;
    Test(){
        data=0;
        ptr=new char[10];
    }
    Test(int data,const char *src){
        this->data=data;
        if (src){
            ptr = new char[::strlen(src)];
            strcpy(ptr,src);
        } else
            ptr=new char[10];
    }
    Test(const Test&t){
        this->data=t.data;
        if (strlen(t.ptr)){
            this->ptr = new char[strlen(t.ptr)+1];
            strcpy(this->ptr,t.ptr);
        } else{
            this->ptr = new char[10];
        }
    }
    ~Test(){
        if (ptr!= nullptr){
            delete[] ptr;
        }
    }


    // 该运算符重载函数由 左操作数调用 右操作数为实参 t1+t2 -> t1.operator +(t2)
    Test operator +(const Test &t){
        Test val;//保存结果
        val.data = this->data+t.data;
        // +1是用来给字符串结尾的 \0 腾出来空间
        val.ptr = new char [strlen(this->ptr)+ strlen(t.ptr) + 1];
        //将分配的堆空间初始化为\0  初始化清空分配的堆空间
        memset(val.ptr,0, strlen(this->ptr)+ strlen(t.ptr) + 1);
        strcat(val.ptr, this->ptr);
        strcat(val.ptr,t.ptr);
        cout<<val.ptr<<endl;
        return val;
    }

};
int main() {

    Test t1(10,"hello");
    Test t2(50,"world");
    Test t3 = t1+t2;
    /*
     * 执行上述语句执行两个函数:
     * 1.+运算符重载函数  t1+t2
     * 2.Test t3 = val 触发拷贝构造函数
	 *  但是我们重载的返回的是一个局部变量,当函数执行完毕的时候局部变量会被销毁,
	 * 但是由于是返回值,所以会开辟另外的空间作为临时对象保存返回值(以拷贝的形式 调用拷贝构造函数)
	 * 当赋值的时候(Test t3=val)又会触发拷贝构造函数 这样会调用两次拷贝构造函数 效率低下  
	 * 所以编译器在执行的时候对Test t3 = t1+t2;是分开来的  
	 * Test t3; t3 = t1+t2;  
	 * 第一步调用无参拷贝构造函数  
	 * 第二步是赋值 不是初始化 不会调用拷贝构造函数 而是调用=符号的重载函数(g++自动实现= 浅拷贝)
     * */
     * 
    cout<<t3.data<<endl;
    cout<<t3.ptr<<endl;

    return 0;
}

>重载

Test(){
...
/*
比较的是test类中的data成员属性
*/
    bool operator >(const Test &t){
        return this->data>t.data;
    }
...
}

[]重载

Test(){
...
    char operator [](int index){
        if (index<0 || index >= strlen(ptr)){
            return '\0';
        }
        return ptr[index];
    }
    ...
}

=重载 深拷贝

    //t1=t2 返回的是t1 t1调用 this指针指向t1 所以可以返回*this
    // 那么返回类型是Test类型的,但是如果直接 Test 接受,那么创建临时对象 效率低
    // 由于t1是本来就存在的 所以我们可以返回t1的引用
    Test &operator =(const Test&t){
        delete[] this->ptr;
        this->ptr = new char[strlen(t.ptr+1)];
        strcpy(this->ptr,t.ptr);
        this->data = t.data;
        return *this;
    }

++重载

++分为前置++和后置++

    //前置++  
    Test& operator ++ (){
        ++data;
        return *this;
    }

    //后置++
   Test operator ++(int){
        //使用临时对象
        Test tmp = *this;
        data++;
        return tmp;
    }

<<重载

    //左操作数不是一个 类的对象 那么将左操作数和右操作时当作实参传给函数
    //返回的值应该还是ostream  
    //cout<<t1<<endl; 执行完 cout<<t1 然后变成  cout<<endl 继续执行
    // cout<<t1<<endl; 的时候 cout 传给 os  t1 传给 t
    // frined 允许传入多个形参
    friend ostream &operator << (ostream &os,const Test&t){
        os<<t.data<< endl;
        os<<t.ptr<<endl;
        return os;
    }

友元

可以理解为:朋友,可以直接访问私有成员

友元函数

友元函数是可以直接访问类的私有成员的非成员函数。
定义在类的外的普通函数,不属于任何类,但要在类里声明

friend 类型 函数名(形参);

友元函数使用:

  • 声明可以放在public 也可以放在 private protected,没有任何区别,都说明是该类的一个友元函数
  • 一个函数可以是多个类的友元函数,只需要在各个类里面声明
  • 友元函数的调用和普通函数的调用方式是一样的
  • 友元函数没有this指针
  • 两个类要共享数据可以使用友元函数
  • <<运算符的重载需要用到友元函数

为什么有些运算符的重载需要友元函数?

如果重载双目运算符(类的成员函数),就只要设置一个参数为右侧运算量,而之左侧运算量就是对象本身。
<<和>>左侧运算符是cin和cout,不是对象本身,不满足后面一点,所以要申明友元函数

在这里插入图片描述

友元类

一个类是另外一个类的友元

class A{
public:
    void func1(B &b);
    void func2(B &b);
};

class B{
private:
    int x;

    friend class A; // 将A设置为友元类
};
void A::func1(B &b){
    cout<<"func1:"<<b.x<<endl;
}
void A::func2(B &b) {
    cout<<"func2:"<<b.x<<endl;
}

使用友元的注意事项

1、友元关系不能继承(父亲的朋友不一定是自己的朋友)
2、友元关系是单向的,不具备交换性(若类B是类A的友元,类A不一定是类B的友元 )
3、友元关系具有非传递性,(若类B是类A的友元,类C是B的友元,类C不一定是类A的友元)

友元增加了程序允许的效率(设置为友元后,不需要检查访问控制符等操作,减少了类型检查和安全性检查等),但是破环了类的封装性和隐蔽性,使得非成员函数可以访问类的私有成员,不建议使用

动态联编

动态多态,运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应方法

也就是说,在运行到该语句的时候,才知道它调用哪一个函数/模块

动态多态满足三个条件:

  • 有继承关系
  • 有虚函数
  • 有基类指针指向派生类对象 或者 基类的引用变量引用派生类对象

虚函数

作用主要是实现多态机制,子类可以重写该函数

在类的成员函数里定义(构造函数不能是虚函数)
virtual 函数类型 函数名(形参)

为什么我要变成虚函数?
派生类需要重写基类的该函数 该行为在绝大数的派生类中都有自己特点

class Base{
public:
    virtual void func1(){
        cout<<"Base::func1"<<endl;
    }

    void func2(){
        cout<<"Base::func2"<<endl;
    }
};
class A : public Base{
public:
    void func2(){//对基类Base中func2函数的隐藏
        cout<<"A::func2"<<endl;
    }

    void func1(){//对基类Base中虚函数func1函数的重写 override   覆盖
        cout<<"A::func1"<<endl;
    }
};


 	Base *p;
    p=&a;//基类指针指向派生类
         // 基类的指针和引用调用虚函数时,
         //如果被派生类重写了那么调用的时派生类重写之后的函数;如果没有重写,那么调用的时基类中的函数

    p->func1(); //调用的派生类的func1
    p->func2(); //调用的基类的func1

虚函数实现原理

实现是由两个部分组成的,虚函数指针和虚函数表

虚函数指针

1、virtual function pointer vptr,本质上说就是一个指向函数的指针,与普通的指针无区别。
2、指向用户所定义的虚函数,具体是在子类里实现,当子类调用虚函数的时候,实际上是通过调用该虚函数指针从而找到接口。

只有拥有虚函数的类才会拥有虚函数指针,每一个虚函数都有一个对应的虚函数指针

虚函数指针是在编译器对类进行编译完成之后就产生了,而且是共享的、只读的、属于类的
存放在数据段上,而是是只读数据段(.rodata)

如果是在实例化之后才产生虚函数指针,那么应该打印16B,但是不是 说明在编译完成就产生了。
为什么是8B呢? 虚表指针的大小
在这里插入图片描述
在这里插入图片描述

虚函数表

存放虚函数指针的数组 为虚函数表,virtual function table vtbl

每个包含虚函数的类都包含一个虚表。

当一个类(B)继承另外一个类(A)时,类B会继承类A的函数。一个类继承了包含虚函数的基类时,此类也有自己的虚函数表。

虚表是属于类的,不是属于某个具体的对象,一个类只需要一个虚表,同一个类的所有对象都使用同一个虚表

类B继承类A

class A{
public:
    virtual void func1(){}
    virtual void func2(){}
    void func3(){}
};

class B : public A{};

在这里插入图片描述
派生类中也有虚函数

class A{
public:
    virtual void func1(){}
    virtual void func2(){}
    void func3(){}
    virtual void func4(){}
};

class B : public A{
public:
    virtual void func(){}
    virtual void func1(){}
    //也可以写 void func1(){},也是虚函数。因为基类是虚函数  重写之后也是虚函数
};

在这里插入图片描述

虚函数表指针(虚表指针)

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*_vfptr,用来指向虚表。

当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表

虚表指针存在每一个被实例化的对象中,前提是该对象有虚函数,它总是被存放在该对象的地址首位,这种做法目的是保证运行的快速性

在这里插入图片描述

继承关系中各个类的虚函数表以及对象的内存模型

class A{
public:
    virtual void vfunc1(){}
    virtual void vfunc2(){}
    void func1(){}
    void func2(){}
private:
    int m_data1,m_data2;
};

class B : public A{
public:
    virtual void vfunc1(){}
    void func1(){}
private:
    int m_data3;
};
class C : public B{
public:
    virtual void vfunc2(){}
    void func2(){}
private:
    int m_data1,m_data4;
};

在这里插入图片描述

虚函数指针、虚函数、虚函数表的访问

class Base{
    virtual void f(){std::cout<<"f"<<std::endl;}
    virtual void g(){std::cout<<"g"<<std::endl;}
    virtual void h(){std::cout<<"h"<<std::endl;}
};

using u64_t = long long;
//定义一个函数指针  虚函数返回值void 形参列表为空
// 虚函数指针指向是虚函数的地址  所以需要一个函数指针
typedef void (*Func)();


//main
	Base b;
    // 虚表指针是对象的第一个元素,所以对象的首地址就是虚表指针的地址
    printf("%p\n",(u64_t*)(&b));

    //(int*)&b 显示类型转化   (int*)&b == &_vfptr 虚函数表指针的地址
    //*(int*)&b虚函数表指针的值,也是一个地址 即为指针类型 我们要返回它的值 也就是虚函数表的地址
    printf("%p\n",(u64_t*)(*(u64_t*)(&b)));

    //访问虚函数表中的虚函数指针
    //(int*)(*(int*)(&b))+0  虚函数表中第0个元素的地址
    //(int*)(*(int*)(&b))+0  虚函数表中第1个元素的地址
    //(int*)(*(int*)(&b))+0  虚函数表中第2个元素的地址
    //为什么是+2 +4? int*是认为int类型 即占4B,但是64bit指针占8B 所以要+2 +4 +6... 这样有错误 不方便
    //所以要使用 using u64_t=long long; 即使用long long代替int  8B
    printf("%p\n",(u64_t*)(*(u64_t*)(&b))+0);
    printf("%p\n",(u64_t*)(*(u64_t*)(&b))+1);
    printf("%p\n",(u64_t*)(*(u64_t*)(&b))+2);

    //取虚函数表中虚函数地址
    printf("%p\n",*((u64_t *)(*(u64_t *)(&b)) + 0)); //f函数的地址
    printf("%p\n",*((u64_t *)(*(u64_t *)(&b)) + 1)); //g函数的地址
    printf("%p\n",*((u64_t *)(*(u64_t *)(&b)) + 2)); //h函数的地址

    Func f;
    f=(Func)(*((u64_t *)(*(u64_t *)(&b)) + 0));
    f();//输出f
    f=(Func)*((u64_t *)(*(u64_t *)(&b)) + 1);
    f();//输出g
    f=(Func)*((u64_t *)(*(u64_t *)(&b)) + 2);
    f();//输出h
    //说明指向的是虚函数的地址

动态绑定

class A{
public:
    virtual void vfunc1(){cout<<"A::virtual void vfunc1()"<<endl;}
    virtual void vfunc2(){cout<<"A::virtual void vfunc2()"<<endl;}
    void func1(){cout<<"A::void func1()"<<endl;}
    void func2(){cout<<"A::void func2()"<<endl;}

};

class B : public A{
public:
    //重写基类虚函数 自身也是虚函数
    void vfunc1(){cout<<"B::virtual void vfunc1()"<<endl;}
    //隐藏基类的func1函数
    void func1(){cout<<"B::void func1()"<<endl;}

};


   	B b;
    b.vfunc1();//B::virtual void vfunc1()
    b.vfunc2();//A::virtual void vfunc2()
    b.func1();//B::void func1()
    b.func2();//A::void func2()

    A *p;
    p=&b;
    p->vfunc1();//B::virtual void vfunc1() 动态多态
    p->vfunc2();//A::virtual void vfunc2() 动态多态
    p->func1();//A::void func1()           静态多态
    p->func2();//A::void func2()           静态多态

    /*
     * 如果有基类指针指向派生类对象,并且通过基类指针调用某些函数时,编译器如何处理?
     * 编译器先检查调用的函数f是否为虚函数,
     *   如果f不是虚函数,那么采用静态编译 直接调用基类中的函数,不管派生类是否隐藏了该函数
     *   如果f是虚函数,那么会访问被指向(被引用)的派生类中的虚表指针,从而找到派生类的虚函数表,在虚函数表中查找调用该函数的虚函数指针
     *    编译时不知道该函数该调用哪一个函数,只有在运行的时候指针指向了具体的函数
     *      如果派生类重写基类中的虚函数,那么虚函数表保存的是派生类重写之后的虚函数指针。因此通过基类指针调用f函数时,最终调用的是派生类的函数f
     *      如果派生类没有重写基类中的虚函数,虚函数表中保存的是基类的虚函数指针,那么调用的是基类的函数f
     * */

非虚函数->静态联编,编译时候已经可以确定函数的具体调用
虚函数->动态联编,运行的时候才可以确定函数的具体调用
所以,非虚函数的执行效率高于虚函数

基类指针指向派生类对象调用函数注意事项

class A{
public:
    virtual void vfunc1(){cout<<"A::virtual void vfunc1()"<<endl;}
    virtual void vfunc2(){cout<<"A::virtual void vfunc2()"<<endl;}
    void func1(){cout<<"A::void func1()"<<endl;}
    void func2(){cout<<"A::void func2()"<<endl;}

};

class B : public A{
public:
    //重写基类虚函数 自身也是虚函数
    void vfunc1(){cout<<"B::virtual void vfunc1()"<<endl;}
    //隐藏基类的func1函数
    void func1(){cout<<"B::void func1()"<<endl;}
	void funcB(){cout<<"B::void func1()"<<endl;}
};

B b;
A *p;
p=&b;
p->funcB();//报错 
//编译器要先检查语法错误,检查p的类型为A的,看funcB为非虚函数,在A中没有funcB的函数 所以报错

//不要妄想使用基类指针访问派生类所独有的成员函数

纯虚函数

1、一般来说,许多时候基类并不能确定函数的实现方法,只能确定函数的功能。但是函数调用的时候必须要用到该函数。这种情况下,C++提供了一种机制,成为纯虚函数,属于虚函数的一种,体现了面向对象的多态性
2、定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。
3、纯虚函数的语法格式“

virtual 返回值类型 函数名(形参列表) = 0;

4、纯虚函数没有函数体,只有函数声明,在虚函数声明结尾加上=0,表明此函数为纯虚函数

最后的=0不表示函数返回值为0,只是形式上的作用,告诉编译器这个是纯虚函数

5、纯虚函数在代码段不占空间,只读数据段上的虚函数表中纯虚函数置为0

class Hero{
public:
    virtual void huicheng()=0;
};

在这里插入图片描述

抽象类

有纯虚函数的类就是抽象类(只要有就是),抽象类不可以实例化对象

抽象类作为基类,让派生类去实现虚函数的。派生类必须实现纯虚函数才能被实例化。

对基类中的纯虚函数要在派生类重新声明。也就是对派生类中重写基类的函数要重新声明

派生类是否一定要重写基类中的纯虚函数?

视情况而定,如果要派生类作为抽象类,那么不需要。如果要派生类作为普通类,那么必须实现

什么时候要把一个类定义为抽象类?

当某个类只需要描述一类事物的特征的时候

#include <iostream>
using u32_t = unsigned int;

class Shape{
public:
    virtual double getGirth()=0;
    virtual double getArea()=0;
};

class Circle:public Shape{
private:
    u32_t _r;
public:
    static double PI;
    Circle():_r(10){}
    Circle(u32_t r):_r(r){}
    double getGirth();
    double getArea();
};
double Circle::PI=3.14;
double Circle::getArea() {
    return double(PI* _r*_r);
}

double Circle::getGirth() {
    return double(2*PI*_r);
}

void get_Girth(Shape &s){
    std::cout<<s.getGirth()<<std::endl;
}
void get_Area(Shape &s){
    std::cout<<s.getArea()<<std::endl;
}
int main() {
    Circle c;
    get_Girth(c);
    get_Area(c);
    return 0;
}

虚析构函数

构造函数不可以是虚函数(为什么》?)

总结下面,一句话:基类的析构函数设置为虚函数

避免基类指针指向派生类对象,当通过基类指针释放派生类对象时不调用派生类的析构函数

class a{
public:
    a(){
        std::cout<<"a()"<<std::endl;
    }
    //虚析构函数
    ~a(){
        std::cout<<"~a()"<<std::endl;
    }
};

class b:public a{
public:
    b(){
        std::cout<<"b()"<<std::endl;
    }
    ~b(){
        std::cout<<"~b()"<<std::endl;
    }
};

    a *p = new b();//构造一个派生类对象
    delete p; //通过基类指针,释放一个派生类对象
     /*
     * 结果
     * a()
     * b()
     * ~b()
     * ~a()
     * */


    /*
     * 如果不把基类的析构函数设置为虚函数,结果:
     * a()
     * b()
     * ~a()
     *
     * 没有释放派生类实例对象,会造成内存泄露,所以在开发过程中,将基类的析构函数设置为虚函数
     * 开发要使用大量的继承 多态,基类指针指向派生类对象
     * */

引用作为函数返回值

/*
warning
	原因:当func函数调用完成的时候,局部变量t已经被销毁,但是我们的返回值还是指向原先的空间
	不能返回本函数栈上的变量、对象的引用
*/
int &func(){
    int t;
    return t;
}

重载 重写 覆盖 隐藏

重载:只发生在同一个作用域中,比如一个类中成员函数的函数名相同,但是函数的形参个数、类型、类型顺序不同,那么就是函数的重载。
为什么可以重载? g++编译器在编译的时候会生成符号表,而符号表中符号是以函数名+形参类型来命名的,所以在函数的形参个数、类型、类型顺序不同时,生成的符号是不同的,即认为是不同的函数

重写/覆盖:发生在不同的作用域中(发生在基类和派生类),派生类中的成员函数的名字、返回值、形参列表和基类中的虚函数都完全相同。

隐藏:发生在不同的作用域中(发生在基类和派生类),派生类中的成员函数的名字、返回值、形参列表和基类中的普通函数都完全相同;派生类中的成员函数的名字和基类中的函数都相同,但是形参列表不同或者返回值不同也是隐藏,但此时基类中无论是普通函数还是 虚函数都会被隐藏。

文章来源:https://blog.csdn.net/bossDDYY/article/details/135391139
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。