首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
Search
1
在IC617中进行xa+vcs数模混仿
81 阅读
2
科普:Memory Compiler生成的Register file和SRAM有何区别?
73 阅读
3
virtuoso和empyrean alps模拟仿真和混仿教程
73 阅读
4
后仿中$setup,$hold与$setuphold
44 阅读
5
文档内容搜索哪家强? 15款文件搜索软件横向评测
35 阅读
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
登录
Search
标签搜索
python
Docker
vscode
linux
systemverilog
vcs
STM32
PyQT
EDA
FPGA
gvim
cadence
Alist
xilinx
UVM
uos
macos
package
MCU
risc-v
bennyhe
累计撰写
378
篇文章
累计收到
31
条评论
首页
栏目
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
搜索到
32
篇与
的结果
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日
0 阅读
0 评论
0 点赞
2025-08-26
C语言实现面向对象三大特性 : 封装、继承、多态
不知道有多少人去了解过语言的发展史,早期C语言的语法功能其实比较简单。随着应用需求和场景的变化,C语言的语法功能在不断升级变化。虽然我们的教材有这么一个结论:C语言是面向过程的语言,C++是面向对象的编程语言,但面向对象的概念是在C语言阶段就有了,而且应用到了很多地方,比如某些操作系统内核、通信协议等。面向对象编程,也就是大家说的OOP(Object Oriented Programming)并不是一种特定的语言或者工具,它只是一种设计方法、设计思想,它表现出来的三个最基本的特性就是封装、继承与多态。1、为什么用C实现OOP 阅读文本之前肯定有读者会问这样的问题:我们有C++面向对象的语言,为什么还要用C语言实现面向对象呢?C语言这种非面向对象的语言,同样也可以使用面向对象的思路来编写程序的。只是用面向对象的C++语言来实现面向对象编程会更简单一些,但是C语言的高效性是其他面向对象编程语言无法比拟的。当然,使用C语言来实现面向对象的开发相对不容易理解,这就是为什么大多数人学过C语言却看不懂Linux内核源码。所以这个问题其实很好理解,只要有一定C语言编程经验的读者都应该能明白:面向过程的C语言和面向对象的C++语言相比,代码运行效率、代码量都有很大差异。在性能不是很好、资源不是很多的MCU中使用C语言面向对象编程就显得尤为重要。2、所具备的条件 要想使用C语言实现面向对象,首先需要具备一些基础知识。比如:(C语言中的)结构体、函数、指针,以及函数指针等,(C++中的)基类、派生、多态、继承等。首先,不仅仅是了解这些基础知识,而是有一定的编程经验,因为上面说了“面向对象是一种设计方法、设计思想”,如果只是停留在字面意思的理解,没有这种设计思想肯定不行。因此,不建议初学者使用C语言实现面向对象,特别是在真正项目中。建议把基本功练好,再使用。利用C语言实现面向对象的方法很多,下面就来描述最基本的封装、继承和多态。3、封装 封装就是把数据和函数打包到一个类里面,其实大部分C语言编程者都已近接触过了。C 标准库中的 fopen(), fclose(), fread(), fwrite()等函数的操作对象就是 FILE。数据内容就是 FILE,数据的读写操作就是 fread()、fwrite(),fopen() 类比于构造函数,fclose() 就是析构函数。这个看起来似乎很好理解,那下面我们实现一下基本的封装特性。#ifndef SHAPE_H #define SHAPE_H #include <stdint.h> // Shape 的属性 typedef struct { int16_t x; int16_t y; } Shape; // Shape 的操作函数,接口函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y); void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy); int16_t Shape_getX(Shape const * const me); int16_t Shape_getY(Shape const * const me); #endif /* SHAPE_H */这是 Shape 类的声明,非常简单,很好理解。一般会把声明放到头文件里面 “Shape.h”。来看下 Shape 类相关的定义,当然是在 “Shape.c” 里面。 #include "shape.h" // 构造函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y) { me->x = x; me->y = y; } void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) { me->x += dx; me->y += dy; } // 获取属性值函数 int16_t Shape_getX(Shape const * const me) { return me->x; } int16_t Shape_getY(Shape const * const me) { return me->y; }再看下 main.c#include "shape.h" /* Shape class interface */ #include <stdio.h> /* for printf() */ int main() { Shape s1, s2; /* multiple instances of Shape */ Shape_ctor(&s1, 0, 1); Shape_ctor(&s2, -1, 2); printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1)); printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2)); Shape_moveBy(&s1, 2, -4); Shape_moveBy(&s2, 1, -2); printf("Shape s1(x=%d,y=%d)\n", Shape_getX(&s1), Shape_getY(&s1)); printf("Shape s2(x=%d,y=%d)\n", Shape_getX(&s2), Shape_getY(&s2)); return 0; }编译之后,看看执行结果:Shape s1(x=0,y=1)Shape s2(x=-1,y=2)Shape s1(x=2,y=-3)Shape s2(x=0,y=0)整个例子,非常简单,非常好理解。以后写代码时候,要多去想想标准库的文件IO操作,这样也有意识的去培养面向对象编程的思维。4、继承 继承就是基于现有的一个类去定义一个新类,这样有助于重用代码,更好的组织代码。在 C 语言里面,去实现单继承也非常简单,只要把基类放到继承类的第一个数据成员的位置就行了。例如,我们现在要创建一个 Rectangle 类,我们只要继承 Shape 类已经存在的属性和操作,再添加不同于 Shape 的属性和操作到 Rectangle 中。下面是 Rectangle 的声明与定义:#ifndef RECT_H #define RECT_H #include "shape.h" // 基类接口 // 矩形的属性 typedef struct { Shape super; // 继承 Shape // 自己的属性 uint16_t width; uint16_t height; } Rectangle; // 构造函数 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height); #endif /* RECT_H */ #include "rect.h" // 构造函数 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height) { /* first call superclass’ ctor */ Shape_ctor(&me->super, x, y); /* next, you initialize the attributes added by this subclass... */ me->width = width; me->height = height; }我们来看一下 Rectangle 的继承关系和内存布局:因为有这样的内存布局,所以你可以很安全的传一个指向 Rectangle 对象的指针到一个期望传入 Shape 对象的指针的函数中,就是一个函数的参数是 “Shape ”,你可以传入 “Rectangle ”,并且这是非常安全的。这样的话,基类的所有属性和方法都可以被继承类继承!#include "rect.h" #include <stdio.h> int main() { Rectangle r1, r2; // 实例化对象 Rectangle_ctor(&r1, 0, 2, 10, 15); Rectangle_ctor(&r2, -1, 3, 5, 8); printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r1.super), Shape_getY(&r1.super), r1.width, r1.height); printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r2.super), Shape_getY(&r2.super), r2.width, r2.height); // 注意,这里有两种方式,一是强转类型,二是直接使用成员地址 Shape_moveBy((Shape *)&r1, -2, 3); Shape_moveBy(&r2.super, 2, -1); printf("Rect r1(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r1.super), Shape_getY(&r1.super), r1.width, r1.height); printf("Rect r2(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(&r2.super), Shape_getY(&r2.super), r2.width, r2.height); return 0; } 输出结果:Rect r1(x=0,y=2,width=10,height=15)Rect r2(x=-1,y=3,width=5,height=8)Rect r1(x=-2,y=5,width=10,height=15)Rect r2(x=1,y=2,width=5,height=8)5、多态 C++ 语言实现多态就是使用虚函数。在 C 语言里面,也可以实现多态。现在,我们又要增加一个圆形,并且在 Shape 要扩展功能,我们要增加 area() 和 draw() 函数。但是 Shape 相当于抽象类,不知道怎么去计算自己的面积,更不知道怎么去画出来自己。而且,矩形和圆形的面积计算方式和几何图像也是不一样的。下面让我们重新声明一下 Shape 类:#ifndef SHAPE_H #define SHAPE_H #include <stdint.h> struct ShapeVtbl; // Shape 的属性 typedef struct { struct ShapeVtbl const *vptr; int16_t x; int16_t y; } Shape; // Shape 的虚表 struct ShapeVtbl { uint32_t (*area)(Shape const * const me); void (*draw)(Shape const * const me); }; // Shape 的操作函数,接口函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y); void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy); int16_t Shape_getX(Shape const * const me); int16_t Shape_getY(Shape const * const me); static inline uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); } static inline void Shape_draw(Shape const * const me) { (*me->vptr->draw)(me); } Shape const *largestShape(Shape const *shapes[], uint32_t nShapes); void drawAllShapes(Shape const *shapes[], uint32_t nShapes); #endif /* SHAPE_H */看下加上虚函数之后的类关系图:5.1 虚表和虚指针虚表(Virtual Table)是这个类所有虚函数的函数指针的集合。虚指针(Virtual Pointer)是一个指向虚表的指针。这个虚指针必须存在于每个对象实例中,会被所有子类继承。在《Inside The C++ Object Model》的第一章内容中,有这些介绍。5.2 在构造函数中设置vptr在每一个对象实例中,vptr 必须被初始化指向其 vtbl。最好的初始化位置就是在类的构造函数中。事实上,在构造函数中,C++ 编译器隐式的创建了一个初始化的vptr。在 C 语言里面, 我们必须显示的初始化vptr。下面就展示一下,在 Shape 的构造函数里面,如何去初始化这个 vptr。 #include "shape.h" #include <assert.h> // Shape 的虚函数 static uint32_t Shape_area_(Shape const * const me); static void Shape_draw_(Shape const * const me); // 构造函数 void Shape_ctor(Shape * const me, int16_t x, int16_t y) { // Shape 类的虚表 static struct ShapeVtbl const vtbl = { &Shape_area_, &Shape_draw_ }; me->vptr = &vtbl; me->x = x; me->y = y; } void Shape_moveBy(Shape * const me, int16_t dx, int16_t dy) { me->x += dx; me->y += dy; } int16_t Shape_getX(Shape const * const me) { return me->x; } int16_t Shape_getY(Shape const * const me) { return me->y; } // Shape 类的虚函数实现 static uint32_t Shape_area_(Shape const * const me) { assert(0); // 类似纯虚函数 return 0U; // 避免警告 } static void Shape_draw_(Shape const * const me) { assert(0); // 纯虚函数不能被调用 } Shape const *largestShape(Shape const *shapes[], uint32_t nShapes) { Shape const *s = (Shape *)0; uint32_t max = 0U; uint32_t i; for (i = 0U; i < nShapes; ++i) { uint32_t area = Shape_area(shapes[i]);// 虚函数调用 if (area > max) { max = area; s = shapes[i]; } } return s; } void drawAllShapes(Shape const *shapes[], uint32_t nShapes) { uint32_t i; for (i = 0U; i < nShapes; ++i) { Shape_draw(shapes[i]); // 虚函数调用 } }5.3 继承 vtbl 和 重载 vptr上面已经提到过,基类包含 vptr,子类会自动继承。但是,vptr 需要被子类的虚表重新赋值。并且,这也必须发生在子类的构造函数中。下面是 Rectangle 的构造函数。#include "rect.h" #include <stdio.h> // Rectangle 虚函数 static uint32_t Rectangle_area_(Shape const * const me); static void Rectangle_draw_(Shape const * const me); // 构造函数 void Rectangle_ctor(Rectangle * const me, int16_t x, int16_t y, uint16_t width, uint16_t height) { static struct ShapeVtbl const vtbl = { &Rectangle_area_, &Rectangle_draw_ }; Shape_ctor(&me->super, x, y); // 调用基类的构造函数 me->super.vptr = &vtbl; // 重载 vptr me->width = width; me->height = height; } // Rectangle's 虚函数实现 static uint32_t Rectangle_area_(Shape const * const me) { Rectangle const * const me_ = (Rectangle const *)me; //显示的转换 return (uint32_t)me_->width * (uint32_t)me_->height; } static void Rectangle_draw_(Shape const * const me) { Rectangle const * const me_ = (Rectangle const *)me; //显示的转换 printf("Rectangle_draw_(x=%d,y=%d,width=%d,height=%d)\n", Shape_getX(me), Shape_getY(me), me_->width, me_->height); }5.4 虚函数调用有了前面虚表(Virtual Tables)和虚指针(Virtual Pointers)的基础实现,虚拟调用(后期绑定)就可以用下面代码实现了。uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); }这个函数可以放到.c文件里面,但是会带来一个缺点就是每个虚拟调用都有额外的调用开销。为了避免这个缺点,如果编译器支持内联函数(C99)。我们可以把定义放到头文件里面,类似下面:static inline uint32_t Shape_area(Shape const * const me) { return (*me->vptr->area)(me); } 如果是老一点的编译器(C89),我们可以用宏函数来实现,类似下面这样:#define Shape_area(me_) ((*(me_)->vptr->area)((me_)))看一下例子中的调用机制:5.5 main.c #include "rect.h" #include "circle.h" #include <stdio.h> int main() { Rectangle r1, r2; Circle c1, c2; Shape const *shapes[] = { &c1.super, &r2.super, &c2.super, &r1.super }; Shape const *s; // 实例化矩形对象 Rectangle_ctor(&r1, 0, 2, 10, 15); Rectangle_ctor(&r2, -1, 3, 5, 8); // 实例化圆形对象 Circle_ctor(&c1, 1, -2, 12); Circle_ctor(&c2, 1, -3, 6); s = largestShape(shapes, sizeof(shapes)/sizeof(shapes[0])); printf("largetsShape s(x=%d,y=%d)\n", Shape_getX(s), Shape_getY(s)); drawAllShapes(shapes, sizeof(shapes)/sizeof(shapes[0])); return 0; }输出结果:largetsShape s(x=1,y=-2)Circle_draw_(x=1,y=-2,rad=12)Rectangle_draw_(x=-1,y=3,width=5,height=8)Circle_draw_(x=1,y=-3,rad=6)Rectangle_draw_(x=0,y=2,width=10,height=15)6、总结 还是那句话,面向对象编程是一种方法,并不局限于某一种编程语言。用 C 语言实现封装、单继承,理解和实现起来比较简单,多态反而会稍微复杂一点。如果打算广泛的使用多态,还是推荐转到 C++ 语言上,毕竟这层复杂性被这个语言给封装了,你只需要简单的使用就行了。但并不代表,C 语言实现不了多态这个特性。来自微信
2025年08月26日
2 阅读
2 评论
0 点赞
2025-08-22
手把手教你写函数指针与回调函数
嵌入式微处理器2022年01月10日12:03摘要:前面分享了关于指针和结构体使用过程,今天是同系列的函数指针和回调函数。函数指针是指向函数的指针变量。通过函数指针C语言可以实现各种强大的功能与设计方法。而回调函数是函数指针最常见的用途,是C语言的重中之重,也是C语言面试当中的必考知识点和难点。在我们平时开发STM32或者其它单片机时,我们经常都会用到原厂提供的固件库函数,固件库函数中有非常多回调函数。那么,什么是回调函数?回调函数是作为参数传递给另一个函数的函数。接受回调作为参数的函数预计会在某个时间点执行它。回调机制允许下层软件层调用上层软件层定义的函数。应用程序代码和硬件驱动程序之间的交互硬件驱动程序是一个独立的可重用驱动程序,它不了解上面的层(用户应用程序)。硬件驱动程序提供API函数,允许用户应用程序将函数注册为回调。然后,此回调函数由硬件驱动程序作为执行的一部分进行调用。如果不使用回调,就会被编码为直接调用。这将使硬件驱动程序特定于特定的高级软件级别,并降低其可重用性。回调机制的另一个好处是,在程序执行期间可以动态更改被调用的回调函数。一、函数指针函数指针,顾名思义它就是一个指针,只不过它是一个函数指针,所以指向的是一个函数。类比一般的变量指针,指针变量,实质上是一个变量,只不过这个变量存放的是一个地址,在32位单片机中,任何类型的指针变量都存放的是一个大小为4字节的地址。int a; < = > void cal_sum(void); int * p; < = > void (*func_ptr)(void); p=&a; < = > func_ptr= &cal_sum;左边走义变量a,右边定义函数cal_sum;左边定义int指针,右边定义func_ptr;左边赋值指针,右边赋值函数指针;可能这样大家还是不太清楚,我是搞嵌入式单片机的,有本事你在Keil中给我举一个例子啊?可以啊,没问题,请看!#include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ uint8_t (*func_ptr)(uint8_t, uint8_t); /*将函数名赋值给函数指针*/ func_ptr = cal_sum; printf("cal_sum_address =0x%p\r\n", cal_sum); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d + %d = %d\r\n", a, b, cal_sum(a, b)); printf("%d + %d = %d\r\n", a, b, func_ptr(a, b)); while(1) { } }这样写大家应该很熟悉吧,我首先定义了一个函数指针func_ptr,接着将我写得cal_sum函数赋值给了函数指针func_ptr 。然后分别打印函数cal_sum的地址,函数指针func_ptr的地址,以及使用cal_sum计算出来的值,和函数值指针func_ptr计算出来的值。那么,结果是啥样呢?可以发现函数指针func_ptr和cal_sum函数的存储的地址以及他们所计算出来的值是一样的。比如在上面求两个数和的基础上再求两个数的乘积和差,会是啥样的呢?代码是这样的: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } uint8_t cal_sub(uint8_t a, uint8_t b) { return a - b; } uint8_t cal_mul(uint8_t a, uint8_t b) { return a * b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ uint8_t (*func_ptr)(uint8_t, uint8_t); /*将函数名赋值给函数指针*/ func_ptr = cal_sum; printf("cal_sum_address =0x%p\r\n", cal_sum); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d + %d = %d\r\n", a, b, cal_sum(a, b)); printf("%d + %d = %d\r\n\n", a, b, func_ptr(a, b)); /*将函数名赋值给函数指针*/ func_ptr = cal_sub; printf("cal_sub_address =0x%p\r\n", cal_sub); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d - %d = %d\r\n", a, b, cal_sub(a, b)); printf("%d - %d = %d\r\n\n", a, b, func_ptr(a, b)); /*将函数名赋值给函数指针*/ func_ptr = cal_mul; printf("cal_mul_address =0x%p\r\n", cal_mul); printf("func_ptr_address =0x%p\r\n", func_ptr); printf("%d * %d = %d\r\n", a, b, cal_mul(a, b)); printf("%d * %d = %d\r\n", a, b, func_ptr(a, b)); while(1) { } } 截个图,让大家看得更清楚一点:串口打印结果:指向函数的指针被称作是函数指针。通过函数指针,我们可以灵活的调用各种形式相同,但功能不同的函数这样做大大的增加了代码的灵活程度。1、typedef 函数指针我们在定义一个函数指针时常常会这样写:uint8_t (*func_ptr)(void);比较好理解,但下面这个就不好理解了:typedef uint8_t (*func_ptr) (void); 是不是看着有点懵,因为一般的typedef是这样用的:typedef 原类型 别名用法: #include<stdio.h> typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef uint8_t zhiguoxin; void main() { printf("www.zhiguoxin.cn\n"); printf("微信公众号:果果小师弟\n\n"); zhiguoxin a =10; printf("a=%d\n",a); }使用nodepad++编译一下: 然后,在keil中试验。那这样是啥意思呢? typedef uint8_t (*func_ptr) (void); 这里是把定义了一个别名叫(*func_ptr) (void) 的吗,显然不对,其含义是:上面的例子定义func_ptr是一个函数指针, 函数类型是不带形参, 返回参数是uint8_t。要定义的类型是uint8_t (*)(void),没有输入参数,返回值为uint8_t 的函数指针,定义的别名是func_ptr。在分析这种形式的定义的时候可以这样看:先去掉typedef和别名, 剩下的就是原变量的类型。去掉typedef和func_ptr以后就剩:uint8_t (*)(void)。2.为啥使用typedef定义函数指针答:typedef定义的函数指针类型是比较方便和明了的,因为typedef实际上就是定义一个新的数据类型,typedef有这样的一个作用,就可以用它来定义函数指针类型,这个定义的函数指针类型是能够指向返回值是uint8_t的,并且函数的参数是void类型。这里定义的typedef uint8_t (*func_ptr) (void);;就相当于把uint8_t (*) (void); 定义成了另一个别名 func_ptr了。这个func_ptr就表示了函数指针类型。注意:这里的uint8_t (*) (void);实际上不存在这样的写法,只是为了方便理解,这样的写法是不允许的,也是错误的!这样的写法并不代表是一个类型!C语言真是博大精深!3.函数指针常规定义如果不使用typedef就应该这样定义: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ uint8_t (*func_ptr)(uint8_t, uint8_t); /*将函数名赋值给函数指针*/ func_ptr = cal_sum; printf("%d + %d = %d\r\n", a, b, func_ptr(a, b)); while(1) { } } 在keil中测试:4.函数指针typedef定义如果使用typedef就应该这样定义: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*定义一个函数指针*/ typedef uint8_t (*func_ptr)(uint8_t, uint8_t); /*声明了一个函数指针变量 pFun*/ func_ptr pFun; /*将这个pFun指向了cal_sum函数*/ pFun = cal_sum; printf("%d + %d = %d\r\n", a, b, pFun(a, b)); while(1) { } }为啥要这样?为啥要使用typedef来定义函数指针?其实这个也是类比结构体的操作,在结构体中我们也常常给结构体起别名。综上所述:定义函数指针就有了两种方法。 /* 方法1 */ uint8_t (*func_ptr)(uint8_t, uint8_t) = NULL; /* 方法2 */ typedef uint8_t (*func_ptr)(uint8_t, uint8_t);; func_ptr pFun = NULL; 函数指针也有两种赋值方法: uint8_t (*func_ptr)(uint8_t, uint8_t) = NULL; /* 方法1 */ func_ptr= &cal_sum; /* 方法2 */ func_ptr= cal_sum;上面两种方法都是合法的。其实函数名就是函数的地址,你将函数名cal_sum赋值给函数指针func_ptr,与将函数的地址&cal_sum赋值给函数指针func_ptr是一样的。同样调用函数也有两种方法: /* 方法1 */ func_ptr(a,b) /* 方法2 */ (*func_ptr)(a,b) 二、回调函数既然函数指针和去普通的指针一样,普通的指针可以作为函数的形参,那么函数指针是不是也可以作为函数的形参呢?答:是的,肯定可以。那么函数指针作为函数的形参我们把这个函数指针叫啥呢?答:回调函数。回调函数原来是这样得来的啊,学到了!能不能举一个简单的例子呢?uint8_t compute_func(uint8_t, uint8_t);首先我们这样写一个函数是没有问题的,但是我们现在要将函数指针作为函数的形参,这样合法吗?uint8_t compute_func(uint8_t (*func_ptr)(uint8_t, uint8_t),uint8_t, uint8_t);编译一下:发现没有错误也没有警告,说明我们把函数指针当做函数的形参是没有任何问题的。在这个函数当中,通过该函数指针调用的函数被称为回调函数。这种开发方式的用途非常广泛。具体来说,在回调函数的应用场景当中,会出现两个角色。分别是某功能函数的开发者以及该功能函数的使用者。compute_func函数就是开发者写的函数,是非常牛逼的写库和底层的那一类人写的函数,我们每一个单片机的使用者,需要写出各种各样的具体的功能函数,只要我们写得功能函数的形参和返回值和函数指针的类型相同就可以了。怎么理解? #include "sys.h" #include "delay.h" #include "usart.h" /*使用者写的函数*/ uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } /*开发者写的函数*/ uint8_t (compute_func)(uint8_t (*func_ptr)(uint8_t, uint8_t), uint8_t a, uint8_t b) { return func_ptr(a, b); } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; printf("compute_func(cal_sum,a,b) =%d\r\n", compute_func(cal_sum, a, b)); while(1) { } } 注意:这里要注意的是我们使用者写的函数的类型一定要于开发者写的回调函数类型一样,比如形参和返回值的类型要一样。不然肯定不能调用的。换句话说就是,下面的这两个函数的形参和返回值都必须是相同的类型才可以,不能一个有返回值一个没有,明明函数指针有两个形参,你写的函数却只有一个形参也是不行的。正确写法:uint8_t cal_mul(uint8_t , uint8_t ) uint8_t (*func_ptr)(uint8_t, uint8_t)错误写法:void cal_mul(uint8_t , uint8_t ) //你写的函数却没有返回值 uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有返回值错误写法:uint8_t cal_mul(uint8_t) //你写的函数却只有一个形参 uint8_t (*func_ptr)(uint8_t, uint8_t)//函数指针有两个形参我们来验证一下: #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" /*使用者写的函数*/ uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } /*使用者写的函数*/ void cal_sub(uint8_t a, uint8_t b) { printf("666"); } /*使用者写的函数*/ uint8_t cal_mul( uint8_t a) { return a; } /*开发者写的函数*/ uint8_t (compute_func)(uint8_t (*func_ptr)(uint8_t, uint8_t), uint8_t a, uint8_t b) { return func_ptr(a, b); } int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; printf("%d\r\n", compute_func(cal_sum, a, b)); printf("%d\r\n", compute_func(cal_sub, a, b)); printf("%d\r\n", compute_func(cal_mul, a, b)); while(1) { } } 看到了在keil中编译器不会报错,但是会报警告。因为在keil中编译做了优化。那么,如果我们gcc记事本编译一下,又会是啥样的呢?会发现同样会有两个警告,但还是可以运行的。如何理解回调函数有时候会遇到这样一种情况,当上层人员将一个功能交给下层程序员完成时,上层程序员和下层程序员同步工作,这个时候该功能函数并未完成,这个时候上层程序员可以定义一个API来交给下层程序员,而上层程序员只要关心该API就可以了而无需关心具体实现,具体实现交给下层程序员完成即可(这里的上层和下层程序员不指等级关系,而是项目的分工关系)。这种情况下,就会用到回调函数(Callback Function),现在假设程序员A需要一个FFT算法,这个时候程序员A将FFT算法交给程序员B来完成,现在来让实现这个过程: #include <stdio.h> int InputData[100]={0}; int OutputData[100]={0}; /*定义回调函数*/ void CallBack_FFT_Function(int *inputData,int *outputData,int num) { while(num--) { printf("www.zhiguoxin.cn\r\n"); } } /*用来注册回调函数的功能函数*/ void TaskA(void (*fft)(int*,int*,int)) { fft(InputData,OutputData,5); } int main(void) { /*注册FFT_Function作为回调*/ TaskA(CallBack_FFT_Function); return 0; } 这个例子是不是跟上面的那个例子是相同的,只是我们在这里换了一种说法而已。也就是我们硬件层实现的某个功能,当然可以在应用层直接调用,但这种做法太low了,一看就是小学生的水平,或者说硬件层的东西应用层根本不需要关心,这就是分层的思想。硬件的东西就给硬件工程师做,应用工程师只关心自己的需要实现的任务。这也就是驱动工程师和应用工程师的区别,我硬件工程师只需要写好对应的API函数,你应用层直接调用就好了,你不需要关心这个API函数的内部是怎么实现的。而这两者之间的桥梁就是回调函数。而回调函数的形参就是函数指针,所以本篇最开始讲的是函数指针,只要你函数指针明白了,你就会写回调函数,也就理解了这其中到底只一个什么原理。上面的代码中CallBack_FFT_Function是回调函数,该函数的形参为一个函数指针,TaskA是用来注册回调函数的功能函数。可以看到,用来注册回调函数的功能函数中申明的函数指针必须和回调函数的类型完全相同。函数指针结构体但是,很多时候我们一般在结构体中定义函数指针用的比较多一点。下面,再举一个简单的例子。 #include "sys.h" #include "led.h" #include "delay.h" #include "usart.h" /**************************************** * 函数指针结构体 开发者写的结构体 ***************************************/ typedef struct { uint8_t (*p_sum)(uint8_t, uint8_t); uint8_t (*p_sub)(uint8_t, uint8_t); uint8_t (*p_mul)(uint8_t, uint8_t); float (*p_div)(uint8_t, uint8_t); } Operation_T; /*声明结构体变量g_Operation*/ Operation_T g_Operation; /*使用者写的回调函数*/ uint8_t cal_sum(uint8_t a, uint8_t b) { return a + b; } /*使用者写的回调函数*/ uint8_t cal_sub(uint8_t a, uint8_t b) { return a - b; } /*使用者写的回调函数*/ uint8_t cal_mul( uint8_t a, uint8_t b) { return a * b; } /*使用者写的回调函数*/ float cal_div(uint8_t a, uint8_t b) { return a / b; } /*结构体变量g_Operation初始化*/ Operation_T g_Operation = {cal_sum, cal_sub, cal_mul, cal_div}; int main(void) { delay_init(); uart_init(9600); printf("www.zhiguoxin.cn\r\n"); printf("微信公众号:果果小师弟\r\n"); uint8_t a = 10; uint8_t b = 8; /*使用函数指针调用函数*/ printf("%d\r\n", g_Operation.p_sum(a, b)); printf("%d\r\n", g_Operation.p_sub(a, b)); printf("%d\r\n", g_Operation.p_mul(a, b)); printf("%f\r\n", g_Operation.p_div(a, b)); while(1) { } } 三、回调在嵌入式系统中的实际使用回调可用于多种情况,并广泛用于嵌入式固件开发。它们提供了更大的代码灵活性,并允许我们开发可由最终用户进行微调而无需更改代码的驱动程序。在我们的代码中具有回调功能所需的元素是:将被调用的回调函数cal_sum将用于访问回调函数的函数指针p_sum将调用回调函数的调用函数compute_func在stm32的HAL库中,是使用了大量的回调函数的,串口、定时器等外设都是有对应的回调函数的,回调机制可以更好地分离代码,应用层和驱动层完全分离,降低耦合性。简单来看几个例子,串口回调函数:void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);使用的时候,我们只需要把串口解析处理逻辑放在对应的回调函数中处理即可,拿串口接收来举例,定义的是一个弱函数,我们在自己的文件中重新实现就好。 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { /*****Data Processing********/ } else if (huart->Instance == USART2) { /*****Data Processing********/ } } 又比如,我们在OS中的创建任务的函数,就是一个用来注册回调函数的功能函数,如果我们需要创建一个任务只需要这样写,tcp_client_socket就是我们底层开发者要写的具体的回调函数,就算你底层开发者没有把这个tcp_client_socket函数写完,对我程序的大体框架是没有任何影响的。我不管你底层开发者是否会不写,我的任务完成了。相反我底层应用开发者只要把我底层函数写好就行了,不管写没写好,我只需要把这个tcp_client_socketAPI函数名给你就行了,其他的你也不需要管了,咋们井水不犯河水!static void tcp_client_socket(void *thread_param) { /*****Data Processing********/ }总结指针、结构体、函数指针、回调函数都是在嵌入式开发中非常重要的知识点,也是不好理解的,但如果能够搞清楚它们是怎么来的,对嵌入式的进阶是非常有帮助的,所谓基础不牢,地动山摇。遇到问题一定要刨根问题。搞清楚原理,动手写代码来验证自己的思维过程,就一定能搞明白!来自微信
2025年08月22日
1 阅读
0 评论
0 点赞
2025-08-13
大一新生开源 Python 脚本可视化工具,火了!
Python数据科学2021年01月25日14:05普普通通黑底白字地敲代码太枯燥?那么,把 Python 脚本可视化怎么样?就像这样,从输入图片、调整尺寸到双边滤波,每一步都能看得清清楚楚明明白白。输入一个矩阵,无论是对它进行转置、求共轭还是乘方,都能得到及时的反馈。这样一个 Python 脚本可视化工具,名叫 Ryven,出自一位名叫 Leon Thomm 的大一新生之手。如果你也想试用一番,不妨接着往下看。如何使用使用之前,简单准备一下开发环境:Python3(推荐 3.8 以上)PySide2(推荐 2.14 以上)运行该项目中的 Ryven.py,即可打开编辑器。作者提到,在 Ryven 中,有许多不同的脚本。每个脚本都包含变量、流(或图)以及日志。右键单击就可以轻松操作。不过,想要使用这些节点,需要先导入节点软件包。点击 file,选择 import Nodes,导入 *.rpc 文件即可。这个工具的另外一个特点是,包含了两种不同的算法模式。数据流:每次数据更改(节点数据输出也就更改了)都会向前传播,并在所有连接的节点中触发更新。如下图,滑动滑块,会立即触发右侧节点中结果的更新。执行流:数据不会在更改时立即向前传播,而是只会在某个节点请求输出数据时,在受影响的节点中触发更新。另外,作者还给自己列了一个 to do list,比如加入语法高亮功能、自动代码补全功能,完善视觉体验等等。可视化编程是不是还挺有趣的?不过,作者小哥也指出,可视化编程的目的不是取代文本编程,并且,文本编程中实现的许多工作没有可视化的必要。关于作者作者 Leon Thomm,目前是苏黎世联邦理工学院的大一学生,自称 “业余程序员”,致力于人机交互和可视化编程。根据他的个人简介,早在进入大学前,他已经具备丰富的软件开发经历。传送门项目地址:https://github.com/leon-thomm/Ryven来自微信
2025年08月13日
0 阅读
0 评论
0 点赞
2025-08-13
答应我,调试 Python 代码,不要再用 Print 了!
Python数据科学2021年01月27日14:27相信大部分人学习Python,肯定会用print()这个内置函数,来调试代码的。那么在一个大型的项目中,如果你也是使用print来调试你的Python代码,你就会发现你的终端有多个输出。那么你便不得不去分辨,每一行的输出是哪些代码的运行结果。举个例子,运行下面这个程序。num1 = 30num2 = 40 print(num1)print(num2)输出结果。3040这些输出中哪一个是num1?哪一个又是num2呢?找出两个输出可能不是很困难,但是如果有五个以上的不同输出呢?尝试查找与输出相关的代码可能会很耗时。当然你可以在打印语句中添加文本,使其更容易理解:num1 = 30num2 = 40 print("num1" num1)print("num2" num1)输出结果。num1 30num2 40这个结果就很容易理解了,但是需要时间去写相关的信息。这时就该「Icecream」上场了~什么是Icecream?Icecream是一个Python第三方库,可通过最少的代码使打印调试更清晰明了。使用pip安装Icecream库。pip install icecream下面,让我们通过打印Python函数的输出来进行尝试。from icecream import ic def plus_five(num): return num + 5 ic(plus_five(4)) ic(plus_five(5))输出结果如下。ic| plus_five(4): 9ic| plus_five(5): 10通过使用icecream,我们不仅可以看到函数输出,还可以看到函数及其参数!检查执行情况如果你想要找到执行代码的位置,可以通过执行如下所示的操作,来查找执行了哪个语句。def hello(user:bool): if user: print("I'm user") else: print("I'm not user") hello(user=True)输出结果。I'm user使用icecream则无需多余的文本信息,就可以轻松地完成上述的操作。from icecream import ic def hello(user:bool): if user: ic() else: ic() hello(user=True) 输出结果如下。ic| ice_1.py:5 in hello() at 02:34:41.391从输出结果看,函数hello中的第5行的代码已被执行,而第7行的代码未执行。自定义前缀如果您想在打印语句中插入自定义前缀(例如代码执行时间),icecream也是能实现的。from datetime import datetime from icecream import ic import time from datetime import datetime def time_format(): return f'{datetime.now()}|> ' ic.configureOutput(prefix=time_format) for _ in range(3): time.sleep(1) ic('Hello') 输出结果如下。2021-01-24 10:38:23.509304|> 'Hello'2021-01-24 10:38:24.545628|> 'Hello'2021-01-24 10:38:25.550777|> 'Hello'可以看到代码的执行时间,就显示在输出的前面。获取更多的信息除了知道和输出相关的代码之外,你可能还想知道代码执行的行和代码文件。在ic.configureOutput()中,设置includeecontext的参数值为True即可。from icecream import ic def plus_five(num): return num + 5 ic.configureOutput(includeContext=True) ic(plus_five(4)) ic(plus_five(5))输出结果如下。ic| ice_test.py:7 in - plus_five(4): 9ic| ice_test.py:8 in - plus_five(5): 10这里我们就知道了,第一个输出是由函数plus_five在文件icecream_example.py的第7行执行的。第二个输出则是由函数plus_five在代码文件的第8行执行的。上述两个操作都用到了ic.configureOutput()函数。通过查看源码,可知有四个可供设置的参数。prefix,自定义输出前缀outputFunction,更改输出函数argToStringFunction,自定义参数序列化字符串includeContext,显示文件名、代码行、函数信息删除Icecream代码最后你可以将icecream仅用于调试,而将print用于其他目的(例如漂亮的打印)。from icecream import ic def plus_five(num): return num + 5 ic.configureOutput(includeContext=True) ic(plus_five(4)) ic(plus_five(5)) for i in range(10): print(f'****** Training model {i} ******') 输出结果。ic| ice_1.py:7 in - plus_five(4): 9ic| ice_1.py:8 in - plus_five(5): 10 Training model 0 ** Training model 1 ** Training model 2 ** Training model 3 ** Training model 4 ** Training model 5 ** Training model 6 ** Training model 7 ** Training model 8 ** Training model 9 **由于你可以区分调试打印和漂亮打印,因此搜索和删除所有ic调试语句非常容易。删除所有调试代码后,你的Python代码就整洁了。总结到此,你就应该就学会了如何使用icecream去打印调试。更多功能可以访问「GitHub」,了解详情~https://github.com/gruns/icecream来自微信
2025年08月13日
0 阅读
0 评论
0 点赞
1
2
3
4
...
7