当前位置: 首页 > news >正文

C++(面向对象编程——多态)

多态

1.概念

多态(Polymorphism)是面向对象编程的三大核心特性之一(封装、继承、多态),它允许不同对象对同一消息做出不同响应。

2.函数覆盖

2.1 概念:

函数覆盖是实现运行时多态的核心,它允许派生类重新定义基类的虚函数,实现特定于派生类的行为,是继承体系中最重要的特性之一。被virtual修饰的成员函数就是虚函数,如果一个类有虚函数,就会给该类生成一个vtable(虚机类表)。

2.2 虚函数性质:

(1)虚函数具有传递性,基类的虚函数可以把派生类新覆盖的函数编程虚函数。
(2)成员函数可以设置为虚函数,静态成员函数不能设置为虚函数。
(3)构造函数不能设置为虚函数,析构函数可以设置为虚函数。
(4)如果函数声明与定义分离,只需要使用virtual修饰声明即可。
(5)在C++11中,可以使用override关键字验证是否覆盖成功

2.3 和函数隐藏的区别

特点函数重写(函数覆盖)函数隐藏
关键字virtual/override无需关键字
参数列表必须完全一致可以不同
返回类型必须一致(或协变)可以不同
多态支持(虚函数)

不支持

3.多态的原理

3.1 定义,继承和声明的原理:

定义类的时候,当一个类中有虚函数的时候,编译器会为这个类分配一个表记录这些虚函数,这个表就是虚机类表。简单理解就是你创建一个类的时候,如果有使用virtual修饰函数,就认定这个类中存在虚函数,就会自动分配一个表(虚函数表)来存放这个类的虚函数,是把你这个类所有的虚函数都放在这一个表里。所以虚函数表是一个类只可能存在一个。

继承存在虚函数的类的时候,派生类是复制一份虚函数表,单独给派生类使用,如果派生类和基类共用一个表,函数覆盖就会改变基类的虚函数,会改变基类的原始功能,基类可能会定义其他对象或者被别的类继承,“动一发而牵全身”,就会乱套了。

声明存在虚函数的类的对象的时候,就会自动生成一个隐藏成员指针,这个指针指向类的虚机类表。

3.2 补充:

如图中定义Dog和Cat类的对象,对象中会自动生成一个成员变量,以便能调用虚函数和覆盖(重写)的虚函数。(图中类对应源代码中的类)

我们都知道指针类型大小是4字节,所以成员Dog的大小是12(两个string和一个隐藏的成员指针)。

如果类中还存在其他没有被virtual修饰的函数,这些函数存在哪里呢?为什么不算在对象所占的大小能?这不是虚函数的函数被存在特定的代码段里,这就涉及到底层设计,我们只需要知道这些函数被存放在其他位置存在对象中就可以了。这样做很明显是为了减少代码复用,每创建一次对象就存一份函数,开销太大了。

#include <iostream>using namespace std;class Animal{
private:string breed;
public:Animal(string breed):breed(breed){}string get_breed(){return breed;}void set_breed(string breed){this->breed = breed;}virtual void print(){           //使用virtual修饰,函数覆盖,实现多态的条件cout << breed << endl;}/*其他虚函数*/
};class Dog:public Animal{
private:string name;
public:Dog(string breed,string name):Animal(breed),name(name){}void print(){                   //函数覆盖Animal::print();            //调用基类的print函数打印breed成员变量cout << name << endl;}
};class Cat:public Animal{
private:string favorite;
public:Cat(string breed,string favorite):Animal(breed),favorite(favorite){}void print(){                   //函数覆盖Animal::print();cout << favorite << endl;}
};int main (){return 0;
}

 4.多态的使用

4.1 分类

 广义上将,多态分为静态多态动态多态

静态多态被称为编译时多态,发生在程序编译阶段;(函数重载)

动态多态也被称为运行时多态,发生在程序的运行阶段,这才算是正真意义上的多态,后面称呼的多态都是动态多态。

4.2 静态多态和动态多态的区别:

特点静态多态(Static)动态多态(Dynamic)
决定时机编译时运行时
实现方式重载、模板虚函数、继承
性能高,无虚表开销有虚表开销
典型场景算法泛型、重载多态接口、插件架构
关键字无需 virtual需要 virtual/override

4.3 多态实现的三个条件:

(1)必须是拥有继承

(2)发生函数覆盖

(3)基类指针指向/引用派生类对象

4.4 实现多态的思想:

多态按照字面理解就是“多种状态”可以概括为“一个接口,多种状态”例如一个函数接收参数程序运行过程根据传入参数类型的不同自动决定调用不同函数逻辑。说白了就是可以通过基类定义其他任意子类的对象,还能实现不同类之间转换(下面会说)。

下面代码中,注释掉的部分是使用最多的方式,可以使用基类创建不同类型的派生类对象,只要满足多态三个条件,这就体现出一个接口,多个状态。

第二种常用的就是成员函数接口,为什么要写两遍?这是为了对应定义的两中存储方式,一个是堆区指针,一个是栈区引用。

#include <iostream>using namespace std;class Animal{
private:string breed;
public:Animal(string breed):breed(breed){}string get_breed(){return breed;}void set_breed(string breed){this->breed = breed;}virtual void print(){           //使用virtual修饰,函数覆盖,实现多态的条件cout << breed << endl;}
};
class Dog:public Animal{            //1.公有继承
private:string name;
public:Dog(string breed,string name):Animal(breed),name(name){}void print(){                   //2.函数覆盖Animal::print();cout << name << endl;}
};
class Cat:public Animal{
private:string favorite;
public:Cat(string breed,string favorite):Animal(breed),favorite(favorite){}void print(){                   //函数覆盖Animal::print();cout << favorite << endl;}
};void print(Animal &s){          //3.引用派生类s.print();
}
void print(Animal *s){            //3.基类函数指向s->print();
}int main (){/*Dog d1("狗","金毛");Animal &a1 = d1;                        //栈类型定义a1.print();Animal *a2 = new Cat("猫","鲨鱼");       //堆类型定义a2->print();delete a2;*/Animal a1("猫");Dog d1("狗","金毛");Cat c1("猫","鱼");print(a1);                //调用函数print(d1);print(c1);Animal *a2 = new Animal("狗");Dog *d2 = new Dog("狗","哈巴狗");Cat *c2 = new Cat("猫","猫条");print(a2);print(d2);print(c2);delete a2;delete d2;delete c2;return 0;
}

 5.虚析构函数

当基类指针指向或引用派生类对象时,容易出现内存泄露的问题,因为派生类构造函数可能不会被调用。

当你使用上面第一种常用的方式声明一个堆区对象,编译器只会调用基类的析构函数,继承的时候,析构函数和构造函数是没有继承的,因为虚函数表中没有析构函数,只会调用基类析构函数,不然就会发生内存泄露问题(内存被没有释放的派生类一直占用知道程序结束才释放)。

下面代码基类的析构函数就是没有添加virtual修饰的,delete堆区多态的对象就会只调用基类的析构函数。可以自行在基类析构函数前面添加virtual试试。

设计如果使用默认析构函数存在上面隐患建议所有顶层基类析构函数都设置虚析构函数

#include <iostream>using namespace std;class Animal{
private:string breed;
public:Animal(string breed):breed(breed){}virtual void print(){           //使用virtual修饰,函数覆盖,实现多态的条件cout << breed << endl;}//其他虚函数~ Animal(){cout << breed << "析构函数" << endl;}
};
class Dog:public Animal{
private:string name;
public:Dog(string breed,string name):Animal(breed),name(name){}void print(){                   //函数覆盖Animal::print();cout << name << endl;}~ Dog(){cout << "Dog" << endl;}
};
class Cat:public Animal{
private:string favorite;
public:Cat(string breed,string favorite):Animal(breed),favorite(favorite){}void print(){                   //函数覆盖Animal::print();cout << favorite << endl;}~ Cat(){cout << "Cat" << endl;}
};int main (){/*        //不使用多态定义和不使用堆区定义对象都不会出问题Dog d1("狗","金毛");Cat c1("猫","鱼");Dog *d2 = new Dog("狗","哈巴狗");Cat *c2 = new Cat("猫","猫条");*/Dog d0("狗","金毛");Animal &d1 = d0;Cat c0("猫","鱼");Animal &c1 = c0;Animal *d2 = new Dog("狗","哈巴狗");Animal *c2 = new Cat("猫","猫条");delete d2;delete c2;cout << "#####################" << endl;return 0;
}

类型转换

1.概念:

就是类型转换,延续了C语言类型转换还增加了其他的转换。为什么放在多态里说,是因为这里包含的同属于一个基类的不同状态之间的转换。

2.分类:

static_cast 静态转换

dynamic_cast 动态转换

const_cast 常量转换

reinterpret_cast 解释转换

2.1 static_cast

(1)主要用于基本数据类型转换。

#include <iostream>using namespace std;int main()
{int x = 1;
//    double y = (double)x;double y = static_cast<double>(x);cout << y << endl;return 0;
}

(2)static_cast相比于C转换,可以在编译时进行类型检查,确保转换是合法的。如果转换不合法,编译器将报错,从而帮助开发者在编译阶段就发现并修复问题。

(3)但是它没有运行时类型检查来保证转换的安全性,需要程序员来判断转换是否安全。(比如double转换成int精度丢失不会提示)

(4)static_cast也可以用于类层次结构转换(但不擅长)即基类派生类之间指针或引用的转换,但是只能安全的进行上行转换:派生类指针引用转换基类

上行转换:

是把不是基类的成员直接去掉,如果对上行转换过的对象再下行转换,是可以成功的,上行转换去掉的成员会保存。

#include <iostream>using namespace std;class Animal{
public:string breed;Animal(){}Animal(string breed):breed(breed){}virtual void print(){cout << breed << endl;}
};
class Dog:public Animal{
public:string name;Dog(){}Dog(string breed,string name):Animal(breed),name(name){}void print(){Animal::print();cout << name << endl;}
};int main (){//上行转换Dog d0("狗","金毛");Animal &d1 = d0;                        //定义栈区多态对象Animal& a1 = static_cast<Animal&>(d1);      //上行转换//d1.print();cout << a1.breed << endl;//cout << a1.name << endl;                //错误(上行转换后不存在name成员函数)return 0;
}

下行转换:

对没有上行转换的对象,直接下行转换的时候,static_cast不知道该如何补齐少的成员,可能会访问到电脑的内存,能运行,但是会输出很多访问到的电脑信息(不安全)。

#include <iostream>using namespace std;class Animal{
public:string breed;Animal(){}Animal(string breed):breed(breed){}virtual void print(){cout << breed << endl;}
};
class Dog:public Animal{
public:string name;Dog(){}Dog(string breed,string name):Animal(breed),name(name){}void print(){Animal::print();cout << name << endl;}
};int main (){//上行转换Dog d0("狗","金毛");Animal &d1 = d0;                        //定义栈区多态对象Animal &a1 = static_cast<Animal&>(d1);      //上行转换//d1.print();cout << a1.breed << endl;//cout << a1.name << endl;                //错误(上行转换后不存在name成员函数)Animal a2("猫");Dog &d2 = static_cast<Dog&>(a2);cout << d2.breed << endl;cout << d2.name << endl;return 0;
}

(6)static_cast特殊情况也可以转换为自定义类型实际上隐式调用构造函数

下面对Dog类进行隐式调用构造函数,传了两个值,但是结果不是“猫”和“金渐层”,这是static_cast只能传最后一个参数,所以只把金渐层当做第一个参数传入。(了解)

2.2 dynamic_cast

dynamic_cast主要用于层次之间转换

        上行转换static_cast完全相同

        下行转换static_cast更加安全类型检查功能

#include <iostream>using namespace std;class Animal{
public:string breed;Animal(){}Animal(string breed):breed(breed){}virtual void print(){cout << breed << endl;}
};
class Dog:public Animal{
public:string name;Dog(){}Dog(string breed,string name):Animal(breed),name(name){}void print(){Animal::print();cout << name << endl;}
};int main (){//上行转换Dog d("狗","金毛");Animal &a = d;                        //定义栈区多态对象Animal& d1 = dynamic_cast<Animal&>(a);      //上行转换//d1.print();cout << d1.breed << endl;//cout << d1.name << endl;                //错误(上行转换后不存在name成员函数)return 0;
}

2.3 const_cast

const_cast用于添加移除对象const修饰

主要用法改变指针引用const特性以便于某些情况修改const变量正常代码不应该使用const_cast而是应该通过设计良好代码逻辑避免const_cast出现

#include <iostream>using namespace std;class Test
{
public:string str = "A";
};int main()
{const Test* t1 = new Test;cout << t1->str << endl;
//    t1->str = "B"; 错误Test* t2 = const_cast<Test*>(t1);cout << t1 << " " << t2 << endl; // 0x1001118 0x1001118t2->str = "B";cout << t1->str << endl; // Breturn 0;
}

2.4 reinterpret_cast

reinterpret_cast可以内存按照制定的规则重新解释这种转换风险极高慎用

#include <iostream>using namespace std;class A
{
public:string str = "A";
};class B
{
public:char str = 'B';
};int main()
{A* a = new A;B* b = reinterpret_cast<B*>(a);cout << a << " " << b << endl; // 0x10b1118 0x10b1118cout << b->str << endl;char c = '@';char* ptr_c = &c;int* ptr_i = reinterpret_cast<int*>(ptr_c);cout << *ptr_i << endl; // 1644068672return 0;
}

const_cast和reinterpret_cast都偏向底层操作的,一般用不到,对系统感兴趣的可以详细学习。

抽象类

1.基本使用:

1.1 概念:

抽象类是一种不能被实例化的类,通常用于定义其他类的通用结构和行为。它允许声明抽象方法(即没有具体实现的方法),子类必须实现这些方法。可以用来做框架。

1.2 使用:

存在一个纯虚函数的类就是抽象类,这样的抽象类就不能用来声明对象。什么是纯虚函数呢?就是像下面代码中set_breed函数和set_name函数,前面加上virtual修饰,后面加上=0,这样的没有函数体的函数就是纯虚函数。(纯虚函数就是特殊的虚函数)

class Animal{
public:string breed;string name;virtual void set_breed(string breed) = 0;virtual void set_name(string breed) = 0;void print(){cout << breed << " " << name << endl;}
};

2.继承

继承抽象类的派生类是不是抽象类取决于派生类是否实现基类中的纯虚函数。

2.1 派生类实现所有的纯虚函数

实现(reimplement)指的是覆盖基类的纯虚函数,并增加函数体。

如果派生类实现了所有的纯虚函数(继承来的),此时派生类中就没有纯虚函数了,派生类可以作为普通类使用。

#include <iostream>using namespace std;class Animal{
public:string breed;string name;virtual void set_breed(string breed) = 0;virtual void set_name(string breed) = 0;void print(){cout << breed << " " << name << endl;}
};class Cat:public Animal{
public:void set_breed(string breed){            //覆盖纯虚函数this->breed = breed;}void set_name(string name){                //覆盖纯虚函数this->name = name;}
};int main (){Cat c;c.set_breed("猫");c.set_name("mike");c.print();return 0;
}

2.2 派生类没有实现所有纯虚函数

这种情况下,派生类(Polygon)中仍然存在纯虚函数(继承来的),此时派生类仍然是一个抽象类,需要继续向下继承,直到某个派生类(Rectangle)全部实现纯虚函数。

#include <iostream>using namespace std;class Animal{
public:string breed;string name;virtual void set_breed(string breed) = 0;virtual void set_name(string breed) = 0;void print(){cout << breed << " " << name << endl;}
};class Cat:public Animal{
public:void set_breed(string breed){            //覆盖纯虚函数this->breed = breed;}//此时派生类还存在一个纯虚函数,此类仍是抽象类,不可以用来声明对象/*void set_name(string name){                //覆盖纯虚函数this->name = name;}*/
};class Mycat:public Cat{
public:void set_name(string name){                //覆盖纯虚函数this->name = name;}
};int main (){//Cat c0;                    //错误!Mycat c;                c.set_breed("猫");c.set_name("mike");c.print();return 0;
}

3.多态

抽象类型不能声明,但是抽象类型的引用和指针可以声明,因为抽象类支持多态。如果派生类实现了纯虚函数,可以使用基类声明对象,但是必须使用派生类构造。

#include <iostream>using namespace std;class Animal{
public:virtual void print()=0;virtual ~Animal(){}             //防止堆区内存泄露,基类虚析构
};class Cat:public Animal{
public:void print(){cout << "这是猫,喵喵喵" << endl;}
};class Dog:public Animal{
public:void print(){cout << "这是狗,汪汪汪" << endl;}
};int main (){//栈区对象多态Cat c0;Animal &c = c0;c.print();Dog d0;Animal &d = d0;d.print();cout << "######################" << endl;//堆区对象多态Animal *d1 = new Dog;Animal *c1 = new Cat;d1->print();c1->print();delete d1;delete c1;return 0;
}

4.纯虚析构函数:

如果一个类没有纯虚函数又想成为抽象类,可以把析构函数写成纯虚析构函数“占坑”。

#include <iostream>using namespace std;class Animal{
public:virtual void print(){cout << "hello" << endl;}// 声明纯虚析构函数virtual ~Animal() = 0;
};// 为了不产生析构问题,添加析构函数的定义
Animal::~Animal(){      cout << "必要" << endl;
}class Cat:public Animal{
public:void print(){cout << "这是猫,喵喵喵" << endl;}
};class Dog:public Animal{
public:void print(){cout << "这是狗,汪汪汪" << endl;}
};int main (){//栈区对象多态Cat c0;Animal &c = c0;c.print();Dog d0;Animal &d = d0;d.print();cout << "######################" << endl;//堆区对象多态Animal *d1 = new Dog;Animal *c1 = new Cat;d1->print();c1->print();delete d1;delete c1;cout << "######################" << endl;return 0;
}

http://www.lqws.cn/news/502381.html

相关文章:

  • 曼昆《经济学原理》第九版 宏观经济学 第二十六章货币增长与通货膨胀
  • python中学物理实验模拟:摩檫力
  • BI财务分析 – 反映盈利水平利润占比的指标如何分析(下)
  • iwebsec靶场sqli注入(2)
  • [Linux] Linux用户和组管理
  • GoAdmin代码生成器实践
  • 大模型项目实战:业务场景和解决方案
  • TongWeb替换tomcat
  • Linux Sonic Agent 端部署(详细版)(腾讯云)
  • MySQL:深入总结锁机制
  • 系统架构设计的全方位视角:深入解析4+1视图模型及其应用实践
  • prometheus+grafana+MySQL监控
  • 飞算 JavaAI 插件炸场!一小时搭图书管理系统
  • 从拥塞控制算法热交换到内核错误修复
  • Golang Kratos 系列:业务分层的若干思考(一)
  • 从用户到社区Committer:小米工程师隋亮亮的Apache Fory成长之路
  • WPF/Net Core 简单显示PDF
  • Windows WSL安装Emscripten‌/emsdk(失败)
  • Netty内存池核心PoolArena源码解析
  • OSS监控体系搭建:Prometheus+Grafana实时监控流量、错误码、存储量(开源方案替代云监控自定义视图)
  • RAG大模型开发初探 || 动手大模型应用开发
  • 医疗AI数智立体化体系V2.0泛化多模块编程操作手册--架构师版(下)
  • 第一篇Anaconda/Pytorch/Opencv/Yolov5环境搭建
  • 本地部署搜索引擎 Elasticearch 并实现外网访问(Windows 版本)
  • 6.24_JAVA_微服务day07_RabbitMQ高级
  • 基于C#实现(WinForm)P2P聊天小程序
  • NLP基础1_word-embedding
  • SQL关键字三分钟入门:UPDATE —— 修改数据
  • 秋招Day14 - MySQL - 场景题
  • 3D看房实现房屋的切换