1-3 多态、模板

1 多态

多态分两类:

  • 静态多态:函数重载和运算符重载,即复用函数名
  • 动态多态:派生类和虚函数来实现运行时多态

区别:

  • 静态多态在编译阶段确定函数地址
  • 动态多态在运行阶段确定函数地址,根据传入的对象不同确定具体的执行函数

动态多态满足条件:

  • 首先要有继承关系
  • 子类要重写父类的虚函数,重写的时候函数名和形参列表以及返回类型都要相同

动态多态的使用:
父类的指针或引用指向子类的对象

class Animal
{
public:
    //虚函数
    virtual void speak(){
        cout<<"动物在说话"<<endl;
    }
};

class Cat:public Animal
{
public:
    void speak(){
        cout<<"猫在说话"<<endl;
    }
};

void test01(Animal &animal){
    animal.speak();
}

int main()
{
    Cat cat;
    test01(cat);//父类引用可以指向子类
    return 0;
}

如果父类中不是虚函数,输出就是动物在说话
变成虚函数后,输出就是猫在说话

父类函数为普通函数时,编译器就会在编译时认定调用对象就是animal,调用的函数地址就确定为animal中的speak;而虚函数会在运行阶段依据传入的参数来确定调用对象,进而确定是执行哪一个派生类的函数代码

2 多态的本质

前面已经知道,非静态成员和类是分开存储的,但是当我们用sizeof查看定义了虚函数的类之后,可以发现虚函数也是和静态成员一样和类存储在一起,它们是属于类的。

当我们在定义一个虚函数之后,类内会维护一个虚函数指针vfptr,而这个虚函数指针指向一个虚函数表vftable,在这个虚函数表中类的虚函数地址。当子类继承父类时,拷贝一份虚函数指针和虚函数表,如果子类重写了父类虚函数,那么子类会将虚函数表中的虚函数地址覆盖为自己的。

而多态就是可以传入不同的子类对象,子类对象拥有的虚函数表指针不同,会覆盖掉父类的虚函数表指针;因此会出现父类指向子类虚函数表,调用子类的虚函数,实现地址动态绑定,要注意的是,父类和子类会各自维护自己的虚函数表和表指针

3 多态的应用

设定一个计算器类

class Calculator
{
public:
    int m_Num1;
    int m_Num2;
    int getResult(string opt){
        if(opt == "+"){
            return m_Num1 + m_Num2;
        }else if(opt == "-"){
            return m_Num1 - m_Num2;
        }else if (opt == "*"){
            return m_Num2 * m_Num1;
        }
    }
    
};

void test01(){
    Calculator c;
    c.m_Num1 = 10;
    c.m_Num2 = 10;
    cout<<c.getResult("+");
}

int main()
{
    test01();
    return 0;
}

如果想要添加新的操作,这个时候就只能修改源码,实际中最好减少修改,但是允许扩展,所以接下来用多态实现

class AbstractCalculator//抽象类基类
{
public:
    int m_Num1;
    int m_Num2;

    virtual int getResult(){

    }
};

class AddCalculator:public AbstractCalculator
{
public:
    int getResult(){
        return m_Num1 + m_Num2;
    }
};

class SubCalculator:public AbstractCalculator
{
public:
    int getResult(){
        return m_Num1 - m_Num2;
    }
};

void test02(){
    //加法
    AbstractCalculator *abc = new AddCalculator;//父类指针指向子类对象
    abc->m_Num1 = 10;
    abc->m_Num2 = 5;
    cout<<abc->getResult()<<endl;
    delete abc;
    //减法
    abc = new SubCalculator;
    abc->m_Num2 = 5;
    abc->m_Num1 = 10;
    cout<<abc->getResult()<<endl;
    delete abc;
}

int main()
{
    test02();
    return 0;
}

由此可以看出多态的好处:

  • 我们只需要拓展的去写新功能,基类定义好要做什么,子类进行具体实现,利用多态实现
  • 结构清楚,利于维护

4 纯虚函数和抽象类

在多态中,通常父类中的虚函数不进行具体实现,主要还是子类重写内容

因此完全可以写成纯虚函数

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

当类中有纯虚函数时,我们称该类为抽象类。

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

5 虚析构和纯虚析构

使用多态时。如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数

解决方法:将父类中的析构函数改为虚析构或者纯虚析构,用以解决父类指针释放子类对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名()= 0;

但是纯虚析构必须要有实现,比如在类外进行类名::~类名(){}的实现,且拥有纯虚析构的类也属于抽象类,无法实例化对象

总结:

  • 子类中有堆区数据时必须实现虚析构或纯虚析构,没有时不用写;
  • 抽象类不能实例化对象

6 模板

1 模板的概念

模板就是建立通用的模具,大大提高复用性,比如PPt中的模板,样式框架已经放好了,把内容放进去就行,不同的内容实现了不同的ppt

C++中泛型编程主要用模板,模板有两种:

  • 函数模板
  • 类模板

2 函数模板

建立一个通用函数,函数返回值类型形参类型可以不具体指定,用一个虚拟的类型表示

语法:

template<typename T>
函数声明或定义
  • template--声明创建模板
  • typename--表明其后面的符号是一种数据类型,可用class替代
  • T--通用数据类型,也可用任意大写字母替换

使用:
比如我们要交换两个数,但是由于数据类型有多个,我们需要写多个函数swapInt、swapDouble....于是我们用模板来减少重复代码

template<typename T>//声明一个模板
void mySwap(T &a, T &b){
    T temp = a;
    a = b;
    b = temp;
}

//使用函数模板
//1自动类型推到
  mySwap(a,b);
//2.显示指定类型
  mySwap<int>(a, b);

注意事项:

  • 自动类型推导,必须推导出一致的数据类型T才可以使用
  • 模板只有确定了类型T才能使用

比如:

template<typename T>
void func(){
    cout<<1;
}

//使用
func<int>();

3 普通函数和函数模板之间的区别:

  • 普通函数调用时可以发生自动类型转换(隐式转换)
  • 函数模板调用时如果利用自动类型推导,没有自动类型转换
  • 但是用显示调用函数模板时,可以自动类型转换

4 普通函数和函数模板的调用规则

  • 如果函数模板和普通函数都实现了,优先调用普通函数
  • 可以通过空模板参数列表来强制调用函数模板,如myPrint<>(a, b);
  • 函数模板也可以重载
  • 如果函数模板可以产生更优的匹配,优先调用函数模板

5 函数模板的局限性

在使用函数模板的时候,内部会有比较之类的操作,如果遇到类对象作为参数的时候,就会出问题,这个时候需要为类对象另外开辟一条路径,比如:

template<typename T>
bool myCompare<T &a,T &b);

templat<> bool myCompare(Person &p1, Person &p2)

6 类模板

建立一个通用的类,类中成员和数据类型可以不具体指定

template<typename T,...>
类名

使用实例:

template<class NameType, class AgeType>
class Person
{
public:
    NameType m_Name;
    AgeType m_Age;
    Person(NameType name, AgeType age){
        this->m_Name = name;
        this->m_Age = age;
    }
};


int main()
{
    Person<string, int> p1("qqqq",28);

    return 0;
}

7 类模板和函数模板的区别

  1. 类模板没有自动类型推导,要显示使用给出参数类型
  2. 类模板允许默认参数,如:
template<class NameType, class AgeType = int>

8 类模板中成员函数创建时机

普通类中成员函数一开始就可以创建,类模板中成员函数在模板被调用时才去创建,因为类模板的类型一开始无法确定

9 类模板与继承

类模板被继承时要遵循以下规则:

  • 子类继承的是父模板时,子类在声明的时候要指定父类中的T
  • 如果不指定,编译器无法为子类分配内存
  • 如果需要灵活指定出父类中T类型,子类也需要变为类模板

比如:

template<class T>
class Base
{
    T m;
};

//如果不指定int,会报错
class Son:public Base<int>
{
    
};

//如果想灵活指定父类中T的类型,子类也写成可变模板,用T2来指定T
template<class T1, class T2>
class Son2:public Base<T2>
{
    
};

10 成员函数的类外实现

模板头和模板参数列表都要写

template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age){}

11 类模板分文件编写

类模板中成员函数的创建实在调用阶段,导致分文件编写时链接不到

解决:

  • 方式1:直接包含.cpp源文件
  • 方式2:将声明和实现写到同一个文件中,并改后缀为.hpp,hpp是约定名称,不强制
posted @ 2023-08-08 10:47  白日梦想家-c  阅读(22)  评论(0编辑  收藏  举报