数字电路课有个绕不开的体力活:画真值表。一个 4 变量的逻辑函数就是 16 行,5 变量 32 行,手写一遍只为验证表达式对不对。2023 年 7 月我先写了个卡诺图绘制的小程序,2024 年 3 月 15 日晚上把它扩成了一个完整的工具——数电帮你算 :给任意 2~5 元逻辑函数自动生成全部输入组合,配上函数指针直接打出整张真值表。当天夜里又迭代了一版 V1.1。两个程序的源码都整理在 GitHub 仓库 Digital-Logic-Helper 里,提交历史就是它们的开发顺序。
📌 更新(2026-08-31):为写这篇重新编译复现了全套流程,并在卡诺图程序里当场抓到并修复了一个 scanf 越界 bug——详见复盘一节。
先说清楚要做什么 工具的核心流程三步:
生成测试矢量(test vector):把 n 个变量的 2ⁿ 种取值组合全部枚举出来;
拿逻辑函数逐行代入,打印出 x1 x2 ... | y 的完整真值表;
顺手把内存收拾干净。
加上之前的卡诺图绘制(输入 16 个输出值,反打最小项表达式 + 格雷码排序的卡诺图网格),数电作业的两大苦力活就都自动化了。
测试矢量:位运算枚举一切组合 n 个变量有 2ⁿ 种组合,这个”全部枚举”用一个整数循环就能搞定——整数 i 的二进制位天然就是一组输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Testvector* generateTestVector (int n) { int totalCombinations = 1 << n; Testvector* testvector = (Testvector*)malloc (sizeof (Testvector)); testvector->free = freeTestVector; testvector->n = n; testvector->data = (char **)malloc (sizeof (char *) * (totalCombinations)); for (int i = 0 ; i < totalCombinations; ++i) { testvector->data[i] = (char *)malloc (sizeof (char ) * (n)); for (int j = n - 1 ; j >= 0 ; --j) { int value = (i >> j) & 1 ; testvector->data[i][n - j - 1 ] = value + '0' ; } } StuctObjectArr[StuctObjectArrCnt++] = (__TestXStuct__*)testvector; return testvector; }
(i >> j) & 1 把整数 i 的第 j 位抠出来,+ '0' 转成字符——i 从 0 数到 2ⁿ-1,所有组合一行不落。这里有个小细节:每个组合存的是 '0'/'1' 的字符 而不是 int,为的是后面打印真值表时可以直接按字符串输出,不用再转一道。
函数指针:参数个数不同的函数怎么统一调 测试矢量好办,麻烦的是”代入逻辑函数”这一步——2 元函数 y2_and(x1, x2) 和 5 元函数 y5(x1, x2, x3, x4, x5) 签名完全不同,C 又没有反射,怎么写一个统一的 test() 去调它们?
第一层答案是按参数个数准备一套函数指针类型 :
1 2 3 4 5 6 7 8 9 10 typedef int (*p_y2Type) (int , int ) ;typedef int (*p_y3Type) (int , int , int ) ;typedef int (*p_y4Type) (int , int , int , int ) ;typedef int (*p_y5Type) (int , int , int , int , int ) ;int y2_and (int x1, int x2) { return (x1 & x2); }int y3 (int x1, int x2, int x3) { return ((x1 & x2) | x3); }int y4 (int x1, int x2, int x3, int x4) { return ((x1 & x2) | x3) & x4; }int y5 (int x1, int x2, int x3, int x4, int x5) { return (((x1 & x2) | x3) & x4) ^ x5; }
第二层是 test() 里按矢量维度 switch,把存成 void* 的函数指针强转回真实签名 再调用:
1 2 3 4 5 6 7 8 9 10 switch (testvector->n){ case 2 : x1 = testvector->data[i][0 ] - '0' ; x2 = testvector->data[i][1 ] - '0' ; y = ((p_y2Type)(testfun->ptr_testfun))(x1, x2); printf ("%d\t%d\t| %d\t" , x1, x2, y); break ; }
TestFun 结构体里函数本体就是存成一个 void* ptr_testfun——函数指针本来就可以不经转换存进 void*,调用时再转回它真实的类型。这个”按个数分发”的思路是不是有点眼熟?Print.h 开发记(四) 里那次是用宏拼接在编译期 生成 _print2、_print3 系列函数名,这里是运行期 switch 强转——同一个问题(C 没有重载)的两种解法。
C 语言的析构函数:虚结构体 + 对象池 这个工具我最得意的设计是内存管理。C 没有 class 也没有析构函数,malloc 出来的 Testvector、TestFun 散落在各处,忘了 free 就是泄漏。我的解法分三步。
第一步,给所有对象装一个统一的”头” ——一个只含函数指针的虚结构体:
1 2 3 4 5 typedef struct __TestXStuctTypedef__ { void (*free )(void *); }__TestXStuct__;
Testvector 和 TestFun 的第一个成员都是它 :
1 2 3 4 5 6 typedef struct TestvectorTypedef { void (*free )(void *); int n; char ** data; }Testvector;
C 标准保证结构体首成员的地址就是结构体自身的地址,所以任何一种对象都能安全地强转成 __TestXStuct__* 来用——每个对象出生时就自带”怎么销毁我”的说明书。这个套路是不是也很眼熟?glist 里每个元素自带 data_to_str,这里每个对象自带 free——把行为跟着数据走 ,C 语言里万物皆可此法。
第二步,出生即登记。 每个构造函数(generateTestVector、generateTestFun)在 return 前把新对象塞进全局对象池:
1 2 3 4 5 6 7 8 9 10 11 12 TestFun* generateTestFun (const char *name, void *ptr_testfun) { TestFun* testfun = (TestFun*)malloc (sizeof (TestFun)); testfun->free = freeTestFun; testfun->name = (char *)malloc (strlen (name) * sizeof (char )); strcpy (testfun->name, name); testfun->ptr_testfun = ptr_testfun; StuctObjectArr[StuctObjectArrCnt++] = (__TestXStuct__*)testfun; return testfun; }
第三步,注册一个”main 之后的析构函数”:
1 2 3 4 5 6 7 8 9 10 11 12 static __attribute__((destructor(101 ))) void freeStruct () { for (int i = 0 ; i < StuctObjectArrCnt; i++) { if (StuctObjectArr[i]) { StuctObjectArr[i]->free (StuctObjectArr[i]); } } printf ("Thank you for using it\n" ); }
__attribute__((destructor(101))) 是 GCC 的扩展:被它标记的函数会在 main 返回之后自动执行,括号里的数字决定多个析构函数之间的先后。于是 main 里可以尽情地 generateTestVector、generateTestFun,一个 free 都不用写——程序退出时对象池统一逐个调用自己的 free 销毁,最后打一句 “Thank you for using it”。后面的运行效果里能看到这句话,它就是析构链跑完的证明。
对象池这套思路我并不是只用了这一次——BigInterge 开发记 里有个变体:每个对象登记进账本、配一个用变量名当 key 的哈希表,最后统一销毁。一个靠对象自带销毁函数,一个靠外挂账本,两条路都在那篇文章里有完整的代码。
V1.1:一个变参函数的小实验 V1.0 和 V1.1 是同一天晚上先后完成的。V1.1 加了一个实验性质的变参与函数——参数个数不写死在签名里,而是作为第一个参数传进来,后面的参数用 va_list 逐个读:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int y_2and (int num, ...) { va_list valist; int i; int *x = (int *)malloc (sizeof (int ) * (num + 1 )); va_start(valist, num); for (i = 1 ; i <= num; i++) { x[i] = va_arg(valist, int ); } return x[1 ] & x[2 ]; }
这算是给”参数个数分发”找的第三条路的雏形:一个函数吃下任意个参数。va_start/va_arg 这套机制的原理在可变参数实战 里拆过,这里不重复。这个实验函数还没接进 main 的测试流程,算是给下一版探路——但方向已经清楚了:有了变参,switch 那座五层楼梯迟早能拆掉。
卡诺图:真值表反打表达式,外加格雷码排序 更早的卡诺图程序(2023 年 7 月)走的是反方向:不动逻辑函数,只问你要 16 个输出值,然后反过来给你最小项之和的表达式 和一张排好序的卡诺图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void ChacterPrint () { printf ("Now print:" ); for (OutArray_i = 0 ; OutArray_i < 16 ; OutArray_i++) { if (OutArray[OutArray_i]) { char b[8 ] = "" ; char i = 7 ; char n = OutArray_i; while (n > 0 ) { b[i--] = n % 2 ; n /= 2 ; } for (int i = 4 ; i <= 7 ; ++i) { printf ("%c" ,"ABCD" [i-4 ]); if ( b[i] == 0 ) { putchar ('\'' ); } } putchar ('+' ); } } }
把每个取值为 1 的行号拆成 4 位二进制,1 就印出对应变量、0 就印出变量的反(加撇 '),最后一个 + 连接——A'B'CD 这种教科书样式的最小项表达式就是这么拼出来的。
卡诺图的灵魂是格雷码排序 。普通二进制数相邻会跳变多位(01→10 两位全变),而卡诺图的列标号是 00 01 11 10——相邻格永远只差一位,这样”圈 1 合并”才有几何意义。程序里这张网格是直接按格雷码位置硬编码的:
1 2 3 4 5 6 7 8 9 void ListPrint () { printf ("\nNow print List:\n" ); printf ("AB\\CD\t00\t01\t11\t10\n" ); printf (" 00 " );S(OutArray[0x0 ]);S(OutArray[0x1 ]);S(OutArray[0x3 ]);S(OutArray[0x2 ]);putchar ('\n' ); printf (" 01 " );S(OutArray[0x4 ]);S(OutArray[0x5 ]);S(OutArray[0x7 ]);S(OutArray[0x6 ]);putchar ('\n' ); printf (" 11 " );S(OutArray[0xC ]);S(OutArray[0xD ]);S(OutArray[0xF ]);S(OutArray[0xE ]);putchar ('\n' ); printf (" 10 " );S(OutArray[0x8 ]);S(OutArray[0x9 ]);S(OutArray[0xB ]);S(OutArray[0xA ]);putchar ('\n' ); }
注意行的取址顺序 0x0, 0x1, 0x3, 0x2——第三列取 0x3、第四列取 0x2,正是格雷码的排列。教科书上”为什么卡诺图不按 00 01 10 11 排”的问题,这四行代码就是答案。
运行效果 主程序一次跑完 2~5 元函数的真值表。先是 4 维测试矢量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 4 Dimension Test Vector: 0 0 0 0 0 0 0 1 0 0 1 0 0 0 1 1 0 1 0 0 0 1 0 1 0 1 1 0 0 1 1 1 1 0 0 0 1 0 0 1 1 0 1 0 1 0 1 1 1 1 0 0 1 1 0 1 1 1 1 0 1 1 1 1
然后逐个函数代真值表,(x1 & x2) | x3 的 3 元表:
1 2 3 4 5 6 7 8 9 10 11 Test 3 Dimension vector: (x1 & x2) | x3 ------------------------------------ x1 x2 x3 | y ------------------------------------ 0 0 0 | 0 0 0 1 | 1 0 1 0 | 0 0 1 1 | 1 1 0 0 | 0 1 0 1 | 1 ……
5 元表跑完最后一行,析构函数收尾:
1 2 3 4 1 1 1 1 1 | 0 ------------------------------------------------------ Thank you for using it
那句 “Thank you for using it” 不是 main 打的——main 早就 return 了,它是 destructor 属性挂的清理函数在进程收尾时打出来的。
复盘:一个藏在 scanf 里的真 bug 这次为了写文章把两个程序重新编译跑了一遍,抓到一个潜伏一年多的真 bug,就在卡诺图程序的输入处:
1 2 3 4 5 6 7 8 char OutArray[16 ];char OutArray_i = 0 ;void BinNumPrint (char n) { ... scanf ("%d" ,&OutArray[OutArray_i++]); }
scanf("%d", ...) 需要一个*int **,而 &OutArray[i] 是个 char* ——%d 会老老实实写满 4 个字节,但每个格子只有 1 字节,多出来的 3 个字节全部写到相邻的格子里去了 。这是如假包换的未定义行为。
实测一把:只喂 m3 = 1 一行,其余 15 行全输 0——后面的 0 写入会把前面写入的 1 冲掉,最后画出的卡诺图全零 ,那个 1 凭空消失。喂一张 y = AB + CD 的完整真值表,打出来的表达式是 A'B'C'D'+A'B'C'D+A'BCD+AB'CD+ABC'D'+ABC'D+——最小项集合整个是错的。当年手输的时候大概输入模式和某个”恰好没被冲掉”的路径撞上了,一直没暴露。
修复只要三行:先读进 int,再缩小塞进 char:
1 2 3 int tmp = 0 ;scanf ("%d" , &tmp);OutArray[OutArray_i++] = (char )tmp;
修完再喂同一张 AB+CD 真值表,卡诺图就对了:
1 2 3 4 5 6 Now print List: AB\CD 00 01 11 10 00 0 0 1 0 01 0 0 1 0 11 1 1 1 1 10 0 0 1 0
AB=11 那一横排全 1 加上 CD=11 那一竖列全 1——两个四格圈,正是 AB + CD。这个 bug 的教训一句话就能说完:格式符的宽度必须和目标一样宽 ,%d 配 char*、%s 配单个 char 的地址,都是同一家族的错。编译器其实一直在提醒——-Wall 会报 format '%d' expects argument of type 'int *'——当初没开警告,它就沉默了一年。
完整代码 数电帮你算.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdarg.h> #define ERROR(message) do { \ printf("ERROR, %s" , message);\ exit(-1); \ } while (0) #define MAXObject 100 typedef struct __TestXStuctTypedef__ { void (*free )(void *); }__TestXStuct__; int StuctObjectArrCnt = 0 ;__TestXStuct__* StuctObjectArr[MAXObject]; typedef struct TestvectorTypedef { void (*free )(void *); int n; char ** data; }Testvector; typedef struct TestFunTypedef { void (*free )(void *); char * name; void * ptr_testfun; }TestFun; static __attribute__((destructor(101 ))) void freeStruct () { for (int i = 0 ; i < StuctObjectArrCnt; i++) { if (StuctObjectArr[i]) { StuctObjectArr[i]->free (StuctObjectArr[i]); } } printf ("Thank you for using it\n" ); } void printTestTitle (int n, TestFun* testfun) { printf ("Test %d Dimension vector: %s\n" , n, testfun->name); for (int i = 0 ; i <= n; i++) printf ("---------" ); putchar ('\n' ); for (int i = 1 ; i <= n; i++) { printf ("x%d\t" , i); } printf ("| y\t" ); putchar ('\n' ); for (int i = 0 ; i <= n; i++) printf ("---------" ); putchar ('\n' ); } void printTestEnd (int n) { for (int i = 0 ; i <= n; i++) printf ("---------" ); putchar ('\n' ); putchar ('\n' ); } typedef int (*p_y2Type) (int , int ) ;typedef int (*p_y3Type) (int , int , int ) ;typedef int (*p_y4Type) (int , int , int , int ) ;typedef int (*p_y5Type) (int , int , int , int , int ) ;int y2_and (int x1, int x2) { return (x1 & x2); } int y2_or (int x1, int x2) { return (x1 | x2); } int y3 (int x1, int x2, int x3) { return ((x1 & x2) | x3); } int y4 (int x1, int x2, int x3, int x4) { return ((x1 & x2) | x3) & x4; } int y5 (int x1, int x2, int x3, int x4, int x5) { return (((x1 & x2) | x3) & x4) ^ x5; } int y_2and (int num, ...) { va_list valist; int i; int *x = (int *)malloc (sizeof (int ) * (num + 1 )); va_start(valist, num); for (i = 1 ; i <= num; i++) { x[i] = va_arg(valist, int ); } return x[1 ] & x[2 ]; } void freeTestFun (void * testfun) { if (!testfun) return ; free (((TestFun*)testfun)->name); free (testfun); } TestFun* generateTestFun (const char *name, void *ptr_testfun) { TestFun* testfun = (TestFun*)malloc (sizeof (TestFun)); testfun->free = freeTestFun; testfun->name = (char *)malloc (strlen (name) * sizeof (char )); strcpy (testfun->name, name); testfun->ptr_testfun = ptr_testfun; StuctObjectArr[StuctObjectArrCnt++] = (__TestXStuct__*)testfun; return testfun; } void freeTestVector (void * testvector) { if (!testvector) return ; for (int i = 0 ; i < (1 << (((Testvector*)(testvector))->n)); ++i) free (((Testvector*)testvector)->data[i]); free (testvector); } Testvector* generateTestVector (int n) { int totalCombinations = 1 << n; Testvector* testvector = (Testvector*)malloc (sizeof (Testvector)); testvector->free = freeTestVector; testvector->n = n; testvector->data = (char **)malloc (sizeof (char *) * (totalCombinations)); for (int i = 0 ; i < totalCombinations; ++i) { testvector->data[i] = (char *)malloc (sizeof (char ) * (n)); for (int j = n - 1 ; j >= 0 ; --j) { int value = (i >> j) & 1 ; testvector->data[i][n - j - 1 ] = value + '0' ; } } StuctObjectArr[StuctObjectArrCnt++] = (__TestXStuct__*)testvector; return testvector; } void printTestVector (Testvector* testvector) { printf ("%d Dimension Test Vector:\n" , testvector->n); for (int i = 0 ; i < (1 << testvector->n); i++) { for (int j = 0 ; j < testvector->n; j++) { printf ("%c%c" , testvector->data[i][j], " \n" [j == testvector->n - 1 ]); } } putchar ('\n' ); } void test (Testvector* testvector, TestFun* testfun) { int x1; int x2; int x3; int x4; int x5; int y; printTestTitle(testvector->n, testfun); for (int i = 0 ; i < (1 << testvector->n); i++) { switch (testvector->n) { case 2 : x1 = testvector->data[i][0 ] - '0' ; x2 = testvector->data[i][1 ] - '0' ; y = ((p_y2Type)(testfun->ptr_testfun))(x1, x2); printf ("%d\t%d\t| %d\t" , x1, x2, y); break ; case 3 : x1 = testvector->data[i][0 ] - '0' ; x2 = testvector->data[i][1 ] - '0' ; x3 = testvector->data[i][2 ] - '0' ; y = ((p_y3Type)(testfun->ptr_testfun))(x1, x2, x3); printf ("%d\t%d\t%d\t| %d\t" , x1, x2, x3, y); break ; case 4 : x1 = testvector->data[i][0 ] - '0' ; x2 = testvector->data[i][1 ] - '0' ; x3 = testvector->data[i][2 ] - '0' ; x4 = testvector->data[i][3 ] - '0' ; y = ((p_y4Type)(testfun->ptr_testfun))(x1, x2, x3, x4); printf ("%d\t%d\t%d\t%d\t| %d\t" , x1, x2, x3, x4, y); break ; case 5 : x1 = testvector->data[i][0 ] - '0' ; x2 = testvector->data[i][1 ] - '0' ; x3 = testvector->data[i][2 ] - '0' ; x4 = testvector->data[i][3 ] - '0' ; x5 = testvector->data[i][4 ] - '0' ; y = ((p_y5Type)(testfun->ptr_testfun))(x1, x2, x3, x4, x5); printf ("%d\t%d\t%d\t%d\t%d\t| %d\t" , x1, x2, x3, x4, x5, y); break ; default : ERROR("Testvector is too big!" ); break ; } putchar ('\n' ); } printTestEnd(testvector->n); } int main () { Testvector* vector4 = generateTestVector(4 ); printTestVector(vector4); Testvector* vector2 = generateTestVector(2 ); TestFun* testfun_2and = generateTestFun("x1 & x2" , y2_and); TestFun* testfun_2or = generateTestFun("x1 | x2" , y2_or); test(vector2, testfun_2and); test(vector2, testfun_2or); Testvector* vector3 = generateTestVector(3 ); TestFun* testfun_3 = generateTestFun("(x1 & x2) | x3" , y3); test(vector3, testfun_3); TestFun* testfun_4 = generateTestFun("((x1 & x2) | x3) & x4" , y4); test(vector4, testfun_4); Testvector* vector5 = generateTestVector(5 ); TestFun* testfun_5 = generateTestFun("(((x1 & x2) | x3) & x4) ^ x5" , y5); test(vector5, testfun_5); return 0 ; }
数电帮你算-卡诺图绘制.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <stdio.h> char OutArray[16 ];char OutArray_i = 0 ;void BinNumPrint (char n) { char b[8 ]; char i = 7 ; while (n > 0 ) { b[i--] = n % 2 ; n /= 2 ; } for (int i = 4 ; i <= 7 ; ++i) { printf ("%d " , b[i]); } scanf ("%d" ,&OutArray[OutArray_i++]); } void ChacterPrint () { printf ("Now print:" ); for (OutArray_i = 0 ; OutArray_i < 16 ; OutArray_i++) { if (OutArray[OutArray_i]) { char b[8 ] = "" ; char i = 7 ; char n = OutArray_i; while (n > 0 ) { b[i--] = n % 2 ; n /= 2 ; } for (int i = 4 ; i <= 7 ; ++i) { printf ("%c" ,"ABCD" [i-4 ]); if ( b[i] == 0 ) { putchar ('\'' ); } } putchar ('+' ); } } } void S (char n) { printf ("\t%d" , n); } void ListPrint () { printf ("\nNow print List:\n" ); printf ("AB\\CD\t00\t01\t11\t10\n" ); printf (" 00 " );S(OutArray[0x0 ]);S(OutArray[0x1 ]);S(OutArray[0x3 ]);S(OutArray[0x2 ]);putchar ('\n' ); printf (" 01 " );S(OutArray[0x4 ]);S(OutArray[0x5 ]);S(OutArray[0x7 ]);S(OutArray[0x6 ]);putchar ('\n' ); printf (" 11 " );S(OutArray[0xC ]);S(OutArray[0xD ]);S(OutArray[0xF ]);S(OutArray[0xE ]);putchar ('\n' ); printf (" 10 " );S(OutArray[0x8 ]);S(OutArray[0x9 ]);S(OutArray[0xB ]);S(OutArray[0xA ]);putchar ('\n' ); } int main () { printf ("A B C D Out\n" ); for (int i = 0 ; i < 16 ; ++i) { BinNumPrint(i); } ChacterPrint(); ListPrint(); getchar(); getchar(); return 0 ; }
注意卡诺图这份完整代码里保留着原文的 scanf("%d",&OutArray[i]) 写法 ——就是复盘里那个 bug。它作为”错误示范”原样封存,你如果要实际用这个程序,记得先按复盘里的三行补丁修掉它。