杭电网安复试编程准备到第19天,我总算把那些"看着简单、上手就错"的基础题折腾明白了。如果你也在准备杭电网安复试编程,或者正在纠结网安方向机试到底会考什么,这篇记录应该能给你一个比较具体的参考——我会把Day19这一天的完整练习过程、题目拆解、踩坑经历都写出来,顺手把这段时间梳理出的网安复试编程准备路线也放在最后。
说起来,网安的复试编程和其他工科专业不太一样。它不单考你会不会写代码,还暗暗考察你有没有安全思维——比如数据边界、内存溢出、输入异常处理,这些在普通编程题里是附加分,在网安复试里可能就是核心考点。杭电的机试向来有难度,刷题不能只盯着"AC"两个字母,得想一想每道题背后到底想考什么。
1. 杭电网安复试编程的真实构成:环境、题型与给分逻辑
在进入Day19的题目之前,先把目标拆清楚。我花了两天时间翻经验帖、问学长,得出一个比较可靠的结论:杭电网安的复试编程环节,考察范围并不是你想象的"网络安全渗透工具脚本",而是以基础编程能力为主、兼带安全敏感度测试。
1.1 上机环境与语言选择
杭电机试一般提供C/C++、Java、Python三种语言环境,不同年份环境版本会有差异,但核心是:编辑器基本是纯文本,没有智能补全,编译运行靠命令行或简单的IDE。这个细节很重要,平时用惯了PyCharm自动补全和调试工具的人,冷不丁切到Vi/Vim或记事本写代码,手感会差很多。
我的建议是主攻C语言或者Python,选一个你最不依赖ide辅助的。C语言的好处是贴近底层,网上大部分复试经验帖都是C写的,遇到问题好查;Python的好处是写起来快、字符串处理方便,但要注意运行时间。网安方向复试时间一般是2到3小时,题量在3到5道之间,如果选Python,写代码快的同时也可能因为解释执行慢在极限数据下失分,这点需要权衡。
1.2 题型分布与考察点
从学长反馈和我看到的多套回忆题来看,杭电网安复试编程基本围绕这几类:
| 题型类别 | 常见出法 | 实际考察点 |
|---|---|---|
| 字符串处理 | 字符统计、字符串移位、子串匹配 | 指针/下标操作、边界判断 |
| 进制与编码转换 | 十进制转二进制、十六进制与字符互转 | 位运算、取模、ASCII码 |
| 数据结构和简单算法 | 链表删除、栈和队列模拟、排序查找 | 指针操作、结构体、递归 |
| 安全编程基础 | 简单的加密/解密、hash校验、异或操作 | 理解算法过程、异常处理 |
这就能看出,它其实是在用一种"平实"的方式检验你有没有编程功底。网络安全方向强调底层理解,所以C语言的指针、内存、位操作被考到的概率极大。单纯刷LeetCode偏算法题可能不够,还得回归基础,把课本上的经典小题写熟练。
1.3 给分逻辑与隐性要求
机试一般按用例给分,过几个用例给几分,大设计占分值不高。这里有一个很多新手忽略的点:输入输出格式必须严格匹配。有时候一个多余的换行、一个多余的空格,就会判错。网安方向还可能包含"防御性编程"的隐性要求——比如题目要求读取用户输入,你得考虑输入过长怎么办、非法字符怎么办,这些虽然不是显式用例,但可能留下代码评审的印象分。
Day19前我大致刷了排序、链表、字符串三类,今天开始集中突破进制转换、位运算和安全算法这一块,因为它是网安复试的特色区域,普通复试经验贴讲得比较少。
2. Day19的练习计划:为什么把重点放在进制、异或和输入陷阱上
今天没有贪多,只做了三道题、复盘了五个错误。说实话,比贪图一天刷十道题收获大得多。我的练习计划是按"经典题+变体+易错点"来设计的,目的不是背题,而是把一类知识吃透。
2.1 选题背景:从一道回忆题说起
学长提到过一道回忆题:"输入一个十六进制字符串,输出对应的二进制字符串,要求二进制高位补0"。看起来平平无奇,但考场上有相当一部分人没做全对。原因很简单:十六进制位数不是固定的,二进制高位补0的规则要想清楚,还有人忘了考虑输入里有小写字母。这种题目就是典型的"基础里藏坑",非常适合作为复试编程训练的切入点。
Day19的第一道题,就是类似题:十六进制转二进制,但我加了一个变体——要求过滤掉输入中的非法字符。
2.2 日训练题清单及目标
我把今天的四小时分成三段:1小时复习位运算原理并刷一道经典题,1.5小时做两道综合题,1小时整理错题和总结输入陷阱。这里需要说明的是,位运算和进制转换是后续做加密、CRC、权限控制代码的基础,网安复试若涉及简单加解密,底子全在这。
具体题目:
- 十六进制转二进制(过滤非法字符版)。
- 简单异或加密/解密(输入一段明文,用给定密钥做异或,再输出密文;再写反向解密)。
- 判断一个字符串是否为合法IPv4地址(输出合法/非法及非法原因摘要)。
这三道题覆盖了"进制转换、位运算、字符串分割与校验"三个高频考点,而且在网安场景里都是活知识。做题时我全程模拟机试环境——没有断点调试,靠printf打印中间变量,并且严格注意输出格式。
2.3 设计变体的用意
做完基础题,我给第一题加了一个改动:原始输入可能包含0x前缀,也可能包含空格和无效字符,需要输出清洗后的二进制串。这个变体不是拍脑袋,它的价值在于:网安方向日常处理数据时,输入往往是不干净的,你必须写代码去容错。一个合格的网安从业者读入数据时,第一反应就应该是"这个输入能害死我吗"。复试虽然不会考渗透,但考这种容错逻辑很常见,它是安全意识的编程化体现。
3. 三道典型机试编程题的完整解析与踩坑过程
下面把Day19的三道题具体拆开,像我考场上一步一步推演那样。代码用C语言写,因为C在机试里最稳妥,能体现指针和位运算功底。如果你选Python,思路一致,代码风格可以更简洁。
3.1 题一:带容错的十六进制转二进制
题目要求:输入一行字符串,可能包含0x前缀、空格、字母a-f大小写、数字0-9以及非法字符。要求输出这些合法十六进制字符对应的二进制字符串,每个十六进制字符转换为4位二进制,高位补零。遇到非法字符忽略。
输入示例:0x1A z!2f
合法字符序列:1 A 2 f
输出:0001 1010 0010 1111,去掉空格连起来就是0001101000101111。
先说思路。十六进制字符转二进制,最直白的办法是查表,也可以先转十进制整数再转二进制。但这里要求每个字符单独变4位二进制,查表法最简单:
#include <stdio.h> #include <string.h> #include <ctype.h> const char* hex_to_bin(char c) { switch (toupper(c)) { case '0': return "0000"; case '1': return "0001"; case '2': return "0010"; case '3': return "0011"; case '4': return "0100"; case '5': return "0101"; case '6': return "0110"; case '7': return "0111"; case '8': return "1000"; case '9': return "1001"; case 'A': return "1010"; case 'B': return "1011"; case 'C': return "1100"; case 'D': return "1101"; case 'E': return "1110"; case 'F': return "1111"; default: return NULL; } } int main() { char s[1000]; fgets(s, sizeof(s), stdin); // 去掉换行符 s[strcspn(s, "\n")] = 0; int len = strlen(s); int first = 1; for (int i = 0; i < len; i++) { if (i == 0 && s[i] == '0' && (s[i+1] == 'x' || s[i+1] == 'X')) { i++; // 跳过0x前缀 continue; } const char* bin = hex_to_bin(s[i]); if (bin != NULL) { if (!first) putchar(' '); printf("%s", bin); first = 0; } } if (first) printf("(empty)"); putchar('\n'); return 0; }这段代码有一个关键点:判断非法字符时直接用hex_to_bin返回值是否为NULL,避免写复杂条件判断。不过这里有个坑,检查0x前缀时我直接访问s[i+1],如果字符串是“0”(长度1,且只有一个0),s[i+1]其实是字符串结束符\0,不会越界,但逻辑上会误判吗?不会,因为循环条件i < len,i最大是0,判断时s[1]等于\0,既不等于x也不等于X,所以不会跳过。这个习惯好,但我得承认,第一次写的时候没有检查边界,后来才想到。
再说我踩的第二个坑:输出格式。题目要求“每个十六进制字符转换为4位二进制”,如果原题没要求空格,你就绝不能输出空格。我写代码时先输出了空格方便自己看,结果在自测时发现和预期输出不一样,调了半天才发现是格式问题。考场上一旦输出格式不符,用例全错。
3.2 题二:异或加密与解密
异或运算是网安方向最基础也最常用的运算。加密原理很简单:明文与密钥逐位异或得到密文,密文再与同一个密钥异或又得到明文。在C语言里就是对字节做^操作。
题目设计:读入一个字符串和单字节密钥key,输出每个字符与key异或后的十六进制表示(大写),然后程序再次读入这串十六进制密文和相同key,恢复原字符串。
这个题目比第一题绕,因为它必须自己实现"字节与十六进制字符串互转"。我先只写了加密部分:
#include <stdio.h> #include <string.h> int main() { char plain[1000]; int key; fgets(plain, sizeof(plain), stdin); scanf("%d", &key); plain[strcspn(plain, "\n")] = 0; for (int i = 0; plain[i] != '\0'; i++) { unsigned char c = (unsigned char)plain[i]; unsigned char enc = c ^ (unsigned char)key; printf("%02X", enc); } printf("\n"); return 0; }在这里我踩了一个特别典型的坑:%02X输出的是大写十六进制,但char类型如果直接参与位运算,可能因为符号位扩展出问题。我一开始写的是printf("%02X", plain[i] ^ key);,当plain[i]是一个ASCII码大于127的字符(比如中文或扩展字符)时,它可能是带符号的负数,异或后按整型提升,高位会补1,导致输出变成FFFFFF80这样的8位十六进制。虽然考试用例大概率是纯英文,但万一遇到扩展字符,这就是失分点。所以要用unsigned char强制转换。这个坑非常隐蔽,如果你写Python就不会遇到,但C语言机试里很常见。
解密部分本质是"从十六进制字符串读取两位,转成一个字节,再异或key"。要注意的坑是:每个密文字符恰好是两位十六进制,所以循环步长是2。代码:
#include <stdio.h> int hex_char_to_val(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'A' && c <= 'F') return c - 'A' + 10; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return -1; } int main() { char hex[2000]; int key; fgets(hex, sizeof(hex), stdin); scanf("%d", &key); hex[strcspn(hex, "\n")] = 0; int len = strlen(hex); for (int i = 0; i + 1 < len; i += 2) { int high = hex_char_to_val(hex[i]); int low = hex_char_to_val(hex[i+1]); if (high < 0 || low < 0) continue; unsigned char enc = (unsigned char)((high << 4) | low); unsigned char dec = enc ^ (unsigned char)key; putchar(dec); } putchar('\n'); return 0; }这道题最值得复盘的是"位运算优先级"的问题。(high << 4) | low这里,移位运算优先级比按位或高,所以(high << 4) | low不会写成high << (4 | low)吗?实际上<<优先级确实高于|,但我写了括号,保险。机试里,凡是混合位运算,全部加括号,别吝啬,这是减少低级错误的最好习惯。
另外一个容易出错的点:明文中可能有换行符,如果你用gets读,换行符不会包含在内,而用fgets会读进来,所以要去掉换行。我原先想直接用gets,但现在的机试环境有的编译器会警告甚至禁止gets,所以建议都用fgets。
3.3 题三:合法IPv4地址判断
这道题是我给自己加的安全校验训练。网安方向经常要处理IP地址,写这类判断逻辑很能体现编程基本功。题目要求:输入一个字符串,判断它是否是合法的IPv4地址。合法条件是:分成4段,每段是0-255的十进制数字,不能有前导零(除了单个0),不能包含空格和其他字符。
示例:192.168.1.1合法,192.168.01.1非法(前导零),256.1.1.1非法,1.2.3.4.5非法,.1.2.3非法。
我一开始想当然地用sscanf去解析,比如sscanf(s, "%d.%d.%d.%d", &a,&b,&c,&d),但是有个致命问题:sscanf会把1.2.3.4abc也解析成功,后面多的字符它不管。而且对192.168.01.1这种前导零情况sscanf也能解析成数字,但它其实非法。所以必须逐字符扫描,自己判断。
我的最终思路是:
- 用
strtok按点分割,但strtok会修改原字符串,且连续两个点会跳过空段,容易误判,所以还是手写。 - 手写解析函数:用两个指针,
start指向每段开始,end向后扫描直到点或结尾。 - 每段检查:长度为1到3,每个字符都是数字,若长度大于1则首字符不能是'0',转成整数后判断≤255。
- 统计段的个数必须正好是4。
- 最后检查整个字符串没有多余字符(比如开头结尾不能有空白和点)。
下面是我的代码(简化版):
#include <stdio.h> #include <string.h> #include <ctype.h> int is_valid_part(const char* start, const char* end) { int len = (int)(end - start); if (len < 1 || len > 3) return 0; if (len > 1 && start[0] == '0') return 0; // 前导零非法 int sum = 0; for (int i = 0; i < len; i++) { if (!isdigit(start[i])) return 0; sum = sum * 10 + (start[i] - '0'); } return sum <= 255; } int main() { char s[1000]; fgets(s, sizeof(s), stdin); s[strcspn(s, "\n")] = 0; int len = strlen(s); if (len == 0) { printf("invalid\n"); return 0; } const char* p = s; int dots = 0; while (*p) { if (*p == '.') { dots++; p++; } else { const char* start = p; while (*p && *p != '.') p++; if (!is_valid_part(start, p)) { printf("invalid\n"); return 0; } } } if (dots == 3 && s[0] != '.' && s[len-1] != '.') { printf("valid\n"); } else { printf("invalid\n"); } return 0; } }这段代码我自测了几个用例,发现一个隐藏bug:while (*p)循环里,遇到点就dots++然后p++。但如果是连续两个点192.168..1,处理完第一个点后p指向第二个点,下一个循环还是点,dots再++,然后p指向1,此时is_valid_part会解析“1”,但中间缺少一段,最终dots=3,s开头结尾也不是点,结果竟然被判valid了。这是严重逻辑漏洞。
所以不能简单统计dots数量,应该按"段-点-段-点-段-点-段"严格结构扫描。正确做法是:用parse_part读取一段并紧跟着判断其后是否为点,循环4次,最后一次点的出现次数应为0。我重新写了:
#include <stdio.h> #include <string.h> #include <ctype.h> int is_valid_part(const char* start, const char* end) { int len = (int)(end - start); if (len < 1 || len > 3) return 0; if (len > 1 && start[0] == '0') return 0; int sum = 0; for (int i = 0; i < len; i++) { if (!isdigit(start[i])) return 0; sum = sum * 10 + (start[i] - '0'); } return sum <= 255; } int main() { char s[1000]; fgets(s, sizeof(s), stdin); s[strcspn(s, "\n")] = 0; const char* p = s; int seg_count = 0; for (int i = 0; i < 4; i++) { const char* start = p; while (*p && *p != '.') p++; if (!is_valid_part(start, p)) { printf("invalid\n"); return 0; } seg_count++; if (i < 3) { if (*p != '.') { printf("invalid\n"); return 0; } p++; // 跳过点 } } // 此时p应该已经指向末尾 if (*p != '\0' || seg_count != 4) { printf("invalid\n"); return 0; } printf("valid\n"); return 0; }这个写法用循环次数固定为4次,每次解析一段,再强制要求前3段后面必须有点,第4段后面必须没有点。这样连续两个点的情况,第一段后面是点,第二段是空段,is_valid_part(start,p)中start和p相等(len=0)直接返回0,判断为非法。这才是正确的。为什么我第一版会写错?因为我跳过了“每一段后面跟什么符号”的结构检查,只盯着点的个数。这类经验特别适合复试场景:字符串合法性判断,一定要把分隔符的出现时机一起约束好,而不是简单统计。
3.4 三道题之外的边角料:输入读取的“第一道防线”
今天这三道题集中暴露了我一个老毛病:对输入读取的防御不足。比如使用fgets后一定要处理末尾换行;用scanf("%d", &key)后,如果接下来还要用fgets读字符串,缓冲区会遗留换行符,导致fgets直接读到空行。这个问题几乎每次都会在别人身上看到。我在做第二题时就是先fgets后scanf,顺序对了就没事;但如果反过来,就要用getchar()吃掉多余换行,或者重新组织输入顺序。
对机试而言,最简单的策略是:统一使用fgets一行一行读,然后用sscanf或手动解析。这样可以把换行、空格都控制住,虽然代码多几行,但稳定性高很多。比如第二题,可以先用fgets读明文,再fgets读密钥,再用atoi转换,这样就不会有缓冲区错乱问题。
4. 机试实战中的高频踩坑清单:今天我踩了五个,你提前避雷
今天犯了五个错,都记下来。这些错误不是“不会写”,而是“不细心”,放在考场上特别可惜。
4.1 坑一:输出格式里的空格与换行
第一题我多输出了空格,第三题我少判断了结尾。机试判题通常用diff逐字符比对,多出来的空格会和答案不匹配。解决办法是:看清题目的输出样例;如果样例里的二进制串之间用了空格,那必须有;如果连在一起,就不能有。拿不准时,尽量按最简格式输出,不要自己加易读性修饰。我第一题代码里保留了空格,是为了自己看方便,但最终提交前必须把空格逻辑删掉。
4.2 坑二:有符号char参与位运算
第二题里plain[i] ^ key在没有unsigned char强制转换时,若plain[i]负数,位扩展会污染结果。这个坑特别推荐所有用C语言机试的同学留意。处理方法是:涉及位运算的字符变量,一律转成unsigned char再操作。记住一个原则:C语言中char到底是有符号还是无符号,由实现定义,很多OJ环境里它就是有符号的。所以不要赌环境,显式转换才是王道。
4.3 坑三:字符串结束符与数组越界
我在检查0x前缀时写了if (s[i] == '0' && (s[i+1] == 'x' ...)),只要i+1在字符串长度范围内就不会越界。但如果我在一个循环里频繁访问i+1,就要留意循环终止条件。今天复盘时我在第三题重写版本里也有类似访问p是否越界的问题,因为while (*p)会确保p指向有效字符或\0,不会越界,但如果你使用p[i+1]这种形式,就要额外小心。
4.4 坑四:strtok的副作用
我在做IPv4判断时一开始想过用strtok,但它会把连续分隔符视为一个,还会修改原字符串、在内部维护静态指针,不适合多次调用。网安复试可能会遇到类似"按逗号解析CSV""按空格解析命令"的题目,如果你只用strtok,遇到空字段就会出问题。手写解析虽然笨,但可控。
4.5 坑五:只跑题面样例就提交
很多题目题面给的样例非常温和,比如十六进制只有大写、IP地址没有连续点等,一旦你只跑过题面样例,就会错过边界。今天我每个题都额外构造了至少5组用例:空输入、全非法、首位是点、末尾是点、连续点、大写小写混合、最大数字255和256、前导零。在复试机试里,边界用例往往就是区分及格和优秀的线。可以自己准备几个固定用例,形成肌肉记忆,每题跑完先来一遍边界套餐。
4.6 一个额外的提分习惯:写代码时顺带写注释
机试大多不要求提交注释,但如果题目有文档评审或面试官看你代码,注释会大大加分。更重要的是,写注释能强迫自己理清思路。比如第三题,我写// 前3段后必须有点,第4段后必须无点,写着写着就发现了第一版的结构漏洞。注释不是给评委看的,是给你自己的思维上保险。
5. 网安复试编程的准备路线:从Day1到Day19的复盘与延伸
前19天我大致是按这个路线走的,分享出来供参考。这条路线既考虑了杭电的机试风格,也覆盖了网安方向可能涉及的基础。
5.1 第一个周期(Day1-Day6):C语言地基与编译调试
前6天我把C语言最核心的语法过了一遍,重点不是"学会",而是"熟练"。每天写至少5个程序:数组排序、链表插入删除、字符串拷贝、逆序、查找子串、二进制文件读写。这个阶段必须做到不查手册能流畅写出结构体和指针操作。杭电网安复试编程最怕的不是题目难,而是你连链表节点定义都要想半天。
我还有一个训练方法:不开IDE自动补全,只在vim里写,编译报错后自己看代码找出错因。一开始很痛苦,但几天后对语法和常见报错形成了条件反射。
5.2 第二个周期(Day7-Day12):数据结构与常见算法
这个阶段练了栈、队列、链表、二叉树基础遍历,以及排序、二分查找、递归回溯。网安复试很少考动态规划、图论这种难题,但基础数据结构出现的频率很高。比如"用两个栈实现队列""判断链表是否有环"这类题,可以作为复试练习的一档难度。这个阶段我用的是LeetCode入门题+教材课后题,每天控制在3道左右,但每道题都要分析时间和空间复杂度。
Day12那天我做了个小总结,发现链表操作是我的薄弱点,于是后两天专门补了链表专项。复试准备不是均匀用力,而是针对薄弱点突击,这个道理大家都懂,但执行起来容易陷入"手到擒来题刷爽"的舒适区。
5.3 第三个周期(Day13-Day18):安全相关编程基础
网安复试和普通机试的区别在这个阶段体现出来。我开始练习十六进制与字节流互转、异或加密、简单的CRC校验、Base64编码、字符串哈希(MD5的简化模拟)、大小端转换等。这些题目技术含量不算高,但会强制你用位运算、用指针、用unsigned char去处理字节,同时让你不知不觉地形成"底层数据运算"的安全思维。
Day15我做了一道模拟TCP校验和的题,就是把数据按16位分组求和再取反。这道题非常综合,既考了位运算、无符号溢出,又和网络安全的完整性校验相关。当时我踩了"溢出截断"的坑,后来想明白:校验和计算时进位要循环回加,和普通求和使用不同的逻辑。这种题如果没练过,上考场会觉得摸不着头脑;练过以后,再遇到同类题就有脉络了。
5.4 Day19及以后:综合模拟与节奏训练
像Day19这样,用三到四道涵盖"进制、位运算、字符串校验"的题做一次完整模拟,是非常好的节奏。后面我打算每两天做一次90分钟的模拟机试,尽量为自己创造考试环境:不查资料、不做多余调试、快速写、快速验。模拟时故意用一些第三方数据(比如从日志文件里截取原始数据),锻炼自己从"不干净的输入"中提取有效信息的能力。
再往后要补充的点包括:多线程或异步编程思想——网安方向可能会涉及并发请求或数据包并行处理,但注意,复试编程机试大概率不会直接考多线程,因为环境很难保证公平,所以只要理解基本概念即可。热搜里提到的MapReduce、HDFS编程实践,如果是大数据方向复试可能有用,但对网安复试来说不是核心。网安的核心还是C/Python基本功、算法基础、密码学/编码的简单实现。学习路线方面,不建议一上来就啃渗透测试工具,复试编程考的是基本功,先把手写代码能力提上来,后面学工具才有底子。
5.5 给不同基础的同学的调整建议
- 如果C语言还停留在"看懂了但写不出来"阶段,别急着刷题,先每天手写10个小函数,比如字符串长度、字符串拷贝、字符大小写转换、十进制转二进制。这些写顺了,后面才谈得上结构体、链表。
- 如果已经能流畅写基础题,可以尝试把每个题改造成更严格的版本,比如给字符串处理题加上“输入可能包含非法字符”的前提条件,锻炼容错能力。
- 如果追求高分,一定注意代码风格和边界处理,杭电机试成绩有时候零点几分就能拉开名次,边界用例尤其关键。
写在最后:Day19的一点实操心得
今天最大的收获不是把三道题做出来,而是意识到"能做出来"和"稳定地做出来"之间有巨大的鸿沟。第一版IPv4判断看起来逻辑自洽,实际却漏了连续点的情况;异或加密一开始也没关注符号扩展。这些问题靠“看着对”是不行的,必须靠测试用例、靠边界思维、靠事后复盘去堵住。
另一个体会是:刷网安复试编程题,不必贪多求快。Day19我全程只练了三道题,但每道题都做了变体、加了限制,还认真整理了错因。如果你离复试还有一段时间,与其每天刷十道简单题,不如每天吃透三到五道有代表性的题,把输入输出、边界、位运算、指针这些基本功磨扎实。
最后再分享一个小技巧:准备一个属于自己的“边界用例测试集”,每次写完程序先跑一遍。我的测试集包括:空字符串、超长字符串、只有前缀没有数字、连续分隔符、最大值边界、最小值边界、负数、大小写混合、末尾换行符、带空格字符。花两分钟跑完这些,至少能挡住一大半低级失分点。别嫌麻烦,机试考场上一次语法错误、一次格式不匹配,比一道题不会做更让人懊恼。Day20我会开始练数据包解析类的模拟题,到时候继续记录。