资讯动态

27个趣味C++程序实战:排序、递归、链表与调试技巧

发布时间:2026/10/17 2:52:43 来源:尧图企业网站定制
简介这是一份精选的二十七个经典趣味C程序合集面向C初学者、编程爱好者以及需要课堂教学案例的开发者。代码示例由浅入深从Hello World变体、基础语法与各数据类型操作到数组和字符串处理、指针运算与内存管理、函数重载与默认参数再到面向对象的封装、继承、多态以及STL容器和算法、异常处理、文件输入输出等并附有简单游戏、数学问题求解器和文本分析工具等可运行小项目。压缩包共一百八十四个文件以二十八个cpp源文件为主体另有十九个doc说明文档、六个txt笔记和Visual C工程文件dsp/dsw及配套opt、ncb、plg整体仅二百二十二KB便于快速下载和对照学习。目前已有一百七十四人学习使用适合自学巩固、课程实验或复习备考示例注释清晰便于逐项调试。1. 27个经典趣味C程序一份能直接跑起来的编程手感训练集“27个经典趣味C程序”这个压缩包听名字像是大学期末作业合集实际拆开看里面是27个可以直接编译运行的C源码覆盖排序、递归、链表、栈、大数运算、约瑟夫环、汉诺塔这些经典题目每个.cpp都自带完整主函数和输入输出拿到手编译就能运行、运行就能看到效果。它解决的是很多自学者最尴尬的一段空窗期语法书看完了、OJ题也能磕磕绊绊刷几道但脱离题目模板很多人根本不知道一个完整程序该怎么组织。这套资源正好补上这段——从“写片段”到“写程序”的过渡适合刚学完C语法想上手完整代码的人也适合面试前快速过一遍基础算法的人。2. 先把套路吃透排序、贪心与递归类程序的通用骨架这27个程序里占比最大的就是“算法基础类”题目。我建议不要按压缩包默认顺序看先把排序、递归、贪心三类吃透。它们往后的链表和栈程序实际上是换了载体重复同一套工程化思维——先设计数据结构再写操作函数最后用main组织输入输出。2.1 排序类程序从冒泡到快排的递进包里最常见的排序程序是冒泡排序和选择排序的完整实现。冒泡排序能被选进这套合集不是因为它效率高而是因为它能把“比较—交换—循环不变式”完整展示出来运行时的每一轮输出都能看到元素逐步归位的过程。#include iostream #include vector using namespace std; // 每轮把最大的元素冒泡到末尾 void bubbleSort(vectorint arr) { int n arr.size(); for (int i 0; i n - 1; i) { bool swapped false; // 记录本轮是否发生交换可提前退出 for (int j 0; j n - 1 - i; j) { if (arr[j] arr[j 1]) { std::swap(arr[j], arr[j 1]); swapped true; } } if (!swapped) break; // 没有交换说明已经有序提前结束 } }这段代码注意两个参数第一层循环上限是n-1因为n个元素最多需要n-1轮第二层上限n-1-i是因为每轮确定一个最大值放到末尾下一轮就不用再碰它。swapped标志位是常见优化对已经有序的输入可以把复杂度从O(n²)降到O(n)。如果包里的排序程序是固定数组版本我一般会建议改成vector引用传参这样排序函数不拷贝整个数组且调用方直接看到排序结果。包里通常还有快速排序。快排一开始容易绕核心就一句话每一趟选一个基准值pivot小于它的放左边、大于它的放右边然后对左右两个子区间递归做同样的事。分治递归要看清华每一步的职责递归出口是区间长度为0或1递归调用是对左右两个子区间。快速排序平均复杂度是O(n log n)但最坏情况比如数组已经升序会退化成O(n²)面试时大概率会被追问这个退化的触发条件也是很多人容易翻车的地方。2.2 递归经典汉诺塔、八皇后背后的栈思维汉诺塔和八皇后是这套包里递归最有代表性的两个。它们的共同点是问题本身自带“递归结构”拆成子问题之后规模变小但性质不变。#include iostream using namespace std; // 把n个盘子从from借助helper移到to void hanoi(int n, char from, char helper, char to) { if (n 1) { // 递归出口只剩一个盘子直接移 cout from - to endl; return; } // 三步走先把上面n-1个移到helper再把最底层移到to最后把n-1个移到to hanoi(n - 1, from, to, helper); cout from - to endl; hanoi(n - 1, helper, from, to); }递归代码不长但每个参数都有明确的工程含义from/helper/to代表“从哪里来、借助哪里、到哪里去”每次递归调用时角色互换。hanoi(n-1, from, to, helper)的意思是把上面n-1层当做一个整体借助C柱从A移到B这样最大的盘子就露出来了可以单独从A移到C最后再把n-1层从B借助A移回C。很多新手在这里犹豫是因为盯着“盘子如何移动”而没把“n-1个盘子看成一个整体”。调试递归我常用笨办法n设成3手动走一遍输出观察移动是否合规比空想效率高得多。八皇后则是递归回溯的典型。核心思路是逐行放置皇后每放一个都要检查列、主对角线、副对角线是否冲突。最容易写的坑在冲突判断常见做法是维护三个数组分别记录列、两条对角线是否被占用位置(r, c)对应的两个对角线索引分别是r - c n和r c。很多程序集里的八皇后用的是二维数组逐格判断能跑但效率低、代码长我的建议是改成三个状态数组的写法。2.3 贪心与模拟经典题型的代码骨架贪心类程序在包里最常见的是“找零钱”“活动安排”这类。贪心代码本身通常很短难在选择贪心策略。找零问题的策略是“每次取面额不超过剩余金额的最大硬币”只在货币体系满足特定条件时成立包里的示例通常默认成立你拿去做扩展时要注意这一点。#include iostream #include vector using namespace std; // 金额单位元coinValues必须按降序排列 vectorint makeChange(int amount, const vectorint coinValues) { vectorint count(coinValues.size(), 0); int remain amount; for (size_t i 0; i coinValues.size(); i) { count[i] remain / coinValues[i]; // 当前面额最多可以拿几枚 remain % coinValues[i]; // 剩余金额进入下一轮 } return count; }这里最关键的是参数设计coinValues降序排列amount是整数count[i]记录第i种面额用了多少枚。如果数组不是降序结果完全错误如果amount为0所有count都是0这属于边界情况。我拿到这类程序通常会做两个验证金额能正好等值的情况以及金额小于最小面额的情况。这两种场景能暴露大多数实现问题。约瑟夫环属于“模拟类”程序在包里也经常出现。经典解法是用循环链表或数组模拟“数到m的人出列”。数组模拟版本要注意的是出列时不能直接删除元素否则下标全乱常见做法用标记数组或把出列位置置为-1遍历时跳过。循环链表的工程点在于“环形结构”和“删除节点时保留前驱指针”——这两点和下一章链表操作直接相关。3. 数据结构实战链表、栈队列与大数运算的落地写法第二章解决的是“控制流”问题第三章的程序开始转向“数据组织”。27个程序里有不少是手写链表、栈、队列、大数运算在OJ上这些题目容易直接用容器库跳过但在这套资源里会让你老老实实用结构体和数组自己造轮子。我建议按固定顺序读先把链表反转看懂再看栈和队列最后看大数运算。3.1 手写链表反转、合并与环检测链表程序在这套包里通常是“学生信息管理”“成绩统计”这类带节点增删的题目真正需要吃透的是反转链表它是链表面试题的根基。#include iostream using namespace std; struct Node { int data; Node* next; Node(int val) : data(val), next(nullptr) {} }; // 头插法反转链表prev指向已反转部分cur指向剩余部分 Node* reverseList(Node* head) { Node* prev nullptr; Node* cur head; while (cur ! nullptr) { Node* nextTemp cur-next; // 先记住下一个节点否则一会就丢了 cur-next prev; // 把当前节点的指针指向前一个 prev cur; // prev前移 cur nextTemp; // cur前移 } return prev; // 新的头 }这个实现最关键的是nextTemp这个临时变量。如果不先保存cur-next执行cur-next prev这一步后原来的后继节点就找不到了。参数说明函数接收头指针返回新头指针空链表和单节点链表都能正确返回。如果包里某个链表程序是“按值插入”注意它维护的通常是升序链表插入时要遍历到第一个比当前值大的节点插在它前面边界是插入头部和尾部这两处容易因为空指针访问翻车。我用这套反转逻辑去改“合并两个有序链表”的程序时发现只需要一个dummy头节点和一个循环里比较两个链表的当前值就能把两轮遍历合并成一轮代码会简洁很多。3.2 栈与队列括号匹配和循环队列的实现括号匹配是这套资源里“栈”的典型应用。栈的LIFO特性正好对应“最近出现的左括号最先配对”的规则。#include iostream #include stack #include string using namespace std; // 支持小/中/大括号的括号匹配 bool isBalanced(const string s) { stackchar st; for (char c : s) { if (c ( || c [ || c {) { st.push(c); // 左括号入栈等待配对 } else if (c )) { if (st.empty() || st.top() ! () return false; st.pop(); } else if (c ]) { if (st.empty() || st.top() ! [) return false; st.pop(); } else if (c }) { if (st.empty() || st.top() ! {) return false; st.pop(); } } return st.empty(); // 栈里还能残留说明有左括号没配对 }这里的边界检查是新手最容易漏的弹栈前要先st.empty()否则空栈上调用top()是未定义行为程序可能在运行后输出了一个看起来合理的结果实际已经越界。三种括号分开判断代码冗余但逻辑清晰包里的程序也基本都是这种“把情况分开处理”的风格。注意最后不能直接return true必须return st.empty()——即使所有右括号都配对了残留的未配对左括号依然是非法输入。很多人在这一步只检查“所有右括号都有左括号”忘了检查“左括号没有被右括号全部消耗”。队列类程序里循环队列是另一个重点。它的亮点在于用数组头尾指针模拟环形避免普通数组队列反复搬移数据。入队时rear (rear 1) % capacity出队时front (front 1) % capacity队空判断是front rear队满判断是(rear 1) % capacity front。我第一次看这段时觉得队满条件别扭后来明白了它牺牲一个存储单元来区分空和满否则在front rear时就无法判断到底是空还是满——这是教科书级的设计取舍面试里也经常被当作追问点。3.3 大数运算超越int/long long的加减乘除27个程序里几乎总有一两个大数运算常见的是大数加法和大数阶乘。大数的范围远超long long所以这类程序用字符串或vector存数字按位运算。包里若是大数加法代码通常长这样#include iostream #include string #include algorithm using namespace std; // 大数加法以字符串形式输入两个非负整数返回结果字符串 string addBigNumbers(const string a, const string b) { int i a.size() - 1, j b.size() - 1; int carry 0; string result; while (i 0 || j 0 || carry) { int sum carry; if (i 0) sum a[i--] - 0; // 字符转数字 if (j 0) sum b[j--] - 0; result.push_back((sum % 10) 0); // 当前位的结果 carry sum / 10; // 进位给下一位 } reverse(result.begin(), result.end()); // 从低位往高位算最后要反转 return result; }这里几个参数值得展开说。a和b只读不修改用下标从右往左读自然处理了低位对齐carry是进位标量范围是0到1因为单个位最大是99119result最后必须reverse因为是从低位开始push_back的直接返回得到的是逆序结果。如果输入字符串有前导零程序不会报错但结果可能带着错误的零头。所以我看这类程序时的第一个改进建议是先对输入做trimLeadingZeros。大数乘法比加法多一个维度临时数组长度是a.size()b.size()两层循环把a[i]*b[j]累加到临时数组的ij位最后统一处理进位。这套资源里的乘法程序如果走简化路线通常不用二维数组而用一维数组累加逻辑更绕但空间省很多。看这类代码时我总会先确认下标是从0开始还是1开始这个细节能坑掉大半天。4. 编译、调试与移植五个高频问题排查与避坑指南这套资源里的程序大多带“经典代码”的风格——可读性好但也意味着老写法多。老代码在现代编译器上跑起来会遇到一批很典型的问题。我拿到这套包后逐个编译时踩了不少坑下面五个是最常见的。4.1 现象控制台中文乱码现象在Windows上打开老代码运行后中文输出变成“锟斤拷”或一串问号。原因源文件编码和编译器/终端解释编码不一致。老代码大多是GBK/GB2312编码而现代编辑器比如VS Code默认UTF-8打开后直接把字节流按UTF-8解读要么显示乱码要么编译期报字符串常量非法。解决一种是把源文件用编辑器“另存为”显式转成GBK编码再交给老编译器另一种是改用现代编译器并在代码开头加setlocale(LC_ALL, )。我的习惯是统一把源码改成UTF-8所有输出改成英文或拼音一劳永逸不给编码留后患。4.2 现象编译通过但一运行就闪退现象编译成功窗口一闪而过完全看不到输出。原因代码末尾用了getch()或system(pause)这类暂停语句它们依赖conio.h和stdlib.h在不同的编译器上行为不一致也可能是IDE没有保持控制台窗口打开。解决老代码里system(pause)的位置我直接改成cin.get()或者IDE设置里打开“运行后暂停”。另一个容易被忽略的原因程序用了非标准头文件比如graphics.h、dos.h这类头文件在现代编译器里根本不存在。遇到依赖graphics.h的“趣味程序”建议直接跳过或改用标准库实现因为图形库的跨平台兼容性是个大坑。4.3 现象cin和getline混用导致输入被“跳过”现象先cinn再getline(cin, s)结果第一行getline读到空串。原因cinn读取时把换行符留在输入缓冲区紧接着的getline读到的就是换行符直接结束。解决在cinn后加一行cin.ignore()或者统一用getline读所有行再用stringstream解析。这个问题几乎出现在每一个用到字符串输入的经典程序里。我的记忆方法是cin读数据遇到空白就停但不消费后面的空白所以要在两种输入方式之间手动清空缓冲区。避坑的唯一可靠做法是不混用。4.4 现象代码在A环境编译通过换到B环境一堆报错现象同一份.cpp在老编译器里顺利编译放到新IDE或Linux环境后报“void main()”错误。原因老代码里的void main()不是标准C写法老编译器默认支持旧标准新编译器按C17/20标准对main返回值、类型检查、隐式转换要求更严。解决把void main()统一改成int main()并在末尾return 0。这类问题在包里的“趣味”程序里相当多。我的处理套路是现代编译器编译一遍把报错信息当代码规范体检报告看改完代码也就完成了现代化改造。4.5 现象运行结果“玄学”时对时错现象同一个程序输入相同有时输出正确有时错误或随机崩溃。原因几乎都是未初始化变量、数组越界、全局变量被无意修改。经典代码里的坑包括定义int a[10]却访问a[10]声明变量没赋初值就直接使用多个函数共用全局计数变量调用顺序不同就出错。解决拿到源码先搜所有变量声明一律初始化哪怕先给0只读的全局改成const共享计数改成函数参数传递不要用全局量。我后来养成了一个习惯拿到任何源码第一件事是搜有没有定义未初始化的指针再看所有数组下标是否可能越界。这一步能排除掉80%的“怎么这次又对了”问题。5. 把27个程序变成自己的东西加测例、重构与验证看到这里你已经知道每个程序的功能也知道会踩的坑。距离这套资源真正产生价值还差一步把这些“别人的代码”改造成“自己能跑能教能扩展的代码”。我的习惯是先验证、再重构。验证一套经典算法程序最快的方法是加一个测试函数把主干函数提取出来喂三组测例正常输入、边界输入长度0、数值0、负数、非法输入空字符串、栈空时弹栈。比如对排序程序我会在main之前加一个测试宏开关把排序函数从“固定数组版本”改成“支持任意vector ”的版本#include iostream #include vector #include cassert #include algorithm using namespace std; void testBubbleSort() { vectorint a {5, 1, 4, 2, 8}; vectorint expected a; sort(expected.begin(), expected.end()); // 调用bubbleSort后用assert判断结果是否等于std::sort的结果 // 建议再加空数组、单元素数组、完全逆序数组三组边界测例 // 断言失败会直接中止程序不需要手动盯输出 }这样做的价值有两个第一给排序程序加测例不是多此一举是在为将来复用做铺垫——没有测试的算法代码扔进项目库迟早会被某次边界调用触发问题第二重构本身是理解代码的最佳方式——当你把一段代码从“只能跑固定示例”改成“能处理任意输入”时你才算真正读懂了它这比“看懂输出”深一层。最后还有一个收尾技巧把包里的27个程序按“算法类型”重新建目录每个程序配一个README文件写上功能、输入范围、复杂度、可改进点。这样我随时可以在一周内把基础算法重新过一遍面试前复习也不用到处翻文件。从那以后我每次拿到别人的程序集都会强制走一遍“先编译、再喂边界测例、最后检查隐藏的全局变量和缓冲区残留”这三步。看起来慢实际上省掉了无数个下午的调试时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑