简介:C++编程实例源码合集收录了100个可直接运行的示例工程,覆盖基础语法、类与对象、模板与泛型、文件I/O、异常处理、STL容器及C++11新特性等主题,适合C++初学者、备考者及需要常用例程参考的开发者。压缩包内共557个文件,主体为107个cpp源文件和16个h头文件,并附带100个exe演示程序与106个bmp运行截图,另有dsw、dsp等工程文件便于重新编译,整体大小仅2.73MB。目前已有1659人学习下载。实例按难度渐进组织,从变量、循环、函数到继承、多态、模板层层递进,并结合图形输出与可执行程序展示运行结果,能帮助读者直观理解代码逻辑与效果,既可用于自主练习,也适合作为教学或自学阶段的参考素材。
1. 拿到 C++编程实例100篇 源码包,先别急着删 BGI
解压 C++编程实例100篇 C++软件源码.zip 后,很多人第一反应是“包坏了”:目录里没有整齐的一百个 .cpp,而是一堆 SVGA64K.BGI、EGAVGA.BGI、66.BMP 这类看起来跟编程毫无关系的文件。这份压缩包其实是早年 Turbo C / Borland C++ 时代的编程实例合集,BGI 是 Borland Graphics Interface 的显示驱动文件,BMP 是实例运行时加载的图片素材,源码本身隐含在 graphics.h 那一整套图形 API 里。它的价值不在目录整洁,而在于把 C++ 的变量声明、流程控制、函数、类与对象、模板、STL、文件 I/O、异常处理这些知识点拆成了 100 个可以直接编译跑起来的片段。适合两类人:一类是把 c++ 八股背熟了但没手写过完整程序的新手;另一类是接手老游戏源码、需要快速搞懂 BGI 图形接口和旧式文件操作的开发者。下面这套读法,能把这堆老文件变成你自己的 c++ 复习底稿。
2. 拆解实例里的基础语法与经典算法:字符串、质数、冒泡
2.1 字符串数组初始化:char 数组、指针与 string 到底差在哪
实例里老代码最常见的一段就是“C++字符串数组初始化”,写法五花八门,但多数人只记住了char s[] = "hello",却没搞懂它和char*的区别。看下面这段:
#include <iostream> #include <string> using namespace std; int main() { char arr[] = "hello"; // 栈上分配 6 字节,内容可改 const char* ptr = "hello"; // 指向字符串字面量,通常位于只读区 string str = arr; // 拷贝到 string 对象,自动管理内存 arr[0] = 'H'; // 正确,arr 可写 // ptr[0] = 'H'; // 运行期崩溃,只读区不可写 cout << sizeof(arr) << endl; // 输出 6,包含结尾 '\0' cout << str.size() << endl; // 输出 5,不包含结尾 '\0' return 0; }char arr[]在栈上分配的实际字节数是strlen("hello") + 1,那个隐藏的'\0'是很多新手排查“为什么数组大小不对”的起点。const char* ptr指向的字符串字面量在多数编译器的只读段里,写它等于往只读内存写数据,连报错都可能不是编译期而是运行期。string则把长度和容量封装好了,size()返回有效字符数,不需要关心结尾标记。从这 100 篇实例里挑相关源码时,先拿这三行做“初始化对照”,再看老代码里频繁出现的strcpy越界问题就一目了然了。
2.2 流程控制落点:把“判断质数”的 if / for 写成可优化版本
判断质数是实例里出现频率最高的流程控制练习题。新手版本循环到n本身,复杂一点到sqrt(n),真正能写进代码库的是下面这个 6k ± 1 优化版:
#include <iostream> #include <cmath> using namespace std; bool isPrime(int n) { if (n < 2) return false; if (n == 2 || n == 3) return true; if (n % 2 == 0 || n % 3 == 0) return false; for (int i = 5; i * i <= n; i += 6) { if (n % i == 0 || n % (i + 2) == 0) { return false; } } return true; } int main() { int count = 0; for (int i = 1; i <= 200; i++) { if (isPrime(i)) { cout << i << " "; if (++count % 10 == 0) cout << endl; } } return 0; }i * i <= n避免了sqrt(n)的浮点转换和精度损失,也防止了大数下sqrt带来的时间开销。步长 6 的原理是:大于 3 的质数必然落在6k-1或6k+1这两列,所以循环只需要检查i和i+2两个候选,整体复杂度比逐个奇数试除又低了一半。这里if (++count % 10 == 0)也顺便示范了前置自增和短路求值的组合用法,实例里对应的for、while、switch-case都能从这种小函数里找到真实上下文。
2.3 冒泡排序算法 c++ 实例:swap 与提前退出的边界
冒泡排序是 C++编程实例 里必然出现的基础算法,但很多人不知道老源码里的冒泡有两个问题:一是没加提前退出,二是用 C 风格指针对数组名做sizeof拿不到长度。看一下加上理想剪枝的版本:
#include <iostream> #include <algorithm> // std::swap using namespace std; void bubbleSort(int arr[], int n) { 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]) { swap(arr[j], arr[j + 1]); swapped = true; } } if (!swapped) break; // 内层没有交换,说明已经有序 } } int main() { int data[] = {5, 3, 8, 1, 9, 2}; int n = sizeof(data) / sizeof(data[0]); // 这一步必须在原数组上做 bubbleSort(data, n); for (int v : data) cout << v << " "; return 0; }swapped标志位让最好情况下的时间复杂度降到 O(n)。sizeof(data) / sizeof(data[0])只能在数组定义处计算,一旦把数组名当作参数传进bubbleSort,函数内部拿到的只是退化成指针的首地址,sizeof(arr)会变成指针大小而非数组大小,这是 C/C++ 语法里最容易和“引用传递”混淆的坑。老实例里大量使用这种int arr[]形参写法,读的时候要意识到形参列表里的[]本质是*arr。
3. 类、模板与 STL:从实例源码里挖出面向对象的完整链条
3.1 构造函数与析构函数:三法则是在哪儿翻车的
实例中“类的定义、对象的创建、构造函数与析构函数”通常放在一起讲,但不少例子里埋着浅拷贝的隐患。下面的Counter用一个动态分配的int来演示为什么拷贝构造和拷贝赋值必须和析构函数一起出现:
class Counter { private: int* count; public: explicit Counter(int v = 0) : count(new int(v)) {} // 拷贝构造函数:深拷贝 Counter(const Counter& other) : count(new int(*other.count)) {} // 拷贝赋值运算符:先释放旧资源再拷贝 Counter& operator=(const Counter& other) { if (this == &other) return *this; // 自赋值检查 delete count; count = new int(*other.count); return *this; } ~Counter() { delete count; } void inc() { (*count)++; } int value() const { return *count; } };如果没有自赋值检查,a = a会把a.count先释放再读取,产生悬垂指针。析构、拷贝构造、拷贝赋值得一起定义,这就是 C++ 的“三法则”。实例里的类通常只写了析构函数去delete资源,却忘了拷贝构造,导致用一个对象初始化另一个对象时发生 double-free。现代做法是直接用shared_ptr或unique_ptr替代裸指针,但在“C++编程知识库”语境下,理解三法则依然是看懂老源码的前提。
3.2 模板类链表:C++模板和泛型编程的最小完整实现
c++模板类链表是很多实例里的压轴小题,它同时考察模板语法、指针操作和析构释放。实现一个只带头插和遍历的最小单向链表:
template <typename T> struct Node { T data; Node* next; Node(const T& val, Node* nxt = nullptr) : data(val), next(nxt) {} }; template <typename T> class LinkedList { private: Node<T>* head; public: LinkedList() : head(nullptr) {} void push_front(const T& value) { head = new Node<T>(value, head); } void print() const { for (Node<T>* cur = head; cur != nullptr; cur = cur->next) { cout << cur->data << " "; } cout << endl; } ~LinkedList() { while (head != nullptr) { Node<T>* tmp = head; head = head->next; delete tmp; } } };模板不是“把一个类型换成 T”,而是编译期生成多份代码。push_front里new Node<T>(value, head)这行同时做了两件事:分配新节点、让新节点的next指向旧头节点,然后用head指向新节点,顺序一旦写反链表就会断。析构函数必须显式遍历删除每个节点,否则内存泄漏,这也是“c++ 栈空间”面试题里常追问的点:局部对象自动调用析构不代表堆上资源会自动释放。
3.3 STL 容器实战:vector 扩容与 map 查找的典型写法
实例里 STL 部分常见的组合是“vector 存数据 + map做索引 + sort/find 做检索”,它比手写数组灵活得多:
#include <vector> #include <map> #include <string> #include <algorithm> using namespace std; vector<pair<string, int>> buildRank(const map<string, int>& scores) { vector<pair<string, int>> items(scores.begin(), scores.end()); sort(items.begin(), items.end(), [](const auto& a, const auto& b) { return a.second > b.second; }); return items; }vector的构造函数直接接受一对迭代器,把map里的键值对整体拷贝进来,这一行省掉了手写循环。sort的第三个参数是 lambda 表达式,按second降序排列,这是 C++11 之后标准库最常用的写法,也是实例里“lambda表达式、范围for循环”这些新特性最自然的落点。关于容器选型,实例代码用到的容器和适用场景可以参照下面这张表:
| 容器 | 底层结构 | 典型操作 | 适用场景 |
|---|---|---|---|
| vector | 连续内存 | push_back、operator[] | 随机访问、尾部插入 |
| list | 双向链表 | push_front、insert | 中间频繁插入删除 |
| map | 红黑树 | find、operator[] | 有序键值查找 |
| unordered_map | 哈希表 | find、emplace | 无序 O(1) 查找 |
选型原则一句话:需要按下标随机访问就选vector,需要键值快速定位且不关心顺序就选unordered_map,如果实例代码中出现“按字典序遍历”的需求,那才轮到map。读这 100 篇实例时,先拿这张表去套代码里出现的容器,比逐行读push_back效率高得多。
4. BGI 图形驱动与文件 I/O:老 C++ 小游戏的源码读法
4.1 BGI 驱动是什么:SVGA64K.BGI 和 EGAVGA.BGI 的用途
压缩包里的SVGA64K.bgi和EGAVGA.BGI是 Borland Graphics Interface 的显示驱动文件,前者对应 Super VGA 640x480 256 色,后者是对早期 EGA/VGA 显卡的兼容驱动。老实例里要玩图形编程,第一步就是加载 BGI 驱动:
#include <graphics.h> #include <conio.h> int main() { int gd = DETECT, gm; initgraph(&gd, &gm, "C:\\TC\\BGI"); // 第三个参数是BGI驱动查找路径 setcolor(RED); circle(320, 240, 100); outtextxy(280, 240, "Hello C++"); getch(); closegraph(); return 0; }initgraph的第三个参数告诉库去哪里找.BGI驱动文件,路径错就报错,很多初学者卡在这一步。DETECT表示让库自动检测当前显卡模式;如果指定gd = VGA; gm = VGAHI;则跳过检测直接按 640x480x16 色初始化。这个接口不是标准 C++,而是 Borland 对自家编译器的扩展,所以现在的 Visual Studio 直接编译会报graphics.h not found。常见做法是保留graphics.h的调用逻辑,用 Win-TC 或 DOSBox 复原老环境,或者用 SDL2 把这几个绘图函数重写成跨平台版本。
4.2 小游戏实例的主循环:状态更新、碰撞检测、渲染分离
这 100 篇里的 c++ 小游戏实例,最常见的是贪吃蛇、俄罗斯方块、飞机大战这一挂。它们的框架高度相似,核心是一个“永远在跑”的游戏循环:
while (!gameOver) { if (kbhit()) { // 有没有按键 char key = getch(); // 读取方向键 handleInput(key); } updatePosition(); // 移动蛇头/方块下落 checkCollision(); // 撞墙、撞自己、消行 render(); // 清屏 + 重绘所有图形 delay(100); // 控制帧率 }kbhit()检查键盘缓冲区是否有输入,getch()不经过回车直接读取按键,这两者是conio.h提供的,只在老式控制台环境下有效。这里的精妙点在于输入、逻辑、渲染三者不混在一起:输入函数只负责标记方向,updatePosition才真正修改数据,render只读数据画图。即使现在用 Unity 或 UE 写游戏,主循环的分层思路也没变。读这类源码时重点看updatePosition里怎么处理边界和 self-collision,那才是小游戏编程实例的真正难点。
4.3 文件 I/O 与异常处理:读 BMP 和 try-catch 的关系
实例里常有把 BMP 图片读进内存、再通过getimage画到屏幕上的例子。老做法是FILE*加fread一行行解析 BMP 头,新做法用ifstream加try-catch:
#include <fstream> #include <vector> #include <stdexcept> using namespace std; vector<unsigned char> loadBMP(const string& path) { ifstream file(path, ios::binary); if (!file.is_open()) { throw runtime_error("cannot open file: " + path); } // 跳过 BMP 文件头,只读取像素数据区 file.seekg(54, ios::beg); return vector<unsigned char>(istreambuf_iterator<char>(file), istreambuf_iterator<char>()); }seekg(54, ios::beg)跳过的 54 字节就是 BMP 标准文件头,这个数字是 BMP 格式定死的,老实例里通常用sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)替代,语义更清晰。把构造vector和文件流迭代器放在 return 语句里,利用了 C++11 的移动语义,避免大图像的二次拷贝。实例里异常处理经常只演示try-catch怎么捕获,但实际项目中真正要设计的是“异常边界”:在哪个函数捕获、要不要把资源释放放在 RAII 类里。对照实例里的fread失败分支,能明显看出新旧两种风格对资源安全的关注差异。
5. 二分查找、面试八股与老工程排错:临门一脚
5.1 手写 lower_bound 与标准库二分查找的差异
实例里“c++ 二分查找”一般给的是标准库用法:
#include <algorithm> vector<int> v = {1, 3, 5, 7, 9}; bool exist = binary_search(v.begin(), v.end(), 5);但面试手撕时更常考一个限定条件:找到第一个不小于 target 的位置。标准库对应的是lower_bound:
int lowerBound(const vector<int>& a, int target) { int lo = 0, hi = (int)a.size(); // 左闭右开 while (lo < hi) { int mid = lo + (hi - lo) / 2; if (a[mid] < target) lo = mid + 1; else hi = mid; } return lo; }lo + (hi - lo) / 2不是炫技,而是防止lo + hi整型溢出。左闭右开区间有个好处:当lo == hi时循环退出,此时lo就是答案,不需要额外的边界标志,这和 STL 迭代器的end()习惯一致。
5.2 覆盖、隐藏、重载:C++ 八股的高频陷阱
百篇实例里只要涉及继承,基本都会出现这三个概念的混淆。重载发生在同一作用域,覆盖发生在虚函数上,而隐藏是派生类中的同名函数把基类同名函数“藏起来”:
class Base { public: virtual void show() { cout << "Base"; } void print(int x) { cout << x; } }; class Derived : public Base { public: void show() override { cout << "Derived"; } // 覆盖 void print() { cout << "no arg"; } // 隐藏,不是重载 };show加override后,通过基类指针调用会走虚表到派生类版本;print不加override,派生类对象调print()时基类的print(int)完全不可见,传参会编译失败。这正是 c++ 面试题里“问实现、问区别、问后果”三连问的标准素材。
5.3 老源码编译排错与解压修复技巧
如果解压时看到invalid zip archive: could not find EOCD,先别急着换工具,这是压缩包文件尾记录缺失或文件被截断的信号,常见做法是用 7-Zip 的“打开压缩包”强制读取,能救出部分文件就尽快复制。老实例里的源码文件多数是 ANSI 编码,Visual Studio 打开中文注释变乱码时,用“文件 -> 高级保存选项”把编码改成 GB2312 再编译。vscode 配置 c/c++ 环境时,记得在tasks.json的编译命令加上-std=c++11,否则 lambda 和 range-for 会直接编不过——这几乎是新手复现实例失败率最高的一个原因。
本文还有配套的精品资源,点击获取