ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GObject 核心原理与 C 语言面向对象实践:从类型系统到信号机制

GObject 核心原理与 C 语言面向对象实践:从类型系统到信号机制 做 GNOME 或者 GTK 开发的兄弟应该都见过 GObject 的大名。刚接触那会儿我一度觉得这就是“为了复杂而复杂”——明明写着 C 语言却到处都是GObjectClass、g_signal_connect、g_object_new看起来像 C 又不像 C。真正花时间啃下来之后我才意识到这套对象系统就是 GNOME 这个项目能三十多年不崩、还能继续迭代的底气。今天这篇不聊虚的直接把它拆开GObject 到底解决了 C 语言的什么问题核心机制是什么怎么写一个真正能用的 GObject 类以及那些文档里不会写清楚的坑和心得。内容适合三类人看一是做 GTK/GNOME 应用开发想搞懂底层对象模型的二是嵌入式或者系统级 C 程序员想用 C 写出可维护的大型项目又不想整个切到 C 的三是纯粹想看看“C 语言里的面向对象到底长什么样”的好奇人士。如果你只是想在 Linux 上配置 GNOME 环境或者美化桌面那这篇帮不上忙但如果你想写真正的 C 库、C 应用这篇文章值得你花二十分钟读完。1. C 语言搞“面向对象”到底是在解决什么痛点1.1 单靠结构体和函数C 写大项目会碰到哪些真实问题很多老工程师会说“C 语言也可以面向对象”嗯话没错但得加个前提得靠纪律和约定。你可以在结构体里放函数指针也可以按模块名规范前缀函数比如counter_init、counter_increment。小项目这么干没问题一旦模块变多麻烦就来了。举个我见过很多次的例子。你写了一个文件监视模块需要在文件变化时通知其他模块。传统 C 的写法是留一个全局回调注册表register_file_change_callback(callback_fn, user_data)。模块 A 注册回调模块 B 也注册回调模块 C 忘了注销文件一删除回调还是触发了直接野指针崩溃。这种问题在 C 项目里太常见核心原因是三个结构体只是数据的集合行为和数据的绑定全靠在函数命名上做文章没有任何语言层面的约束。每个模块的生命周期管理全靠“约定”谁分配谁释放说不清内存泄漏和双重释放就来了。没有统一的事件通知机制模块间的通信只能靠全局变量、回调注册表或者自己造消息队列代码越写越拧巴。GObject 就是把这些问题系统性地解决掉类型系统让每个对象有唯一身份和继承关系引用计数让生命周期有规则可循信号机制内置了一套标准的事件发布订阅模型。它不是什么玄学就是把你平时“靠自觉”做的事用一套标准化的机制固定下来。1.2 为什么 GNOME 没有直接选 C而是自己造了一套这个问题我当年也困惑过。GNOME 项目 1997 年启动C 早就存在了为什么不直接拿 C 写Linux 桌面圈子里这个问题到最后往往变成“立场之争”。但从工程角度看C 依然是底层系统更稳妥的选择ABI 稳定、编译器支持广泛、和 C 库互操作零摩擦、性能可预期。C 的对象模型每个编译器实现都有差异动态库跨编译器使用容易踩坑。GObject 的路线是“用 C 的机制模拟出对象模型”。这样做的好处很明显所有对象本质都是结构体和函数指针任何支持 C 的语言都能理解和调用对外暴露接口非常稳定。内存布局是自定义的不受编译器厂商的 C ABI 限制跨版本可以做到二进制兼容。可以方便地做跨语言绑定这直接催生了后来的 GObject Introspection 体系让 Python、JavaScript、Rust 都能调用 GNOME 平台库。所以 GObject 不是“为了面向对象而面向对象”而是 GNOME 为长期工程稳定性和生态系统扩展性做的一个非常理性的技术选型。1.3 与“手写结构体加函数指针”的土办法对比GObject 强在哪肯定有人问我自己封装一个结构体加函数指针不也能实现差不多的效果吗确实能但你在重新发明一个轮子而且是一个没经过大规模验证的轮子。手写方案做到后面你迟早要自己实现这些机制类型唯一标识和运行时类型判断、继承中的父类方法链、对象复制和释放的统一入口、属性读写与序列化、事件信号的分发与断连、跨线程调度的处理。每一条都是深坑。GObject 的价值在于这些机制已经经过 GNOME 生态二十多年的打磨踩坑成本由整个社区替你付过了。具体优势上我用一个表格来对比会更直观能力手写结构体函数指针GObject类型唯一标识自己维护枚举或字符串GType 全局注册类型天然唯一运行时类型判断手写if 类型字段G_TYPE_CHECK_INSTANCE_TYPE等宏继承与方法覆写纯靠函数指针赋值类结构体 父类方法链体系固定内存管理各自发明规则引用计数 浮动引用规则统一属性系统手动 getter/setterGParamSpec 注册可序列化、可绑定事件机制手写回调注册表Signal 系统支持断连和自动清理一句话总结手写方案解决的是“今天能跑”的问题GObject 解决的是“这个项目五年后还在跑”的问题。2. GObject 核心机制类型、类结构体、对象的三角关系2.1 实例对象与类对象房子和户型图的关系理解 GObject 最关键的一步是分清两个结构体实例结构体Instance Struct和类结构体Class Struct。实例结构体比如struct _MyCounter { GObject parent_instance; gint value; }每个对象创建一份存的是对象自己的数据相当于“我家的房子的实际装修”。类结构体struct _MyCounterClass { GObjectClass parent_class; void (*reset)(MyCounter *self); }同一个类只存在一份存的是函数指针相当于“户型图”所有同类型的对象共享。当你调用g_object_new(MY_TYPE_COUNTER, NULL)时GObject 系统会做三件事分配实例结构体内存、调用实例初始化函数my_counter_init初始化成员、返回给你一个指针。这个指针的类型虽然是GObject *但实际内存是完整的MyCounter结构体所以你可以通过MY_COUNTER(obj)这个宏安全地把基类指针转成子类指针。类结构体的初始化发生在类型第一次被g_object_new或g_type_class_ref的时候只执行一次。所以如果你的类里面有什么“所有实例共享的缓存”放在类结构体里比放在全局变量更优雅。2.2 G_DECLARE 和 G_DEFINE 系列宏几行宏替代几百行样板代码老版本 GObject 代码里定义一个类需要在头文件里手写大量类型转换宏比如G_TYPE_CHECK_INSTANCE_CAST那一堆非常劝退。现在有了G_DECLARE_DERIVABLE_TYPE和G_DECLARE_FINAL_TYPE这两个宏头文件能精简到一个很舒服的程度。拿一个最终类不能被继承举例头文件里写#define MY_TYPE_COUNTER (my_counter_get_type ()) G_DECLARE_FINAL_TYPE (MyCounter, my_counter, MY, COUNTER, GObject) MyCounter *my_counter_new (gint initial_value); gint my_counter_get_value (MyCounter *self); void my_counter_set_value (MyCounter *self, gint value); void my_counter_increment (MyCounter *self);G_DECLARE_FINAL_TYPE一个宏替你展开了my_counter_get_type函数声明、MY_COUNTER转换宏、MY_IS_COUNTER类型判断宏。源文件里再用G_DEFINE_TYPE (MyCounter, my_counter, G_TYPE_OBJECT)自动生成类型注册函数和安全转换逻辑。整个样板代码被压缩到了两行可读性直接上一个台阶。要注意的是G_DECLARE_FINAL_TYPE生成的类型不支持自定义类结构体因为这个类被定义为 Final也就没有子类去覆写虚函数了。需要让别人继承的类要用G_DECLARE_DERIVABLE_TYPE并且要在头文件里手动定义类结构体便于子类扩展。2.3 继承与多态C 语言里的虚函数是怎么实现的GObject 的继承建立在“类结构体指针”的覆写机制上。子类的类结构体里会把父类的类函数指针换成自己的实现。我以计数器为例定义一个可扩展的计数器类typedef struct _MyAdjustableCounter MyAdjustableCounter; typedef struct _MyAdjustableCounterClass MyAdjustableCounterClass; struct _MyAdjustableCounter { GObject parent_instance; gint value; }; struct _MyAdjustableCounterClass { GObjectClass parent_class; void (*adjust) (MyAdjustableCounter *self, gint delta); }; MyAdjustableCounter *my_adjustable_counter_new (gint initial_value); gint my_adjustable_counter_get_value (MyAdjustableCounter *self); void my_adjustable_counter_adjust (MyAdjustableCounter *self, gint delta);子类要覆写adjust这个虚函数时在子类的class_init里直接给函数指针重新赋值static void my_double_counter_class_init (MyDoubleCounterClass *klass) { MyAdjustableCounterClass *adjustable_class MY_ADJUSTABLE_COUNTER_CLASS (klass); adjustable_class-adjust my_double_counter_adjust; }调用方拿到的可能是一个指向父类的指针但调用my_adjustable_counter_adjust(counter, 1)时函数内部会通过MY_ADJUSTABLE_COUNTER_GET_CLASS(counter)-adjust(counter, 1)来间接调用最终执行的是子类覆写后的版本。这就是 C 语言里的“多态”。实际写代码时建议把所有需要被子类覆写的方法都放到类结构体里保持命名统一为类名_方法名并且在暴露给外部的接口函数里加一个防御判断如果函数指针为 NULL给出 warn 并返回默认值避免调用时直接空指针崩溃。2.4 引用计数决定对象生死的游戏规则GObject 对象的生命周期是g_object_ref和g_object_unref控制的。g_object_new创建出来的对象计数为 1每次g_object_ref加 1每次g_object_unref减 1减到 0 时会触发finalize释放对象。这里最容易出问题的是浮动引用floating reference。很多 GObject 构造函数比如gtk_button_new返回的其实是一个浮动引用对象引用计数虽然为 1但这个引用是“被附加到容器或调用者身上”的。当你把按钮加入窗口容器后容器会接管它的所有权所有权接管时会调用g_object_ref_sink把浮动引用变成正式引用。如果是普通引用自己 new 出来的对象用完必须手动g_object_unref。我的实操建议是自定义构造函数里尽量避开浮动引用老老实实g_object_new后返回正式引用调用方拿到对象就拥有一份所有权用完必须释放。规则越简单越不会出错。3. 手写一个最小 GObject 类从定义到编译运行的完整过程3.1 场景设定做一个支持属性、信号、还有虚函数的计数器概念讲再多不如跑一个能编译的示例。我们做一个MyCounter类要求是继承GObject。有一个 int 类型的属性value可读可写。修改value时发出value-changed信号。提供一个my_counter_new工厂函数。提供increment和get_value两个外部接口。源码分三个文件头文件my-counter.h、实现文件my-counter.c、测试文件main.c。3.2 头文件与实现文件代码逐段拆解头文件#ifndef MY_COUNTER_H #define MY_COUNTER_H #include glib-object.h G_BEGIN_DECLS #define MY_TYPE_COUNTER (my_counter_get_type ()) G_DECLARE_FINAL_TYPE (MyCounter, my_counter, MY, COUNTER, GObject) MyCounter *my_counter_new (gint initial_value); gint my_counter_get_value (MyCounter *self); void my_counter_set_value (MyCounter *self, gint value); void my_counter_increment (MyCounter *self); G_END_DECLS #endifG_BEGIN_DECLS和G_END_DECLS是为了让 C 代码也能以 C 方式链接这个头文件属于 GNOME 库的标准习惯写法。实现文件#include my-counter.h struct _MyCounter { GObject parent_instance; gint value; }; enum { PROP_0, PROP_VALUE, N_PROPERTIES }; enum { SIGNAL_VALUE_CHANGED, SIGNAL_LAST }; static GParamSpec *properties[N_PROPERTIES]; static guint signals[SIGNAL_LAST]; G_DEFINE_TYPE (MyCounter, my_counter, G_TYPE_OBJECT) static void my_counter_set_value_internal (MyCounter *self, gint value) { if (self-value value) return; self-value value; g_signal_emit (self, signals[SIGNAL_VALUE_CHANGED], 0); } MyCounter *my_counter_new (gint initial_value) { MyCounter *self g_object_new (MY_TYPE_COUNTER, NULL); my_counter_set_value (self, initial_value); return self; } gint my_counter_get_value (MyCounter *self) { g_return_val_if_fail (MY_IS_COUNTER (self), 0); return self-value; } void my_counter_set_value (MyCounter *self, gint value) { g_return_if_fail (MY_IS_COUNTER (self)); my_counter_set_value_internal (self, value); } void my_counter_increment (MyCounter *self) { g_return_if_fail (MY_IS_COUNTER (self)); my_counter_set_value_internal (self, self-value 1); } static void my_counter_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { MyCounter *self MY_COUNTER (object); switch (prop_id) { case PROP_VALUE: my_counter_set_value (self, g_value_get_int (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void my_counter_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { MyCounter *self MY_COUNTER (object); switch (prop_id) { case PROP_VALUE: g_value_set_int (value, self-value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void my_counter_class_init (MyCounterClass *klass) { GObjectClass *gobject_class G_OBJECT_CLASS (klass); gobject_class-set_property my_counter_set_property; gobject_class-get_property my_counter_get_property; properties[PROP_VALUE] g_param_spec_int (value, Value, Current counter value., G_MININT, G_MAXINT, 0, G_PARAM_READWRITE); g_object_class_install_properties (gobject_class, N_PROPERTIES, properties); signals[SIGNAL_VALUE_CHANGED] g_signal_new (value-changed, G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } static void my_counter_init (MyCounter *self) { self-value 0; }这里有两个细节值得展开my_counter_set_value_internal里判断新旧值是否相等相等就直接返回。这个判断很重要不然g_object_set(counter, value, 11, NULL)设置相同值时也会触发一次信号导致外部无谓的刷新逻辑性能损耗积少成多。set_property内部没有直接改self-value而是调用my_counter_set_value这样既能触发属性通知也能统一走信号逻辑。属性系统和业务接口通过这种方式合流是 GObject 里很常见的设计。3.3 main.c 测试程序与编译命令测试程序#include glib-object.h #include my-counter.h static void on_value_changed (MyCounter *counter, gpointer user_data) { g_print (value changed to %d\n, my_counter_get_value (counter)); } int main (int argc, char *argv[]) { MyCounter *counter; counter my_counter_new (10); g_signal_connect (counter, value-changed, G_CALLBACK (on_value_changed), NULL); my_counter_increment (counter); g_object_set (counter, value, 42, NULL); g_print (final value: %d\n, my_counter_get_value (counter)); g_object_unref (counter); return 0; }编译命令gcc main.c my-counter.c -o counter-demo $(pkg-config --cflags --libs gobject-2.0)运行结果value changed to 11 value changed to 42 final value: 42第一次my_counter_increment从 10 变成 11触发信号。第二次通过g_object_set设置属性值从 11 变成 42又触发信号。整体链路完整走通。用pkg-config取编译参数这一点我多说一句。开发机上如果没安装对应的 dev 包pkg-config --cflags --libs gobject-2.0会直接报错提示找不到文件。在 Debian/Ubuntu 上先执行sudo apt install libglib2.0-dev其他发行版对应的包名不同但思路是一样的必须安装开发版库只有运行时库是不够的。4. 属性系统与信号机制让对象具备“可观察性”4.1 属性系统外部世界操作对象的统一入口属性系统是 GObject 一个很容易被低估的设计。很多人以为属性就是 getter/setter 的语法糖其实它的用途远不止这个。通过 GParamSpec 注册的属性天然支持“按名字读写”所以你可以用g_object_set(counter, value, 42, NULL)这种名字加可变参数的方式设置属性。更妙的是这种命名机制让对象可以轻松对接外部世界GDBus 里可以自动把属性暴露成 D-Bus 的属性。GtkBuilder 可以从 XML 界面文件里按属性名初始化控件。GObject Introspection 可以跨语言绑定。序列化框架可以按属性名逐一读取数据。注册属性的代码里最重要的参数是GParamSpec的名称、类型、默认值以及读写标志。g_param_spec_int的前两个参数“最小值”和“最大值”可别乱填。如果业务上 value 允许负数那你得把最小值设成G_MININT否则设置一个超出范围的属性值会被系统自动 clamp造成难以排查的“诡异”行为。在set_property和get_property里switch判断时default分支一定要写G_OBJECT_WARN_INVALID_PROPERTY_ID。少了这个属性 ID 对不上时静默失败调试起来非常痛苦。4.2 信号机制对象如何对外说“我变了”信号是 GObject 事件机制的核心。上面的例子已经演示了完整流程g_signal_new注册g_signal_emit发射g_signal_connect连接处理函数。信号注册时有几个参数值得研究G_SIGNAL_RUN_FIRST表示处理函数在对象方法执行前调用还是在后调用。比如按钮点击信号一般是 RUN_FIRST让用户处理先于内部默认处理。g_signal_new最后两个参数返回值类型和参数个数。如果定义错了回调函数签名不匹配运行时会踩栈上的内存直接堆栈错乱非常难查。信号可以有参数比如g_signal_new (value-changed, ..., G_TYPE_NONE, 1, G_TYPE_INT)这样发射时可以带上旧值或新值回调函数签名也要相应多一个参数。连接信号时g_signal_connect返回一个 handler ID。如果某个处理函数只关心前几次通知用完可以直接g_signal_handler_disconnect(instance, handler_id)断开。如果不确定instance会不会先被销毁就改用g_signal_connect_object这个函数能在对象销毁时自动断开连接避免出现回调对象已经释放信号却还在尝试调用的崩溃。4.3 一个高频率出现的设计套路属性变化自动触发信号实际项目里属性变化和信号发射几乎是绑定的。正确姿势是先实现一个_internal版本的 setter在里面完成“比较旧值→修改新值→发射信号”三个步骤。对外公开的 setter 和set_property都调到这个内部函数上。这样做有什么好处一致性。不管调用方用的是普通 API 还是属性系统都能保证信号被正确触发。这个模式在 GTK 源码里到处都是比如gtk_widget_set_visible最终会触发notify::visible属性通知信号。自己设计 GObject 类时我强烈建议照这个模式来每个需要被观察的成员变量都注册成属性。属性变化时都发一个“语义化”的信号比如value-changed。外部代码优先连接“语义化信号”而不是依赖属性名。如果你发现自己的对象有几十个属性、十几个信号先别急着扩展回头看看是不是类设计太“重”了。GObject 类应该保持小而专一个类只做一件事。太复杂的对象维护起来比普通 C 代码更痛苦。5. 常见问题与避坑心得我在 GObject 开发中踩过的那些坑5.1 引用计数泄漏和释放时机90% 的问题都出在这GObject 开发里最常见的崩溃几乎都跟引用计数有关。我在项目里排查过的泄漏主要有三类g_object_new之后忘了g_object_unrefg_object_ref保留下来的指针任务结束后没释放信号回调里user_data引用了对象回调对象和信号承载对象互相引用形成循环引用谁都无法释放。排查时有个很实用的办法在finalize函数里加一条 debug 打印static void my_counter_finalize (GObject *object) { g_print (MyCounter finalized\n); G_OBJECT_CLASS (my_counter_parent_class)-finalize (object); }对象正常销毁时这个打印就会出现。如果你的程序退出时预期所有对象都该销毁但打印没出现那基本可以断定有引用泄漏。再加上 valgrind 跑一遍内存检测定位谁多拿了一次引用就相对容易。另外CLI 工具的禁用词我也提一嘴如果用弱引用跟踪对象生命周期可以用g_object_add_weak_pointer(ptr)对象销毁后ptr会自动被置成 NULL。这个机制比你自己保存一个野指针然后祈祷它不崩要可靠得多。5.2 类型转换失败和 G_IS_ 检查一个措手不及的空指针新手很容易在类型转换上吃亏。比如把GObject *obj强转成MyCounter *但实际这个对象根本不是 MyCounter 实例。此时直接用MY_COUNTER(obj)转出来的指针可能带着错误的类型数据访问成员变量时轻则读到脏数据重则直接段错误。我的习惯是对象变量从外部传入时先做一次防御检查g_return_if_fail (MY_IS_COUNTER (self));MY_IS_COUNTER宏内部会调用类型系统确认对象真的继承自 MyCounter。这个检查有性能开销但在边界调用、回调入口、跨界传参这种高风险场景值得多写一行。另一个常见问题是在G_DEFINE_TYPE里把父类类型写错了。比如想继承GObject写成了G_TYPE_INITIALLY_UNOWNED或者反过来导致class_init里的G_OBJECT_CLASS (klass)转换拿到错误的父类结构。这类问题往往只在运行时报错编译期发现不了。遇到莫名其妙的崩溃先检查G_DEFINE_TYPE的第三、第四个参数。5.3 信号连接与断连回调被释放后还来“敲门”这类问题属于生命周期管理的延展。g_signal_connect本质上是把一个回调函数和一个对象绑定在一起。如果回调函数内部会被调用时依赖于某个临时对象的存活而这个临时对象在信号触发前就被释放了那么你拿到的是一个悬空指针。解决思路有两个用g_signal_connect_object让信号发射对象销毁时自动断连。在回调里只持有弱引用比如g_object_add_weak_pointer跟踪目标对象进入回调先判断指针是否为 NULL。还有一个小技巧信号处理函数的user_data尽量传入“弱引用友好的数据”不要直接放一个裸GObject *。如果你在信号回调里要调用对象方法先做类型检查再强转别嫌麻烦。我见过太多因为删除一个控件、销毁一个对象数组结果把整个界面进程搞崩的事故几乎都出在回调兵临城下时对象已经不在了。5.4 线程安全GObject 对象默认不是线程安全的GObject 对象本身不是线程安全的同一个对象不能在多个线程里同时访问。信号发射时哪个线程调用g_signal_emit回调函数就在哪个线程执行不会自动派发到主线程。所以如果你在子线程里往 UI 线程派发任务不要直接在线程里修改 GTK 控件。正确做法是通过g_main_context_invoke或者g_idle_add把任务投递到主循环g_main_context_invoke (NULL, update_label_handler, g_object_ref (label));注意这里要g_object_ref一下因为任务回调执行是异步的主循环执行前调用者可能已经释放了原对象。回调结束后再g_object_unref一次保证引用计数配对。如果想在对象内部共享状态就用GMutex或者GRecMutex把成员变量保护起来。GObject 的 getter/setter 在跨线程场景下不是原子的别指望 GObject 帮你做线程同步。5.5 调试工具与环境变量事半功倍的组合拳GLib 提供了一些环境变量调试 GObject 时非常有用G_DEBUGfatal-warnings把 CRITICAL 级别日志当成致命错误程序直接中断方便在 gdb 里抓调用栈。设置方式export G_DEBUGfatal-warnings再运行程序。G_SLICEalways-malloc让 GLib 的内存切片分配器退回到系统 malloc这样 valgrind 能更准确追踪内存块来源。G_MESSAGES_DEBUGall打印所有调试日志排查信号是否触发、属性是否更新时很有用。配合 gdb 使用时我的建议是不要打开全部调试开关先开G_DEBUGfatal-warnings等崩溃点出现后再用bt看调用栈。定位到具体函数后给g_signal_emit或者g_object_unref加断点输入条件断点指定对象指针能省掉大量排查时间。5.6 GObject 生态的扩展方向Introspection 与跨语言绑定写完一个 GObject 类你以为只服务了 C 语言其实只要配合 GObject Introspection一个 GObject 库可以自动生成 Python、JavaScript、Rust 等语言的绑定。GNOME 桌面的 Settings 系统、D-Bus 调用库、GTK 界面库都建立在这套机制之上。实际开发中如果你在用 Python 写 GNOME 应用底层调用的就是 PyGObject它运行时加载 GObject Introspection 生成的元数据把 GObject 类映射成 Python 类。所以你可以在 Python 里这样写counter MyCounter.new(10) counter.connect(value-changed, on_changed) counter.increment()这套设计让 C 库的“投资回报率”变得非常高写一次 C 对象模型整个语言生态都能复用。这也是 GNOME 平台能持续吸引开发者的核心原因之一。写在最后一个小习惯我很喜欢 GObject 这种“所有机制都是可见、可查、可控”的设计哲学。刚开始写会觉得啰嗦一个计数器都要整这么多代码但当你真的在一个大项目里摸爬滚打一年你一定会感谢这套繁琐的规则因为每个对象从哪里创建、谁持有引用、什么时候销毁、某个时刻会不会发信号、信号由哪个线程执行全都是可以系统化回答的问题而不是靠“我记得谁释放过”这种模糊记忆。最后分享一个小习惯新写代码优先用G_DECLARE_FINAL_TYPE定义最终类只有在确定需要被继承时才用G_DECLARE_DERIVABLE_TYPE。能用宏生成样板代码的地方绝不手写。用属性统一暴露成员变量用语义化信号向外传递事件内部 setter 统一走_internal函数。这套组合拳打下来你写的 C 代码会有一种平时很难体会到的“工程感”——依然是 C但好像已经不像 C 了。希望这篇能帮你少踩几个坑。
返回列表