本章导读

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恰好一方有两份配置的差异对比

四者的共同契约:返回新分配的堆集合,迭代顺序沿用左操作数的插入序。结果可以链式参与下一次运算——"先并再交"的多步策略一行一个运算自然书写。

集合运算的数据流

一次策略计算通常是多步代数的串联,把数据流向画出来比读代码直观:

1装载两组原始 ID 各自 Add 成集合(去重免费拿到)
2交集请求 ∩ 启用——两边都点头的才放行
3差集候选 − 黑名单——从交集中再排除
4输出迭代最终集合沿左操作数插入序,逐行可复现

注意每一步的产物都是独立的堆集合:中间结果用完即 Destroy,最终结果迭代后再 Destroy。链式运算的清理代码建议与运算写在一起(求值即清理),避免"先全部算完再统一清理"的长列表——后者在错误路径上更容易漏。

值语义与比较

元素是值:Add 拷贝元素字节进槽,sizeof(元素类型) 在 Init 时声明。元素相等的判定就是字节相等——结构体里有填充字节时先清零再入集合(否则同值不同填充被判不等)。存指针可以,但集合比较的是指针值不是指向的对象——"对象身份集合"与"对象值集合"是两回事,需要后者时以业务键(如 ID)为元素。

迭代与查询

Has 判断成员、Remove 删除元素、Count 是公开字段——容器三约定(第 14 章)原样生效。迭代按插入序输出、可精确复现;set_tour 范例还演示了逆序迭代与访问器风格的遍历(回调版),两者与正序三段式同源。

19.3示例

完整程序:端口白名单交集

来自仓库范例 examples/containers/set/main.c:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/containers/set/main.c -lws2_32 -liphlpapiallowed port: 443allowed port: 8080

刚才发生了什么。 ① 两个栈句柄集合各自 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,覆盖生命周期、查询、代数与两种遍历:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/containers/set_tour/main.c -lws2_32 -liphlpapiset: lifecycle count=2 cap>=2 trim=1 clear=0set: queries get-or-add new=0/1 has=1/0 remove=1set: algebra union=4 diff=2 sym=2 subset/super/disjointset: rbegin order=3..1 visit=stopped

刚才发生了什么。 ① 生命周期行: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 章的统计配合做一次体检:跑一段集合密集的逻辑,观察活跃字节曲线是否呈"锯齿"(分配-释放配对)而非"楼梯"(只升不降)——锯齿健康、楼梯泄漏,这个判读法比逐行审查快得多。

常见错误

坑 1:运算结果用 Unit 收尾

症状:内存泄漏(第 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);
坑 2:结构体元素的填充字节未清零(随编译器漂移的幽灵 bug)

症状:"明明相等的两个元素"有时判等有时不等;集合里出现"重复"元素;行为随编译器与优化级别漂移。

原因:结构体字节包含编译器插入的填充;两份逻辑相同的结构体填充字节不同,字节比较就不等。

错误写法
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 个条目)

点击查看完整签名、参数约束、返回值与错误说明