C语言实现Qt中的信号与槽机制

C语言实现信号与槽

引言

信号与槽机制源自 Qt 框架,它是一种强大而优雅的事件驱动模型,旨在促进组件之间的松散耦合通信。该模式允许对象之间无需直接相互引用即可响应彼此的状态变更,从而极大地提高了系统的可扩展性和可维护性。本文将探索如何在限制更多的 C 语言环境中,构建一种类似的信号与槽机制,特别强调其实现的灵活性及适用性于资源有限的嵌入式系统中,如串口通信、按键检测和 LED 控制等模块。

核心理念

信号与槽的核心思想在于“发布-订阅”模式的运用,其中信号充当发布者的角色,负责广播状态的变化;而槽,则扮演着订阅者的角色,监听并响应相应的信号,执行预定的任务。这种机制不仅有助于减少代码间的硬性依赖,还促进了模块的独立性和可测试性,使得系统设计更加灵活且易于调整。

关键特性概览

在本文的C 语言实现中,信号与槽具备以下核心特征:

1. 不定长参数支持:为了增强通用性和灵活性,信号可以携带可变数量的参数,允许发送方自由地附带任何必要的数据。

2. 多路复用能力:一个信号可以同时通知多个槽函数,反之亦然,即单一槽函数也可响应多种信号,这极大地丰富了事件处理的多样性,同时也简化了事件分发的复杂度。

3. 连接与断开机制:除了建立信号与槽之间的初始联系外,我们还实现了动态断开的能力,允许在运行时调整事件处理逻辑,增强了系统的动态配置能力和调试便利性。

4. 资源管理意识:鉴于嵌入式环境的资源限制,我们的实现在设计之初便充分考虑到内存效率和处理速度,力求最小化的资源消耗和最大化的执行效率。

构建原理

为了实现以上特性,本文将采用以下策略和技术:

1. 数据结构:利用单向链表来存储信号及其对应的槽函数集合,便于增加或删除信号-槽映射关系。

2. 变长参数处理:借助 C 语言标准库的宏__VA_ARGS__有效地处理信号携带的不定长参数。

3. 资源管理:通过精心设计的数据结构和算法,确保在有限资源下仍能有效管理信号与槽的关系,避免内存泄露和资源浪费。

原理讲解 —— 信号与槽的概念实现

在深入讨论信号与槽机制的C语言实现前,让我们先从一个简单的示例出发,了解信号与槽的基本定义和工作流程。尽管这里的代码片段尚不具备完整的信号与槽功能,但它为我们提供了一种思考信号和槽函数如何交互的基础框架。

定义信号与槽函数

在 C 语言环境中,信号和槽函数本质上就是普通的函数,但我们在设计上赋予它们特殊的意义——信号代表事件的发生,而槽函数则负责响应这些事件。以下是定义的一些示例信号与槽函数:

#include <stdio.h>

// 定义信号1,只是一个占位函数,无需功能
void signal1(int a) {}

// 槽函数slot1,接收一个整数参数,并打印出来。
void slot1(int a)
{
    printf("slot1: %d\n", a);
}

// 另一个槽函数slot2,同样接收一个整数参数并输出。
void slot2(int a)
{
    printf("slot2: %d\n", a);
}

int main()
{
    printf("hello world!!");
    return 0;
}

在上述代码中,signal1将作为信号的代表,而 slot1slot2则分别表示两种不同的响应动作,它们都将接收一个整型参数,并简单地将其打印出来。当然,这只是信号与槽机制的一个极简示例,在实际应用中,信号可以携带不同类型和数量的信息,而槽函数则根据接收到的信号内容做出相应的处理。

接下来,需要构建一套机制,使信号能在发生时找到并调用相应的槽函数。这将涉及到如何存储信号与槽函数之间的对应关系、如何在信号触发时检索并激活这些槽函数等问题,而这正是信号与槽机制设计的核心部分。

信号和槽函数数据结构

为了实现信号与槽的功能,需要设计一套数据结构来保存信号-槽函数的映射关系,同时还要有一套接口来注册、连接、发射信号以及断开信号与槽函数的连接。

为了在 C 语言中实现信号与槽机制,采用了单向链表作为一种灵活的数据结构来存储信号与槽函数的映射关系。下面详细解释这一过程:

定义链表节点结构体

首先,定义一个名为 Node 的结构体类型,用于表示链表中的每个节点。每个节点包含三个成员变量:

  • signal: 表示与该节点相关的信号。在实际实现中,我们可以用函数指针来代替,这样可以直接指向信号函数。
  • slot: 表示与该信号关联的槽函数。同样的,我们也使用函数指针类型。
  • next: 指向链表中下一个节点的指针,这是构成单链表的关键组成部分。
初始化节点实例

随后,创建几个 Node 类型的实例,分别代表 signal1 信号下的两个槽函数 slot1 和 slot2。值得注意的是,由于我们尚未真正定义信号和槽函数,因此暂时将这些指针成员设为 NULL

    // 节点初始化
    Node signal1_n; // 链表头
    signal1_n.next = NULL;
    signal1_n.signal = NULL;
    signal1_n.slot = NULL;

    // 节点初始化
    Node slot1_n; 
    slot1_n.next = NULL;
    slot1_n.signal = NULL;
    slot1_n.slot = NULL;

    // 节点初始化
    Node slot2_n;
    slot2_n.next = NULL;
    slot2_n.signal = NULL;
    slot2_n.slot = NULL;

信号和槽函数连接

为了在信号与槽机制中建立信号与相应槽函数之间的实际连接,我们采用链表作为中间桥梁,将多个槽函数按顺序串联起来,形成一条清晰的响应路径。下面是对连接逻辑的详述,包括如何将槽函数节点整合进信号链表中,以及为何选择使用 do...while(0)循环结构而非直接函数封装的原因。

为了建立起信号与槽函数的实际关联,需要将slot1_nslot2_n这两个节点添加到 signal1_n的链表中。具体而言,可以通过更新next成员,形成链式的连接。例如signal1_n.next应指向 slot1_n,而 slot1_n.next则应指向 slot2_n,以此类推,直至达到链表尾部,此时 next成员应为 NULL

实现连接

对于每一个需要与信号相连接的槽函数,我们都要执行相似的操作,即将其作为一个新节点追加到该信号的链表末尾。这个过程分为几个步骤:

1. 定位链表尾部:首先,我们从信号的链表头部 (&signal1_n)开始,通过不断访问当前节点的 next成员,直到找到最后一个节点(即 next成员为 NULL的节点)。

2. 创建新节点:然后,创建一个指向槽函数的新节点 (newNode)并填充相关信息,包括将 signal成员指向对应的信号函数,以及将 slot成员指向具体的槽函数。

3. 插入新节点:最后,将新节点插入到链表的末尾,即修改链表原尾部节点的 next成员使其指向 newNode

以下是针对 slot1slot2分别进行的连接操作:

    // slot1槽函数加入信号链表
    do
    {
        Node *temp = &signal1_n;
        Node *newNode = &slot1_n;
        newNode->signal = signal1;
        newNode->slot = slot1;     
        while (temp->next != NULL)     
        {                              
            temp = temp->next;         
        }                              
        temp->next = newNode; 
    }while(0);

    // slot2槽函数加入信号链表
    do
    {
        Node *temp = &signal1_n;
        Node *newNode = &slot2_n;
        newNode->signal = signal1;
        newNode->slot = slot2;     
        while (temp->next != NULL)     
        {                              
            temp = temp->next;         
        }                              
        temp->next = newNode; 
    }while(0);
使用 do…while(0) 的考量

细心的朋友可能注意到了,我们在这里选择了 do…while(0) 循环结构而不是常规的函数封装方式。原因主要有二:

1. 变量作用域控制:通过在 do...while(0)内定义局部变量,如tempnewNode,可以确保它们的作用范围仅限于此循环内部,不会干扰外部的变量名空间,从而减少了变量冲突的风险,尤其是在多次重复类似操作时。

2. 宏定义预处理兼容性:虽然这里直接使用了代码块,但在后续的代码演化中,我们可能倾向于将这些重复的代码段转化为宏定义,以方便统一管理和快速调用。do...while(0)的使用正好符合这一目的,因为在宏展开时它可以被当作一个不可分割的整体,即使在条件表达式或函数调用中使用也不会产生意料之外的语法错误。

信号的触发与槽函数的调用

在建立了信号与槽函数的连接之后,下一步便是当信号触发时能够正确地遍历链表,并调用所有关联的槽函数。这涉及到如何在遍历过程中识别出正确的槽函数,并以适当的方式调用它们,尤其是要处理槽函数可能接受的不同长度参数的情况

遍历与识别

遍历信号链表以寻找与当前信号匹配的槽函数是一项基础任务,但关键在于如何准确地识别并调用槽函数。在之前的代码片段中,我们展示了如何通过比较节点的 signal成员与当前触发的信号标识符来确定是否应当调用对应的槽函数。然而,直接使用 func来表示槽函数存在一定的问题,因为它并未显式地指出函数的类型信息,特别是对于那些带有参数的槽函数来说。

do{                                                          
    Node *temp = &signal1_n;              
    while(temp !=NULL)                   
    {                                    
        if(temp->signal == signal1)         
        {                                
            func = temp->slot;         
        }                                
        temp = temp->next;               
    }                                    
}while(0)
解决方案:使用强类型的函数指针

为了避免上述问题,我们引入了类型更明确的函数指针,以确保槽函数的正确调用。在这个案例中,假设所有的槽函数均接受一个整型参数,我们定义了 signal1_t这一函数指针类型,用以精确描述槽函数的签名:

typedef void(*signal1_t)(int a);

do{    
    signal_t func;                                                 
    Node *temp = &signal1_n;              
    while(temp !=NULL)                   
    {                                    
        if(temp->signal == signal1)         
        {                                
            func = temp->slot;     
            func(10);    
        }                                
        temp = temp->next;               
    }                                    
}while(0);    

通过使用类型化的函数指针,我们不仅保证了代码的简洁性和可读性,更重要的是提升了程序的安全性。这是因为 C 编译器能够基于类型信息对函数调用进行更严格的检查,防止潜在的类型不匹配错误,从而降低运行时异常的风险。

完整脚本:信号与槽机制的C语言实现

在深入探讨了信号与槽机制的各个组成要素后,是时候整合所学知识,编写一份完整且功能全面的信号与槽机制实现脚本了。这份脚本将涵盖信号和槽函数的定义、信号链表的构造、信号与槽函数的连接,以及信号触发与槽函数调用的全过程。


#include <stdio.h>

/* signal1 */
typedef void(*signal1_t)(int a);
void signal1(int a){}

void slot1(int a)
{
    printf("slot1 %d\n",a);
}

void slot2(int a)
{
    printf("slot2 %d\n",a);
}

typedef struct Node
{
    void        *signal;      // signal
    void        *slot  ;      // slot
    struct Node *next  ;      // the pointer of next struct
} Node;

int main(void)
{

    // 节点初始化
    Node signal1_n; // 链表头
    signal1_n.next = NULL;
    signal1_n.signal = NULL;
    signal1_n.slot = NULL;

    // 节点初始化
    Node slot1_n; 
    slot1_n.next = NULL;
    slot1_n.signal = NULL;
    slot1_n.slot = NULL;

    // 槽函数加入信号链表
    do
    {
        Node *temp = &signal1_n;
        Node *newNode = &slot1_n;
        newNode->signal = signal1;
        newNode->slot = slot1;     
        while (temp->next != NULL)     
        {                              
            temp = temp->next;         
        }                              
        temp->next = newNode; 
    }while(0);
         

    // 节点初始化
    Node slot2_n;
    slot2_n.next = NULL;
    slot2_n.signal = NULL;
    slot2_n.slot = NULL;

    // 槽函数加入信号链表
    do
    {
        Node *temp = &signal1_n;
        Node *newNode = &slot2_n;
        newNode->signal = signal1;
        newNode->slot = slot2;     
        while (temp->next != NULL)     
        {                              
            temp = temp->next;         
        }                              
        temp->next = newNode; 
    }while(0);

    do{    
        signal1_t func;                                                     
        Node *temp = &signal1_n;              
        while(temp !=NULL)                   
        {                                    
            if(temp->signal == signal1)         
            {                                
                func = temp->slot;     
                func(10);    
            }                                
            temp = temp->next;               
        }                                    
    }while(0);                           

    printf("hello world!!");
    return 0;
}

实现结果

slot1 10
slot2 10
hello world!!

宏实现

在深入了解了信号与槽机制的具体实现细节之后,接下来的目标是通过宏定义的方式来封装这一机制,使之更加易于集成和使用。宏定义允许我们在不改动源码的情况下,以简洁的语法创建信号与槽函数的声明,同时保持代码的整洁度和可维护性。

信号与槽函数的宏声明(h文件)

考虑到信号与槽函数的通用性及其在不同上下文中的应用需求,我们设计了两组专门的宏定义:SIGNAL_DECL用于信号的声明,SLOT_DECL用于槽函数的声明。这两个宏都利用了 C 预处理器的特性,特别是 __VA_ARGS__和省略号 ...的组合,使得宏能够接受可变数量和类型的参数。

信号与槽函数声明概览

回顾我们先前的脚本片段,可以看到信号和槽函数的声明部分如下:

/* SIGNAL */
typedef void(*signal1_t)(int a);
void signal1(int a);
Node signal1_n;

/* SLOT */
void slot1(int a);
Node slot1_n;
  • 声明函数头
  • 声明函数指针
  • 声明全局变量
改进的宏定义策略

下面是更新后的宏定义,它们已经被精心设计,以便在 .h 文件中使用:

/**
 * decl a signal (use in .h file)
 */
#define SIGNAL_DECL(name, ...)             \
    void name(__VA_ARGS__);                \
    typedef void (*name##_t)(__VA_ARGS__); \
    extern struct Node name##_n;           \


/**
 * decl a slot (use in .h file)
 */
#define SLOT_DECL(name, ...)               \
    void name(__VA_ARGS__);                \
    extern struct Node name##_n;           \

在信号与槽机制的设计中,链表头 Node的作用至关重要——它是整个链表的入口点,负责存储信号与槽函数之间的连接信息。为了让其他模块或文件能够访问这些链接信息,我们必须将Node变量声明为全局变量。这意味着无论在哪一个源文件中定义了信号或槽函数,只要包含了相应的头文件,就能够无缝地访问到这些链表,进而实现信号与槽之间的动态连接和响应。

这样的设计不仅极大地简化了信号与槽机制的初始设定过程,同时也增强了代码的可扩展性和复用能力。无论是增加新的信号还是槽函数,抑或是更改现有函数的参数列表,你都可以通过简单的宏调用来完成,无须担心底层数据结构或函数实现的变动。

信号和槽函数的定义与初始化 (c文件)

在完成了信号与槽函数的基本声明之后,下一步则是要在 .c 文件中对这些声明进行定义和必要的初始化工作。这包括为信号和槽函数提供实际的功能实现,以及设置每个 Node 结构体成员的默认值。这样做的目的是确保在信号被触发时,系统能按照预期的规则正确地寻址并调用相应的槽函数。

信号与槽函数定义概览

回顾我们先前的脚本片段,可以看到信号和槽函数的基础定义与初始化部分如下:

void signal1(int a){}

signal1_n.next = NULL;
signal1_n.signal = NULL;
signal1_n.slot = NULL;

void slot1(int a)
{
    printf("slot1 %d\n",a);
}

slot1_n.next = NULL;
slot1_n.signal = NULL;
slot1_n.slot = NULL;

这里需要注意的是,槽函数的具体实现完全取决于用户的业务逻辑要求,因此我们不能在宏定义中给出完整的实现代码,而仅能提供函数原型的框架。

改进的宏定义策略

基于以上的理解和分析,我们可以重新审视并改进我们的宏定义策略,确保其能够满足实际开发中的灵活性与效率需求。以下是经过调整的宏定义,它们分别用于定义信号和槽函数:

/**
 * define a signal (use in .c file)
 */
#define SIGNAL_DEF(name, ...)           \
    void name(__VA_ARGS__){}; \
    struct Node name##_n =              \
    {                                   \
        .next   = NULL,                 \
        .slot   = NULL,                 \
        .signal = NULL,                 \
    };                                  \


/**
 * define a slot (use in .c file)
 */
#define SLOT_DEF(name, ...)          \
    struct Node name##_n =           \
    {                                \
        .next   = NULL,              \
        .slot   = NULL,              \
        .signal = NULL,              \
    };                               \
    void name(__VA_ARGS__)           \
深入解析
  • 信号定义:为 SIGNAL_DEF 宏增加了信号函数的静态定义部分,并初始化了 Node结构的 signal成员,使其指向信号函数本身。这样做的好处是,当遍历信号链表时,可以直接从节点获取到与之关联的信号信息,而不需要额外查找。

  • 槽函数定义: 对于 SLOT_DEF宏,同样进行了静态初始化,不过由于槽函数的具体行为未知,因此没有在宏中包含其实现代码。相反,我们将实现部分留给了用户自行填充。

通过上述的宏定义,不仅简化了代码书写,还提高了信号与槽机制的健壮性和易用性,使得开发者能够在不影响核心逻辑的前提下自由拓展和定制自己的功能组件。这对于构建高度模块化和可扩展的应用程序框架尤为重要。

信号与槽函数的连接宏定义

在信号与槽机制中,建立信号和槽函数间的连接是一项基本操作。此步骤涉及将特定的槽函数注册至某一信号下,以期在信号触发时能够调用到相应的槽函数。原生的代码实现虽然可行,但在多次重复相同模式的操作时显得不够优雅。为了解决这个问题,我们可以通过定义一个专用的宏来自动处理信号与槽函数的连接过程,提高代码的可读性和重用性。

原始连接逻辑

原始的代码片段展现了如何手动将一个槽函数加入指定信号的链表中:

    // 槽函数加入信号链表
    do
    {
        Node *temp = &signal1_n;
        Node *newNode = &slot1_n;
        newNode->signal = signal1;
        newNode->slot = slot1;     
        while (temp->next != NULL)     
        {                              
            temp = temp->next;         
        }                              
        temp->next = newNode; 
    }while(0);

这段代码清晰地展示了如何定位到信号链表的尾部,并将一个新的节点(代表槽函数)附加到链表上。

使用宏提升连接效率

通过观察上述逻辑,我们可以发现其中存在着固定的模式和结构。因此,采用宏定义来抽象这个过程是一种理想的选择。宏 CONNECT不仅简化了代码,而且避免了每次连接信号和槽函数时都要重复相同的模板代码,显著提升了编程效率。

/**
 * @brief connect signal and slot
 * @param signal_name
 * @param slot_name
 */
#define CONNECT(signal_name,slot_name) \
    do{                                 \
        Node *temp = &signal_name##_n;  \
        Node *newNode = &slot_name##_n; \
        newNode->signal = signal_name;  \
        newNode->slot = slot_name;      \
        while (temp->next != NULL)      \
        {                               \
            temp = temp->next;          \
        }                               \
        temp->next = newNode;           \
    }while(0)                           \

断开信号与槽函数连接的宏定义

正如信号与槽机制的连接部分一样重要,能够有效地解除已有的连接也是维持系统状态一致性和资源管理的一个关键环节。为此,我们设计了一个宏 DISCONNECT来实现信号与槽函数之间连接的断开,即移除信号链表上的特定槽函数节点。这一宏定义遵循了与 CONNECT相似的理念 —— 简化复杂的迭代逻辑,减少错误的可能性,同时保持代码的精简与可读性。

// disconnect signal and slot
#define DISCONNECT(signal_name,slot_name)      \
    do{                                        \
        Node *temp = &signal_name##_n;         \
        Node *newNode = &slot_name##_n;        \
        while (1)                              \
        {                                      \
            if(temp->next == newNode)          \
            {                                  \
                temp->next = temp->next->next; \
                break;                         \
            }                                  \
            temp = temp->next;                 \
        }                                      \
    }while(0)                                  \

触发信号并执行所有相关联的槽函数

在信号与槽机制的核心运作中,除了信号与槽的连接与断开之外,最关键的部分之一便是信号的触发 —— 即一旦信号被激发,所有与其相连的槽函数都将被执行。这一过程确保了应用程序内部事件的及时传播与响应。以下是对信号触发逻辑的详细探讨,结合宏定义的运用,旨在简化并优化这一操作,使之既高效又易于实施。

原始信号触发逻辑

起初的代码片段描述了如何遍历信号链表,并执行与信号相关的所有槽函数:

    do{    
        signal1_t func;                       
        Node *temp = &signal1_n;              
        while(temp !=NULL)                   
        {                                    
            if(temp->signal == signal1)         
            {                                
                func = temp->slot;     
                func(10);    
            }                                
            temp = temp->next;               
        }                                    
    }while(0);   

这段代码逐个节点检查,寻找与当前信号匹配的节点,并执行其对应的槽函数。但是,它存在一定的局限性,如硬编码的参数传递等,这降低了代码的泛用性和维护性。

利用宏优化信号触发
为了解决这些问题,我们引入了一个名为 EMIT的宏,它不仅消除了上述限制,还提供了更强大的功能和更好的代码可读性

/**
 * emit all slots of signal
 */
#define EMIT(name,...)                       \
    do{                                      \
        name##_t func;                       \
        Node *temp = &name##_n;              \
        while(temp !=NULL)                   \
        {                                    \
            if(temp->signal == name)         \
            {                                \
                func = (name##_t)temp->slot; \
                func(__VA_ARGS__);           \
            }                                \
            temp = temp->next;               \
        }                                    \
    }while(0)                                \

EMIT宏不仅简化了触发信号的操作,还通过支持可变参数列表的方式,增强了机制的适应能力和代码的整体质量。这无疑为信号与槽机制的使用者带来了极大的便利,特别是在需要处理复杂多变的应用场景时。

完整宏定义实现信号与槽函数机制

#ifndef _META_OBJECT_H
#define _META_OBJECT_H

#include "stdio.h"
#include "stdint.h"
#include "string.h"

/**
 * @brief linked list node struct
 */
typedef struct Node
{
    void        *signal;      // signal
    void        *slot  ;      // slot
    struct Node *next  ;      // the pointer of next struct
} Node;


/**
 * @brief connect signal and slot
 * @param signal_name
 * @param slot_name
 */
#define CONNECT(signal_name,slot_name) \
    do{                                 \
        Node *temp = &signal_name##_n;  \
        Node *newNode = &slot_name##_n; \
        newNode->signal = signal_name;  \
        newNode->slot = slot_name;      \
        while (temp->next != NULL)      \
        {                               \
            temp = temp->next;          \
        }                               \
        temp->next = newNode;           \
    }while(0)                           \


// disconnect signal and slot
#define DISCONNECT(signal_name,slot_name)      \
    do{                                        \
        Node *temp = &signal_name##_n;         \
        Node *newNode = &slot_name##_n;        \
        while (1)                              \
        {                                      \
            if(temp->next == newNode)          \
            {                                  \
                temp->next = temp->next->next; \
                break;                         \
            }                                  \
            temp = temp->next;                 \
        }                                      \
    }while(0)                                  \


/**
 * emit all slots of signal
 */
#define EMIT(name,...)                       \
    do{                                      \
        name##_t func;                       \
        Node *temp = &name##_n;              \
        while(temp !=NULL)                   \
        {                                    \
            if(temp->signal == name)         \
            {                                \
                func = (name##_t)temp->slot; \
                func(__VA_ARGS__);           \
            }                                \
            temp = temp->next;               \
        }                                    \
    }while(0)                                \


/**
 * decl a signal (use in .h file)
 */
#define SIGNAL_DECL(name, ...)             \
    void name(__VA_ARGS__);                \
    typedef void (*name##_t)(__VA_ARGS__); \
    extern struct Node name##_n;           \


/**
 * decl a slot (use in .h file)
 */
#define SLOT_DECL(name, ...)               \
    void name(__VA_ARGS__);                \
    typedef void (*name##_t)(__VA_ARGS__); \
    extern struct Node name##_n;           \


/**
 * define a signal (use in .c file)
 */
#define SIGNAL_DEF(name, ...)           \
    void name(__VA_ARGS__){}; \
    struct Node name##_n =              \
    {                                   \
        .next   = NULL,                 \
        .slot   = NULL,                 \
        .signal = NULL,                 \
    };                                  \


/**
 * define a slot (use in .c file)
 */
#define SLOT_DEF(name, ...)          \
    struct Node name##_n =           \
    {                                \
        .next   = NULL,              \
        .slot   = NULL,              \
        .signal = NULL,              \
    };                               \
    void name(__VA_ARGS__)           \


#endif // _META_OBJECT_H

实现案例

信号
signal_test.h

#ifndef __SIGNAL_TEST_H__
#define __SIGNAL_TEST_H__

#include "MetaObject.h"

SIGNAL_DECL(test_signal,uint8_t *a,int l);
SIGNAL_DECL(test_signal2,int l);

#endif
signal_test.c

#include "signal_test.h"

SIGNAL_DEF(test_signal,uint8_t *a,int l);
SIGNAL_DEF(test_signal2,int l);
槽函数
slot_test.h

#ifndef __SLOT_TEST_H__
#define __SLOT_TEST_H__

#include "MetaObject.h"

SLOT_DECL(test_slot,uint8_t *a,int l);
SLOT_DECL(test_slot3,uint8_t *a,int l);

SLOT_DECL(test_slot4,int l);

#endif
slot_test.c

#include "slot_test.h"

SLOT_DEF(test_slot,uint8_t *a,int l)
{
    printf("slot ");
    for(int i=0;i<l;i++)
        printf("%d ",a[i]);
    printf("\n");
}

SLOT_DEF(test_slot3,uint8_t *a,int l)
{
    printf("slot3 ");
    for(int i=0;i<l;i++)
        printf("%d ",a[i]);
    printf("\n");
}

SLOT_DEF(test_slot4,int l)
{
    printf("slot4 %d\n",l);
}
MAIN
#include "MetaObject.h"
#include "signal_test.h"
#include "slot_test.h"

int main(void)
{

    uint8_t a[4] = {0x01,0x02,0x03,0x04};

    CONNECT(test_signal,test_slot);
    CONNECT(test_signal,test_slot3);
    CONNECT(test_signal2,test_slot4);
    CONNECT(test_signal,test_signal2);

    EMIT(test_signal,a,4);
    EMIT(test_signal2,20);

    printf("hello world!!");

    return 0;
}

实现结果
slot 1 2 3 4
slot3 1 2 3 4
slot4 20
hello world!!

常见问题解答

自定义结构体的传递

当涉及到信号与槽机制中较为复杂的类型数据传递时,例如自定义的结构体或者类,确实需要采取一些额外的步骤来保证类型安全且有效的通信。以下是一套推荐的步骤,帮助您顺利地在信号与槽间传输自定义的数据类型:

1. 创建自定义类型头文件:
首先,您需要在您的项目目录下创建一个新的头文件,比如命名为 CustomType.h,在这里定义您的自定义结构体。

2. 引入自定义类型的定义:
接下来,您应该在 MetaObject.h中或者其他相关的信号槽机制实现的头文件里,通过 #include "CustomType.h"引入您的自定义类型的定义。这样做是为了确保信号与槽机制能够识别并适当地处理这些类型。

3. 在信号和槽函数中声明自定义类型参数:
当您在 Signal_DECLSlot_DECL宏中声明信号和槽函数时,确保参数列表中包含您的自定义类型的实例。这将允许信号携带并传递给相应槽函数所需的复杂数据。

通过以上步骤,您就可以在信号与槽机制中自如地使用自定义的数据类型,无需担心类型不兼容的问题。

编译失败排查

如果遇到编译失败的情况,首先应确认是否遵循了C99标准。C99是C语言的标准之一,提供了许多新特性,包括复合字面量、变长数组、枚举类型等,这些都是编写现代C程序时常用的语言特性。如果您在编译过程中遇到与语法相关的错误,尤其是关于新的C语言特性的,那么检查编译器是否支持C99是很重要的一步。您可以尝试添加 -std=c99参数到GCC编译命令中,以确保您的代码按照C99标准被编译。

链表的修改建议

在信号与槽机制中,链表作为数据结构扮演着至关重要的角色。虽然当前实现采用了单向链表的形式,但这并不意味着不可更改。实际上,根据个人偏好或性能需求,将链表改造成双向链表是完全可行的,尤其是在需要频繁的插入和删除操作的情况下,双向链表往往提供更好的性能表现。

然而,考虑到大多数应用场景下的信号槽交互主要集中在添加与触发,而非大量中间节点的删除,单向链表通常足以应对。因此,除非有具体的性能瓶颈指出需要改进链表结构,否则保持现有形式不失为一种平衡简洁与效率的好选择。

内存管理注意事项

最后,关于内存管理的一点提醒:尽可能使用静态内存分配,而不是动态分配(如使用 malloc())。这是因为动态内存分配不仅增加了运行时的开销,而且还可能导致难以追踪的内存泄漏或越界等问题,尤其是在信号槽机制这种频繁调用和回调的环境中。使用静态内存可以规避这类风险,同时也利于代码的稳定性和性能优化。当然,如果确有必要使用动态内存,务必配合良好的内存管理和清理策略,防止潜在的内存泄露。

总结

在本次深入探索的过程中,我们共同见证了一项核心任务——在纯C环境下搭建一套完整的信号与槽机制。这项工作不仅是对传统信号处理范式的突破,更是对软件设计模式创新的一次有力证明。下面是对整个实现过程的综合概述,旨在巩固学习成果,提炼关键知识点,以及启发未来的进一步研究方向。

关键要素回顾

  • 信号与槽的基本概念:信号作为事件发生的标志,而槽则是响应事件的具体动作载体。它们通过宏定义紧密连接,构成了事件驱动架构的基础单元。

  • 宏定义的妙用:通过一系列精心设计的宏,如 SIGNAL_DEF, SLOT_DEF, CONNECT, DISCONNECT, 及 EMIT,将原本繁琐的信号与槽的定义、连接、断开及触发操作,转化为几行简洁高效的代码,极大提高了编程效率。

  • 自定义数据类型的支持:为了满足复杂应用的需求,我们探讨了如何通过适当的设计让信号与槽机制支持自定义结构体的传递,拓展了机制的功能边界。

  • 性能与安全性考量:通过对链表结构的选择与优化建议,以及内存管理的最佳实践,我们确保了机制的高性能和高可靠性,避免了常见的陷阱如内存泄露和溢出。

  • 面向未来的发展思考:随着技术的进步和应用场景的变化,信号与槽机制也将面临新的挑战与机遇。例如,多线程环境下的同步问题、大规模分布式系统中的消息传递延迟等,都需要我们在现有基础上持续探索和创新。

结语

通过本文,不仅掌握了在C语言中实现信号与槽机制的技术细节,更重要的是,学会了如何站在更高的视角审视软件设计的本质,以及如何运用设计模式的力量来解决实际问题。信号与槽机制不仅仅是一套简单的事件处理框架,它体现了模块化、解耦和可扩展性等软件工程的核心原则,为我们提供了构建健壮、灵活、易维护的软件系统的强大工具箱。

后续应用

后续文章会基于STM32F1介绍如何使用本文实现的信号和槽机制来实现串口,蓝牙,按键,灯等模块的解耦独立化调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值