C++ 深入理解模板实现多态思想

news2024/11/17 8:21:07

文章目录

  • 前言
  • 一、模板与多态基础
    • 1.模板
    • 2.多态
  • 二、模板实现多态
  • 三、实际应用


前言

对C/C++学习感兴趣的可以看看这篇文章噢:C/C++教程

最近有时间,便用WTL写了一个兼具群聊单聊以及传输文件的聊天软件,过几天应该就能更新到 C/C++教程系列 中了

所以在这里提前讲解一下WTL中的一个非常重要的概念:模板实现多态

一、模板与多态基础

再进一步了解如何用模板来实现多态前,我们还是来看一看这两个概念的基础理解

1.模板

首先是模板,其主要用途在于让我们程序员少写代码

比如像下面两个函数类似的一系列函数:

int add(int a, int b) {
	return a + b;
}
double add(double a, double b) {
	return a + b;
}

就可以用模板简写为:

template<class T>
T add(T a, T b) {
	return a + b;
}

使用的方式如下:

	add<int>(1,3);
	add<double>(1.1, 3.4);
	add<char>('s','a');

但由于C++编译器可以自动推断参数类型,所以中间的<int>是可以省略的

这里要注意一个非常重要的问题,虽然我们只写了一个模板函数,但实际上并不止有一个函数

比如这里我们用了三种类型的add函数,那么编译器就会为我们分别生成三个函数

也就是说,这是编译器根据我们写的模板。帮我们自动生成的函数

进一步来说,模板是完全给编译器看的,并不会参与到最终的可执行文件中

上面这一点便是模板的精髓!

为了更加直观的理解,我们来看一下最终生成的三个函数的内存地址:

#include<iostream>
template<class T>
T add(T a, T b) {
	return a + b;
}

int main() {
	printf("%p\n", add<int>);
	printf("%p\n", add<double>);
	printf("%p\n", add<char>);
}

这样我们就能实际的看到最终确实是生成了三个函数,因为三个函数的地址完全不同,分别就代表着三个版本的add函数
在这里插入图片描述
总结来说就是,模板并没有减少最终的代码量,它仅仅只是减少了我们程序员需要写的代码量

并且这个过程是在编译期间就完成了的,这一点很重要!

之所以要用模板来实现多态,就是看重了它是在编译期间就完成的,而不会去影响最终的可执行文件的执行时间、大小

2.多态

然后便是多态了,多态是类中一个很重要的概念,其主要用途就是使得函数接口统一化

比如下面这段代码:

#include <iostream> 
using namespace std;
class A {
public:
    virtual void area() {
        cout << "这是基类A" << endl;
    }
};
class B : public A {
public:
    void area()
    {
        cout << "这是子类B" << endl;
    }
};
class C : public A {
public:
    void area()
    {
        cout << "这是子类C" << endl;
    }
};
// 程序的主函数
int main()
{
    A* a;
    a = new B();
    a->area();
    a = new C();
    a->area();
    return 0;
}

逻辑并不复杂,就是B,C两个类都继承于A

并且在基类中我们用到了关键字virtual 定义area为虚函数,还在两个子类里面都分别重写了这个函数

因为BC类都继承于A类,所以我们可以用A类指针来接收BC对象

从占用内存上考虑,子类是继承父类的,所以子类所占用的内存量肯定大于或等于父类占用内存,那么子类申请一块内存,赋值给父类的指针,父类就不可能会内存访问越界,而反过来,如果用子类指针存储父类对象,由于子类访问的内存大于等于父类,就可能造成内存访问越界,因此一般禁止这样使用

此时我们发现,我们只用了一个A调用同一个函数area,却可以完成两个类的调用!

在这里插入图片描述

所以很多时候,当我们使用别人的提供给我们的类时,只要知道了它的父类有哪些函数,那么其子类就必然有对应的函数

这可以极大方便类的管理、升级以及使用

虽然它的好处很多,但同样也有坏处,那就是它是动态绑定函数的,依靠了一个叫做虚函数表的东西,导致其内存占用更大,运行时间更长

比如上面的代码我们就可以在调试窗口中看到其虚函数表:

在这里插入图片描述

就是这个名为 _vfptr的变量名称,他就是指向虚函数表的函数指针,而虚函数表中就存有我们的虚函数

父类指针想要正确使用子类重写的函数,就必须要在这个虚函数表中进行遍历查询对应的函数地址

所以一旦你的类中有虚函数,那么你的类就肯定会多出一个指针大小的内存用于存储虚函数表的地址,并且最终生成的可执行文件也会变大很多字节

这取决于你的虚函数个数,每多一个虚函数,那么虚函数表就需要多一个指针大小的内存来存储

如果依旧不太懂的,可以自行在浏览器中搜索一下,有很多优秀的文章对此有解释

总结来说就是:使用传统类的多态特性,会导致程序效率变低,最终生成的可执行文件体积变大

原因就是它生成了虚函数表、虚函数指针,在程序运行过程中执行查询函数的操作

MFC就是因为大量使用的这种多态,公共控件都继承于基本窗口类,一般都有数十上百个虚函数,所以这就导致即使你什么都没干,一个MFC程序都至少有数兆大小,并且运行效率还较低

二、模板实现多态

了解了上面所说的两个基本概念的优缺点之后,现在我们就可以来到如何使用模板来实现多态了

因为模板就是编译期间就完成的操作,如果让模板来实现多态,那么就不存在运行期间去遍历虚函数表来找对应的函数,也不需要开辟一个虚函数表来存储虚函数地址

既能节约内存,又能提高程序运行效率,是不是非常的完美!

下面我们就来看一看模板实现多态的基本流程

#include <iostream> 
using namespace std;
template<class T>
class A {
public:
    void Show() {
        T* p=static_cast<T*>(this);
        p->area();
    }
    void area() {
        cout << "这是基类A" << endl;
    }
};
class B : public A<B> {
public:
    void area()
    {
        cout << "这是子类B" << endl;
    }
};
class C : public A<C> {
public:
    void area()
    {
        cout << "这是子类C" << endl;
    }
};
// 程序的主函数
int main()
{
    B b;
    b.Show();
    C c;
    c.Show();
    return 0;
}

这里同样是B,C两个类都继承自A类,但不同点就在于A类带了一个模板变量

所以B,C类在继承A的时候,就需要将自己这个类型传递进去

此时三个类都写了area函数,但只有基类写了show方法对吧

但由于B,C类都是继承自A类,所以它们其实也已经含有了show方法

然后便是最重要的一步,在基类的show方法中,我将this指针转化为T类型指针

static_cast与强制转化基本等价,唯一很大一点的区别就是,强制转换可以任意使用,比如B没有继承自A类,强制转换仍然可以将两者指针进行转换,而static_cast无法转换两个毫不相干的东西,这样就保证了传入的类型是继承自基类的,否则编译会直接报错

此时这里的p指针

T* p=static_cast<T*>(this);

实际就转化为了调用者的指针,以B举例子:

B b;
b.Show(); //调用Show方法后,完成了指针的转换,指代的B,那么B调用area函数,也就是调用自己重写的area函数

如果现在再多出一个子类D继承于A,但里面什么都没有:

class D : A<D>{
}

那么当你使用D时:

D d;
d.Show(); //将指针转换为D类型,由于D类型没有重写area方法,所以将调用继承下来的基类area方法

同样是一个show函数,能够却能根据情况选择出不同的函数调用,而且还是在编译期间就完成了的

这便是模板实现多态的基本原理

在这里插入图片描述

三、实际应用

由于上面说的都是实现原理,例子比较奇怪,下面我们来直接看一看ATL中的代码:

WTL是基于ATL之上开发的,而ATL库则是vs开发环境中自带,WTL库需要自己去下载

可以输入以下代码

#include<atlwin.h>
class MyWindow :public CWindowImpl<MyWindow> 
{

};

然后右键速览CWindowImpl类,接着在跳出的文件中搜索static_cast:

在这里插入图片描述
就能看到很多像上图这样的调用

  1. 先将this指针还原为子类
  2. 然后再调用对应的函数
  3. 如果子类重写了这个函数,那么就调用子类的函数,否则就调用父类的函数

当然这并不完全如此,比如上图中的那一出,是将其转化为子类后,传给某个函数进行处理

不过总体逻辑是一致的:在父类中操作子类,以实现静态多态的目的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/192560.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

EasyGBS+EasyNVS技术方案,如何实现对多现场国标视频平台的统一管理?

一、平台能力 1&#xff09;EasyGBS EasyGBS国标视频云服务平台支持无缝、完整接入内网或者公网的国标设备&#xff0c;在输出上&#xff0c;实现全平台、全终端输出。EasyGBS可将GB/T28181设备/平台推送的PS流转成ES流&#xff0c;并提供RTSP、RTMP、FLV、HLS、WebRTC等多种…

使用shell进行简单操作

目录 1、shell实现乘法表的打印 2、shell判定成绩等级 3、循环创建用户 1、shell实现乘法表的打印 要求&#xff1a;嵌套循环实现9*9乘法表&#xff08;两种方式&#xff09; 创建脚本文件&#xff1a;vim mcl.sh #!/bin/bash ######################### #File name:mcl.s…

idea插件及插件使用方法

CamelCase (下划线转驼峰) 使用快捷键&#xff1a;altshiftu。 按住altshift再不停的按U&#xff0c;会把选中内容的单词的下划线转驼峰转大写等&#xff0c;不停的转换。 Maven Helper Maven助手 安装之后再次打开pom文件&#xff0c;文件左下角会多出一个视图。 切换到"…

软件测试基础(三) 之 软件的生命周期

软件的生命周期一、软件的生命周期简述软件的生命周期中最早可能是客户&#xff0c;可能是产品的一个想法阶段&#xff0c;然后再到后来的一个需求阶段&#xff0c;再到开发人员去进行编码&#xff0c;去进行自己的自测&#xff0c;再提到软件测试人员进行综合测试&#xff0c;…

C语言之初识指针

前言 &#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f; c语言初阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>:介绍c语言中的新知识—指针有关的知识. 金句分享: ✨知…

大数据舆情监控流程,TOOM大数据舆情监控范围

大数据舆情监控是一种通过大数据技术&#xff0c;分析社会舆情信息&#xff0c;掌握舆情动态的方法。它利用大数据的存储和处理能力&#xff0c;对海量的网络舆情数据进行收集、清洗、分析、呈现&#xff0c;帮助企业和机构了解公众的想法和评价&#xff0c;掌握舆情动态&#…

【正点原子FPGA连载】第三十一章Linux内核定时器实验 摘自【正点原子】DFZU2EG_4EV MPSoC之嵌入式Linux开发指南

1&#xff09;实验平台&#xff1a;正点原子MPSoC开发板 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id692450874670 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html 第三十一章Linux…

【Linux】多线程详解(中)

&#x1f387;Linux&#xff1a; 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看见坚持…

哈希表的概念(散列表)

一、基本概念 散列表特点 &#xff1a; 数据元素的关键字与存储地址直接相关 通过哈希函数建立“关键字”与“存储地址”的联系 若不同的关键字通过散列函数映射到同一个值&#xff0c;则称它们为 “同义词” 通过散列函数确定的位置已经存放了其他元素&#xff0c;则称这种…

今日题目分享(两个维度的思考,双指针/动态规划,b站视频讲解)

先直接上连接 941有效的山脉数组 845数组中的最长山脉 2100适合打劫银行的日子. 2420找到所有好下标. 什么是两个维度&#xff1f; 这里是从代码随想录里面学习到的思考方式&#xff0c;开门见山地说&#xff0c;就是两个方面去考虑题目&#xff0c;比如&#xff0c;要求第…

vue前端框架应用案例(二)实现简单的SPA应用

目录路由使用步骤案例效果案例目录结构App.vueAbout.vueHome.vueindex.jsmain.jsindex.html本博客参考尚硅谷官方课程&#xff0c;详细请参考 【尚硅谷bilibili官方】 本博客以vue2作为学习目标&#xff08;请勿混淆v2与v3的代码规范&#xff0c;否则可能出现报错&#xff09…

docker搭建nacos集群

一、先搭建MySQL主从模式 Nacos使用delby作为内嵌数据库&#xff0c;在使用集群作为部署方式时&#xff0c;内嵌数据库无法保持数据同步与数据一致&#xff0c;故一般使用外接MySQL数据库的方式保存配置文件。使用一主一从的方式搭建&#xff0c;实现主从复制与读写分离。 1.…

线缆也可能是静电危害的罪魁祸首?

众所周知&#xff0c;几乎所有的电子元器件都是对静电敏感的&#xff0c;如果处理不当&#xff0c;将恶化元器件的性能&#xff0c;甚至造成彻底损坏。在低温干燥的环境中&#xff0c;极易产生静电&#xff0c;当然静电主要还是通过摩擦产生的。除了我们所熟知的静电产生的原因…

史上最全的测试用例设计方法

目录 前言 等价类划分方法&#xff1a; 边界值分析方法&#xff1a; 错误推测方法 因果图方法 判定表驱动分析方法 总结 前言 今天还是给大家带来一些干货&#xff0c;总结了一下测试用例的设计方法。具体内容太多我总结成了文档&#xff0c;获取方法在文末。这里截取部…

Swagger2Swagger3

一、什么是Swagger swagger是当下比较流行的实时接口文文档生成工具。接口文档是当前前后端分离项目中必不可少的工具&#xff0c;在前后端开发之前&#xff0c;后端要先出接口文档&#xff0c;前端根据接口文档来进行项目的开发&#xff0c;双方开发结束后在进行联调测试。 所…

Python程序设计之 —— 简易学生信息管理系统

大家好&#xff0c;我是 Enovo飞鱼&#xff0c;今天分享一个 Python程序设计之 —— 简易学生信息管理系统 &#xff0c;小白或者正在学习Python的小伙伴推荐阅读&#xff0c;加油&#x1f4aa;。 目录 前言 Python 简介 Python 特点 一、项目来源及背景 二、功能设计 …

PTA L1-023 输出GPLT(详解)

前言&#xff1a;本期是关于输出GPLT的详解&#xff0c;内容包括四大模块&#xff1a;题目&#xff0c;代码实现&#xff0c;大致思路&#xff0c;代码解读&#xff0c;今天你c了吗&#xff1f; 题目&#xff1a; 给定一个长度不超过10000的、仅由英文字母构成的字符串。请将字…

概论_第7章_参数估计__区间估计

先看知识结构图 一 置信区间 定义 定义&#xff1a; 设σ\sigmaσ 为总体的未知参数&#xff0c; θ^1θ^1(x1,x2,...,xn),θ^2θ^2(x1,x2,...,xn)\hat \theta_1 \hat\theta_1(x_1,x_2, ..., x_n), \hat \theta_2 \hat\theta_2(x_1,x_2, ..., x_n)θ^1​θ^1​(x1​,x2​,...,x…

YB菜菜的机器学习自学之路(七)——简单了解keras框架

YB菜菜的机器学习自学之路&#xff08;七&#xff09;——简单了解keras框架前提说明1. 机器学习框架-keras1.1 keras框架的特点1.2 keras框架实现一个神经元的建立的过程2. 举例说明2.1 一个神经元 和输入特征为1的案例2.2 多神经元 和单输入特征为1的案例2.3 多输入&#xff…

第11-15章

第11章 枚举和注解 11.1举例 要求创建季节(Season) 对象&#xff0c;请设计并完成。 但是&#xff0c;季节的值是有限的几个值&#xff08;4个季节&#xff09;&#xff0c;不可以再多。 就可以用枚举来解决 枚举&#xff08;enumeration,简写enum&#xff09;,是一组常量的集…