LVGL 是一个轻量级的嵌入式图形库,它用于微控制器和其他资源受限的环境中。为了实现面向对象的设计,LVGL 的开发团队选择使用 C 语言,而不是直接使用 C++,乍一看有点费解。
C 语言在嵌入式系统中的地位与特性
在嵌入式系统的开发中,C 语言是开发人员最常用的语言之一。它的普及源于以下几个重要特性:
- 控制力和低级访问:C 语言提供了对内存的直接访问和指针操作,这对于嵌入式开发至关重要,因为开发人员通常需要直接与硬件交互。在嵌入式设备中,硬件资源非常有限,因此开发人员必须尽可能地控制每一位元数据,以优化资源的利用率。C 语言的这一特性使其能够直接进行硬件操作,而这些在 C++ 中虽然也能实现,但通常由于更高层次的抽象和封装导致代码额外的开销。
- 紧凑的可执行代码:C 语言编译出的可执行代码往往较为紧凑。C++ 的特性(例如虚函数、多态性、异常处理等)会增加额外的开销,这在资源受限的嵌入式系统中是不可取的。举例来说,假设我们需要在一个只有 256 KB Flash 和 64 KB RAM 的微控制器上运行一个图形用户界面(GUI),选择 C++ 实现面向对象的特性就可能导致代码膨胀,从而无法满足存储和内存限制。
- 广泛的硬件支持:C 编译器的兼容性非常好,几乎所有的嵌入式平台都可以使用 C 编译器。许多微控制器的厂商提供的 SDK 也是基于 C 语言的,直接选择 C 可以避免跨语言开发的兼容性问题。
因此,LVGL 在设计初期就选择了 C 语言作为基础。C 语言具有极高的可移植性和对硬件的直接控制能力,尤其适合嵌入式开发环境中对性能和资源的高要求。
C++ 的缺点与其对嵌入式系统的影响
从表面上看,C++ 似乎是实现面向对象编程的更自然选择,因为它内建了类、继承和多态等特性。然而,C++ 的一些特性在嵌入式开发中会造成不利的影响:
- 运行时开销和内存占用:C++ 的面向对象特性(例如虚函数表)需要额外的内存开销。对于微控制器这类嵌入式设备来说,内存资源极其有限,无法容忍额外的开销。举个例子,在一个低功耗的 ARM Cortex-M0 微控制器中,假如每个对象都需要虚函数表来支持多态,那么系统整体的内存使用量会显著增加,这对资源的紧张程度无疑是雪上加霜。
- 不可预测的代码行为:C++ 的复杂特性(如 RTTI 和异常处理)使得代码行为难以预测。例如,在嵌入式环境中进行异常处理时,可能会导致程序意外崩溃,或者占用过多的堆栈空间。而嵌入式系统往往需要程序有极高的可靠性和确定性,因此这种不可预测性对于稳定性要求较高的嵌入式系统是不可接受的。
- 初始化和析构的复杂性:C++ 类的构造函数和析构函数在嵌入式环境中可能引发问题。嵌入式系统在开机时通常需要快速启动,C++ 的对象构造和析构会增加启动时间。特别是在静态对象的场景中,初始化的顺序不易控制,导致系统在启动过程中出现潜在的不稳定因素。例如,如果某个硬件相关的对象未完成初始化,而另一个依赖它的对象已经开始执行,就会导致系统行为异常。
所以我个人认为,LVGL 的开发团队权衡了嵌入式环境中的诸多因素,决定不直接采用 C++,而是选择在 C 语言中实现面向对象的设计思想。
如何在 C 中实现面向对象设计
C 语言本身不是面向对象的,但这并不意味着我们无法在 C 中采用面向对象的设计思想。LVGL 就是通过一些技巧实现了面向对象的特性,以下是几种常用的方法:
结构体与函数指针模拟类与继承
在 C 语言中,开发者可以使用结构体和函数指针来模拟类和继承的概念。结构体用于存储对象的数据,而函数指针则用于定义操作这些数据的行为。
举一个例子:假设我们需要设计一个图形界面的组件系统,其中有基本的 Widget
类,以及从它派生的 Button
和 Slider
。在 C 语言中,我们可以通过以下方式实现:
// 定义基础类 Widget
typedef struct {
int x;
int y;
void (*draw)(void* self); // 函数指针,用于多态的绘制函数
} Widget;
// 定义 Button 类,继承自 Widget
typedef struct {
Widget base;
char* label;
} Button;
// 定义 Slider 类,继承自 Widget
typedef struct {
Widget base;
int value;
} Slider;
// 实现具体的 draw 方法
void draw_button(void* self) {
Button* button = (Button*)self;
printf("Drawing button at (%d, %d) with label: %s\n", button->base.x, button->base.y, button->label);
}
void draw_slider(void* self) {
Slider* slider = (Slider*)self;
printf("Drawing slider at (%d, %d) with value: %d\n", slider->base.x, slider->base.y, slider->value);
}
在这个例子中,Widget
结构体可以看作是一个基类,包含基本的位置信息和一个函数指针,用于实现多态行为。Button
和 Slider
结构体通过包含 Widget
结构体来实现类似继承的效果。而函数指针 draw
则实现了类似于虚函数的功能,允许子类重写绘制行为。
模拟构造函数和析构函数
在 C++ 中,类的构造函数和析构函数自动处理对象的初始化和销毁,而在 C 中,我们需要手动实现这些逻辑。LVGL 通过显式的初始化函数来模拟构造函数,例如:
void button_init(Button* button, int x, int y, char* label) {
button->base.x = x;
button->base.y = y;
button->base.draw = draw_button;
button->label = label;
}
通过这样的方式,button_init
函数负责初始化 Button
对象的所有字段,相当于 C++ 中的构造函数。类似地,销毁对象的逻辑也可以通过专门的清理函数来实现。
实际案例:LVGL 的对象系统
LVGL 利用了上述技术,在 C 中实现了一个相对完整的对象系统。例如,LVGL 中的对象 lv_obj_t
是所有 GUI 元素的基类。每个 GUI 元素的数据和行为都被封装在一个结构体中,通过函数指针实现多态。所有对象都使用一个通用的 lv_obj_t
作为基础,具体的控件类型(如按钮、滑块等)通过包含或扩展该结构体来实现。
这样的设计使得 LVGL 保持了代码的简洁和轻量,同时也具备了面向对象的灵活性。这种方式的好处在于,在保证了嵌入式系统中性能和内存消耗最小化的前提下,依然能够实现模块化和代码复用。
为什么选择 C 而不直接使用 C++:综合对比
LVGL 的选择并非偶然,而是基于对 C 与 C++ 特性的深度考量。
- 代码的可移植性:C 语言的简单性使得其编译器在所有的嵌入式平台上都有很好的支持,而 C++ 的编译器在某些小型微控制器上可能并不完善,甚至完全不被支持。C++ 的某些特性在不同的编译器间实现上存在差异,增加了移植的难度。而 LVGL 需要在各种不同架构的硬件平台上运行,选择 C 语言可以确保代码的高可移植性。
- 内存占用与执行效率:嵌入式系统中内存和 CPU 的资源都非常有限,C++ 的一些高级特性会引入不必要的内存消耗和执行时间开销。例如,C++ 的标准库非常庞大,而 LVGL 需要尽可能减少依赖,以保证代码在嵌入式平台上的轻量和高效。使用 C 语言,开发者可以精细控制内存的分配与释放,确保每一字节的内存都被有效利用。
- 语言特性的限制与选择:C++ 的面向对象特性虽然强大,但并不总是适合嵌入式系统。例如,异常处理的机制在嵌入式系统中并不实用,因为异常处理会显著增加代码的体积,并且嵌入式系统中通常通过特定的错误处理机制来确保系统的可靠性。LVGL 在设计时选择了一种手动管理错误的方式,以避免 C++ 异常处理的额外开销。
- 开发团队与维护的实际需求:对于 LVGL 这样的开源项目,使用 C 语言可以吸引更多的开发者参与。C 语言的学习曲线较 C++ 更加平缓,并且在嵌入式开发者中更为普及。选择 C 可以降低开发和维护的门槛,鼓励更多开发者为项目贡献代码。
真实案例中的对比与经验总结
以嵌入式开发领域的另一个图形库 uGFX 为例,这个库也选择了使用 C 语言而非 C++。开发者团队在讨论中指出,uGFX 的目标是最大限度地减少对系统资源的占用,并且确保代码可以在各种微控制器上顺利运行。他们发现,C++ 带来的内存开销和调试复杂性使得在资源受限的嵌入式环境中难以管理,而 C 的简洁性和对硬件的直接控制能力使得开发更加可预测和可靠。
此外,一些硬件厂商提供的驱动和 API 通常也是用 C 语言编写的,因此直接选择 C 语言可以与这些驱动和 API 进行无缝集成,减少代码的桥接工作。对于 LVGL 来说,这样的选择不仅提高了开发效率,还确保了与底层硬件的兼容性。