首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
Search
1
科普:Memory Compiler生成的Register file和SRAM有何区别?
40 阅读
2
在IC617中进行xa+vcs数模混仿
35 阅读
3
virtuoso和empyrean alps模拟仿真和混仿教程
32 阅读
4
文档内容搜索哪家强? 15款文件搜索软件横向评测
19 阅读
5
vcs debug rtl或者netlist 中的loop
17 阅读
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
登录
Search
标签搜索
python
Docker
vcs
PyQT
STM32
cadence
linux
systemverilog
EDA
Alist
vscode
uos
package
C
QT
CXL
sed
sv
webdav
FPGA
bennyhe
累计撰写
341
篇文章
累计收到
31
条评论
首页
栏目
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
DFX与量产封装
PC&Server OS设置
移动OS设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
搜索到
28
篇与
的结果
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日
0 阅读
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日
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日
0 阅读
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 点赞
1
2
3
...
6