首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的云盘
我的搜索
我的记录
我的图片
我的图书
我的笔记
我的音乐
我的影视
我的邮件
我的游戏
Search
1
virtuoso和empyrean alps模拟仿真和混仿教程
359 阅读
2
在IC617中进行xa+vcs数模混仿
300 阅读
3
文档内容搜索哪家强? 15款文件搜索软件横向评测
264 阅读
4
科普:Memory Compiler生成的Register file和SRAM有何区别?
257 阅读
5
通俗易懂的AI知识体系图及其产业链全景图
198 阅读
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
登录
Search
标签搜索
AI
python
Docker
vcs
PyQT
STM32
cadence
linux
systemverilog
EDA
Alist
vscode
uos
package
MCU
C
QT
CXL
sed
sv
bennyhe
累计撰写
387
篇文章
累计收到
33
条评论
首页
栏目
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的云盘
我的搜索
我的记录
我的图片
我的图书
我的笔记
我的音乐
我的影视
我的邮件
我的游戏
搜索到
31
篇与
的结果
2025-09-20
Python 必杀技:用 print() 函数实现的三个特效
print() 应该是初学者最先接触到的第一个 Python 函数,因为几乎所有的启蒙课程都是从 print('Hello world') 开始的。事实上, print() 也是程序员使用频率最高的函数之一,同时也是很多程序员喜欢的代码调试利器。但是关于 print() 函数,你真的了解吗?打字机效果不了解 print() 的 flush 参数,很难实现下图所示的打字机效果:动图封面print() 像个调皮的小朋友,你让他帮你打印,他一定会做,但未必是立即去做,也许会攒够了多个打印任务才执行一次。设置 flush=True,可以让这位小朋友立刻去执行命令。 import time def printer(text, delay=0.2): """打字机效果""" for ch in text: print(ch, end='', flush=True) time.sleep(delay) printer('玄铁重剑,是金庸小说笔下第一神剑,持之则无敌于天下。')旋转式进度指示Linux 系统文本界面下,最常用的进度指示是用横竖斜杠构成的旋转图案。动图封面Python也可以轻松实现这个效果,秘诀就在于 '\b' 字符。 '\b' 相当于键盘上的退格键,可以让我们把刚刚打印过的最后一个字符擦掉重新打印。这个效果,同样需要设置参数 flush 为真。 import time def waiting(cycle=20, delay=0.1): """旋转式进度指示""" for i in range(cycle): for ch in ['-', '\\', '|', '/']: print('\b%s'%ch, end='', flush=True) time.sleep(delay) waiting() 反转字符顺序,就可以改变旋转方向。将第一个字符 '-' 改成 '-- ',还可以实现这样的效果:动图封面覆盖式打印效果'\b' 的作用是回退一个字符,'\r' 则可以退回到行首。借助于 '\r',可以实现整行覆盖式的打印效果:动图封面需要注意的是,整行覆盖的话,新的字符串长度不能小于原字符串长度,否则会留下前一次的打印内容。这个效果,同样需要设置参数 flush 为真。 import time def cover(cycle=100, delay=0.2): """覆盖式打印效果""" for i in range(cycle): s = '\r%d'%i print(s.ljust(3), end='', flush=True) time.sleep(delay) cover()
2025年09月20日
69 阅读
0 评论
0 点赞
2025-09-05
4个令人惊艳的ChatGPT项目,开源了!
1T服务圈儿2023年04月18日17:32江苏来源丨经授权转自 Jack Cui(ID:JackCui-AI)作者丨Jack Cui自从 ChatGPT、Stable Diffusion 发布以来,各种相关开源项目百花齐放,着实让人应接不暇。今天,我将着重挑选几个优质的开源项目,对我们的日常工作、学习生活,都会有很大的帮助。今天整理分享给大家,希望对你有所帮助。一、Visual ChatGPT这个是微软开源的项目,一周多的时间,就斩获了 23.6k+ star。简单概括它,那就是一个多模态的问答系统。支持AI绘画、语言问答、看图问答,将 AI 届近期的 3 大热点集于一身。效果展示:系统实现框架如下:Visual ChatGPT的系统实现框架这是一个“大力出奇迹”的开源项目,集多方研究成果于一身:BLIP、CLIP、ChatGPT、pix2pix、inpainting、vqa 等。说白了,就是教你怎样使用这些项目,搭建一个多模态的问答系统,这个系统架构很有参考价值。项目地址:https://github.com/microsoft/visual-chatgpt二、SadTalker这是一篇 2023 年的 CVPR 论文对应的开源项目。刚刚开源,新鲜热乎~功能就是:根据一张图片、一段音频,合成面部说这段语音的视频。结合 ChatGPT、AIGC、音频文字转换,虚拟二次元 or 三次元形象,就能“活”过来了。此外,还项目还做成了 stable diffusion webui 的插件,也就是直接能在 stable diffusion 里使用。生成的图片,直接配合一段音频,就能生成合成的视频。项目地址:https://github.com/winfredy/sadtalker三、FateZero文本能编辑生成图片?那视频能编辑吗?FateZero:我可以!左图是原图,右图是生成效果,输入的文本是:增加 Pokémon 动漫风格除了视频的风格迁移,也支持修改里面的内容。比如:松鼠是胡萝卜,变成,兔子吃茄子。这个项目也是基于sd做的,离一键生成视频,又进了一步。项目地址:https://github.com/chenyangqiqi/fatezero四、ChatPaperarXiv 想必大家都知道,当下最流行的论文托管网站,上面有来自世界各地的科学家、研究学者。为了提高 arXiv 用户阅读论文的效率,有人开源了一款利用 ChatGPT 总结 arXiv 论文的开源工具 ChatPaper。简而言之,该项目可根据用户关键词下载 arXiv 上的最新论文,利用 ChatGPT3.5 API 强大的归纳能力,将其浓缩成固定格式,文字少且易读。同时,项目支持个人自己部署,或者直接去 Hugge Face 体验。项目地址:https://github.com/kaixindelele/ChatPaperhttps://huggingface.co/spaces/wangrongsheng/ChatPaper来自微信
2025年09月05日
32 阅读
0 评论
0 点赞
2025-09-01
C语言关键字的应用技巧
嵌入式微处理器2022年05月26日12:00北京摘要:嵌入式C开发关键字的应用技巧1、volatilevolatile修饰表示变量是易变的,编译器中的优化器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份,有效的防止编译器自动优化,从而与软件设计相符合。中断服务与主程序共享变量://volatile uint8_t flag=1; uint8_t flag=1; void test(void) { while(flag) { //do something } } //interrupt service routine void isr_test(void) { flag=0; }如果没使用volatile定义flag,可能在优化后test陷入死循环,因为test里使用的flag并没修改它,开启优化后,编译器可能会固定从某个内存取值。例如:for(int i=0; i<100000; i++); //对比 for(volatile int i=0; i<100000; i++);前者可能被优化掉,虽然编码本意是需要执行操作延时,但编译器认为代码无意义。总的来说,volatile是告知编译器,不管代码如何,必须保留,而且使用时需要重新从内存读取更新,不能使用先前读取的缓存,一般在驱动代码中使用较多。2、constconst是恒定不变的意思,其修饰的各种数据类似只读效果。1)、 修饰变量采用const修饰变量,即变量声明为只读,保护变量值以防被修改。例如const int i = 1;上面这个例子表明,变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10;属于错误操作。特别说明,定义变量的同时进行初始化,写成int const i=1,是正确的。2)、 修饰数组C语言中const还可以修饰数组,举例如下:const int array[5] = {1,2,3,4,5}; array[0] = array[0]+1; //错误,array是只读的,禁止修改数组元素与变量类似,具有只读属性,不能被更改;一旦更改,编译时就会报错。使用大数组存储固定的信息,例如查表(表驱动法的键值表),可以使用const节省ram。编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。3)、 修饰指针C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。举例如下:int i = 1; int j = 2; const int *p1 = &i; int* const p2 = &j;上面定义了两个指针p1和p2,区别是const后面是指针本身还是指向的内容。在定义1中const限定的是 p1,即其指向空间的值不可改变,若改变其指向空间的值如 p1=10,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但 p2,即其所指向空间的值可以改变,如 p2=20是没有问题的,程序正常执行。4)、 修饰函数参数const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改。所限定的函数参数可以是普通变量,也可以是指针变量。例如:void fun(const int i) { …… i++; //对i的值进行了修改,程序报错 }常用的函数如strlensize_t strlen(const char *string);const在库函数中使用非常普遍,是一种自我保护的安全编码思维。3、struct与union对于struct 结构体和union共联体在嵌入式领域是使用得非常频繁的,一些可编程芯片提供的寄存器库都是采用结构体和共联体结合的方式来提供给软件人员进行开发,同时在平时的编码过程中这两个数据类型的灵活应用也能够实现代码更好的封装与简化。如下面的简单示例,就可以非常灵活的访问Val中的bit位。 typedef union { BYTE Val; struct __packed { BYTE b0:1; BYTE b1:1; BYTE b2:1; BYTE b3:1; BYTE b4:1; BYTE b5:1; BYTE b6:1; BYTE b7:1; } bits; }BYTE_VAL, BYTE_BITS;其中:1表示按位操作。不只是位-字节可以,单字节与多字节也可以简化拼接。#include "stdio.h" typedef struct { union { struct { unsigned char low; unsigned char high; }; unsigned short result; }; }test_t; int main(int argc, char *argv[]) { test_t hello; hello.high=0x12; hello.low=0x34; printf("result=%04X\r\n",hello.result);//输出 result=1234 return 0; }运行输出 result=1234 (win7系统下QT开发环境),原本需要 (high<<8)|low 运算,可以简化为共用体类型自动完成,但必须注意平台的字节顺序,属于大端还是小端模式。在应用层面,如果明确某个数据可能存在两种可能,而且两种结果不会同时存在,也可以使用结构体与共用体组合的方式,确保模块对外接口统一。例如移动通信模块,使用数据结构保存其基站信息,因为制式不同,模块可能工作在2G-GSM,也可能在4G-Cat1,为保证上层读取基站信息接口唯一,使用共用体就非常合适,否则需定义两套接口。如果觉得文章可以,可关注微信公众号【嵌入式系统】获取更多信息。4、预定义标识符一般编译器都支持预定义标识符,这些标识符结合printf等打印信息帮助程序员调试程序是非常有用的,一般编译器会自动根据用户指定完成替换和处理。部分标识:__FILE__ //表示编译的源文件名 __LINE__ //表示当前文件的行号 __FUNCTION__ //表示函数名 __DATE__ //表示编译日期 __TIME__ //表示编译时间使用范例:printf("file:%s,line:%d,date:%s,time:%s",__FILE__,__LINE__,__DATE__,__TIME__);这些比较常见,主要用于日志分析、版本记录,便于调试。5、#与###:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量。 ##:是一种运算符,是将两个运算对象连接在一起,也只能出现在带参宏定义的文本替换中。#include "stdio.h" #define TO_STR(s) #s #define COMB(str1,str2) str1##str2 int main(int argc, char *argv[]) { int UART0= 115200; printf("UART0=%d\n", COMB(UART, 0));//字符串合并为变量UART0 printf("%s\n", TO_STR(3.14));//将数字变成字符串 return 0; }6、void 与 void*void表示的是无类型,不能声明变量或常量,但是可以把指针定义为void类型,如void ptr。void 指针可以指向任意类型的数据,在C语言指针操作中,任意类型的数据地址都可转为void* 指针。因为指针本质上都是unsigned int。常用的内存块操作库函数:void * memcpy( void *dest, const void *src, size_t len ); void * memset( void *buffer, int c, size_t num);数据指针为void* 类型,对传入任意类型数据的指针都可以操作。另外其中memcpy第二个参数,const现在也如前文所述,拷贝时对传入的原数据内容禁止修改。特殊说明,指针是不能使用sizeof求内容大小的,在ARM系统固定为int 4字节。对于函数无输入参数的,也尽量加上void,如void fun(void);7、weak一般简化定义#define _WEAK __attribute__((weak)) 函数名称前面加上__WEAK属性修饰符称为“弱函数”,类似C++的虚函数。链接时优先链接为非weak定义的函数,如果找不到则再链接带weak函数。_WEAK void fun(void) { //do this } //不在同一个.c,两同名函数不能在同一个文件 void fun(void) { //do that } 这种自动选择的机制,在代码移植和多模块配合工作的场景下应用较多。例如前期移植代码,需要调用某个接口fun,但当前该接口不存在或者未移植完整使用,可以使用weak关键字定义为空函数先保证编译正常。后续移植完成实现了fun,即软件中有2个fun函数没有任何错误,编译器自动会识别使用后者。当然也粗暴的#if 0屏蔽对fun的调用,但要确保后续记得放开。8、总结存在即合理,C语言里面的关键字,每个都有其特殊的意义,只是一般使用较少,譬如作文,使用常用的汉字可以;但引经据典,使用特殊的修饰辞藻更能显出水平。后续对section 进行详细说明,它和动态加载(OTA)、接口自启动相关。来自微信
2025年09月01日
29 阅读
0 评论
0 点赞
2025-08-27
C++ 类的构造函数和析构函数
类的构造函数类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。下面的实例有助于更好地理解构造函数的概念:实例 #include <iostream> using namespace std; class Line { public: void setLength( double len ); double getLength( void ); Line(); // 这是构造函数 private: double length; }; // 成员函数定义,包括构造函数 Line::Line(void) { cout << "Object is being created" << endl; } void Line::setLength( double len ) { length = len; } double Line::getLength( void ) { return length; } // 程序的主函数 int main( ) { Line line; // 设置长度 line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl; return 0; } 当上面的代码被编译和执行时,它会产生下列结果:Object is being createdLength of line : 6带参数的构造函数默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值,如下面的例子所示:实例 #include <iostream> using namespace std; class Line { public: void setLength( double len ); double getLength( void ); Line(double len); // 这是构造函数 private: double length; }; // 成员函数定义,包括构造函数 Line::Line( double len) { cout << "Object is being created, length = " << len << endl; length = len; } void Line::setLength( double len ) { length = len; } double Line::getLength( void ) { return length; } // 程序的主函数 int main( ) { Line line(10.0); // 获取默认设置的长度 cout << "Length of line : " << line.getLength() <<endl; // 再次设置长度 line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl; return 0; } 当上面的代码被编译和执行时,它会产生下列结果:Object is being created, length = 10Length of line : 10Length of line : 6使用初始化列表来初始化字段使用初始化列表来初始化字段: Line::Line( double len): length(len) { cout << "Object is being created, length = " << len << endl; } 上面的语法等同于如下语法: Line::Line( double len) { length = len; cout << "Object is being created, length = " << len << endl; } 假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示: C::C( double a, double b, double c): X(a), Y(b), Z(c) { .... } 类的析构函数类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。下面的实例有助于更好地理解析构函数的概念:实例 #include <iostream> using namespace std; class Line { public: void setLength( double len ); double getLength( void ); Line(); // 这是构造函数声明 ~Line(); // 这是析构函数声明 private: double length; }; // 成员函数定义,包括构造函数 Line::Line(void) { cout << "Object is being created" << endl; } Line::~Line(void) { cout << "Object is being deleted" << endl; } void Line::setLength( double len ) { length = len; } double Line::getLength( void ) { return length; } // 程序的主函数 int main( ) { Line line; // 设置长度 line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl; return 0; } 当上面的代码被编译和执行时,它会产生下列结果:Object is being createdLength of line : 6Object is being deleted为什么C++的构造函数在class new 的就自动执行呢?在C++中,构造函数在对象创建时自动执行的设计是语言的核心特性,其背后蕴含着深刻的工程哲学和系统设计思想。以下是结构化解析:内存分配与构造分离new操作符实际执行两个独立操作:MyClass* obj = new MyClass(); // 等价于: void* mem = operator new(sizeof(MyClass)); // 1. 仅分配原始内存 MyClass* obj = new(mem) MyClass(); // 2. 在内存地址调用构造函数构造函数的本质编译器将构造函数视为类型初始化器:编译期在所有对象创建点自动插入构造函数调用构造函数签名经特殊处理(如添加隐式this指针参数)设计哲学解析一、对象生命周期完整性保障阶段 自动行为 意义创建期 构造自动执行 确保对象从诞生起就处于有效状态生存期 方法手动调用 赋予开发者控制权销毁期 析构自动执行 防止资源泄漏(RAII核心)二、类型系统安全的基石class Database { public: Database() { connect("default_server"); } // 强制初始化 }; // 若不自动构造: Database* db = (Database*)malloc(sizeof(Database)); db->query("SELECT..."); // 崩溃!未初始化的连接三、面向对象范式的实现基础继承链构造:自动按顺序调用基类构造函数class Base { Base() {/*基类初始化*/} }; class Derived : public Base { Derived() : Base() {/*编译器自动插入基类构造*/} };成员初始化:按声明顺序自动构造成员对象💡 关键优势分析防御性编程强化class File { HANDLE hFile; public: File(const char* name) : hFile(OpenFile(name)) { if(!hFile) throw "打开失败"; // 创建即验证 } }; // 使用时无法绕过验证 File log("data.bin"); // 要么有效对象,要么异常设计权衡与边界与C语言的本质区别🌟 总结:自动构造的核心价值安全屏障:消除未初始化对象风险范式实现:支撑RAII/异常安全等核心特性系统契约:对象诞生即满足类不变式(class invariant)工程效率:减少样板代码(对比C的手动初始化)类型完备:实现真正意义上的抽象数据类型(ADT)正如C++之父Bjarne Stroustrup所言:“构造函数不是可选的——它们定义了对象存在的意义”。这种设计强制保证了对象语义完整性,是C++类型安全的基石。怎么判断储C++的class 内部的function哪一个是构造函数,哪一个是析构函数在C++中,构造函数和析构函数有明确的语法特征和功能定位。以下是简明清晰的判断指南:🔍 构造函数识别特征命名规则函数名 必须与类名完全相同(大小写敏感)class MyClass { public: MyClass(); // ✅ 构造函数 };无返回类型不声明任何返回类型(连 void 都不需要)class Student { public: Student(string name); // ✅ 构造(无返回类型) void setGrade(); // ❌ 普通方法(有void) };可重载特性一个类可以有 多个构造函数(参数列表不同)class Point { public: Point(); // ✅ 默认构造 Point(int x, int y); // ✅ 带参构造 };析构函数识别特征命名规则函数名 必须是 ~类名(波浪号前缀)class Database { public: ~Database(); // ✅ 析构函数 };无参数无返回值严格无参数,且不声明返回类型class FileHandler { public: ~FileHandler(); // ✅ 正确形式 ~FileHandler(int); // ❌ 错误(不可带参数) };唯一性每个类 有且仅有一个 析构函数(不可重载)遇到类成员函数时问:是否与类名同名? → 构造函数候选是否带波浪号前缀? → 析构函数候选是否有返回类型? → 普通方法(非构造/析构)析构函数带参数? → 语法错误(必须无参)掌握这些规则后,您能瞬间识别任何C++类中的构造/析构函数。这种设计确保对象 诞生即初始化(构造),消亡即清理(析构),是C++资源管理的基石 💪。特殊场景辨析默认构造无参数的构造函数(编译器自动生成若未定义)class Widget { public: Widget(); // 显式默认构造 // 若未声明,编译器生成隐式默认构造虚析构函数(多态基类必备)class Shape { public: virtual ~Shape(); // ✅ 虚析构(允许派生类正确释放) };.
2025年08月27日
23 阅读
0 评论
0 点赞
2025-08-27
C语言实现面向对象三大特性 : 封装、继承、多态(2)
http://bennyhe.cn/index.php/archives/1709/这个公众号写的例子解释得不清楚,让deepseek帮忙写了一个例子,加上解释。以下是一个使用C语言模拟面向对象编程的完整示例,通过结构体和函数指针实现封装、继承和多态三大特性:代码如下:#include <stdio.h> #include <stdlib.h> #include <math.h> // 基类:Shape(图形) typedef struct Shape { // 虚函数表 struct ShapeVTable* vtable; // 公共属性 float x; float y; } Shape; // 虚函数表定义 typedef struct ShapeVTable { // 多态方法 float (*area)(Shape* self); void (*move)(Shape* self, float dx, float dy); void (*draw)(Shape* self); } ShapeVTable; // 继承自Shape的子类:Circle(圆形) typedef struct Circle { Shape base; // 继承基类 float radius; // 特有属性 } Circle; // 继承自Shape的子类:Rectangle(矩形) typedef struct Rectangle { Shape base; // 继承基类 float width; float height; } Rectangle; // ======= Shape的方法实现 ======= float shape_area(Shape* self) { printf("调用基类area虚方法\n"); return 0.0f; } void shape_move(Shape* self, float dx, float dy) { self->x += dx; self->y += dy; printf("图形移动到(%.1f, %.1f)\n", self->x, self->y); } void shape_draw(Shape* self) { printf("在(%.1f, %.1f)绘制图形\n", self->x, self->y); } // 基类虚函数表实例 static ShapeVTable shape_vtable = { .area = shape_area, .move = shape_move, .draw = shape_draw }; // ======= Circle的方法实现 ======= float circle_area(Shape* self) { Circle* circle = (Circle*)self; return M_PI * circle->radius * circle->radius; } void circle_draw(Shape* self) { Circle* circle = (Circle*)self; printf("在(%.1f, %.1f)绘制圆形,半径: %.1f\n", circle->base.x, circle->base.y, circle->radius); } // Circle虚函数表(覆盖基类方法) static ShapeVTable circle_vtable = { .area = circle_area, .move = shape_move, // 复用基类方法 .draw = circle_draw }; // Circle构造函数 Circle* create_circle(float x, float y, float radius) { Circle* circle = malloc(sizeof(Circle)); circle->base.vtable = &circle_vtable; circle->base.x = x; circle->base.y = y; circle->radius = radius; return circle; } // ======= Rectangle的方法实现 ======= float rectangle_area(Shape* self) { Rectangle* rect = (Rectangle*)self; return rect->width * rect->height; } void rectangle_draw(Shape* self) { Rectangle* rect = (Rectangle*)self; printf("在(%.1f, %.1f)绘制矩形,尺寸: %.1fx%.1f\n", rect->base.x, rect->base.y, rect->width, rect->height); } // Rectangle虚函数表 static ShapeVTable rectangle_vtable = { .area = rectangle_area, .move = shape_move, .draw = rectangle_draw }; // Rectangle构造函数 Rectangle* create_rectangle(float x, float y, float width, float height) { Rectangle* rect = malloc(sizeof(Rectangle)); rect->base.vtable = &rectangle_vtable; rect->base.x = x; rect->base.y = y; rect->width = width; rect->height = height; return rect; } // ======= 多态演示函数 ======= void process_shape(Shape* shape) { printf("\n处理图形对象:\n"); shape->vtable->draw(shape); printf("面积: %.2f\n", shape->vtable->area(shape)); shape->vtable->move(shape, 2.0f, 3.0f); } // ======= 主函数 ======= int main() { // 创建派生类对象 Circle* circle = create_circle(1.0f, 1.0f, 5.0f); Rectangle* rect = create_rectangle(3.0f, 4.0f, 6.0f, 8.0f); // 演示多态 process_shape((Shape*)circle); process_shape((Shape*)rect); // 释放内存 free(circle); free(rect); return 0; }面向对象特性实现解析:1. 封装(Encapsulation)结构体封装:将数据和方法绑定在Shape结构体中私有性模拟:通过.c文件隔离实现细节(示例中为简化展示在单文件)构造函数:create_circle()和create_rectangle()实现对象初始化2. 继承(Inheritance)结构体嵌套:派生类包含基类结构体作为第一个成员typedef struct Circle { Shape base; // 继承基类成员 float radius; // 新增属性 } Circle;内存布局兼容:派生类指针可直接转为基类指针3. 多态(Polymorphism)虚函数表(VTable):通过ShapeVTable实现动态绑定typedef struct ShapeVTable { float (*area)(Shape* self); void (*move)(Shape* self, float dx, float dy); } ShapeVTable;方法覆盖:派生类重写虚表函数指针static ShapeVTable circle_vtable = { .area = circle_area, // 覆盖基类方法 .move = shape_move // 复用基类方法 };4. 多态调用// 统一接口处理不同派生类 void process_shape(Shape* shape) { shape->vtable->draw(shape); // 动态绑定到具体实现 shape->vtable->move(shape, 2, 3); }执行结果示例:处理图形对象:在(1.0, 1.0)绘制圆形,半径: 5.0面积: 78.54 图形移动到(3.0, 4.0)处理图形对象:在(3.0, 4.0)绘制矩形,尺寸: 6.0x8.0面积: 48.00 图形移动到(5.0, 7.0)️扩展实现技巧:内存管理:可添加destroy()虚函数实现对象析构类型安全:增加type_id字段实现运行时类型检查接口抽象:定义纯虚函数表作为接口规范多重继承:通过多个基类结构体嵌套实现(需谨慎处理内存布局)动态创建:使用注册表和字符串ID创建对象💡 实际应用场景:Linux内核(task_struct)、GTK+ GUI库(GObject)、CPython解释器(PyObject)均使用类似技术实现面向对象编程此方案展示了C语言实现OOP的核心模式,虽然需要手动管理虚表和内存,但提供了接近原生OOP语言的功能和性能。问题1:针对上面例子,typedef struct ShapeVTable { float (*area)(Shape* self); void (*move)(Shape* self, float dx, float dy); } ShapeVTable;中area 是指什么?是一个function 还是struct?在C语言面向对象编程示例中,typedef struct ShapeVTable 定义中的 area 是一个函数指针,而非普通函数或结构体。以下是详细解析:typedef struct ShapeVTable { float (*area)(Shape* self); // ← 核心声明 void (*move)(Shape* self, float dx, float dy); } ShapeVTable;特性 说明类型 函数指针 (Function Pointer)作用 指向计算图形面积的具体实现函数内存表示 存储函数入口地址的指针(通常4/8字节)调用方式 通过指针间接调用:shape->vtable->area(shape)多态关键 不同子类(Circle/Rectangle)可指向不同的实现函数float:函数返回值类型(area):声明名为 area 的指针( 表示指针)(Shape* self):函数参数列表(接收Shape指针)关键价值:实现多态// 统一接口处理不同图形 void print_area(Shape* shape) { // 动态调用具体实现 float a = shape->vtable->area(shape); printf("面积: %.2f\n", a); }圆形对象:实际调用 circle_area()矩形对象:实际调用 rectangle_area()新增图形:只需实现新的area函数并更新虚表问题2:上面例子中// Circle虚函数表(覆盖基类方法) static ShapeVTable circle_vtable = { .area = circle_area, .move = shape_move, // 复用基类方法 .draw = circle_draw }中 虚函数表就是C语言的列表吗? .area 表示什么意思?本质:虚函数表是包含函数指针成员的静态结构体实例,通过为不同子类创建不同的静态实例实现多态。🔑 .area 成员详解.area 的双重身份作为结构体成员在ShapeVTable定义中声明为成员变量:typedef struct ShapeVTable { float (*area)(Shape* self); // 成员声明 } ShapeVTable;作为函数指针容器初始化时赋予具体函数地址.area = circle_area // 函数指针赋值️ 运行时工作原理多态调用流程sequenceDiagram participant Main as main() participant Shape as Shape指针 participant VTable as circle_vtable participant Function as circle_area() Main ->> Shape: (Shape*)circle Shape ->> VTable: 访问vtable VTable ->> Function: 调用.area成员指向的函数 Function -->> Shape: 计算圆形面积 Function -->> Main: 返回结果 键技术点静态初始化static关键字确保虚函数表:在编译期分配内存在程序启动时初始化生命周期贯穿整个程序点运算符(.)作用在初始化中使用.area是C99的指定初始化器(Designated Initializer):函数指针转换编译器自动处理函数类型转换:// circle_area实际类型:float (*)(Circle*) // 虚表要求类型:float (*)(Shape*) .area = (float(*)(Shape*))circle_area // 编译器隐式转换 核心价值体现// 统一接口 float calculate_area(Shape* shape) { return shape->vtable->area(shape); // 多态魔法发生处 }圆形对象:调用circle_area()radius² × π矩形对象:调用rectangle_area()width × height新增图形:只需实现新函数并更新虚表⚠️ 重要注意事项:函数签名必须严格匹配(返回类型+参数类型)派生类函数需通过强制转换访问子类特有成员问题3:上面例子中main 函数里面的process_shape((Shape*)circle);process_shape((Shape)rect); 中 process_shape((Shape)circle); 就是shape->vtable->draw(shape); 这里draw 就是static ShapeVTable circle_vtable 中绑定的circle_draw 这个方法吗? process_shape((Shape*)rect); 做shape->vtable->draw(shape); 这里draw 就是static ShapeVTable rectangle_vtable 中绑定的rectangle_draw 这个方法吗? 是怎么做到在rectangle_vtable 和 circle_vtable 自动匹配的?是的,您完全理解正确!下面我用清晰的逻辑解析这个多态调用的精妙机制:对于圆形对象:✅ process_shape((Shape*)circle) 中的 draw 调用✅ 确实绑定到 circle_vtable 的 circle_draw🔄 调用链:shape->vtable->draw → circle_vtable.draw → circle_draw对于矩形对象:✅ process_shape((Shape*)rect) 中的 draw 调用✅ 确实绑定到 rectangle_vtable 的 rectangle_draw🔄 调用链:shape->vtable->draw → rectangle_vtable.draw → rectangle_draw自动匹配的实现原理一、对象创建时的类型烙印// 创建圆形对象时 Circle* create_circle(...) { Circle* circle = malloc(sizeof(Circle)); circle->base.vtable = &circle_vtable; // 关键烙印! }circle->base.vtable 被永久赋值为 &circle_vtable 的地址即使转为基类指针 (Shape*)circle,内存中的 vtable 指针保持不变调用时的动态解析当执行 shape->vtable->draw(shape):访问 vtable 指针// 从对象内存中读取vtable地址 ShapeVTable* vtable_ptr = shape->vtable; 对 circle:获得 circle_vtable 地址对 rect:获得 rectangle_vtable 地址定位函数指针// 从vtable结构体获取draw成员 void (*draw_func)(Shape*) = vtable_ptr->draw;编译器自动计算成员偏移量(固定偏移)间接函数调用draw_func(shape); // 执行指针指向的函数 CPU直接跳转到函数入口地址circle_vtable.draw → circle_drawrectangle_vtable.draw → rectangle_draw⚙️ 关键技术保障内存布局一致性对象类型 内存偏移0位置 意义Circle Shape base C标准保证基类位于起始地址Rectangle Shape base 使得 (Circle)→(Shape) 转换安全静态绑定机制cpp复制// 编译时即确定的绑定关系(非运行时查找)static ShapeVTable circle_vtable = { .draw = circle_draw // 编译期硬编码绑定 };函数指针本质typedef void (*DrawFunc)(Shape*); // 统一函数签名 // 等价于: draw_func(shape); // 汇编实现: mov rax, [shape] ; 取vtable地址 mov rbx, [rax+8] ; 取draw指针(假设偏移8字节) call rbx ; 间接调用💡 多态效果图解mermaidsequenceDiagramparticipant Caller as process_shape() participant Object as Shape对象 participant VTable as 虚函数表 participant Impl as 具体实现 Caller ->> Object: shape->vtable->draw() Object ->> VTable: 查询我的vtable alt 圆形对象 VTable ->> Impl: 调用circle_draw() else 矩形对象 VTable ->> Impl: 调用rectangle_draw() end Impl -->> Caller: 执行结果 关键结论:自动匹配的核心在于每个对象创建时,其内部 vtable 指针已被永久绑定到专属虚函数表。类型转换只改变指针解释方式,不改变对象内存本质。当通过基类指针调用虚函数时,实际通过原始 vtable 指针找到最终实现。这种设计完美模拟了C++的虚函数机制,Linux内核(如file_operations)、Windows驱动模型(WDF_OBJECT_ATTRIBUTES)等核心系统均采用此方案实现多态。
2025年08月27日
27 阅读
0 评论
0 点赞
1
2
3
...
7