【C++ | 虚函数】虚函数详解 及 例子代码演示(包含虚函数使用、动态绑定、虚函数表、虚表指针)

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
🤣本文内容🤣:🍭介绍C++的虚函数 🍭
😎金句分享😎:🍭你不能选择最好的,但最好的会来选择你——泰戈尔🍭
⏰发布时间⏰:2024-07-13 14:33:55

本文未经允许,不得转发!!!

目录

  • 🎄一、什么是虚函数?为什么需要虚函数?
  • 🎄二、静态绑定、动态绑定
  • 🎄三、虚函数的使用
    • ✨3.1 虚函数在基类的定义
    • ✨3.2 虚函数在派生类的使用
    • ✨3.3 finaloverride 说明符
  • 🎄四、虚函数表、虚表指针
    • ✨4.1 虚表指针
    • ✨4.2 虚函数表
  • 🎄五、总结



在这里插入图片描述

🎄一、什么是虚函数?为什么需要虚函数?

定义:虚函数就是在函数声明时使用关键字virtual修饰的成员函数。其格式一般如下:

class CAnimal{virtual void eat();	// 声明了虚函数 eat()
}

为什么需要虚函数?

在C++中使用虚函数的主要目的是实现多态。多态是多种形态的意思,也就是同一个方法在派生类和基类中的行为是不同的。

C++怎样通过虚函数实现多态的呢?一般情况下,通过指针或引用调用一个类成员函数时,程序会 根据引用类型或指针类型 选择对应类的成员函数,如果被调用的成员函数是虚函数的话,则程序将根据 引用或指针指向的对象 的类型来选择方法。

下面用代码演示基类指针调用 正常函数 和 虚函数 的区别:

// g++ 23_Virtual.cpp 
#include <iostream>using namespace std;class CAnimal{
public:CAnimal(){cout << "Calling CAnimal(): this=" << this << endl;}void eat(){cout << "Animal eat" << endl;}virtual void run(){cout << "Animal run" << endl;}private:
};class CDog : public CAnimal{
public:CDog(){cout << "Calling CDog(), this=" << this << endl;}void eat(){		// 重写了基类的eat(),基类的会隐藏cout << "Dog eat" << endl;}void run(){cout << "Dog run" << endl;}
};int main ()
{CAnimal animal;CDog dog;CAnimal* pAnimal = &animal;	// 基类指针指向基类对象pAnimal->eat();	// 调用非虚函数,按照指针类型调用,打印 Animal eatpAnimal->run();	// 调用虚函数,按照对象类型调用,打印 Animal runcout << endl;pAnimal = &dog;	// 基类指针指向派生类对象pAnimal->eat();	// 调用非虚函数,按照指针类型调用,Animal eatpAnimal->run();	// 调用虚函数,按照对象类型调用,打印 Dog runcout << endl;return 0;
}

运行结果如下:
在这里插入图片描述


在这里插入图片描述

🎄二、静态绑定、动态绑定

绑定(binding):编译器将源代码中的函数调用解释为执行特定的函数代码块被称为函数名绑定(binding)

在 C 语言中,函数名绑定非常简单,因为每个函数名都对应一个不同的函数。 在 C++ 中,由于重载函数的出现,函数名绑定变得复杂,编译器必须査看函数参数以及函数名才能确定使用哪个函数。编译器可以在编译期间完成这样的函数名绑定。但是虚函数的出现使编译器无法在编译期间知道调用的是哪个函数,必须在运行时才完成函数名绑定。

静态绑定(binding):在编译期间就可以完成的函数名绑定。
动态绑定(binding):在运行期间才可以完成的函数名绑定。

当 我 们 使 用 基 类 的 引 用 或 指 针 调 用 基 类 中 定 义 的 一 个 函 数 时 , 我 们 并 不 知 道 该 函 数 真 正 作 用 的 对 象 是 什 么 类 型 , 因 为 它 可 能 是 一 个 基 类 的 对 象 也 可 能 是 一 个 派 生 类 的 对 象。 如 果 该 函 数 是 虚 函 数, 则 直 到 运 行 时 才 会 决 定 到 底 执 行 哪 个 版 本, 判 断 的 依 据 是 引 用 或 指 针 所 綁 定 的 对 象 的 真 实 类 型

当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

之前总是不理解一点,我在定义基类指针或引用时,明明已经指明的对象了,为什么它需要在运行时才知道呢?那是因为考虑少了。如果当基类指针或引用作为函数参数使用,没运行时是不清楚传入的是基类对象或子类对象的。又或者从输入根据条件去创建对象,也是无法事先知道基类指针或引用指向哪个对象的。下面代码演示C++的动态绑定:

// g++ 23_Dynamic_binding.cpp -std=c++11
#include <iostream>
using namespace std;class CAnimal{
public:virtual void run(){cout << "Animal run" << endl;}
};class CDog : public CAnimal{
public:virtual void run() override{cout << "Dog run" << endl;}
};int main ()
{const int OBJ_NUM = 4;CAnimal *pAnimal[OBJ_NUM];	// 基类指针数组,用于管理对象// 程序运行后,根据输入类型创建对象char kind;		// 用于获取类型for(int i=0; i<OBJ_NUM; i++){cout << "请输入要创建的对象:1表示CAnimal, 2表示CDog" << endl;while(cin >> kind && (kind != '1' && kind != '2'))cout << "请输入1或2" << endl;if(kind == '1')pAnimal[i] = new CAnimal();elsepAnimal[i] = new CDog();}cout << endl;// 按照实际的对象打印for(int i=0; i<OBJ_NUM; i++){pAnimal[i]->run();}for(int i=0; i<OBJ_NUM; i++){delete pAnimal[i];	// 释放对象}return 0;
}

运行结果如下:
在这里插入图片描述


在这里插入图片描述

🎄三、虚函数的使用

虚函数一定是在 继承的前提下 才有用的,如果定义了一个类,它不会被继承,那么这个类定义虚函数也没用。
虚函数一定是在 基类的指针或引用 去调用时,才会按照实际对象类型去动态绑定的。

✨3.1 虚函数在基类的定义

在基类定义虚函数时,需要注意下面几点:

  1. 所有虚函数都必须有定义。因为直到运行时才知道调用哪个虚函数,这样做避免出现问题。
  2. 如果某个函数的行为在派生类会存在不同实现,则可以考虑设置成虚函数,例如:CAnimal类的eat方法就可能与其派生类CDog的 eat方法不同实现。
  3. 如果不需要在派生类中重新定义基类的函数,则不应该将其设置为虚函数,节省开销。例如下面例子的setName函数。
  4. 一旦某个函数在基类被声明成虚函数,则在所有派生类中它都是虚函数。
  5. 构造函数不能声明为虚函数。
  6. 基类的析构函数一般声明为虚函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。如果析构函数不是虚的, 则将只调用对应于指针类型的析构函数。如果析构函数是虚的,将调用相应对象类型的析构函数。
  7. 关键字virtual只用于类声明的函数原型中,不出现在定义的时候。

🌰举例子:

// g++ 23_Virtual_usage.cpp 
#include <iostream>
#include <string.h>using namespace std;class CAnimal{
public://virtual CAnimal(){ 	// 5、构造函数不能设置虚函数CAnimal(){cout << "Calling CAnimal(): this=" << this << endl;}virtual void eat(){	// 2、派生类的eat()可能不同实现,设置成虚函数cout << "Animal eat" << endl;}void setName(char *name)	// 3、不需要在派生类重写的函数,不设置成虚函数{strncpy(m_name, name, sizeof(m_name));}virtual ~CAnimal(){	// 6、基类的析构函数声明为虚函数cout << "~CAnimal" << endl;}
private:char m_name[64];
};class CDog : public CAnimal{
public:CDog(){cout << "Calling CDog(), this=" << this << endl;}virtual void eat(){		// 重写了基类的eat()cout << "Dog eat bones" << endl;}~CDog(){cout << "~CDog" << endl;}
};int main ()
{CAnimal* pAnimal = new CAnimal();	// 基类指针指向基类对象pAnimal->eat();delete pAnimal;pAnimal = NULL;cout << endl;pAnimal = new CDog();	// 基类指针指向派生类对象pAnimal->eat();delete pAnimal;pAnimal = NULL;cout << endl;return 0;
}

运行结果如下,下面运行了两次,第一次是基类析构函数没声明为虚函数的。
在这里插入图片描述


✨3.2 虚函数在派生类的使用

基类定义的虚函数,如果派生类有不同的实现,需要重写。

派生类使用虚函数注意几点:

  1. 一旦某个函数在基类被声明成虚函数,则在所有派生类中它都是虚函数。
  2. 如果派生类重新定义基类的虚函数,最好加上关键字virtual,这样使代码更容易读。不加virtual的话,也是虚函数。
  3. 重写基类的虚函数时,需要保证 函数名,参数列表,const属性 都与基类的虚函数声明一致。
    class CAnimal{
    public:virtual void eat(int x) const; // 虚函数
    };class CDog: public CAnimal{
    public:virtual void Eat(int x); // e 写成 E,函数名不一致,新的虚函数 virtual void eat(short x); // 参数列表不一样,新的虚函数 virtual void eat(int x); // const 属性不一样,新的虚函数 virtual void eat(int x) const; // 函数名,参数列表,const属性都一致,重写了基类的虚函数 
    }
    
  4. 重写基类虚函数时,返回值类型也需要一致,否则会编译报错。下面这个情况除外,如果基类虚函数返回值类型是基类指针或引用时,在派生类重写时返回值类型可以在派生类指针或引用。
    class CAnimal{
    public:virtual CAnimal *run(int a) const;
    };class CDog: public CAnimal{
    public:virtual CDog *run(int a) const;	// 重写了基类的 run ,但返回值为 CDog*
    }
    

🌰完整例子:

// g++ 23_Virtual_usage2.cpp 
#include <iostream>
#include <string.h>using namespace std;class CAnimal{
public:CAnimal(){cout << "Calling CAnimal(): this=" << this << endl;}virtual void eat(int a) const{cout << "Animal eat" << endl;}virtual CAnimal *run(int a) const{cout << "Animal run" << endl;return new CAnimal();}virtual ~CAnimal(){cout << "~CAnimal" << endl;}};class CDog : public CAnimal{
public:CDog(){cout << "Calling CDog(), this=" << this << endl;}virtual void Eat(int x){};		// e 写成 E,函数名不一致,新的虚函数 virtual void eat(short x){};	// 参数列表不一样,新的虚函数 virtual void eat(int x){};		// const 属性不一样,新的虚函数 virtual void eat(int a) const{		// 重写了基类的eat()cout << "Dog eat bones" << endl;}virtual CDog *run(int a) const{	// 重写了基类的 run ,但返回值为 CDog*cout << "CDog run" << endl;return new CDog();}~CDog(){cout << "~CDog" << endl;}
};int main ()
{CAnimal* pAnimal = new CAnimal();	// 基类指针指向基类对象CAnimal* pRun = pAnimal->run(1);pAnimal->eat(1);delete pAnimal;pAnimal = NULL;cout << endl;delete pRun;pRun = NULL;cout << endl;pAnimal = new CDog();	// 基类指针指向派生类对象pRun = pAnimal->run(1);pAnimal->eat(1);delete pAnimal;pAnimal = NULL;cout << endl;delete pRun;pRun = NULL;cout << endl;return 0;
}

运行结果:
在这里插入图片描述


✨3.3 finaloverride 说明符

由于派生类重写虚函数时需要 函数名,参数列表,const属性 都一致才行,这样容易导致写错。C++11提供了两个说明符 finaloverride 来帮助我们更好地使用虚函数。

  • final 说明符:如果基类已经把函数定义成 final 了, 则之后任何尝试覆盖该函数的操作都将引发错误:
    class CAnimal{
    public:virtual void finlaFun(int a) const final; // 指定为final,不允许被重写
    };
    class CDog : public CAnimal{
    public:virtual void finlaFun(int a) const{}	// 基类指定为final了,重写会报错
    };
    
  • override 说明符:可以告诉编译器,我们写的这个函数是为了重写基类的虚函数,如果 函数名,参数列表,const属性,返回值 这些不一致,就给我报错。在3.2的例子中,我们声明的虚函数如果和基类不一致会成为新的虚函数,加上override之后,如果不构成重写就会报错
    class CAnimal{
    public:virtual void eat(int x) const; // 虚函数
    };class CDog: public CAnimal{
    public:virtual void Eat(int x)override{};		// e 写成 E,函数名不一致,报错virtual void eat(short x)override{};	// 参数列表不一样,报错virtual void eat(int x)override{};		// const 属性不一样,报错virtual void eat(int a) const override{		// 重写了基类的eat()cout << "Dog eat bones" << endl;}
    }
    

🌰完整例子:

// g++ 23_Virtual_usage3.cpp -std=c++11
#include <iostream>
#include <string.h>using namespace std;
#define DEBUG 1
class CAnimal{
public:CAnimal(){cout << "Calling CAnimal(): this=" << this << endl;}virtual void finlaFun(int a) const final{	// 指定为final,不允许被重写cout << "Animal finlaFun" << endl;}virtual void eat(int a) const{cout << "Animal eat" << endl;}virtual CAnimal *run(int a) const{cout << "Animal run" << endl;return new CAnimal();}virtual ~CAnimal(){cout << "~CAnimal" << endl;}
};class CDog : public CAnimal{
public:CDog(){cout << "Calling CDog(), this=" << this << endl;}
#if DEBUGvirtual void finlaFun(int a) const{}	// 基类指定为final了,重写会报错virtual void Eat(int x)override{};		// e 写成 E,函数名不一致,报错virtual void eat(short x)override{};	// 参数列表不一样,报错virtual void eat(int x)override{};		// const 属性不一样,报错
#endifvirtual void eat(int a) const override{		// 重写了基类的eat()cout << "Dog eat bones" << endl;}virtual CAnimal *run(int a) const override{// 重写了基类的 run ,但返回值为 CDog*cout << "CDog run" << endl;return new CDog();}~CDog(){cout << "~CDog" << endl;}
};int main ()
{CAnimal* pAnimal = new CAnimal();	// 基类指针指向基类对象delete pAnimal;pAnimal = NULL;cout << endl;pAnimal = new CDog();	// 基类指针指向派生类对象delete pAnimal;pAnimal = NULL;cout << endl;return 0;
}

编译上面例子时,需要添加 -std=c++11 让编译器支持C++11标准,编译报错结果如下,想编译通过需要把宏DEBUG的值改为0:
在这里插入图片描述


在这里插入图片描述

🎄四、虚函数表、虚表指针

这个小节介绍虚函数的工作原理。

✨4.1 虚表指针

通常, 编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这个函数地址数组被成为虚函数表,而这个隐藏成员被成为虚表指针(在32位系统占用4个字节,64位系统占用8个字节)。所以,当一个类存在虚函数,那么它就会多占用一个指针大小的内存。这个指针会存在类对象的最前面。下面例子可以证明对象中存在一个虚表指针:

// g++ 23_Virtual_Point.cpp
#include <iostream>
using namespace std;class CAnimal_NULL{	// 空类
};class CAnimal{		// 基类,有一个虚函数
public:virtual void eat(){}
};class CDog : public CAnimal{// 派生类,继承了基类的虚函数
};int main ()
{CAnimal_NULL animal_null;CAnimal animal;CDog dog;cout << sizeof(animal_null) << endl;cout << sizeof(animal) << endl;cout << sizeof(dog) << endl;
}

运行结果,空类的大小是1,只定义了一个虚函数,类大小就变成8了,刚好是一个64位指针大小。
在这里插入图片描述


✨4.2 虚函数表

当基类定义了虚函数,基类对象就会将这些虚函数地址都存放到一个函数地址数组中(虚函数表),然后将这个数组的地址存放到对象的开始位置。

派生类对象的开始位置也会有一个虚表指针,指向一个独立的虚函数表,如果派生类重写(重新定义)了基类的虚函数,则会将重写的函数地址替换掉从基类继承的虚函数地址。

下面用代码演示类对象的虚函数表,甚至可以直接通过函数地址调用类的虚函数:

// g++ 23_Virtual_FunTable.cpp -std=c++11
#include <iostream>
using namespace std;class CAnimal{
public:virtual void eat(){cout << "Animal eat" << endl;}virtual void run(){cout << "Animal run" << endl;}
private:char m_name[64];
};class CDog : public CAnimal{
public:virtual void run() override{	// 重写,会使用新的虚函数地址cout << "Dog run" << endl;}virtual void jump(){cout << "Dog jump" << endl;}
private:int hair_color;
};typedef void(*PFUN)(void);	// 定义函数指针类型
int main ()
{CAnimal animal;CDog dog;// 获取虚表指针,类对象第一个指针大小的内存里的值unsigned long* vptr_animal = (unsigned long*)(*((unsigned long*)(&animal)));unsigned long* vptr_dog = (unsigned long*)*(unsigned long*)&dog;// 打印虚函数表各个虚函数地址cout << "虚表指针:" << vptr_animal << endl;for(int i=0; i<2; i++){cout << "animal虚函数表-第 " << i+1 << " 个虚函数地址:" << *vptr_animal << endl;PFUN pfun = (PFUN)*vptr_animal;pfun();	// 使用函数地址调用类函数vptr_animal++;}cout << endl;cout << "虚表指针:" << vptr_dog << endl;for(int i=0; i<3; i++){cout << "dog虚函数表-第 " << i+1 << " 个虚函数地址:" << *vptr_dog << endl;PFUN pfun = (PFUN)*vptr_dog;pfun();	// 使用函数地址调用类函数vptr_dog++;}
}

运行结果如下:
在这里插入图片描述
可以结合下图去理解代码:

在这里插入图片描述


在这里插入图片描述

🎄五、总结

👉本文介绍了C++的虚函数,包括虚函数的定义、重写,动态绑定、虚函数表、虚表指针等内容。

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

参考:
《C++ primer plus》
《C++ primer》
C++虚函数详解

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

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

相关文章

Matlab-Simulink模型保存为图片的方法

有好多种办法将模型保存为图片&#xff0c;这里直接说经常用的 而且贴到Word文档中清晰、操作简单。 simulink自带有截图功能&#xff0c;这两种方法都可以保存模型图片。选择后直接就复制到截切板上了。直接去文档中粘贴就完事了。 这两个格式效果不太一样&#xff0c;第一种清…

JS登录页源码 —— 可一键复制抱走

前期回顾 https://blog.csdn.net/m0_57904695/article/details/139838176?spm1001.2014.3001.5501https://blog.csdn.net/m0_57904695/article/details/139838176?spm1001.2014.3001.5501 登录页预览效果 <!DOCTYPE html> <html lang"en"><head…

通信协议 | 一文玩转UART协议就够了

文章目录 协议基础1、UART简介2、UART特性3、UART协议帧3.1、起始位3.2、数据位3.3、奇偶校验位3.4、停止位 4、UART通信步骤 最详细的UART通讯协议分析Verilog代码实现UART接口模块驱动 协议基础 1、UART简介 UART&#xff08;Universal Asynchronous Receiver/Transmitter&…

制造业ERP源码 ERP系统源码 ERP小程序源码

制造业ERP源码 ERP系统源码 ERP小程序源码 资料&#xff1a; 委外工厂管理 客户列表 车间管理 供应商管理 账户管理 商品管理 仓库管理 职员管理 自取地址管理 司机管理 辅助资料 客户等级 供应商分类 客户分类 商品分类 支出类别管理 收入类别管…

uniapp使用多列布局显示图片,一行两列

完整代码&#xff1a; <script setup>const src "https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/shuijiao.jpg" </script><template><view class"content"><view class"img-list"><image :src"src…

KnoBo:医书学习知识,辅助图像分析,解决分布外性能下降和可解释性问题

KnoBo&#xff1a;从医书中学习知识&#xff0c;辅助图像分析&#xff0c;解决分布外性能下降问题 提出背景KnoBo 流程图KnoBo 详解问题构成结构先验瓶颈预测器参数先验 解法拆解逻辑链对比 CLIP、Med-CLIPCLIPMed-CLIPKnoBo 训练细节预训练过程OpenCLIP的微调 构建医学语料库文…

【经典面试题】是否形成有环链表

1.环形链表oj 2. oj解法 利用快慢指针&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/typedef struct ListNode ListNode; bool hasCycle(struct ListNode *head) {ListNode* slow head, *fast…

前端练习小项目——方向感应名片

前言&#xff1a;在学习完HTML和CSS之后&#xff0c;我们就可以开始做一些小项目了&#xff0c;本篇文章所讲的小项目为——方向感应名片 ✨✨✨这里是秋刀鱼不做梦的BLOG ✨✨✨想要了解更多内容可以访问我的主页秋刀鱼不做梦-CSDN博客 在开始学习之前&#xff0c;先让我们看一…

Java中的代理机制

代理的概述 代理&#xff1a;给目标对象提供一个代理对象&#xff0c;并且由代理对象控制着对目标对象的引用&#xff08;跟多态一样&#xff09; mybatis、spring都运用了代理机制 跳过创建类的过程&#xff0c;直接产生对象 简单来说&#xff0c;我们使用代理对象来代替对…

【Python】日期和时间模块

个人主页&#xff1a;【&#x1f60a;个人主页】 系列专栏&#xff1a;【❤️Python】 文章目录 前言时间元组datetime 模块datetime类获取当前日期和时间 timedelta类日期和时间运算 strftime()方法格式化时间日期格式化符号 strptime()方法 Time 模块Time 模块内置函数Time 模…

编译适用于 Apple Silicon Macs 的 Chromium 教程

本教程将指导你如何在 Apple Silicon Macs 上编译 Chromium&#xff0c;包括所需的系统要求、工具安装、源码获取、环境配置、编译和运行步骤。 一、系统要求 Apple Silicon Mac&#xff08;如 M1、M2&#xff09;。安装 Xcode 和 macOS SDK&#xff08;通过 App Store 安装最…

AWDAWFAAFAWAWFAWF

创建两张表&#xff1a;部门&#xff08;dept&#xff09;和员工&#xff08;emp&#xff09; 创建视图v_emp_dept_id_1&#xff0c;查询销售部门的员工姓名和家庭住址 创建视图v_emp_dept&#xff0c;查询销售部门员工姓名和家庭住址及部门名称 创建视图v_dept_emp_count(dept…

Ubuntu使用Nginx部署uniapp打包的项目

使用uniapp导出web项目&#xff1a; 安装&#xff1a; sudo apt install nginx解压web.zip unzip web.zip移动到/var/www/html目录下&#xff1a; sudo cp -r ~/web/h5/ /var/www/html/重启Nginx&#xff1a; sudo service nginx restart浏览器访问&#xff1a;http://19…

Java高频面试基础知识点整理4

干货分享&#xff0c;感谢您的阅读&#xff01;背景​​​​​​高频面试题基本总结回顾&#xff08;含笔试高频算法整理&#xff09; 最全文章见&#xff1a;Java高频面试基础知识点整理 &#xff08;一&#xff09;Java基础高频知识考点 针对人员&#xff1a; 1.全部人员都…

算法学习笔记(8.4)-完全背包问题

目录 Question&#xff1a; 图例&#xff1a; 动态规划思路 2 代码实现&#xff1a; 3 空间优化&#xff1a; 代码实现&#xff1a; 下面是0-1背包和完全背包具体的例题&#xff1a; 代码实现&#xff1a; 图例&#xff1a; 空间优化代码示例 Question&#xff1a; 给定n个物品…

Tomcat组件概念和请求流程

Tomcat:是一个Servlet容器(实现了Container接口)&#xff0c;容器分层架构从上到下分为。Engine(List<Host>)->Host(List<Context>)->Context(List<Wrapper>)->Wrapper(List<Servlet>); Engine:引擎&#xff0c;Servlet 的顶层容器&#xff0…

UML建模案例分析-类图中的关系

概要 类图之间的关系比较多&#xff0c;绝大多数情况下重点关注的还是关联关系、组合、聚合这三种&#xff0c;最终是如何对应到代码上的。 例子 以订单为例&#xff1a;订单和订单项之间是组合关系&#xff0c;这和数据库实体之间不一样。数据库实体有主外键&#xff0c;开…

Java聚合跑腿系统对接云洋聚合跑腿系统源码低价快递小程序APP公众号源码

一站式解决方案 &#x1f680;引言&#xff1a;跑腿服务的市场需求与聚合趋势 在快节奏的现代生活中&#xff0c;跑腿服务成为了越来越多人的选择。为了满足这一市场需求&#xff0c;各大跑腿平台纷纷涌现。然而&#xff0c;如何将这些平台进行有效整合&#xff0c;提供更为便…

比特币交易繁忙的一天

早晨:市场开盘与准备工作 6:00 AM - 全球市场监测 交易员们早早起床,开始监测全球市场动态,尤其是亚洲市场的动向。通过查看新闻、分析报告和市场数据,了解可能影响比特币价格的因素。 7:00 AM - 团队会议 召开晨会,讨论当天的交易策略。团队分析前一天的交易情况,评…

HTML网页大设计-家乡天水

代码下载: https://pan.quark.cn/s/6bad4cfaefda