简介:C++编程实例100篇是一份以实践为导向的C++学习资源包,精选100个典型示例,覆盖变量、数据类型、运算符、控制结构、函数、文件输入输出、面向对象(封装、继承、多态)、模板、异常处理及标准库容器等内容。资源包共包含557个文件,其中107个cpp源文件、106个obj中间文件与100个exe可执行程序一一对应,便于边看边练;另有106个bmp图片与7个bgi图形驱动,用于显示图形程序输出;其余为dsp、dsw、prj、plg等工程配置文件和16个头文件,整体采用rar压缩仅2.47MB。目前已吸引42867人学习下载。通过这套实例,读者不仅能阅读关键知识点的完整代码,还能直接运行exe观察结果、对照源码理解逻辑,并基于工程文件进行二次修改与调试,从基础语法到高级特性形成系统认知,是提升C++编码能力的实用参考资料。 “C++编程实例100篇”这个标题,我在很多学习资料和课程里都见过,也经常有读者问我,到底该怎么刷这种实例集才不算白费功夫。今天我就把这两年带新人、自己在项目里反复用C++写底层逻辑攒下的经验,结合“实例驱动”这条主线,拆开揉碎了讲一讲。不管你是刚啃完语法书、准备找C++开发岗的应届生,还是工作中需要用C++写高性能模块、但平时主力语言不是C++的工程师,这篇文章都能给你一些能直接上手的思路。
我先说一个很直接的观点:C++这语言,看十遍语法不如亲手敲一个小例子。实例不是用来“背”的,是用来逼你思考的。一个实例题从“看懂”到“能默写”再到“能改着用”,每一层跨越都对应着真实项目里的某种能力。接下来的内容,我就按我心目中刷完这100个实例最合理的路线来讲,顺带把这些实例背后真正值钱的细节都挖出来。
1. 为什么是实例驱动:C++学习路线的一次重新规划
很多人学C++的路径是先啃完一本大部头,然后才开始刷题,结果啃到第三章就开始怀疑人生。我见过太多人卡在指针和内存管理上,不是因为他们笨,而是因为语法知识和实际运行机制之间缺了一座桥。实例就是这座桥。
1.1 从语法到工程思维,实例帮你补上最关键一环
举个最典型的例子:指针。书上跟你说“指针是变量的地址”,你记住了,但你不知道为什么函数参数要传指针而不是传值。等你写到链表反转、写二叉树的层序遍历、写一个内存池的管理类时,你会突然明白,指针其实是“对内存的直接操作权”,它既是C++高效的原因,也是大部分bug的来源。
实例集里那些看似简单的题目,比如“用指针实现字符串拷贝”“用指针遍历多维数组”,练的就是这种对内存模型的肌肉记忆。我自己的体会是,刷完十几个指针相关的小例子之后,再看那些“指针指向指针”“函数指针数组”之类的复杂声明,恐惧感会消失大半。这种“原来如此”的顿悟,就是实例驱动的核心价值。
1.2 这套实例集的选题逻辑与适合人群
我拿到的这套“C++编程实例100篇”,选题覆盖了变量与数据类型、控制流、数组、指针、字符串、函数、结构体与类、文件操作、算法(排序搜索)以及多线程等十个左右的大类。这个分布非常合理,基本对齐了一个合格的C++开发者日常工作中最高频的知识点使用频率。
适合刷这套实例的人有两类。第一类是刚学完C++基础语法、感觉自己全看懂了但一写就卡壳的初学者,这套实例是最好的“语法测验场”。第二类是准备面试的求职者,很多经典题型如冒泡排序、字符串处理、链表操作都是面试手撕代码的热门原型,把100个实例里算法相关的题目刷透,面试时心态会稳很多。如果已经有五年以上C++经验,这套实例作为查漏补缺也不错,但重心可以放在最后的进阶实例上。
2. 环境与工具链准备:别让配置拖慢你的节奏
工欲善其事,必先利其器。刷实例的一大痛点就是环境搭不好,明明代码没错,却因为编译器的坑浪费大量时间。我见过太多新手在这上面栽跟头。
2.1 最省心的编译器选型:MinGW-w64与MSVC怎么选
Windows平台上最常见的两个选择是MinGW-w64和MSVC。MinGW-w64是GCC在Windows上的移植版,它和Linux的GCC行为几乎一致,语法检查、标准支持都稳定,而且命令行操作简单,对初学者很友好。MSVC是微软自家的编译器,和Visual Studio深度集成,调试体验极佳,但它的编译器对某些C++标准特性的支持方式和GCC有细微差别。
我个人的建议是:如果只是刷语法和算法题,用MinGW-w64就够了,安装方便,命令行可直接用。如果打算长期在Windows上做项目、要用到Windows API或者需要图形化调试,那还是上Visual Studio Community版配MSVC更省心。实测下来,两个编译器对标准C++的代码兼容性都很好,但把一套代码两处编译的习惯养成,对以后跨平台开发非常有益。
2.2 VSCode写C++的完整配置过程
VSCode确实是个很好的选择,轻量、插件生态丰富、启动速度快。我日常刷实例就用它,配好后体验完全不输重型IDE。配置步骤如下:
- 安装VSCode,然后在扩展市场搜“C/C++”,安装微软官方发布的C/C++扩展。
- 安装编译器。如果选了MinGW-w64,可以用winlibs.com上提供的自动解压版本,解压后把bin目录的路径加到系统环境变量PATH里。
- 在终端里输入
g++ --version验证编译器是否就位。能看到版本信息就说明成功了。 - 在VSCode里用快捷键
Ctrl+Shift+P打开命令面板,输入“C/C++: Edit Configurations (UI)”,让扩展自动识别编译器路径。 - 配置调试。按
F5选择“C++ (GDB/LLDB)”环境,VSCode会自动生成launch.json和tasks.json。核心是把tasks.json里的编译命令改成g++ -g 源文件名.cpp -o 输出文件名.exe,这样按F5就能一键编译加调试。
我在配置时踩过最大的坑是乱码问题。Windows终端默认代码页是GBK,而VSCode默认UTF-8,导致中文输出全是乱码。解决办法是在launch.json的调度配置里加上"console": "externalTerminal",或者把源码中文字符串改成英文。更推荐后者,因为刷题阶段用英文输出能提前适应真实开发环境。
3. 核心基础实例解构:数组、指针与字符串
这三大类是C++实例集里最硬核的部分,也是区分“会写C++”和“懂C++”的分水岭。我把这部分的实例拆成三个层次来讲,每一层都有具体的代码和讲解,方便对照练习。
3.1 数组与多维数组:从连续内存理解数据组织
数组是所有容器的基础。一维数组很简单,就是个连续的内存块。但多维数组就没那么直观了,尤其是当它和指针纠缠在一起的时候。我建议先做这几个实例:数组元素逆序、数组查找最大值、二维数组转置、多维数组求和。
二维数组转置这个实例很经典。很多新手第一反应是“交换a[i][j]和a[j][i]不就行了”,但真正写的时候会发现,方阵和对角线对称的数组能转置成功,非方阵就会越界。这就是实例的价值:它逼着你思考下标和内存布局的关系。多维数组在C++里是行优先存储的,也就是说同一行元素在内存中是紧挨着的。理解这一点后,你就能明白为什么遍历二维数组时“外层行、内层列”的效率远高于“外层列、内层行”,因为后者会频繁跳跃内存地址,缓存命中率惨不忍睹。
#include <iostream> const int ROWS = 3; const int COLS = 4; void transpose(const int src[ROWS][COLS], int dst[COLS][ROWS]) { for (int i = 0; i < ROWS; i++) { for (int j = 0; j < COLS; j++) { dst[j][i] = src[i][j]; } } } int main() { int a[ROWS][COLS] = { {1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12} }; int b[COLS][ROWS] = {0}; transpose(a, b); for (int i = 0; i < COLS; i++) { for (int j = 0; j < ROWS; j++) { std::cout << b[i][j] << " "; } std::cout << std::endl; } return 0; }3.2 指针与动态内存:实例里最容易被误解的细节
C++指针的难点不在“解引用”这个操作本身,而在“指针的类型决定了解引用时读取几个字节”这一层。很多实例题就是围绕这个特性设计的,比如用不同类型的指针访问同一个地址、用void*传参再转回原类型、用指针实现一个简单的动态数组。
我在刷“用指针实现动态数组”这个实例时,第一次意识到new[]和delete[]要成对出现。后来在项目里排查内存泄漏时,几乎所有问题都能追溯到这种不对称使用。初学者刷指针实例时,我强烈建议搭配一个内存检测工具,Windows下可以用Visual Studio的CRT调试堆,Linux下可以用valgrind。哪怕只是写一个几行的测试代码,也值得跑一下内存检查,这会帮你建立对内存生命的敏感度。
#include <iostream> void fill_array(int* arr, int size, int value) { for (int i = 0; i < size; i++) { arr[i] = value + i; } } int main() { int n = 5; int* dyn_arr = new int[n]; fill_array(dyn_arr, n, 10); for (int i = 0; i < n; i++) { std::cout << dyn_arr[i] << " "; } std::cout << std::endl; delete[] dyn_arr; return 0; }3.3 字符串处理的五个高频实例:初始化与转换是重头戏
字符串是面试和日常开发里出现频率最高的类型之一。C++有两种字符串形态:C风格字符串(char[])和std::string。实例集里最值得重点练的是这么几类:C风格字符串的拷贝与拼接、std::string的查找与替换、字符串转数字、数字转字符串、以及字符串按分隔符拆分。
其中“C++字符串数组初始化”这个知识点看着简单,其实坑不少。字符数组char str[10] = "hello"会在末尾自动补\0,所以实际占用6字节。而当你用char str[10] = {'h','e','l','l','o'}这种初始化方式时,剩余位置自动补0,但如果没给足大小,就收不到终止符了。我自己刷题时就因为这种方式在输出时多打了一串乱码,从此记住了“C风格字符串必须以\0结尾”这条铁律。
#include <iostream> #include <string> #include <sstream> int main() { std::string num_str = "12345"; int num = 0; std::stringstream ss(num_str); ss >> num; std::cout << num + 1 << std::endl; std::string back = std::to_string(num); std::cout << back + "!" << std::endl; std::string text = "a,b,c,d"; std::string token; std::stringstream stext(text); while (std::getline(stext, token, ',')) { std::cout << token << std::endl; } return 0; }4. 经典算法实例的工程化写法:冒泡排序与查找
算法实例在100篇里占比不小,排序和查找是绝对的主角。这些实例真正的价值不是让你背代码,而是让你理解“同样的逻辑,不同的写法在真实机器上跑出来的效果差别有多大”。
4.1 冒泡排序实例的三种写法与性能差异
冒泡排序是几乎所有C++入门书的第一课。但刷实例时,建议至少写出三种版本:基础版、带标志位的优化版、以及泛型函数模板版。
#include <iostream> template <typename T> void bubble_sort(T arr[], int n) { for (int i = 0; i < n - 1; i++) { bool swapped = false; for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { T temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; swapped = true; } } if (!swapped) { break; } } } int main() { int arr[] = {5, 1, 4, 2, 8}; int n = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, n); for (int i = 0; i < n; i++) { std::cout << arr[i] << " "; } std::cout << std::endl; return 0; }我建议用模板而不是直接写int版本,因为这让你写的排序能直接用于double、float乃至自定义结构体(只要重载了>)。这其实就是工程里泛型编程的雏形。你还可以在bubble_sort函数里加一个bool is_sorted检测,虽然在基础版里不减少比较次数,但遇到基本有序的数组时,能大幅减少交换次数,性能提升明显。
4.2 二分查找实例:为什么“边界条件”这么容易写错
二分查找的代码就那么几行,但能在白纸上完全写对的人不多。最常见的bug出现在边界更新上:当mid不等于target时,到底应该是left = mid + 1还是left = mid?这个选择直接决定了循环条件是left < right还是left <= right。
一个稳妥的写法是“左闭右开”区间。初始化left = 0,right = n,循环条件是while (left < right),中间值mid = left + (right - left) / 2。如果arr[mid] < target,说明目标在右半区,更新left = mid + 1;否则right = mid。这样写的好处是循环结束时left == right,不需要纠结到底是left还是right是答案位置。这个范式在很多标准库实现里都能看到,养成习惯后写很多边界类算法都会顺手很多。
4.3 实例里的边界值测试与数据构造思路
刷实例时我特别想强调一点:不要只测题面给的输入,要自己构造边界值。对排序算法,构造一个空数组、一个全相同元素的数组、一个逆序数组;对查找算法,构造空数组、单元素数组、目标不存在于数组内的情况。这些边界测试能帮你提前暴露大量隐藏bug,也是培养测试思维最低成本的方式。在实际开发中,很多线上事故恰恰是边界条件没覆盖导致的。
5. 进阶实战类实例:多线程、异步与constexpr
当基础实例刷得差不多后,就该接触那些有“现代C++味道”的进阶实例了。这些题目不仅用到了C++11之后的新特性,也贴近真实服务的性能优化要求。
5.1 C++多线程实例:从线程创建到数据竞争
多线程是很多C++开发岗位的硬性要求,也是实例集后面通常会出现的内容。最基础的多线程实例是创建两个线程分别打印奇数和偶数,然后用std::mutex保证输出不错乱。初学者刚接触锁时最常见的错误是忘记加锁或者加锁范围过大。我在初学时写过一个多线程累加的实例,开了四个线程同时对一个全局变量自增,结果加了锁和没加锁跑出来的结果天差地别,这就是数据竞争的直观体现。
#include <iostream> #include <thread> #include <mutex> #include <vector> std::mutex mtx; int counter = 0; void add_count(int times) { for (int i = 0; i < times; i++) { std::lock_guard<std::mutex> lock(mtx); counter++; } } int main() { std::vector<std::thread> threads; for (int i = 0; i < 4; i++) { threads.emplace_back(add_count, 100000); } for (auto& t : threads) { t.join(); } std::cout << "counter = " << counter << std::endl; return 0; }如果你刷完基础多线程实例后还有精力,建议顺便了解一下std::atomic。在某些场景下,原子变量的性能远优于互斥锁,因为它在无竞争时不会陷入系统调用。这个知识点在面试时也经常被问到。
5.2 异步编程与实例中的回调陷阱
异步编程在C++里最常见的实现是std::async和std::future。一个经典实例是:在后台线程计算一个耗时结果,主线程继续做其他事,需要结果时再通过future.get()获取。这个实例看着简单,但暗藏一个陷阱:如果主线程一开始就调用.get(),实际上就变回同步执行了,异步的优势完全丧失。应该在发起异步任务后,先干一些不需要那个结果的工作,最后再等待future。
#include <iostream> #include <future> #include <thread> #include <chrono> int heavy_compute(int x) { std::this_thread::sleep_for(std::chrono::milliseconds(1000)); return x * x; } int main() { std::future<int> result = std::async(std::launch::async, heavy_compute, 12); std::cout << "do something else..." << std::endl; std::cout << "result = " << result.get() << std::endl; return 0; }5.3 constexpr与编译期计算的实用场景
constexpr从C++11引入,之后在C++14、C++17和C++20中逐步放宽。它在实例集里通常以“计算阶乘”“计算斐波那契数列”等形式出现。这类实例的核心意义是让你理解“编译期计算”这个概念:如果函数的入参是编译期常量,那么计算结果也能在编译期算好,运行时直接用结果,完全省去函数调用的开销。
#include <iostream> constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); } int main() { constexpr int result = factorial(10); std::cout << result << std::endl; return 0; }在实例里看到constexpr时,最好顺手验证一下C++标准版本的差异。C++11的constexpr函数体只能有一条return语句,而C++14开始允许在constexpr函数里写循环和局部变量,到C++20连std::vector都能在constexpr里用了。这些差异在实际工程中会直接影响代码的可读性和兼容性。
6. 常见问题与排查技巧:刷实例时最容易卡住的五个坑
刷百例的过程中,很多人真正的瓶颈不是理解算法,而是被环境问题和各种莫名其妙的小错误绊住。这里把我自己和读者群里高频出现的问题整理成一张速查表,方便对照排除。
| 现象 | 可能原因 | 排查与解决办法 |
|---|---|---|
编译报错“undefined reference tomain” | 源文件没有main函数,或者多个源文件都有main且链接时冲突 | 确认只有一个入口函数,或者在编译命令里只编译目标源文件 |
| 程序运行时崩溃“segmentation fault” | 访问了已释放的内存或越界访问数组 | 用gdb或addr2line定位崩溃行号,重点检查指针释放后是否还在使用 |
| 数组输出乱码 | 字符数组没有以\0结尾,或者字符集不一致 | 检查字符串初始化方式,统一使用UTF-8编码编译 |
| scanf读取无效或无限循环 | 输入缓冲区残留换行符或类型不匹配 | 在读取后加while(getchar() != '\n');清空缓冲区,或者改用std::cin更安全 |
| 多线程程序输出顺序不对 | 没有加锁或未对共享变量同步 | 使用std::mutex保护共享输出流,或用std::atomic代替普通变量 |
6.1 scanf和输入缓冲区的经典陷阱
实例集里早期一定会出现scanf()。它功能强大,但对缓冲区敏感。最常见的问题是连续读取多个值时,前一次输入的回车符留在缓冲区,导致下一次读取“跳过”了。比如下面这段代码:
#include <cstdio> int main() { int age; char name[50]; printf("Enter age: "); scanf("%d", &age); printf("Enter name: "); gets(name); // 不推荐,但很多老实例里有 printf("%d %s\n", age, name); return 0; }这种写法在输入年龄后按回车,gets会直接读到空字符串。解决办法是在gets之前加一行while(getchar() != '\n');把残留的回车清掉。需要强调的是,现代C++工程里更推荐用std::cin >>配合getline()来处理输入,它们对缓冲区的处理更直观,虽然也有自己的坑,但对新手更友好。
6.2 字符串初始化的内存细节:从char数组到std::string
很多新手会混淆这几种写法:char str1[] = "hello";、char* str2 = "hello";和std::string str3 = "hello";。第一种分配在栈上,内容可以修改;第二种指向字符串字面量,存放在只读区,修改它会直接崩溃;第三种是标准库容器,自动管理内存,日常最推荐。
我在项目里遇到过一种典型错误:函数返回了一个局部栈上字符数组的指针,调用方再使用它时拿到的全是乱码。原因是局部数组函数结束时已被销毁。如果非要返回C风格字符串,需要new[]动态分配,并明确告知调用方需要delete[]。如果刷实例时碰到这类问题,建议顺手把std::string的拷贝、移动、c_str()这些成员函数也练一遍,它们才是真正适合工程场景的工具。
6.3 指针误用与程序崩溃的定位思路
指针误用是C++新手崩溃的最大来源。定位此类问题,我最推荐的三板斧:第一是编译时打开-Wall -g选项,让编译器帮你检查可疑代码;第二是运行时用gdb,程序崩溃后输入bt(backtrace)直接查看调用栈,一眼就能定位到出错函数;第三步是在可疑位置前后加打印或日志,用二分法缩小问题范围。
6.4 多线程下的输出错乱与死锁
多线程实例里,如果直接在多个线程里std::cout,输出顺序会乱掉。这是因为std::cout内部虽然有缓冲,但多个线程同时写入时没有原子性保证。解决办法是给输出加锁,或者用一个字符串流先格式化好再一次性输出。死锁问题在实例级别的代码里不太常见,但一旦涉及多个互斥锁嵌套,就要注意加锁顺序必须全局一致,不然两个线程互相等待对方释放锁,程序就卡死了。
6.5 编译优化带来的诡异行为
新手刷实例默认不开优化,但一旦在项目里开了-O2或/O2,一些“看似正确”的代码可能行为异常。常见原因是未定义行为(UB),比如有符号整数溢出、数组越界、解引用空指针等。编译器在优化时默认这些行为不会发生,一旦发生,它可能做任何事。刷实例时如果某种写法编译不过,或者在某些优化级别下结果不对,先别怀疑编译器坏了,优先检查代码里有没有未定义行为。
7. 从实例到工程:把百例变成自己的武器库
当100个实例都刷完之后,我强烈建议做一次收尾动作,把零散实例重新整理成自己的代码片段库或者小型工具库。这一步能让学习效果产生质变。
7.1 实例分类整理的模板与算法索引
我自己的整理方式是按功能建目录,而不是按学习顺序。比如把字符串处理相关的实例全部放到string_utils/目录,把排序查找类放到algo/目录,把多线程放到thread/目录。每个目录里放一个readme.md,记录这个模块里的实例分别解决了什么问题、代码里哪个函数可以直接复用。当项目里需要字符串拆分时,我直接从这个库里拷贝,而不是重新翻书。
7.2 如何把实例中的知识迁移到实际开发
实际开发不会给你一个“请用冒泡排序”的需求,但会给你“请对用户列表按注册时间排序”的需求。真实转换的关键是:把算法和业务解耦。比如你已经写过一个模板化的冒泡排序,这时候只要给User结构体重载operator>,就能直接复用。实例教会你的是抽象能力:看到一个具体问题,能把它归类到“这本质上是排序问题” “这本质上是查找问题”。这种抽象能力才是刷100个实例最值得沉淀的收获。
7.3 推荐进阶扩展方向
刷完这套100例后,下一步的进修方向取决于你的目标。如果找服务端开发,可以重点研究std::async、线程池、网络编程;如果找游戏开发,可以研究智能指针、内存管理、ECS架构;如果找高性能计算方向,可以研究模板元编程、SIMD指令、缓存友好设计。实例集的终点,其实是真实工程的起点。
我个人的体会是,刷实例最忌讳的就是“追求数量”。把100个题目每个都粗刷一遍,不如精刷50个,确保每个都能闭卷默写、能讲清楚为什么这么写、能说出另一种写法的优劣。这套C++编程实例集的质量很扎实,但它的价值要你亲手敲完、亲手调试完、亲手踩过坑才能真正体现。如果你刷到某个实例卡住了,先别急着看答案,把报错信息、运行结果、自己的思考写下来,这个过程本身就是最大的收获。
最后分享一个小技巧。我刷实例时习惯给每个题目写一份“复盘日记”,内容就三句话:这道题考察了什么知识点、我错在哪里、下次遇到同类型题我要注意什么。别小看这三句话,它是把短期记忆转化为长期能力最有效的方式。希望这篇文章能让你在刷C++实例这条路上少走点弯路,也欢迎你刷到某个卡点的时候回来对照着看看,可能就正好踩中我当年踩过的那个坑。
本文还有配套的精品资源,点击获取