libevent源码剖析-基本数据结构
1 简介
前面系列文章对libevent源码的主体结构,从reactor框架实现,到evbuffer和bufferevent实现原理,及libevent的例子进行了剖析,自此,我们便可基于libevent开发app了。
从本文开始,主要来介绍下libevent源码的枝叶部分,包括基本数据结构、C开发技巧、工具函数等。
2 基本数据结构
2.1 单链表
单链表相关操作都在libevent源码根目录compat/sys/queue.h文件下。这里简要介绍下单链表原理及其操作。
单链表是一种链式存储的数据结构,每个节点包含数据域和指向下一个节点的指针。下面是单链表的主要优缺点:
优点
动态内存分配:单链表的节点可以在需要时动态分配和释放,不需要提前分配固定大小的内存,适合需要频繁插入和删除的情况。
插入和删除操作效率高:在已知位置进行插入和删除操作只需更改指针即可,时间复杂度为 O(1)。相比数组无需移动其他元素,因此适合频繁插入、删除操作的场景。
节省内存:单链表只需存储数据和一个指向下个节点的指针,比双向链表节省内存开销,适用于内存敏感的应用。
便于扩展:由于链表不需要连续的内存空间,所以扩展链表时只需增加节点,不会涉及整体内存的重新分配和复制操作。
缺点
随机访问性能差:单链表不支持随机访问,要访问某个特定位置的节点,必须从头开始逐个遍历,时间复杂度为 O(n),在访问频繁的场景性能较低。
额外的存储开销:每个节点需要存储一个指针用于链接下一个节点,若数据较小或节点较多,指针的额外存储开销会显得较大。
不便于反向遍历:单链表只能从头到尾顺序遍历,无法反向遍;如果需要反向遍历的功能,需要转换成双向链表或借助栈等辅助数据结构。
链表操作的指针复杂性:在操作链表(如插入、删除)时,指针的使用容易导致错误,例如指针丢失、空指针访问等,增加了开发和调试的难度。
单链表适合数据量动态变化、需要频繁插入和删除、但不要求随机访问的场景。
单链表结构图
2.1.1 定义
C单链表结构定义如下:
/** Singly-linked List definitions.*/
#define SLIST_HEAD(name, type) \
struct name { \struct type *slh_first; /* first element */ \
}#define SLIST_HEAD_INITIALIZER(head) \{ NULL }#ifndef _WIN32
#define SLIST_ENTRY(type) \
struct { \struct type *sle_next; /* next element */ \
}
#endif
2.1.2 访问方法
libevent通过宏定义来访问单链表的数据成员及遍历:
/** Singly-linked List access methods.*/
#define SLIST_FIRST(head) ((head)->slh_first)
#define SLIST_END(head) NULL
#define SLIST_EMPTY(head) (SLIST_FIRST(head) == SLIST_END(head))
#define SLIST_NEXT(elm, field) ((elm)->field.sle_next)#define SLIST_FOREACH(var, head, field) \for((var) = SLIST_FIRST(head); \(var) != SLIST_END(head); \(var) = SLIST_NEXT(var, field))
2.1.3 操作函数
libevent实现的单链表操作主要有:初始化、中间插入、头部插入、尾部插入、头部移除:
/** Singly-linked List functions.*/
#define SLIST_INIT(head) { \SLIST_FIRST(head) = SLIST_END(head); \
}#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \(elm)->field.sle_next = (slistelm)->field.sle_next; \(slistelm)->field.sle_next = (elm); \
} while (0)#define SLIST_INSERT_HEAD(head, elm, field) do { \(elm)->field.sle_next = (head)->slh_first; \(head)->slh_first = (elm); \
} while (0)#define SLIST_REMOVE_HEAD(head, field) do { \(head)->slh_first = (head)->slh_first->field.sle_next; \
} while (0)
以上是SLIST单链表所有源码实现,相对简单,不赘述。
2.2 双向链表
在libevent中,LIS链表是一种双向链表结构,其每个节点包含一个指向前后节点的指针。和单向链表不同,双向链表一般是用来表示事件的多重组织方式,即不同事件类型和优先级的事件链。下面是双向链表的优缺点:
优点
层级化的事件管理:双向链表可以将事件按优先级、事件类型等不同维度进行分层组织,方便事件循环时按需分配处理,尤其在多优先级事件的场景中非常有效。
便于按优先级调度:通过将高优先级事件和低优先级事件划分到不同链表层级中,处理事件时可以优先处理高优先级链表上的事件,便于实现调度控制,保证关键事件的及时性。
插入、删除操作简便:双向链表特性使得在任意位置插入或删除节点非常高效,只需调整前后指针,时间复杂度为 O(1),适合频繁操作。
事件遍历灵活:可以在多层链表中进行迭代,适合多种情况下的事件遍历、优先级切换等。对于复杂事件处理逻辑,可以快速遍历特定层级事件,提升事件处理效率。
缺点
内存消耗较大:每个节点需要存储两个指针(指向前后节点),如果链表层级很多且节点较多,内存开销会明显增大。
操作复杂性增加:双向链表的多层级设计,尤其是在事件分层组织、删除和调整优先级时,对开发者理解和调试的要求较高,可能导致复杂的指针操作错误。
访问深层节点效率降低:如果事件在较低优先级的层级,处理时需要逐层遍历链表才能访问到这些事件,效率相对较低,适合优先级较为集中的场景。
对随机访问支持差:链表结构本身不支持随机访问,查找特定事件或位置的节点时需要顺序遍历,在大规模节点的情况下效率较低。
双向链表结构图
2.2.1 定义
/** List definitions.*/
#define LIST_HEAD(name, type) \
struct name { \struct type *lh_first; /* first element */ \
}#define LIST_HEAD_INITIALIZER(head) \{ NULL }#define LIST_ENTRY(type) \
struct { \struct type *le_next; /* next element */ \struct type **le_prev; /* address of previous next element */ \
}
2.2.2 访问方法
/** List access methods*/
#define LIST_FIRST(head) ((head)->lh_first)
#define LIST_END(head) NULL
#define LIST_EMPTY(head) (LIST_FIRST(head) == LIST_END(head))
#define LIST_NEXT(elm, field) ((elm)->field.le_next)#define LIST_FOREACH(var, head, field) \for((var) = LIST_FIRST(head); \(var)!= LIST_END(head); \(var) = LIST_NEXT(var, field))
2.2.3 操作函数
/** List functions.*/
#define LIST_INIT(head) do { \LIST_FIRST(head) = LIST_END(head); \
} while (0)#define LIST_INSERT_AFTER(listelm, elm, field) do { \if (((elm)->field.le_next = (listelm)->field.le_next) != NULL) \(listelm)->field.le_next->field.le_prev = \&(elm)->field.le_next; \(listelm)->field.le_next = (elm); \(elm)->field.le_prev = &(listelm)->field.le_next; \
} while (0)#define LIST_INSERT_BEFORE(listelm, elm, field) do { \(elm)->field.le_prev = (listelm)->field.le_prev; \(elm)->field.le_next = (listelm); \*(listelm)->field.le_prev = (elm); \(listelm)->field.le_prev = &(elm)->field.le_next; \
} while (0)#define LIST_INSERT_HEAD(head, elm, field) do { \if (((elm)->field.le_next = (head)->lh_first) != NULL) \(head)->lh_first->field.le_prev = &(elm)->field.le_next;\(head)->lh_first = (elm); \(elm)->field.le_prev = &(head)->lh_first; \
} while (0)#define LIST_REMOVE(elm, field) do { \if ((elm)->field.le_next != NULL) \(elm)->field.le_next->field.le_prev = \(elm)->field.le_prev; \*(elm)->field.le_prev = (elm)->field.le_next; \
} while (0)#define LIST_REPLACE(elm, elm2, field) do { \if (((elm2)->field.le_next = (elm)->field.le_next) != NULL) \(elm2)->field.le_next->field.le_prev = \&(elm2)->field.le_next; \(elm2)->field.le_prev = (elm)->field.le_prev; \*(elm2)->field.le_prev = (elm2); \
} while (0)
2.3 简单队列
libevent中的简单队列是一个由单链表来实现的。simple queue 是一种简单的队列数据结构,它通常用于按顺序存储和访问元素。队列是一种先进先出(FIFO, First In First Out)结构,元素总是从队尾插入,从队首删除。下面是 simple queue 的特点、基本操作和优缺点。
特点
- FIFO(先进先出):最先进入队列的元素会最先被处理。
- 单向操作:通常只允许在队尾插入元素、在队首删除元素,保持队列有序性。
- 动态增长:可以在需要时动态添加元素,内存使用通常灵活。
优点
- 操作简单:只需操作队尾和队首,插入和删除的复杂度为 O(1)。
- 内存效率:链表实现的队列可以根据需要动态调整大小,适合存储不确定数量的数据。
- 无锁并发:在一些并发场景中,可以通过特定设计使队列支持无锁操作,提高并发访问的效率。
缺点
- 随机访问效率低:只能按顺序访问元素,无法高效地进行随机访问。
- 可能有内存开销:链表实现的队列会在每个节点上存储额外的指针信息,在大量节点时会增加内存使用。
- 阻塞问题:在生产者-消费者场景中,如果生产或消费速度不匹配,可能会导致队列过长或过短,需额外处理队列满或空的情况。
简单队列结构图
2.3.1定义
/** Simple queue definitions.*/
#define SIMPLEQ_HEAD(name, type) \
struct name { \struct type *sqh_first; /* first element */ \struct type **sqh_last; /* addr of last next element */ \
}#define SIMPLEQ_HEAD_INITIALIZER(head) \{ NULL, &(head).sqh_first }#define SIMPLEQ_ENTRY(type) \
struct { \struct type *sqe_next; /* next element */ \
}
2.3.2 访问方法
/** Simple queue access methods.*/
#define SIMPLEQ_FIRST(head) ((head)->sqh_first)
#define SIMPLEQ_END(head) NULL
#define SIMPLEQ_EMPTY(head) (SIMPLEQ_FIRST(head) == SIMPLEQ_END(head))
#define SIMPLEQ_NEXT(elm, field) ((elm)->field.sqe_next)#define SIMPLEQ_FOREACH(var, head, field) \for((var) = SIMPLEQ_FIRST(head); \(var) != SIMPLEQ_END(head); \(var) = SIMPLEQ_NEXT(var, field))
2.3.3 操作函数
/** Simple queue functions.*/
#define SIMPLEQ_INIT(head) do { \(head)->sqh_first = NULL; \(head)->sqh_last = &(head)->sqh_first; \
} while (0)#define SIMPLEQ_INSERT_HEAD(head, elm, field) do { \if (((elm)->field.sqe_next = (head)->sqh_first) == NULL) \(head)->sqh_last = &(elm)->field.sqe_next; \(head)->sqh_first = (elm); \
} while (0)#define SIMPLEQ_INSERT_TAIL(head, elm, field) do { \(elm)->field.sqe_next = NULL; \*(head)->sqh_last = (elm); \(head)->sqh_last = &(elm)->field.sqe_next; \
} while (0)#define SIMPLEQ_INSERT_AFTER(head, listelm, elm, field) do { \if (((elm)->field.sqe_next = (listelm)->field.sqe_next) == NULL)\(head)->sqh_last = &(elm)->field.sqe_next; \(listelm)->field.sqe_next = (elm); \
} while (0)#define SIMPLEQ_REMOVE_HEAD(head, elm, field) do { \if (((head)->sqh_first = (elm)->field.sqe_next) == NULL) \(head)->sqh_last = &(head)->sqh_first; \
} while (0)
2.4 尾队列
TAILQ 是一种双向链表队列,广泛用于 BSD 系统和 libevent 库中。它支持从队列头部或尾部高效地插入、删除和访问元素,便于实现先进先出(FIFO)队列、双端队列等多种数据结构。TAILQ 的设计特点主要在于它使用双向链表维护队列顺序,尾部始终指向最后一个元素,因此插入和删除操作效率较高。
相关操作在include/event2/event_struct.h文件下,如果对应平台有此<sys/queue.h>文件相应实现,则可直接用。
优点
- 双端操作高效:TAILQ 支持头部和尾部的高效插入和删除操作,时间复杂度均为 O(1)。
- 灵活性高:支持在任意位置插入和删除,适合实现各种类型的队列、链表、双端队列等。
- 结构简洁:TAILQ 数据结构相对简单,内存开销较低,适合内核、系统编程等低资源场景。
缺点
- 访问性能差:TAILQ 不支持随机访问,访问特定位置需要顺序遍历,效率较低。
- 指针操作复杂:TAILQ 的双向指针结构在操作中容易出现指针悬挂、泄漏等错误,增加开发难度。
尾队列结构图
2.4.1 定义
/** Tail queue definitions.*/
#define TAILQ_HEAD(name, type) \
struct name { \struct type *tqh_first; /* first element */ \struct type **tqh_last; /* addr of last next element */ \
}#define TAILQ_HEAD_INITIALIZER(head) \{ NULL, &(head).tqh_first }#define TAILQ_ENTRY(type) \
struct { \struct type *tqe_next; /* next element */ \struct type **tqe_prev; /* address of previous next element */ \
}
2.4.2 方法访问
/** tail queue access methods*/
#define TAILQ_FIRST(head) ((head)->tqh_first)
#define TAILQ_END(head) NULL
#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)
#define TAILQ_LAST(head, headname) \(*(((struct headname *)((head)->tqh_last))->tqh_last))
/* XXX */
#define TAILQ_PREV(elm, headname, field) \(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
#define TAILQ_EMPTY(head) \(TAILQ_FIRST(head) == TAILQ_END(head))#define TAILQ_FOREACH(var, head, field) \for((var) = TAILQ_FIRST(head); \(var) != TAILQ_END(head); \(var) = TAILQ_NEXT(var, field))#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \for((var) = TAILQ_LAST(head, headname); \(var) != TAILQ_END(head); \(var) = TAILQ_PREV(var, headname, field))
2.4.3 操作函数
/** Tail queue functions.*/
#define TAILQ_INIT(head) do { \(head)->tqh_first = NULL; \(head)->tqh_last = &(head)->tqh_first; \
} while (0)#define TAILQ_INSERT_HEAD(head, elm, field) do { \if (((elm)->field.tqe_next = (head)->tqh_first) != NULL) \(head)->tqh_first->field.tqe_prev = \&(elm)->field.tqe_next; \else \(head)->tqh_last = &(elm)->field.tqe_next; \(head)->tqh_first = (elm); \(elm)->field.tqe_prev = &(head)->tqh_first; \
} while (0)#define TAILQ_INSERT_TAIL(head, elm, field) do { \(elm)->field.tqe_next = NULL; \(elm)->field.tqe_prev = (head)->tqh_last; \*(head)->tqh_last = (elm); \(head)->tqh_last = &(elm)->field.tqe_next; \
} while (0)#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\(elm)->field.tqe_next->field.tqe_prev = \&(elm)->field.tqe_next; \else \(head)->tqh_last = &(elm)->field.tqe_next; \(listelm)->field.tqe_next = (elm); \(elm)->field.tqe_prev = &(listelm)->field.tqe_next; \
} while (0)#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \(elm)->field.tqe_prev = (listelm)->field.tqe_prev; \(elm)->field.tqe_next = (listelm); \*(listelm)->field.tqe_prev = (elm); \(listelm)->field.tqe_prev = &(elm)->field.tqe_next; \
} while (0)#define TAILQ_REMOVE(head, elm, field) do { \if (((elm)->field.tqe_next) != NULL) \(elm)->field.tqe_next->field.tqe_prev = \(elm)->field.tqe_prev; \else \(head)->tqh_last = (elm)->field.tqe_prev; \*(elm)->field.tqe_prev = (elm)->field.tqe_next; \
} while (0)#define TAILQ_REPLACE(head, elm, elm2, field) do { \if (((elm2)->field.tqe_next = (elm)->field.tqe_next) != NULL) \(elm2)->field.tqe_next->field.tqe_prev = \&(elm2)->field.tqe_next; \else \(head)->tqh_last = &(elm2)->field.tqe_next; \(elm2)->field.tqe_prev = (elm)->field.tqe_prev; \*(elm2)->field.tqe_prev = (elm2); \
} while (0)
2.4 圆形队列
Circular Queue(循环队列)是一种队列数据结构,它将队列逻辑上视为“环形结构”,通过在固定大小的数组中实现循环,来避免普通线性队列出现的“假溢出”问题。循环队列在系统编程和实时系统中常用,如 CPU 调度、任务管理等。
优点
- 内存效率高:循环队列的最大优势在于通过循环重用数组空间,避免了普通线性队列“假溢出”的问题。
- 固定大小:在实时和嵌入式系统中,循环队列的固定数组大小有助于内存管理。
缺点
- 容量限制:固定数组大小的循环队列有容量上限,超出限制会导致队列溢出。
- 指针计算复杂:由于指针的循环移动,操作比单纯的线性队列复杂,可能会产生边界问题。
环形队列结构图
2.4.1 定义
/** Circular queue definitions.*/
#define CIRCLEQ_HEAD(name, type) \
struct name { \struct type *cqh_first; /* first element */ \struct type *cqh_last; /* last element */ \
}#define CIRCLEQ_HEAD_INITIALIZER(head) \{ CIRCLEQ_END(&head), CIRCLEQ_END(&head) }#define CIRCLEQ_ENTRY(type) \
struct { \struct type *cqe_next; /* next element */ \struct type *cqe_prev; /* previous element */ \
}
2.4.2 访问方法
/** Circular queue access methods*/
#define CIRCLEQ_FIRST(head) ((head)->cqh_first)
#define CIRCLEQ_LAST(head) ((head)->cqh_last)
#define CIRCLEQ_END(head) ((void *)(head))
#define CIRCLEQ_NEXT(elm, field) ((elm)->field.cqe_next)
#define CIRCLEQ_PREV(elm, field) ((elm)->field.cqe_prev)
#define CIRCLEQ_EMPTY(head) \(CIRCLEQ_FIRST(head) == CIRCLEQ_END(head))#define CIRCLEQ_FOREACH(var, head, field) \for((var) = CIRCLEQ_FIRST(head); \(var) != CIRCLEQ_END(head); \(var) = CIRCLEQ_NEXT(var, field))#define CIRCLEQ_FOREACH_REVERSE(var, head, field) \for((var) = CIRCLEQ_LAST(head); \(var) != CIRCLEQ_END(head); \(var) = CIRCLEQ_PREV(var, fie
2.4.3 操作函数
/** Circular queue functions.*/
#define CIRCLEQ_INIT(head) do { \(head)->cqh_first = CIRCLEQ_END(head); \(head)->cqh_last = CIRCLEQ_END(head); \
} while (0)#define CIRCLEQ_INSERT_AFTER(head, listelm, elm, field) do { \(elm)->field.cqe_next = (listelm)->field.cqe_next; \(elm)->field.cqe_prev = (listelm); \if ((listelm)->field.cqe_next == CIRCLEQ_END(head)) \(head)->cqh_last = (elm); \else \(listelm)->field.cqe_next->field.cqe_prev = (elm); \(listelm)->field.cqe_next = (elm); \
} while (0)#define CIRCLEQ_INSERT_BEFORE(head, listelm, elm, field) do { \(elm)->field.cqe_next = (listelm); \(elm)->field.cqe_prev = (listelm)->field.cqe_prev; \if ((listelm)->field.cqe_prev == CIRCLEQ_END(head)) \(head)->cqh_first = (elm); \else \(listelm)->field.cqe_prev->field.cqe_next = (elm); \(listelm)->field.cqe_prev = (elm); \
} while (0)#define CIRCLEQ_INSERT_HEAD(head, elm, field) do { \(elm)->field.cqe_next = (head)->cqh_first; \(elm)->field.cqe_prev = CIRCLEQ_END(head); \if ((head)->cqh_last == CIRCLEQ_END(head)) \(head)->cqh_last = (elm); \else \(head)->cqh_first->field.cqe_prev = (elm); \(head)->cqh_first = (elm); \
} while (0)#define CIRCLEQ_INSERT_TAIL(head, elm, field) do { \(elm)->field.cqe_next = CIRCLEQ_END(head); \(elm)->field.cqe_prev = (head)->cqh_last; \if ((head)->cqh_first == CIRCLEQ_END(head)) \(head)->cqh_first = (elm); \else \(head)->cqh_last->field.cqe_next = (elm); \(head)->cqh_last = (elm); \
} while (0)#define CIRCLEQ_REMOVE(head, elm, field) do { \if ((elm)->field.cqe_next == CIRCLEQ_END(head)) \(head)->cqh_last = (elm)->field.cqe_prev; \else \(elm)->field.cqe_next->field.cqe_prev = \(elm)->field.cqe_prev; \if ((elm)->field.cqe_prev == CIRCLEQ_END(head)) \(head)->cqh_first = (elm)->field.cqe_next; \else \(elm)->field.cqe_prev->field.cqe_next = \(elm)->field.cqe_next; \
} while (0)#define CIRCLEQ_REPLACE(head, elm, elm2, field) do { \if (((elm2)->field.cqe_next = (elm)->field.cqe_next) == \CIRCLEQ_END(head)) \(head).cqh_last = (elm2); \else \(elm2)->field.cqe_next->field.cqe_prev = (elm2); \if (((elm2)->field.cqe_prev = (elm)->field.cqe_prev) == \CIRCLEQ_END(head)) \(head).cqh_first = (elm2); \else \(elm2)->field.cqe_prev->field.cqe_next = (elm2); \
} while (0)
2.5 哈希表
优点
高效查询和插入:哈希表的设计使其查询和插入的平均时间复杂度接近 O(1),在管理大量事件(如定时器事件)时非常高效。
动态扩展:libevent 的哈希表可以根据负载因子动态扩展,降低碰撞率并保持性能。
空间利用率高:哈希表通过散列函数分布元素,避免了链表或树结构的空间浪费,适合事件数量较多的场景。
缺点
内存占用波动:在动态扩展过程中,哈希表需要重新分配内存,这可能导致内存使用不稳定。
碰撞处理开销:尽管有较低的碰撞概率,但在高负载情况下,链表长度可能增加,导致操作复杂度退化为 O(n)。
线程不安全:libevent 中的哈希表设计在多线程环境下不安全,使用时需要额外的锁机制来避免竞争。
libevent在对IO事件和signal事件进行组织管理的时候,采用的哈希表,以IO事件为例,fd为哈希表的桶,哈希槽为evmap_io的双向链表,详见下图:
IO事件哈希表
libevent在对signal事件的组织管理方式,与IO事件基本是一样的,以signal为桶,哈希槽为evmap_signal的双向链表,详见下图:
signal事件结构图
3 小结
- 本文主要对libevent的基本数据结构进行介绍,包括单链表、双向链表、简单队列、尾队列、圆形队列以及哈希表的实现原理;
- 除了timer事件使用了小跟堆以外,libevent对event的组织和管理基本都可以用以上基本数据结构来组织,小跟堆将另起一文来加以介绍。
相关文章:

libevent源码剖析-基本数据结构
1 简介 前面系列文章对libevent源码的主体结构,从reactor框架实现,到evbuffer和bufferevent实现原理,及libevent的例子进行了剖析,自此,我们便可基于libevent开发app了。 从本文开始,主要来介绍下libevent源…...

往期文章汇总——射频测量+无线通信+软件无线电+6G科普
本节目录 一、射频测量系列往期链接 二、无线通信系列往期链接 三、软件无线电系列往期链接 四、6G科普系列往期链接本节内容 一、射频测量系列往期链接 射频测量 | 滤波器的关注指标 射频测量 | 射频电路中的负载与滤波器 射频测量 | 射频衰减器的功率系数 射频测量 | 衰减…...
微信小程序 - 深 / 浅拷贝实现方法,微信小程序深拷贝与浅拷贝,函数方法封装直接调用使用,深拷贝cloneDeep和浅拷贝clone(深复制和浅复制)
前言 在微信小程序中,你无法 直接使用常规浏览器环境中的深浅拷贝方法。 但可以借助 utils.js 实现,下面是方法。 创建深浅拷贝函数 依次打开小程序目录【utils】→【utils.js】,写入深拷贝函数并暴露出去。 // utils.js// 对象深拷贝函数 const deepClone = function(in…...

Log4Net配置详解及输出自定义消息类示例代码
1.简单使用实例 1.1 添加log4net.dll的引用。 在NuGet程序包中搜索log4net并添加,此次我所用版本为2.0.17。如下图: 1.2 添加配置文件 右键项目,添加新建项,搜索选择应用程序配置文件,命名为log4net.config,…...

C++在实际项目中的应用第二节:C++与区块链
第五章:C在实际项目中的应用 第二课:C与区块链 区块链技术因其去中心化、不可篡改和透明性而受到广泛关注。在这门课程中,我们将深入探讨区块链的基本原理、智能合约的开发以及实际应用的案例分析,重点使用 C 作为实现语言&…...

浅记React面试丢人时刻
前提 去面试了,技术面完一轮之后,突发的来了一次React的考察,哥们,猝不及防之下,脑袋直接清空,啥也想不起来了。现在想想,实属丢人,记录一下啥也没答出来的面试,钉在耻辱…...

Python入门:学会Python装饰器让你的代码如虎添翼!(Python如何不改动原有函数代码添加一些额外的功能)
文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 什么是Python装饰器📝 如何编写Python装饰器📝 带参数的装饰器📝 Python装饰器的使用场景📝 注意事项📝 多装饰器的使用⚓️ 相关链接 ⚓️📖 介绍 📖 你是不是在写代码的时候,常常会想有没有…...

【C++】哈希冲突的解决办法:闭散列 与 开散列
哈希冲突解决 上一篇博客提到了,哈希函数的优化可以减小哈希冲突发生的可能性,但无法完全避免。本文就来探讨一下解决哈希冲突的两种常见方法:闭散列和开散列 1.闭散列 闭散列也叫开放定址法,发生哈希冲突时,如果哈…...

复刻系列-原神 5.1 版本先行展示页
复刻原神 5.1 版本先行展示页 0. 视频 BilBil站视频演示 复刻-原神5.1版本先行展示页 1. 基本信息 作者: 啊是特嗷桃系列: 复刻系列官方的网站: 《原神》官方网站-全新5.1版本「命定将焚的虹光」上线!复刻的网站: 《原神》复刻网站-全新5.1版本「命定将焚的虹光」…...

STM32 第3章 如何用串口下载程序
时间:2024.10.28 一、学习内容 1、安装USB转串口驱动 1.1串口下载连接示意图 1、USB转串口模块在开发板上是一个独立的模块,可通过调帽与其他串口连接,USART1/2/3/4/5 2、只有USART1才具有串口下载的功能。 3、CH340是电平转换芯片,将电脑端输出的USB电平和单片机输…...

HT71782 20V,15A全集成同步升压转换器
1、特征 输入电压范围VN:2.7V-20V 输出电压范围VouT:4.5V-20V 可编程峰值电流:15A 高转换效率: 93%(VIN7.4V,VoUT15.5V,IouT 1.5A) 轻载条件下两种调制方式:脉频调制(PFM)和 强制脉宽调试(FPWM) 支持两种tr/t模式,应对EMI挑战 低关断功耗,关断电流1uA 可…...

[含文档+PPT+源码等]精品基于PHP实现的培训机构信息管理系统的设计与实现
基于PHP实现的培训机构信息管理系统的设计与实现背景,可以从以下几个方面进行阐述: 一、社会发展与教育需求 随着经济的不断发展和人口数量的增加,教育培训行业迎来了前所未有的发展机遇。家长对子女教育的重视程度日益提高,课外…...

亚信安全DeepSecurity中标知名寿险机构云主机安全项目
近日,亚信安全DeepSecurity成功中标国内知名寿险机构的云主机安全项目。亚信安全凭借在云主机安全防护领域的突出技术优势,结合安全运营的能力,以“实战化”为指导,为用户提供无惧威胁攻击、无忧安全运营的一站式云安全体系&#…...

论文解析八: GAN:Generative Adversarial Nets(生成对抗网络)
目录 1.GAN:Generative Adversarial Nets(生成对抗网络)1、标题 作者2、摘要 Abstract3、导言 IntroductionGAN的介绍 4、相关工作 Related work5、模型 Adversarial nets总结 6.理论计算 Theoretical Results具体算法公式全局优化 Global O…...
【ARM】ARM架构参考手册_Part B 内存和系统架构(2)
目录 2.1 关于系统控制协处理器 2.2 寄存器 2.1 关于系统控制协处理器 所有标准内存和系统设施都由协处理器15(CP15)控制,因此它被称为系统控制协处理器。有些设施也使用其他控制方法,这些方法在描述这些设施的章节中有描述。例…...
HttpServer模块 --- 封装TcpServer支持Http协议
目录 模块设计思想 模块代码实现 模块设计思想 本模块就是设计一个HttpServer模块,提供便携的搭建http协议的服务器的方法。 那么这个模块需要如何设计呢? 这还需要从Http请求说起。 首先http请求是分为静态资源请求和功能性请求的。 静态资源请求…...

蓝牙资讯|iOS 18.1 正式版下周推送,AirPods Pro 2耳机将带来助听器功能
苹果公司宣布将在下周发布 iOS 18.1 正式版,同时确认该更新将为 AirPods Pro 2 耳机带来新增“临床级”助听器功能。在启用功能后,用户首先需要使用 AirPods 和 iPhone 进行简短的听力测试,如果检测到听力损失,系统将创建一项“个…...
C语言之环形缓冲区概述及实现
在C语言中存在一种高效的数据结构,叫做环形缓存区,其被广泛用于处理数据流与缓存区的管理。如:数据的收发、程序层级之间的数据交换、硬件接收大量数据的场景,同时也可配合DMA实现通信协议收发数据,已确保流量控制、数…...
C++Socket通讯样例(服务端)
1. 创建Socket实例并开启。 private int OpenTcp(int port, string ip "") {//1. 开启服务端try{_tcpServer new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);IPAddress ipAddr IPAddress.Any;if (ip ! "" && i…...

【学术会议论文投稿】大数据治理:解锁数据价值,引领未来创新
第六届国际科技创新学术交流大会(IAECST 2024)_艾思科蓝_学术一站式服务平台 更多学术会议请看:https://ais.cn/u/nuyAF3 目录 引言 一、大数据治理的定义 二、大数据治理的重要性 三、大数据治理的核心组件 四、大数据治理的实践案例…...

使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...

【力扣数据库知识手册笔记】索引
索引 索引的优缺点 优点1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。2. 可以加快数据的检索速度(创建索引的主要原因)。3. 可以加速表和表之间的连接,实现数据的参考完整性。4. 可以在查询过程中,…...

云启出海,智联未来|阿里云网络「企业出海」系列客户沙龙上海站圆满落地
借阿里云中企出海大会的东风,以**「云启出海,智联未来|打造安全可靠的出海云网络引擎」为主题的阿里云企业出海客户沙龙云网络&安全专场于5.28日下午在上海顺利举办,现场吸引了来自携程、小红书、米哈游、哔哩哔哩、波克城市、…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...

ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放
简介 前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...