首页
游戏
影视
直播
广播
听书
音乐
图片
更多
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
Search
1
在IC617中进行xa+vcs数模混仿
84 阅读
2
科普:Memory Compiler生成的Register file和SRAM有何区别?
74 阅读
3
virtuoso和empyrean alps模拟仿真和混仿教程
74 阅读
4
后仿中$setup,$hold与$setuphold
45 阅读
5
文档内容搜索哪家强? 15款文件搜索软件横向评测
36 阅读
默认分类
芯片市场
数字电路
芯片后端
模拟电路
芯片验证
原型与样片验证
算法与架构
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设置
软件方案
新浪备份
有道备份
页面
游戏
影视
直播
广播
听书
音乐
图片
看书
微视
主播
统计
友链
留言
关于
论坛
邮件
推荐
我的硬盘
我的搜索
我的记录
我的文件
我的图书
我的笔记
我的书签
我的微博
搜索到
378
篇与
的结果
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-22
看懂车规级芯片分类
1C咖啡2021年12月13日18:32来自微信
2025年08月22日
0 阅读
0 评论
0 点赞
2025-08-18
半导体产业链详细梳理
半导体在线2021年09月22日16:24现在的全球半导体行业,处于新一轮景气周期上行阶段。而国内的半导体产业,不仅受到全球性大周期的影响,还有一条非常硬核的逻辑,就是国产替代。中国虽已经成为全球最大的半导体消费国,但我国的芯片自给率低,2020年我国集成电路自给率仅为15.9%,国产替代仍有很大的替代空间。从需求端来看,受益于“万物互联+国产替代”以及新能源汽车的助攻,芯片需求全面拉升;供给端受疫情冲击以及制裁下扩产速度减慢。跟传统周期性行业类似,半导体也存在着产能、库存和需求的不可能三角。由于从投资到产能释放的前置时间较长,如此扩大了芯片的缺口。年初开始,晶圆大厂们纷纷公布了一大波超预期的资本开支计划。下图为半导体产业从材料到应用终端流程图:如图:芯片产业链主要包括设计、制造、封装与测试三大环节与半导体设备及材料两大支柱产业。本文将按照该框架梳理整个芯片行业产业链。芯片设计包括工具软件、设计公司芯片制造包括制造厂、制造设备、材料与辅料芯片封测包括封测厂、封测设备、辅材打个通俗的比喻,芯片设计环节就像房地产的图纸设计,圆晶代工就是施工建房,封测就是将毛坯房变为精装房。一、芯片设计芯片设计是产业链中重要的一环,影响后续芯片产品的功能、性能和成本,对研发实力要求较高。根据不同的下游应用,可分为四类:集成电路:存储器、逻辑芯片(CPU、GPU)、微处理器(MPU)、模拟芯片分立器件(含功率半导体):MOSFET、IGBT、二极管、晶闸管传感器:MEMS、图像传感器光电器件(一)集成电路兆易创新:国内闪存芯片存储器及MCU微控制器设计双龙头。在全球NORFlash的市场占有率为6%,已跃居全球第三。公司作为国内MCU微控制器市场的龙头公司,2020年销售接近2亿颗,2021年公司业绩将伴随着工控等领域的快速放量,迎来快速发展的时期。北京君正:收购北京矽成,成为国内稀缺的汽车存储芯片领军企业。北京矽成在全球车用DRAM存储芯片市占率15%,全球排名第二。收益于汽车电动化与智能化对车用存储芯片数量和容量要求的提升,实现量价齐升。微处理器芯片业务收益于智能终端硬件需求的爆发。公告拟定增募集资金拓展车载LED照明芯片。国科微:我国固态存储芯片技术领先代表,最新产品达到国际平均水平、深耕智能安防监控芯片,积极布局AI智能监控芯片。圣邦股份:国内模拟芯片龙头,电源管理芯片及信号链芯片受益于5G、工业驱动、人工智能和汽车升级,叠加国产替代,空间广阔。思瑞浦:业务与圣邦股份高度重合,国产信号链类模拟芯片龙头企业。公司的信号链模拟芯片为5G射频模拟芯片的主要组成部分,综合性能已达国际标准。卓胜微:射频前端领域处于国产领先地位,5G通信技术的发展带动射频前端市场需求的快速增长,公司产品在客户端持续渗透。晶丰明源:在LED照明驱动芯片市场处于龙头地位,技术和市场均处于领先水平,出货量全球第一。晶丰明源今年也发了6次涨价函,原因是原材料价格上涨和产能紧张。明微电子:深耕LED驱动业务,显示与照明业务齐头并进,技术研发能力处于行业领先地位。与晶丰明源比主要为LED显示驱动芯片。2020年显示驱动芯片厂商中排名第三,市占率为13%。中颖电子:公司主要产品包括微控制芯片及OLED显示驱动芯片,受益于下游智能家电领域产品需求量和智能化水平的提高,国产替代空间广阔。景嘉微:A股极其稀缺的图形处理器芯片标的,主要应用于军事装备,在国内机载图显领域占据大部分市场份额,正由军用向信创及民用市场拓展。乐鑫科技:全球物联网WiFi-MCU芯片龙头企业,市场份额全球第一,占比超30%。产品下游主要是智能家居、电子消费产品、传感设备及工业控制等,充分受益下游物联网市场发展。富瀚微:安防芯片设计龙头。2020年原本的安防芯片龙头海思受到美国的制裁被动退出市场,公司在下游客户供应链切换过程中抢占先机,实现海思产品80%的替代。2020年公司在安防前端ISP芯片领域市场份额达到60%-70%。同时公司基于视频处理能力进入汽车摄像头市场,渗透率不断提升,提供新增长动能。全志科技:老牌智能终端处理器SOC芯片设计企业。(SOC即片上系统,可以简单的理解为把系统做在一块芯片上,是未来工业界将采用的最主要的产品开发方式。)其产品主要布局物联网,智能家居等领域,京东的智能音箱以及小米的智能扫地机器人就是搭载全志科技的soc芯片。瑞芯微:专注SOC设计,发力电源管理领域。5G 终端对电池续航能力的要求同步提升,进而对电源和功耗的管理提出了更高要求。公司快充芯片与普通的电源管理芯片相比,在占用体积、能量转换效率和散热量等方面均有较大程度的优化,性能和可靠性指标均处于市场领先水平。上海贝岭:重点发展消费类和工控类芯片设计业务,主要目标市场为电表、手机、液晶电视及平板显示、机顶盒等各类工业及消费电子产品。合并南京微盟提高电源管理芯片的市占率。(二)功率半导体新洁能:专业从事MOSFET、IGBT等半导体芯片和功率器件的研发设计及销售。IGBT是目前增速最快的功率器件细分市场,公司产品线完整,成为公司的营收增长新动力。同时公司重视第三代半导体功率器件研发,积极研发新能源汽车用功率半导体。(三)传感器韦尔股份:国内图像传感器设计龙头,业务位于全球前三,国内第一。2019年成功收购了两个影像传感器CIS设计公司豪威和思比科,设计业务收入超过世界第十的芯片设计上市公司。二、芯片制造 在市场需求、国家政策、资本投入的驱动下,全球晶圆代工产业逐渐向中国大陆转移,中国大陆成为全球增速最快的晶圆代工厂。2016-2020年全球新增投产晶圆厂中在投产在中国大陆的比重达到42%,其他分散在全球各国国家。晶圆制造原理为根据设计版图进行掩膜制作,形成模版,在晶圆上批量制造集成电路,芯片通过多次重复运用掺杂、沉积、光刻等工艺,最终在晶圆上实现高集成度的复杂电路。(一)制造厂不少纯设计厂向IDM转型,具备设计、研发、生产能力,我们将这一类的企业归为制造厂一类。1、集成电路中芯国际:无可争议的国产芯片代工龙头,技术与规模均为国内第一,代表集成电路国产替代的最先进制造水平,市占率全球第四。受到制裁之后研发预计会停滞数年,先进制程的生产规模扩张也会同步停滞。紫光国微:智能安全芯片及国内特种 IC 双龙头,“万能芯片”、车规芯片多点开花。子公司国微电子产品线齐全、技术实力强,是国内特种 IC 龙头企业。子公司紫光同芯国内智能卡芯片龙头,市场渗透率进一步提高。子公司紫光同创为国内民用FPGA领军企业,打破了海外寡头垄断,国产替代空间大。富满电子:主营为LED控制及驱动类芯片、电源管理类芯片,同时具有“设计+封测”一体化供应能力。公司已在小间距&Mini LED 控制驱动核心技术领域取得突破,在快充芯片领域也实现率先突破,技术实力国内领先。积极布局 MOSFET 领域,有望充分受益功率半导体国产替代。2、功率半导体泛指处理电力的芯片,其主要功能是将繁杂不一的电力,处理为终端产品所需的规格。比如电流从电动车的电池输出后,通过不同的功率芯片处理,既可以驱动电机,又可以驱动空调和音响。MOSFET:即场效晶体管。主要应用于电脑功率电源、家用电器等,占全球功率器件市场超4成比重IGBT:应用涵盖从工业电源、变频器、新能源汽车、新能源发电到轨道交通、国家电网等一系列领域。华润微:国内功率半导体IDM龙头之一,是国内营业收入最大,产品系列最全的MOSFET厂商。拟与国家集成电路产业投资基金二期及重庆永微电子共同成立润西微电子,建成后预计达到月产3万片12寸中高端功率半导体晶圆生产能力。斯达半导:国内IGBT半导体龙头,全球IGBT模块市场排名第七,是国内唯一进前十的企业,市场优势地位显著。从纯设计厂向IDM转型,以保证产能供应,产品主要用于工控与电器领域。拟定增募资35亿元购买光刻、显影、蚀刻等设备盖厂,设计产能36万片功率半导体,用于高压特色功率芯片和碳化硅,以尽快推出车规级SiC芯片,以完善车用电源市场的产品布局。士兰微:国内功率半导体IDM龙头之一,主要集中于MEMS传感器、高压集成电路、半导体功率器件这三个主要技术方向。捷捷微电:晶闸管龙头企业,产品性能达到国际领先水平。业务范围涵盖芯片设计、晶圆制造及封装测试等全业务环节。今年7月公告拟扩建功率半导体6英寸晶圆及器件封测项目,产品从晶闸管与防护器件等领域,扩展至IGBT模块。此外公司车规级封测产业项目落地,产业链逐步完善。扬杰科技:公司是国内领先的功率半导体 IDM 厂商,具备完善的芯片设计、晶圆制造、封装检测能力。国内功率二极管龙头,并逐步往 MOSFET、IGBT、第三代半导体功率器件等高端产品延伸。应用领域涵盖电源、家电、照明、安防、仪表、通信、工控及汽车电子等多个领域。公司产品在光伏领域应用占比较高,营收约占 15%。闻泰科技:耗资181亿收购来的安世半导体是全球功率半导体龙头,产品主攻消费电子与汽车领域。其主要产品为逻辑器件、分立器件和MOSFET器件,是一家集设计、制造、封装测试为一体的半导体跨国公司。三大业务均位于全球领先地位,逻辑器件排名第二、二极管和晶体管排名第一、车功率MOSFET排名第二。立昂微:茁壮成长的半导体硅片+功率器件领先企业,主攻肖特基二极管芯片。三安光电:国内LED芯片龙头。总投资160亿元的湖南三安半导体基地一期项目今年6月正式投产,这是国内首条、全球第三条碳化硅IDM生产线。3、传感器赛微电子:原名耐威科技 MEMS芯片晶圆制造。2016年赛微通过收购瑞典Silex,获得全球领先工艺IP,切入MEMS纯代工赛道成为MEMS全球代工领头羊。公司目前可生产微流体、微超声、微镜、光开关等多种器件。(二)材料与辅料半导体制造过程相当复杂,先进制程多达500多道工序,需要用到大量材料。因此半导体材料也是整个产业链中细分领域最多的,具有很高的技术和资本壁垒。其中,硅片(晶圆)价值占比最高,超过1/3,其次为电子特种气体占比13%,光掩模、光刻胶及辅助材料各占比12%左右,其余材料占比均低于10%。半导体材料成本占比分类:1、硅片硅片是集成电路制作中最为重要的原材料,市场规模最大。当前全球50%以上的硅材料产能集中在日本,前五大厂商份额超过90%,国内规模最大的是沪硅产业。沪硅产业:纯正的半导体硅片公司,产能、技术国内领先。率先实现300mm硅片规模化生产,打破了大硅片零国产化僵局,但产能利用率低,尚未实现规模效应。由于折旧费用高300mm硅片毛利率为-34.82%,国内三家硅片厂商中毛利率最低,尚未实现盈利。中环股份:中环以光伏硅片为主,占比接近90%,半导体硅片仅占总收入7%。虽然规模不大,但是中环半导体硅片产能提升很快。12英寸20年产能为7万片/月,21~23年将持续放量,产能预计将超过60万片/月。光伏和新能源布局可以实现协同,未来大硅片产能释放,具备龙头溢价空间。立昂微:立昂微硅片产品尺寸较小,且具备抛光片-外延片-功率器件的一体化优势,因此毛利率最高超过40%。15万片/月的12寸硅片预计今年底建设完成。除了半导体硅片,还有功率器件和射频芯片业务。2、电子特种气体半导体生产中几乎每个环节都要用到电子特气,且其很大程度上决定了最终产品性能的好坏。同时所用气体的品种多、质量要求高,因此被称为半导体制造的“血液”。虽然国产化率还不高,但国产替代速度比较快。南大光电:其自主研发的氢类气体磷烷、砷烷,是电子特气中技术壁垒最高的两种,打破了国外技术封锁和垄断,不仅在 LED 行业市场份额持续增长,在集成电路行业也快速实现了产品进口替代,得到了广大客户的高度认可,目前占据国内市场份额达 75%以上。同时公司MO源产品实现了国内进口替代,是全球主要的 MO 源生产商。MO源系列产品是制备LED、新一代太阳能电池、相变存储器、半导体激光器、射频集成电路芯片等的核心原材料。华特气体:公司研发出的20种进口替代产品已实现规模化生产。特气体也通过了全球最大光刻机供应商ASML公司的产品认证,并为中芯国际、华虹宏力等一线企业供货。雅克科技:是半导体材料中的平台型企业,类似北方华创在设备中的地位。业务包括半导体化学材料,电子特气,光刻胶。产品覆盖半导体薄膜、光刻、沉积、刻蚀、清洗等核心环节。前驱体、SOD,打破海外垄断。2020年收购LG化学彩色光刻胶,获得了彩色光刻胶和TFT光刻胶等成熟技术和量产能力,有效弥补国内彩色光刻胶生产的空白。3、光刻胶光刻胶技术壁垒高,价值含量高,被成为半导体材料中的“明珠”。高端光刻胶市场基本被日本企业所把持,使用量最高的KrF光刻胶和ArF光刻胶国产化率低于5%,进口替代空大。华懋科技:主营为安全气囊材料,通过持有徐州博康29%的股权,进军半导体光刻胶产业。徐州博康的光刻胶单体业务占全球市场份额的 5%,并已存储了全球 80%的光刻胶单体产品技术,成为中国唯一的高端光刻胶单体材料研发和规模化生产企业。目前已成功开发出 10+个高端光刻胶产品系列,半导体光刻胶一体化优势显著。彤程新材:主营为特种橡胶助剂,通过控股北京科华进入半导体光刻胶市场。北京科华是唯一列入全球光刻胶八强的中国光刻胶公司,同时也是国内销售额最高的光刻胶公司。中国i线产品国产替代基本上靠北京科华贡献,同时也是唯一可以批量供应KrF光刻胶给8寸和12寸客户的本土光刻胶公司,产品达到世界一流水准,打入中芯国际、长江存储、华虹半导体等国内厂商。晶瑞股份:晶瑞股份的产品包括超净高纯试剂、光刻胶、 功能性材料、锂电池材料和基础化工材料等。光刻胶领域:子公司苏州瑞红作为国内光刻胶领域的先驱,规模生产光刻胶 30 年,产品技术水平和销售额处于国内领先地位。i 线光刻胶已取得了中芯国际天津、扬杰科技的供货订单;高端KrF(248nm)光刻胶完成中试,建成了中试示范线。南大光电:公司成功自主研发出国内首支通过客户认证的 ArF 光刻胶产品,两次通过客户认证。公司目前已经建成 25 吨生产线(5 吨干式和 20 吨湿式),在光刻胶国产化的大趋势下,光刻胶业务将带动公司业绩迈入新的成长空间。4、CMP抛光材料CMP抛光材料、靶材国产化程度最高,部分产品技术标准已到达世界一流水平,本土公司已实现批量供货。抛光液和抛光垫是CMP抛光的主要材料,属于高价值易耗品,毛利高,认证时间长,客户黏性强,竞争格局也相对较好。产品价值上,抛光液高一些,占总材料成本49%,抛光垫占33%,其他材料合计不到20%。安集科技:国内抛光液龙头,目前安集科技在国内没有对手。国内市场份额超过20%,仅次于卡博特。抛光液的技术含量很高,且由于其专用性,绑定大客户后未来增速也很有保证。鼎龙股份:国内抛光垫龙头,主要是做打印复印耗材的,近年来开始布局半导体材料,抛光垫去年开始放量。成为长江存储的一供,对中芯国际也持续放量。5、高纯湿电子化学品超净高纯试剂是指主体成分纯度高于99.99%的化学试剂,主要用于芯片的清洗、蚀刻等制造领域。江化微:国内湿电子化学品龙头,打破国外企业限制壁垒,逐渐实现中低端市场的国产化替代。其超净高纯试剂、光刻胶配套试剂产品具备为平板显示、半导体、光伏等领域提供全系列湿电子化学品能力。晶瑞股份:超净高纯试剂占其营收的20%。超纯双氧水、超纯氨水及在建的高纯硫酸等主导产品已达到G5等级,其它高纯化学品均普遍在G3、G4等级。6、靶材江丰电子:国内高纯溅射靶材行业龙头,目前已可量产用于90-7nm半导体芯片的钽、铜、钛、铝靶材,其中钽靶材在台积电7nm芯片中已量产,5nm技术节点产品也已进入验证阶段。(三)制造设备半导体景气周期有着“设备先行、制造接力、材料缺货”的传导规律。中国大陆迎来投资建厂热潮,对半导体设备的需求也水涨船高。今年上半年,半导体设备的同比出货量相比去年增长了50%,设备制造商接单不暇。由于半导体设备技术壁垒高,研发难度大、周期长,是整个产业中最关键的环节之一。因此要实现我国半导体产业链的自主可控,半导体设备至关重要。半导体设备价值含量高,投资占晶圆厂建设投资 75-80%,当前我国内半导体设备自制率仍较低,2020年国产化率约为16%。但部分领域国内厂商打破空白,技术不断追赶。北方华创:国产半导体设备的绝对龙头,平台属性强。体量大;研发实力强属于国内一线;市占率领先,刻蚀设备国内市占率7%。产品线丰富,产品涵盖热处理、刻蚀、薄膜沉积(PVD、CVD)、清洗等设备。硅片设备-热处理设备-光刻设备-刻蚀设备-离子注入设备-薄膜沉积设备-抛光设备-清洗设备-检测设备1、硅片设备硅片制造是芯片生产的第一道环节,单晶硅生长炉是生产单晶硅的主要半导体设备。晶盛机电:国内晶体硅生长设备龙头,应用横跨半导体光伏两大产业,国内高端市场占有率第一。技术领先,实现大硅片多项技术突破。同时向切片、抛光、外延等设备拓展,目前已经完成技术认证。甚至还出了第三代碳化硅半导体设备,已交付客户使用。2、热处理设备热处理设备即高温炉,主要是对硅片进行氧化、扩散、退火等工艺处理。供应商主要是国外厂商,国内龙头厂商北方华创在热处理设备的各个细分领域均有成熟的产品线,市占率也在逐年上升。北方华创热处理设备在长江存储的占比已经超过了30%。3、光刻设备光刻是将设计好的电路图从光刻版转印到晶圆表面的光刻胶上,便于后续通过刻蚀和离子注入等工艺实现设计电路。光刻是晶圆生产的核心环节,设备包括光刻机和涂胶显影设备。光刻机是晶圆加工设备中技术壁垒最高的设备,该市场为荷兰企业ASML(阿斯麦)所垄断,目前国内具备光刻机生产能力的企业主要是上海微电子。芯源微:涂胶显影机行业龙头,成功打破了国外厂商垄断成为唯一的本土厂商,产品已经陆续通过了下游晶圆厂线的工艺验证,市占率5%。公司近年也开始切入湿法清洗设备领域,跟盛美股份,至纯科技和北方华创等展开竞争。4、刻蚀设备刻蚀设备在晶圆厂设备支出中仅次于光刻设备,且刻蚀设备数量更多,可替代比例更高,目前我国的刻蚀设备是比较先进的。中微公司:刻蚀设备龙头企业,介质刻蚀机已打入 5nm 制程,技术接近世界级水准,国内市占率达13%。北方华创:技术节点达到28nm,刻蚀设备国内市占率7%5、离子注入设备硅片刻蚀后,需要将一些特殊的杂质离子注入到硅衬底去,这就是离子注入机。国内离子注入标的稀缺,仅凯世通和中科信。万业企业:公司主营房地产业务,2018年收购凯世通切入半导体离子注入设备。产品已进入产线验证阶段,并于去年取得4台订单。6、薄膜沉积设备薄膜沉积工艺,分为物理气相沉积(PVD)、化学气相沉积(CVD)和外延三大类。北方华创的PVD设备已经用于28nm生产线中,14nm工艺设备也已实现重大进展,国产化率约 30%。7、抛光设备晶圆制造的后期,需要对硅片表面进行平坦化处理,这就用到了抛光机。该领域的龙头是华海清科,技术领先,合计市占率达 20%。首发申请获上交所上市委员会通过,将于上交所科创板上市。8、清洗设备清洗设备目前国产化率最高,达到20%。随着工艺流程延长且复杂,每个晶片在整个制造过程中需要至少超过200道清洗步骤。是正在申报科创板IPO的盛美股份,深耕半导体清洗设备十余年,,国内企业中市占率最高,产品进入中芯国际、长江存储、SK海力士等企业供应链。盛美股份:科创板IPO正式提交注册的盛美股份是半导体清洗设备龙头,技术优势领跑同行业。占据国内80%的市场份额,其余20%则由北方华创、芯源微和至纯科技三家公司瓜分。至纯科技:公司目前湿法清洗设备订单充足,排产计划已经到了2022年。设备可实现28nm湿法工艺全覆盖,可以满足国内绝大部分晶圆厂的要求,单片清洗设备预计会在下半年批量交付。9、检测设备检测设备主要用于检测芯片性能与缺陷,贯穿于半导体制造过程中的每一步主要工艺,按不同环节分为前道测试和后道测试设备。前道测试设备主要用于晶圆加工环节,属于质量检测,壁垒更高。目前国内企业竞争力较弱,国产化率低于5%,处于技术、产品突破阶段。精测电子:以半导体前道检测设备为主,同时布局后道,形成半导体测试领域全覆盖。其自动检测设备基本实现国产化,且已在国内一线客户实现批量重复订单。赛腾股份:深耕自动化组装、自动化检测设备,通过收购日本Optima进入半导体检测设备领域。Optima在晶圆缺陷检测设备上有成熟的产品线,该技术储备对国内来说是稀缺的,由此占据了发展先机。三、芯片封装测试中国凭借低廉的劳动力,首先承接了对劳动力需求较大,技术要求较低的半导体封测业务。目前大陆的芯片封装、测试整体上已经达到国际先进水平,产业链条(封测厂、装备、材料)比较完整。(一)封测装备检测设备的后道测试设备主要用于晶圆加工后的封测环节,主要为电学检测。设备主要分为测试机(ATE)、分选机、探针台,三者中,测试机市场最大,价值量更高。伴随三大封测厂商的扩产计划,会直接增加对国产后道测试设备的需求。长川科技:国产半导体测试设备综合龙,头分选机为主、测试机为辅,多赛道布局。产品具备竞争力,主要性能指标已达国内领先、接近国外先进技术水平。华峰测控:测试机设备龙头,已可实现国产替代。技术国内领先,接近世界一流水平,进入了封测龙头供应链。测试机在半导体检测设备中价值占比高,华峰测控的毛利达到80%,领先同行,盈利能力强。但同时受制于单一赛道,成长速度受限。(二)封测材料康强电子:国内半导体封装材料龙头,是我国规模最大引线框架生产企业。(三)封测厂我国是封测大国,拥有 “封测三杰”——长电科技、通富微电、华天科技,此前都宣布了各自的扩产计划。长电科技:国内封测企业的龙头,全球排名第三,市占率达14.4%。高端封装技术与国际第一梯队齐头并进,公司在收购星科金朋后进一步发展了SiP、晶圆级和2.5D/3D等先进封装技术,并实现大规模量产远超传统封装的产量和销量。通富微电:全球第六的封测厂,市占率7%。聚焦大客户战略,长期布局存储、微处理器等产品的先进封装技术。日前开发了国内第一款 SiP 电源模块的封装技术,该技术可应用于包括 5G 在内的各种基站和网络电源,目前已实现批量生产。华天科技:通过自建及收购拥有多个国内外多个厂区,低端、中端、高端全方位布局来自微信
2025年08月18日
0 阅读
0 评论
0 点赞
1
...
15
16
17
...
76