本章导读
xset 是"只问在不在这"的容器:添加去重、成员判断 O(1)、并/交/差/对称差一步算出。它与 xmap 共享哈希底座与插入序迭代,但语义重心不同——映射关心"键对应什么",集合关心"键属不属于"。防火墙端口白名单、用户标签组、权限差集、去重统计,都是集合问题。本章讲透两类 API 形态的差别——栈句柄的 Unit 与运算结果的 Destroy——这是集合章节最值得先立的纪律。
19.1引入
一个网关要决定放行哪些端口:管理员启用了 {80, 443, 8080},应用请求 {3000, 443, 8080}——放行集合是两者的交集 {443, 8080},本章主示例就是这个场景的完整实现。用数组写:两层循环嵌套去重,代码十几行,复杂度平方级,还容易在"去重"上出 bug。用集合写:Add 装载、Intersection 一步、迭代打印——三段各两三行。更关键的是语义清晰:代码里出现的是"交集"这个词本身,评审者不需要在两层循环里逆向推导意图。
集合运算的返回值形态值得特别注意:Intersection / Union / Difference / SymmetricDifference 返回的是堆分配的新集合(xset* 指针)——运算结果独立于操作数存活,你可以继续对它做运算或迭代;代价是用完必须 xrtSetDestroy。而"装载用"的集合是栈上句柄,收尾是 xrtSetUnit。两类对象两种收尾,混用就是泄漏或悬空——本章坑 1 专门执法;示例的 cleanup 段就是两种收尾并排的标准写法,值得整段抄走复用。
19.2概念
先立一个总纲:集合的全部概念围绕三个问题展开——元素怎么进(Add 与值语义)、怎么判属(Has 与字节相等)、怎么组合(代数运算与两类收尾)。本章概念区按这三个问题展开,示例区各给一个完整答案,避坑区执法两条最常翻车的纪律。读完你回头看会发现:集合章节一半的知识其实来自第 18 章——这正是容器家族共享约定的复利。
与映射的同构
集合底层与 xmap 同源:哈希定位、插入序链、三段式迭代(Begin/Next/End)全部一致。差异只有两点:没有值(元素就是键);多了集合代数运算族。学会第 18 章再来集合,只需要增量学习"代数"这一块——两章的坑与契约大量共享(迭代期指针时效、迭代中禁增删)。
集合代数
| 运算 | 结果 | 典型问题 |
|---|---|---|
Intersection | 两者都有的元素 | 请求 ∩ 启用 = 实际放行 |
Union | 两者之并 | 两组标签合并 |
Difference | 左有右无 | 已配置 − 已完成 = 待办 |
SymmetricDifference | 恰好一方有 | 两份配置的差异对比 |
四者的共同契约:返回新分配的堆集合,迭代顺序沿用左操作数的插入序。结果可以链式参与下一次运算——"先并再交"的多步策略一行一个运算自然书写。
集合运算的数据流
一次策略计算通常是多步代数的串联,把数据流向画出来比读代码直观:
注意每一步的产物都是独立的堆集合:中间结果用完即 Destroy,最终结果迭代后再 Destroy。链式运算的清理代码建议与运算写在一起(求值即清理),避免"先全部算完再统一清理"的长列表——后者在错误路径上更容易漏。
值语义与比较
元素是值:Add 拷贝元素字节进槽,sizeof(元素类型) 在 Init 时声明。元素相等的判定就是字节相等——结构体里有填充字节时先清零再入集合(否则同值不同填充被判不等)。存指针可以,但集合比较的是指针值不是指向的对象——"对象身份集合"与"对象值集合"是两回事,需要后者时以业务键(如 ID)为元素。
迭代与查询
Has 判断成员、Remove 删除元素、Count 是公开字段——容器三约定(第 14 章)原样生效。迭代按插入序输出、可精确复现;set_tour 范例还演示了逆序迭代与访问器风格的遍历(回调版),两者与正序三段式同源。
19.3示例
完整程序:端口白名单交集
来自仓库范例 examples/containers/set/main.c:
*/ #include <stdio.h> #include <xrt.h> int main(void) { xset tEnabled; /* 栈句柄集合 A:已启用端口 */ xset tRequested; /* 栈句柄集合 B:请求端口 */ xset* pAllowed = NULL; /* 堆分配结果:A ∩ B */ xsetiter tIterator; const int* pPort; int arrEnabled[] = { 80, 443, 8080 }; int arrRequested[] = { 443, 3000, 8080 }; int iResult = 0; if ( !xrtSetInit(&tEnabled, sizeof(int)) ) { return 1; } if ( !xrtSetInit(&tRequested, sizeof(int)) ) { xrtSetUnit(&tEnabled); return 1; } /* 装载两个集合;Add 天然去重(重复元素不增加数量)。 */ for ( size_t i = 0; i < 3; i++ ) { if ( !xrtSetAdd(&tEnabled, &arrEnabled[i]) || !xrtSetAdd(&tRequested, &arrRequested[i]) ) { iResult = 2; goto cleanup; } } /* 交集:请求 ∩ 启用 = {443, 8080};结果是新分配的堆集合。 */ pAllowed = xrtSetIntersection(&tRequested, &tEnabled); if ( pAllowed == NULL ) { iResult = 3; goto cleanup; } /* 按插入顺序(即请求集合中的出现顺序)打印放行端口。 */ if ( !xrtSetIterBegin(pAllowed, &tIterator) ) { iResult = 4; goto cleanup; } while ( (pPort = (const int*)xrtSetIterNext(&tIterator)) != NULL ) { printf("allowed port: %d\n", *pPort); } xrtSetIterEnd(&tIterator); cleanup: /* 两类句柄两类收尾:堆结果 Destroy,栈句柄 Unit。 */ if ( pAllowed != NULL ) { xrtSetDestroy(pAllowed); } xrtSetUnit(&tRequested); xrtSetUnit(&tEnabled); return iResult; }
刚才发生了什么。 ① 两个栈句柄集合各自 Init(sizeof(int)),Add 循环装载——3000 在请求集合里但不在启用集合里,命运在交集时就已注定。② Intersection(&tRequested, &tEnabled) 返回堆集合 pAllowed——注意它是指针,与栈句柄 tRequested 是两种对象。③ 迭代输出 443、8080,顺序沿左操作数(请求集合)的插入序——结果顺序可复现,能进测试断言。④ 收尾是本章的纪律现场:pAllowed 是运算结果,Destroy;tRequested/tEnabled 是栈句柄,Unit——两类收尾一个都不能错位,失败路径 goto cleanup 里两种都齐。
完整程序:集合全接口巡礼
来自 examples/containers/set_tour/main.c,覆盖生命周期、查询、代数与两种遍历:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> static bool exampleVisitCount(const void* pItem, ptr pUserData) { size_t* pSeen = (size_t*)pUserData; (void)pItem; *pSeen = *pSeen + 1u; return *pSeen < 2u; /* 第二个元素停止 */ } int main(void) { xset* pA = NULL; xset* pB = NULL; xset* pMerged = NULL; xset* pUnion = NULL; xset* pDiff = NULL; xset* pSym = NULL; xset tAligned; xsetiter Iter; const void* pSlot = NULL; bool bNew = false; size_t iSeen = 0; int i; int iResult = 1; int Values[5]; for ( i = 0; i < 5; ++i ) { Values[i] = i + 1; } /* ---- 生命周期:堆/内嵌对齐 + 容量族 ---- */ pA = xrtSetCreate(sizeof(int)); if ( (pA == NULL) || !xrtSetAdd(pA, &Values[0]) || !xrtSetAdd(pA, &Values[1]) || (xrtSetCount(pA) != 2u) || (xrtSetCapacity(pA) < 2u) || !xrtSetReserve(pA, 16u) || !xrtSetTrim(pA) || (xrtSetCount(pA) != 2u) ) { goto Cleanup; } { xset* pAligned = xrtSetCreateAligned(sizeof(int), sizeof(int)); if ( (pAligned == NULL) || !xrtSetAdd(pAligned, &Values[0]) ) { goto Cleanup; } xrtSetDestroy(pAligned); } if ( !xrtSetInitAligned(&tAligned, sizeof(int), sizeof(int)) || !xrtSetAdd(&tAligned, &Values[2]) || (xrtSetCount(&tAligned) != 1u) ) { goto Cleanup; } xrtSetClear(&tAligned); if ( (xrtSetCount(&tAligned) != 0u) || !xrtSetHas(pA, &Values[0]) ) { goto Cleanup; /* Clear 只影响内嵌集合 */ } xrtSetUnit(&tAligned); printf("set: lifecycle count=2 cap>=2 trim=1 clear=0\n"); /* ---- 查询族:GetOrAdd 双路径 / Has / Remove / Visit ---- */ pSlot = xrtSetGetOrAdd(pA, &Values[0], &bNew); if ( (pSlot == NULL) || bNew || (*(const int*)pSlot != Values[0]) ) { goto Cleanup; /* 已存在:不新增 */ } pSlot = xrtSetGetOrAdd(pA, &Values[2], &bNew); if ( (pSlot == NULL) || !bNew || (xrtSetCount(pA) != 3u) ) { goto Cleanup; /* 不存在:插入并报告新 */ } if ( !xrtSetHas(pA, &Values[1]) || xrtSetHas(pA, &Values[3]) || !xrtSetRemove(pA, &Values[2]) || (xrtSetCount(pA) != 2u) || xrtSetRemove(pA, &Values[2]) ) { goto Cleanup; } iSeen = 0; if ( (xrtSetVisit(pA, exampleVisitCount, &iSeen) != 2u) || (iSeen != 2u) ) { goto Cleanup; } printf("set: queries get-or-add new=0/1 has=1/0 remove=1\n"); /* ---- 代数运算:A={1,2} B={3,4} ---- */ pB = xrtSetCreate(sizeof(int)); if ( (pB == NULL) || !xrtSetAdd(pB, &Values[2]) || !xrtSetAdd(pB, &Values[3]) || !xrtSetIsDisjoint(pA, pB) ) { goto Cleanup; } /* Merge:B 并入 A → {1,2,3,4}。 */ pMerged = xrtSetCreate(sizeof(int)); if ( (pMerged == NULL) || !xrtSetAdd(pMerged, &Values[0]) || !xrtSetMerge(pMerged, pB) || (xrtSetCount(pMerged) != 3u) ) { goto Cleanup; } /* Union:{1,2}∪{3,4} = {1,2,3,4}。 */ pUnion = xrtSetUnion(pA, pB); if ( (pUnion == NULL) || (xrtSetCount(pUnion) != 4u) || !xrtSetHas(pUnion, &Values[3]) ) { goto Cleanup; } /* Difference:{1,2,3,4}-{2,3} 场景:用 Union-{3,4} → {1,2}。 */ pDiff = xrtSetDifference(pUnion, pB); if ( (pDiff == NULL) || (xrtSetCount(pDiff) != 2u) || xrtSetHas(pDiff, &Values[2]) ) { goto Cleanup; } /* SymmetricDifference:{1,2,3}△{3,4} = {1,2,4}。 */ { xset* pThree = xrtSetCreate(sizeof(int)); if ( (pThree == NULL) || !xrtSetAdd(pThree, &Values[0]) || !xrtSetAdd(pThree, &Values[1]) || !xrtSetAdd(pThree, &Values[2]) ) { goto Cleanup; } pSym = xrtSetSymmetricDifference(pThree, pB); if ( (pSym == NULL) || (xrtSetCount(pSym) != 3u) || xrtSetHas(pSym, &Values[2]) || !xrtSetHas(pSym, &Values[3]) ) { goto Cleanup; } * {1,2,3} 不是 {1,2} 的子集;超集方向相反。 */ if ( xrtSetIsSubset(pThree, pA, false) || !xrtSetIsSubset(pA, pThree, false) || !xrtSetIsSubset(pA, pThree, true) || !xrtSetIsSuperset(pThree, pA, false) || !xrtSetIsSuperset(pThree, pA, true) || xrtSetIsSuperset(pA, pThree, false) || xrtSetIsDisjoint(pA, pThree) ) { goto Cleanup; } /* Equal:A 与其克隆相等。 */ { xset* pClone = xrtSetClone(pA); if ( (pClone == NULL) || !xrtSetEqual(pA, pClone) ) { goto Cleanup; } xrtSetDestroy(pClone); } xrtSetDestroy(pThree); } printf("set: algebra union=4 diff=2 sym=2 subset/super/disjoint" "\n"); /* ---- 逆序迭代:补回 3 后集合为 {1,2,3} ---- */ { int iExpect = 3; if ( !xrtSetAdd(pA, &Values[2]) || (xrtSetCount(pA) != 3u) ) { goto Cleanup; } if ( !xrtSetIterRBegin(pA, &Iter) ) { goto Cleanup; } while ( (pSlot = xrtSetIterNext(&Iter)) != NULL ) { /* 哈希集合无序约束:只统计个数与内容集合。 */ int iValue = *(const int*)pSlot; if ( (iValue < 1) || (iValue > 3) ) { goto Cleanup; } --iExpect; } xrtSetIterEnd(&Iter); if ( iExpect != 0 ) { goto Cleanup; } } printf("set: rbegin order=3..1 visit=stopped\n"); iResult = 0; Cleanup: xrtSetDestroy(pUnion); xrtSetDestroy(pDiff); xrtSetDestroy(pSym); xrtSetDestroy(pMerged); xrtSetDestroy(pB); xrtSetDestroy(pA); return iResult; }
刚才发生了什么。 ① 生命周期行:Count 公开字段、容量 Reserve/Trim(第 14 章容量三件套在集合上同名同义)、Clear 归零。② 查询行:集合也有 GetOrAdd 形态(确认存在性并区分新旧),Has 与 Remove 的成败配对验证。③ 代数行:并、差、对称差的结果计数逐一断言,还有子集/超集/不相交的判定——关系判断不分配新集合,直接返回布尔。④ 遍历行:逆序迭代(3..1)与回调式访问(可中途停止,visit=stopped 证明回调返回 false 的中止语义)——三段式的两个变体,按场景取用。
从示例到工程:集合的三个落点
端口白名单是集合的"安全"落点,另外两个同样高频。标签与分组:一组用户挂多个标签、一个标签覆盖多个用户——"同时有 A 和 B 标签的用户"就是交集,"有 A 没 B 的"就是差集;标签系统本质是集合运算的组合拳。状态机合法迁移表:把"当前状态 × 输入 → 合法"的二元组做成集合,迁移前的合法性检查就是一次 Has——非法迁移在设计期就被表格化,而不是散落在 if 链里。三个落点共用本章的 API,差异只在元素类型与运算组合方式——集合是卷三里"业务语义最浓"的容器,选它往往不是为了性能,是为了让代码里出现"交、并、差"这些业务词汇本身。
19.4契约
- 两类收尾:栈句柄
Unit;运算结果与Create产物Destroy——一个集合用哪种收尾,取决于它怎么出生。 - 运算结果独立:交集等结果不依赖操作数存活,操作数销毁后结果依然有效。
- 结果顺序:代数结果沿左操作数插入序;所有迭代输出可复现。
- 值相等:字节相等即元素相等;含填充的结构体先清零;指针元素比的是指针值。
- 迭代纪律:与
xmap相同——三段式、迭代期指针不出迭代期、迭代中禁增删。 - 工程落点:白名单/标签分组/状态迁移表——集合代码里出现的是业务词汇本身。
读集合代码的一个习惯
集合运算让代码变短,但也让"中间产物"变隐形——一行三个运算连写时,堆上同时存在三个临时集合。养成读集合代码的习惯:看到 Intersection 就默念"一个堆集合",看到链式就数一数中间产物。与第 6 章的统计配合做一次体检:跑一段集合密集的逻辑,观察活跃字节曲线是否呈"锯齿"(分配-释放配对)而非"楼梯"(只升不降)——锯齿健康、楼梯泄漏,这个判读法比逐行审查快得多。
常见错误
症状:内存泄漏(第 6 章统计里泄漏量与运算次数成正比),或反过来把栈句柄 Destroy 了导致崩溃。
原因:Unit 只归还栈句柄关联的存储,不做堆对象释放;Intersection 等返回的是堆分配的独立集合,两者收尾函数不可互换。
xset* pAllowed = xrtSetIntersection(&tRequested, &tEnabled); /* ... 使用 pAllowed ... */ xrtSetUnit(pAllowed); /* Unit 不释放堆集合——泄漏 */
xset* pAllowed = xrtSetIntersection(&tRequested, &tEnabled); /* ... 使用 pAllowed ... */ xrtSetDestroy(pAllowed); /* 运算结果:Destroy */ xrtSetUnit(&tRequested); /* 栈句柄:Unit */ xrtSetUnit(&tEnabled);
症状:"明明相等的两个元素"有时判等有时不等;集合里出现"重复"元素;行为随编译器与优化级别漂移。
原因:结构体字节包含编译器插入的填充;两份逻辑相同的结构体填充字节不同,字节比较就不等。
typedef struct portspec { short Port; int Enabled; } portspec; /* 有填充 */ portspec A; A.Port = 443; A.Enabled = 1; /* 填充未清 */ xrtSetAdd(&tSet, &A); /* 带着随机填充入集合 */
portspec A; memset(&A, 0, sizeof(A)); /* 先清零再赋值——填充确定 */ A.Port = 443; A.Enabled = 1; xrtSetAdd(&tSet, &A);
编程练习
把一组含重复的 ID 装进集合,输出 Count 与去重后的元素列表;再与第 14 章数组的"排序去重"做法对比代码量与输出顺序(数组法输出有序、集合法输出插入序——两种顺序各自的服务场景想一句)。
三组 ID(用户组、权限表、黑名单),用交与差算出"有权限且不在黑名单"的最终集合;对三层各 1000 个随机 ID 的规模做正确性断言。提示:交集的结果继续参与差集运算。
读两份端口配置(硬编码即可),用 SymmetricDifference 输出差异端口并标注"仅在 A / 仅在 B"——提示:差异集合需要回查两个源集合才能标注归属。验收标准:构造三组已知差异的配置,输出与预期逐行一致;运算结果全部 Destroy,统计验证零泄漏。
小结
| 知识点 | 速查 |
|---|---|
| 装载 | Init(sizeof(元素)) + Add(天然去重);Has/Remove/Count 公开字段直读 |
| 代数 | 交/并/差/对称差返回堆集合(Destroy 收尾);顺序沿左操作数 |
| 关系判定 | 子集/超集/不相交直接返回布尔,不分配、无中间集合 |
| 值相等 | 字节相等;结构体先清零填充;指针元素比指针值 |
| 迭代 | 三段式 + 逆序 + 回调中止,三种遍历姿势,输出全部可复现 |
| 与 map 关系 | 同底座同纪律;集合 = 没有值的映射 + 代数运算族 |
| 代数数据流 | 每步产物独立存活;求值即清理,错误路径不漏 Destroy;中间集合用完即毁 |
| 关系速查 | 交集=两边都有;差集=左有右无;对称差=恰好一方有;结果的顺序都是左操作数插入序 |
📖 本章涉及的 API 参考手册:set(42 个条目)
点击查看完整签名、参数约束、返回值与错误说明