C++课后习题Day73:指针字符串与类综合训练,从基础到算法实战
今天是我坚持C课后习题训练的第73天刚好进入指针、字符串和类三大模块的交叉复习阶段。说实话到这个阶段已经不是单纯刷语法题而是要把数组、指针、结构体、STL容器串起来用稍不留神就会踩到细节坑。如果你也正在自学C或者准备校招笔试这份Day73训练记录应该能帮你少走不少弯路。今天一共做了8道课后题涉及字符串数组初始化、指针传参、结构体链表、冒泡排序、快速幂、单调栈、静态成员和虚函数后面我会一道一道拆开讲。1. 今日训练的整体规划与选题思路1.1 为什么把指针、字符串和类放在同一天说实话一开始我也觉得每天应该只盯一个专题比如今天只做指针明天只做类。但练到第73天我发现一个问题单独看一道题都懂一旦组合起来就懵。比如字符串数组传参表面上是数组问题实际考的是指针退化结构体链表里存字符串又牵扯到深拷贝和内存管理类成员里有指针成员还要考虑析构函数怎么写。所以今天刻意把这几个模块放在一起练的就是“组合拳”。这种交叉训练最大的好处是强迫我对底层细节多问几个为什么而不是背模板。指针的用法、字符串的存储方式、类的封装规则三者在同一个题目里出现时才算真正开始考验C的功底。另一方面今天的算法题目也有意识地避开了纯模板题。冒泡排序看起来简单但优化点和边界条件很容易翻车快速幂如果不理解二进制拆分代码背下来也容易写错单调栈更要搞懂“栈里存的是什么”。算法题和语言细节混在一起练能够提高实战中排查问题的速度。所以我给自己定的标准是每道题不光要跑通还要能说清楚“为什么这么写”。1.2 训练环境VSCode MinGW 的快速搭建我用的开发环境一直是VSCode加MinGW-w64轻量而且接近Linux的g编译行为。VSCode配置C/C环境其实不难装好C/C扩展后主要改两个文件tasks.json负责编译launch.json负责调试。我用的是g -stdc17 -g编译不开O2优化因为调试的时候变量还能看得比较清楚。如果不想折腾配置直接用Dev-C或在线编译器也完全可以做题但调试体验会差一些尤其是看指针变化的时候。这里补一句如果你在Windows上报错说缺少Microsoft Visual C Redistributable多半是某些依赖库使用了MSVC编译而不是MinGW。我一般建议纯做题用MinGW后面需要用某些库时再装Visual Studio Build Tools不要混着用。编译器工具链一旦混用经常会出现莫名其妙的链接错误排查起来比做题本身还耗时间。今天我在配置环境时就踩了一个类似的问题后面在第五章里详细说。1.3 三个小时的做题节奏今天安排的是三个小时前30分钟快速浏览题目把会的先做掉中间90分钟集中攻克中等难度题最后60分钟复盘错题和整理笔记。这个小节奏是我练下来的习惯第一遍浏览不是为了赶进度而是确认今天的能力区间。如果某道题第一遍没思路我会标出来不硬刚等做完其他题再回来看有时候思路会自动打开。今天的前30分钟我基本做完了字符串数组和指针交换两题说明这类基础点已经比较熟了。剩下的时间全花在链表、多态和单调栈上这也是今天真正有收获的部分。复盘的时候我习惯把每道题的关键代码重新敲一遍而不是直接复制粘贴。敲的过程中会发现很多细节比如分号漏了、括号方向错了、const位置放错了这些错误看似低级但正是考试时容易扣分的地方。练到第73天我越来越觉得刷题的本质不是“见得多”而是“错得明白”。2. 核心题目拆解指针、数组与字符串的联动2.1 字符串数组初始化的三种写法与陷阱第一道题是“用C实现字符串数组的初始化并输出每个字符串的首字符”。看似简单其实有三种常见写法const char* arr[]也就是字符指针数组每个元素指向一个字符串字面量char arr[][20]二维字符数组每一行存一个字符串std::string arr[]或std::vectorstd::stringC风格字符串数组。我个人推荐优先使用std::string因为自动管理内存。但题目如果明确要求字符数组就需要小心缓冲区大小。比如char arr[][20]字符串长度一旦超过19就会出现截断甚至越界所以更好的做法是固定大小加安全复制函数。今天我把三种写法都写了一遍发现最容易错的是字符指针数组的初始化const char* arr[] {hello, world, cpp}; for (int i 0; i 3; i) { cout *arr[i] endl; }这里*arr[i]取的是每个字符串的首字符。乍一看没什么但如果你写成arr[i][0]本质上也是一样的。关键是要理解arr是一个数组数组元素是指针指针指向字符串常量区所以不能试图通过arr[i][0]去修改字符否则行为是未定义的。今天我在笔记里专门标了一句字符指针数组里的字符串能读不能写这是和二维字符数组最大的区别。2.2 指针作为函数参数到底传了什么今天有一道题让写一个函数交换两个整数很多新手会写成void swap(int a, int b)结果发现实参没变。正确写法是传指针或引用void swap(int* a, int* b)调用时传地址swap(x, y)。我在笔记里写了一句指针作为函数参数本质上是把变量的地址拷贝一份传进去函数内部拿着地址可以直接改原变量。引用语法上更安全但指针能让你更清楚地看到地址的流动。void swap(int* a, int* b) { int temp *a; *a *b; *b temp; }理解这个问题的关键在于“值传递”三个字。C默认是值传递拷贝的是一份副本。对于指针拷贝的是指针本身也就是地址。两个指针虽然独立但指向的是同一个变量所以通过解引用可以修改原变量。如果只传变量的值函数里改的只是副本实参自然不变。如果你用nullptr调用这个函数程序会直接崩溃所以调用前最好检查指针是否为空。这个细节在工作里写接口时同样适用。2.3 结构体链表的基本语法结构体链表是必考基础。今天这道题要求用结构体实现单链表并写一个函数统计节点个数。基本语法如下struct Node { int data; Node* next; }; int countNodes(Node* head) { int cnt 0; Node* cur head; while (cur ! nullptr) { cnt; cur cur-next; } return cnt; }看起来简单但有一个高频坑创建新节点时只定义了Node* p没有new就赋值p-next段错误直接教做人。正确的创建方式是Node* node new Node{0, nullptr};用完记得delete否则就是内存泄漏。今天我做这道题时还顺手加了释放内存的deleteList函数。链表的题目在所有关于C的课后习题里出现频率极高因为它能一次性考察结构体、指针、动态内存和遍历逻辑。面试时也经常要求现场在白板上写所以我建议至少手写三遍直到不需要思考就能写完。链表题还有一个很隐蔽的坑头节点为空的情况。while (cur ! nullptr)直接处理了空链表但如果你用while (cur-next ! nullptr)空链表就会报错。这取决于你定义的是“带头节点”还是“不带头节点”。初学者最好统一用while (cur ! nullptr)省心。2.4 字符串转字符数组的常用实现今天还练了一道“C字符串转数组”的题目就是把std::string拷贝到char[]里。用strcpy之前一定要确认目标数组足够大否则会越界。更安全的办法是用snprintfstd::string s hello; char buf[32]; snprintf(buf, sizeof(buf), %s, s.c_str());如果换到C17还可以用std::copy配合c_str()。这道题还引申出一个问题c_str()返回的指针在字符串对象修改后会失效所以不要长时间保存它需要的时候临时取。我在实际开发中见过太多因为保存了c_str()返回值导致悬垂指针的bug所以越来越强调这个点。另一种常见实现是把字符串拆成字符数组比如按逗号切分。题目给的是“C字符串转数组”我顺手写了一个用istringstream按空格拆分的版本std::string input 12 34 56; std::vectorint vec; std::istringstream iss(input); int num; while (iss num) { vec.push_back(num); }这个写法在处理数字字符串时非常方便比手动遍历字符再累加要稳得多。字符串与数组的转换是C课后习题里的常客而且经常和指针题混在一起考所以今天花了不少时间把这几种写法全部跑通。3. 算法专项排序、快速幂与单调栈3.1 手写冒泡排序从基础版到优化版第73天还要写冒泡排序很多人觉得幼稚但手写一遍能考察边界感和代码风格。基础版for (int i 0; i n - 1; i) { for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) swap(a[j], a[j 1]); } }优化版加一个flag如果一趟下来没发生交换就提前终止。今天题目要求输出每一轮的排序结果所以我顺便复习了内循环边界为什么是n - 1 - i因为每一轮结束后最后i个元素已经归位不需要再比较。for (int i 0; i n - 1; i) { bool swapped false; for (int j 0; j n - 1 - i; j) { if (a[j] a[j 1]) { swap(a[j], a[j 1]); swapped true; } } if (!swapped) break; }这里有个容易忽略的点如果数组已经完全有序第一轮就可能没有发生交换那么break会提前退出算法时间接近O(n)。这也是“最好情况”为什么是O(n)的原因。如果今天不亲手敲一遍我可能还是会忘记swapped要重置为false。3.2 快速幂把时间复杂度降到 O(log n)作为算法基础快速幂非常适合课后练习。题目是计算a^b mod p直接用循环累乘遇到大指数会超时。快速幂思路就是把指数拆成二进制通过平方减少乘法次数。代码long long fastPow(long long a, long long b, long long p) { long long res 1 % p; while (b 0) { if (b 1) res res * a % p; a a * a % p; b 1; } return res; }这里的b 1就是C按位与的典型应用判断最低位是不是1。今天我在纸上手动模拟了2^10的二进制分解过程感觉比直接背代码清晰很多。10的二进制是1010从低位开始第1位0所以不乘第2位1乘上2^2第3位0不乘第4位1乘上2^8最终就是4乘256等于1024。每一步底数都在平方所以循环次数只有二进制位数次也就是log2(b)1。我在做这道题时还总结了易错点当p1时结果应该是0所以res初始化为1 % p可以避免p1时出错。另外如果a本身已经大于等于p最好先对a取一次模防止首次乘法溢出。用long long够用但如果遇到更大的数值可能还需要快速乘或者高精度。3.3 单调栈的经典场景下一个更大元素单调栈是面试常客今天的题目是给一个数组求每个元素右边第一个比它大的元素。标准做法是维护一个栈底到栈顶递减的栈从右往左遍历。我习惯从左往右遍历遇到更大的元素就出栈效果一样。核心代码vectorint result(n, -1); stackint st; for (int i 0; i n; i) { while (!st.empty() nums[i] nums[st.top()]) { result[st.top()] nums[i]; st.pop(); } st.push(i); }这里栈里存的是下标不是值因为需要回填结果数组。今天第一遍写还漏了st.pop()直接死循环复盘时记了一笔。单调栈里为什么能保证正确因为当新元素比栈顶元素大时栈顶元素右边第一个更大元素一定就是当前新元素于是可以出栈。如果新元素不比栈顶大就继续压栈保持栈的单调性。整个过程每个元素最多入栈一次出栈一次时间复杂度O(n)空间复杂度O(n)。这个算法在“下一个更大元素”“每日温度”“接雨水”里都会出现。建议刷题时把单调栈理解成“我用一个栈记录还没找到答案的下标”这样就不会晕。3.4 随机数的生成与应用随机数也是常见考点。C里不要再用rand()推荐random库。今天的题目是生成10个不重复的随机数核心代码大致是这样std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution dis(1, 100);用std::set或者布尔数组去重。值得注意的是random_device在某些环境下可能退化成伪随机所以如果只是普通练习用mt19937配合固定种子也能复现问题。今天的习题里还有一个猜数字游戏其实就是生成随机数后让用户猜这个想清楚区间就好了。std::setint s; while (s.size() 10) { s.insert(dis(gen)); }把随机数生成的逻辑封装成函数之后做小游戏、抽奖程序都能复用。uniform_int_distribution用来生成整数uniform_real_distribution用来生成浮点数。这里我踩过一个坑如果多次运行同一个程序random_device产生的种子不同结果不同但如果某些环境下random_device实现不随机所有结果每次一样这时可以改用当前时间作为种子。C课后习题里如果遇到“随机数”一定要区分清楚题目是要随机效果还是可复现效果。4. 类与面向对象final、static、const 详解4.1 const 成员函数与 mutable今天有一道题是判断一个类中哪些函数能调用const对象。const成员函数要这样声明int getValue() const { return value_; }它的含义是这个函数不会修改对象的非mutable成员。如果你在const函数里改某个成员变量编译会直接报错。只有当某个计数器之类的成员需要修改时才用mutable修饰。这个细节我一开始总记不住后来统一记成const修饰的是成员函数的this指针让this变成指向const的指针所以通过this访问到的成员都被视为常量。class Counter { private: int value_ 0; mutable int accessCount_ 0; public: int getValue() const { accessCount_; return value_; } };如果不加mutablegetValue里修改accessCount_就会编译失败。有些同学可能会说那我把accessCount_声明成非mutable然后去掉const不就行了但这样const对象就无法调用getValue了。在设计类时const成员函数表示逻辑上不改变对象状态像缓存计数这种细节用mutable修饰是合理的。4.2 static 成员的初始化时机静态成员属于类而不是对象所以不能在构造函数里初始化静态变量。C17之后可以用inline static直接初始化class Test { public: static inline int count 0; };以前必须类内声明类外定义比如int Test::count 0;稍微麻烦。今天训练的最后一道题就是统计一个类创建了多少个对象解决办法就是在构造函数里count析构函数里count--然后通过静态成员函数getCount()读取。坑在于拷贝构造也要计数不然按值传参会漏。struct Obj { static inline int liveCount 0; Obj() { liveCount; } Obj(const Obj) { liveCount; } ~Obj() { liveCount--; } };这里没写拷贝赋值运算符如果发生赋值liveCount不会重复增加因为赋值不会创建新对象。但如果你偷懒不写拷贝构造编译器生成的默认拷贝构造不会增加计数于是test(Obj());时对象临时变量有了又走了拷贝构造计数就会出错。所以要养成“看到动态内存、看到计数就想到拷贝构造”的习惯。4.3 final 和 override 的组合用法final可以修饰类或虚函数表示不允许被继承或重写。override则表示显式标明“我要重写父类的虚函数”编译器能帮你检查父类有没有这个虚函数签名对不对。我今天的题目里有一个基类Animal派生类Dog重写speak()。正确写法是class Animal { public: virtual void speak() const 0; }; class Dog : public Animal { public: void speak() const override { cout Woof endl; } };如果不写override万一基类方法名拼写错误派生类的函数就会变成普通新函数多态失效而且编译器不报错找起来很费劲。今天我还试了一下在override函数后面加final表示这个函数不能在再下一层被重写。当类层次很深时final更像是一种维护约束能让代码的意图更清楚。class Puppy : public Dog { public: void speak() const override {} // 这里会报错因为 Dog::speak 如果是 final 就不能重写 };final和override都不是运行时机制它们只是编译期检查和代码约束所以不会带来性能损失。在大型项目里这两个关键字能显著提升可读性也是C八股中经常被问到的基础点。4.4 一道多态习题的完整解析今天最值回票价的一道题是有一个Shape基类派生Circle和Rect用基类指针数组存储遍历并输出面积。关键点在于把area()声明成virtual否则基类指针只能调用基类版本。代码大致如下class Shape { public: virtual double area() const { return 0; } virtual ~Shape() {} }; class Circle : public Shape { double r; public: Circle(double r) : r(r) {} double area() const override { return 3.14159 * r * r; } }; class Rect : public Shape { double w, h; public: Rect(double w, double h) : w(w), h(h) {} double area() const override { return w * h; } };用基类指针数组时最自然的写法是Shape* shapes[2]; shapes[0] new Circle(2.0); shapes[1] new Rect(3.0, 4.0); for (int i 0; i 2; i) { cout shapes[i]-area() endl; delete shapes[i]; }这里析构函数必须virtual否则delete basePtr时不会调用派生类析构可能导致资源泄漏。这个坑在指针数组场景里尤其明显。如果Circle内部有申请堆内存的资源非虚析构会让内存泄漏成为定时炸弹。今天我在笔记里写的结论是只要类里面有虚函数就顺手把析构函数也写成虚函数。5. 环境与调试今天踩过的坑5.1 VSCode 配置 C/C 环境的完整步骤有同学问我VSCode配置C/C环境怎么做我今天顺手记一下。首先安装C/C扩展然后确认MinGW-w64的bin目录加到系统PATH里命令行能直接执行g --version。接着在项目里创建.vscode/tasks.json核心配置是args: [-g, -stdc17, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe]。再创建.vscode/launch.json指定miDebuggerPath为gdb的完整路径program为刚才生成的可执行文件。这样按F5就能编译加调试看变量、看内存都很方便。如果只做单文件练习tasks.json可以复用不需要每个文件都配置。但一旦开始用CMake或多文件项目最好让VSCode配合CMake Tools扩展工作把编译任务交给CMake调试器再接管可执行文件。今天我把两个项目结构都试了一遍感觉单文件用g命令多文件用CMake是最省心的组合。5.2 编译时报错“Microsoft Visual C Redistributable”怎么办今天在配置环境时还遇到一个很典型的错误使用Python的某个包时提示Microsoft Visual C 14.0 is required而不是C程序本身的错误。这个是因为有些Python扩展是用MSVC编译的运行时依赖对应的Redistributable。如果你只是写C代码最好统一编译器工具链。如果确实需要安装去官网下载对应架构的vc_redist.x64.exe装上就行注意选x64还是x86要看目标程序位数。混用MinGW和MSVC编译的库可能因为ABI不兼容导致链接错误尽量别混。这个问题在C课后习题训练群里经常被问很多人以为是自己的C代码写错了到处改代码实际上只是运行环境缺少运行时库。我的排查思路是先读完整报错信息看到“Redistributable”就意识到是运行库问题然后确认项目使用的编译器类型。如果是MinGW项目就检查g的版本和libstdc-6.dll是否可找到如果是MSVC项目就安装对应版本的Redistributable。另一个常被忽略的点是有时候系统里已安装了x64版本但你的程序是x86的仍然会报错所以要看全平台。5.3 使用 g 直接编译与 CMake 构建的取舍做课后题时我通常直接g main.cpp -o main因为快。但到第73天题目开始分成多个文件比如头文件放类声明源文件放实现主文件放测试逻辑。这时候再用g一条命令编译也能用但依赖关系一多就不方便。我选择引入CMake写一个最简单的CMakeLists.txtcmake_minimum_required(VERSION 3.16) project(Day73) set(CMAKE_CXX_STANDARD 17) add_executable(Day73 main.cpp Shape.cpp Circle.cpp)这样以后加文件只需要改一下add_executable编译和调试的体验都会好很多。初学者不急着上CMake但等到要管理多个文件时迟早要学。CMake不是一个编译器它只是生成构建规则的工具。在Linux下可以生成Makefile在Windows下可以生成Visual Studio工程也可以配合MinGW生成Makefile。今天我的做法是在项目根目录建build目录然后执行cmake -S . -B build -G MinGW Makefiles cmake --build build这样生成的可执行文件在build目录下不会污染源码目录。如果不想用命令行VSCode的CMake Tools扩展会帮我自动选择生成器。整体体验下来单文件题用一键g编译足够多文件题用CMake是必须的。5.4 调试指针一定要看内存地址今天调结构体链表时光靠打印数据看不出问题。我用VSCode调试器给Node* cur加了监视直接看cur的地址和cur-next的地址很快就发现其中一个节点创建后忘了接链尾结点是悬空状态。我的调试习惯是指针相关逻辑出问题时优先检查地址是否为0x0或者是否指向了意料之外的内存区域而不是盯着数值看。这就是我坚持用调试器做链表题的原因。在VSCode的监视窗口里你可以展开cur-data和cur-next也可以看一眼cur也就是指针变量自身的地址。有时候写delete node;之后没有把node置空node就成了悬空指针。调试器显示的值可能仍然是原来的地址但访问那个地址已经是未定义行为。所以“先看地址再看值”是我的铁律。今天链表题里有个节点打印出来是乱码我用调试器看到它的next指向了一个已经释放的内存问题立刻定位到是某个分支忘了更新尾指针。6. 错误记录与复盘6.1 运算符优先级位运算与比较运算混用今天有一道快速幂题目判断指数最低位时我写成了if (b 1 0)结果逻辑完全不正常。原因在于的优先级高于表达式实际变成b (1 0)也就是b 0永远为假。正确写法是加括号(b 1) 0。这个错误值得单独记因为很多人习惯把位运算当成算术运算用忽略了优先级。其实C运算符优先级表我大概记过但写代码时手一快就会出错。后来我给自己定了个规矩凡是在if条件里混用算术、位运算、比较运算符一律加括号。即使是(b 1) ! 0这种也要写清楚。这样虽然看起来啰嗦但能减少大量无意义的调试时间。6.2 数组越界循环边界条件写错冒泡排序的第一版代码我在内层循环写了j n - i没有减1当j n - i - 1时比较a[j] a[j1]最后一个元素本来比较合法但循环多了一次后可能越界。错误的根因是凭感觉写边界没有仔细算下标范围。现在我的习惯是先把数组下标范围写出来再写循环条件比如0 j n - 1 - i然后翻译成j n - 1 - i就绝对不会错。数组越界这种错误在C里最危险因为编译器经常不报错只是运行结果诡异。有时候是某个值被悄悄改掉有时候是段错误有时候只是内存里恰好没被复用所以表现正常。所以能用std::vector就用vector用at()可以在越界时抛异常纯C数组要自己多写断言比如assert(j 1 n)。6.3 忘记初始化指针导致段错误创建链表节点时写了Node* cur;就急着赋值cur-data忘了cur new Node。之前觉得这种低级错误不可能再犯结果今天在压力下又出现了。所以我在代码里给自己定了个规矩凡是指针变量声明时立刻初始化为nullptr局部指针在new之前不允许使用。这个习惯能省很多调试时间。Node* cur nullptr; // cur-data 1; // 不允许肯定崩 cur new Node{1, nullptr};有些同学习惯先声明指针再等条件满足时new但中间一旦有异常或提前返回就容易出现空指针。如果声明时是nullptr判断起来就很直观。这里还牵连到“所有权”的问题谁new谁负责delete如果忘记就是内存泄漏。今天我做链表题最后专门写了一个deleteList函数把所有节点都释放掉算是强迫自己养成习惯。6.4 今日错误速查表我把今天的错误整理成一张速查表方便以后翻看错误类型表现原因对策运算符优先级if判断结果异常b 1 0被解析成b (10)位运算加括号数组越界输出乱码或崩溃循环边界多算1先写下标范围空指针访问段错误指针未初始化就使用声明即置空非虚析构内存泄漏delete基类指针未调用派生析构基类析构加virtual静态计数漏拷贝对象计数不准确拷贝构造未处理count实现拷贝构造这张表我会每天更新过一周回头翻很多重复踩坑都能提前避开。其实错误记录才是每天训练里最值钱的部分因为人不会在同一道题上错两次但会在同一类错误上反复犯。把这些坑总结成条目相当于给自己建了一个“防坑数据库”。7. 额外加餐从今天的题延伸出的几个高频追问7.1 为什么析构函数要加 virtual今天多态题里我再次强调析构函数要加virtual这几乎是C八股里的必问题。它的本质是“删除对象时按动态类型析构”。如果基类析构不是虚函数通过基类指针delete一个派生类对象时只会调用基类析构派生类成员里的资源就无法释放。class Base { public: virtual ~Base() {} }; class Derived : public Base { int* p new int(42); public: ~Derived() { delete p; } };如果Base::~Base去掉virtual而使用Base* b new Derived(); delete b;Derived的析构不会执行p就泄漏了。哪怕是int这种内置类型也属于未定义行为。更稳妥的做法是要么基类有虚析构要么就别用基类指针删除派生类对象。现代C里更推荐用智能指针管理生命周期但理解虚析构仍然是基本功。7.2 string 和 char* 到底有什么本质区别今天字符串数组题引申出一个问题std::string和char*到底有什么本质区别简单说std::string是一个类内部封装了动态内存管理可以自动扩容支持拼接、size()、find()等操作char*只是指向一块字符内存的指针本身不知道字符串长度也没有任何自动管理能力。std::string a hello; std::string b a world; const char* c hello;const char* c指向的是字符串字面量这个字符串存储在只读区不能修改。而std::string内部一般有堆上的缓冲区可以修改。所以当函数参数需要传字符串时优先用std::string或者std::string_view只有和C接口打交道时才用const char*。今天遇到一个同学问为什么char* s abc; s[0]x;会崩溃就是因为字符串字面量不可写这正是初学者非常容易踩的坑。7.3 浅拷贝和深拷贝在结构体链表中的应用今天的结构体链表如果节点里增加了char*或std::string成员就会引出深拷贝问题。默认的拷贝是浅拷贝也就是把指针直接复制一份两个对象指向同一块内存。链表节点一旦复制析构时可能释放同一块内存两次造成double free。struct Node { char* name; Node(const char* s) { name new char[strlen(s) 1]; strcpy(name, s); } ~Node() { delete[] name; } };这种结构在拷贝时就要自定义拷贝构造函数为新对象重新分配一块内存再复制内容这就是深拷贝。C课后习题经常让学生用类封装链表如果不处理拷贝构造、拷贝赋值、析构就会在各种隐蔽场景中崩掉。今天我的链表题虽然只有int data但我顺手在笔记里写了一个深拷贝版本避免将来遇到字符串节点时踩坑。7.4 想练综合能力可以写个小游戏天天做课后题容易枯燥我自己的调节方式是偶尔写点小游戏把今天学的知识点塞进去。比如“猜数字”用随机数菜单选择用函数指针角色属性用类排行榜用链表或std::vector。今天做完题后我还顺手改了一个猜数字小游戏的代码把随机数、循环、输入验证都串了一遍。很多教程喜欢拿“C小游戏”作为实战作业本质就是想让你在项目里熟练使用语法、流程控制和基本数据结构。写小游戏不是不务正业它能把散落的知识点拧成一股绳。如果你刚学完基础语法我建议给自己定一个小目标写一个命令行版的猜数字、贪吃蛇或简易迷宫。不需要图形界面纯C控制台就能完成。这个过程你会用上字符串处理、数组/链表、类设计、随机数和循环比一次刷十道孤立题目收获更多。做到第73天我最大的体会是C的语法点就像乐高积木单个看都很简单但拼在一起就会出现各种隐藏接口问题。今天这8道题虽然不难却让我把指针、字符串、类、算法这几个核心模块重新串了一遍。尤其是指针和内存管理真的是只有亲手调试过几次段错误才会真正理解地址和生命周期的概念。如果你也在坚持刷题建议不要只满足于“能跑出结果”而是把每次报错都当成一次深度复习写进自己的错误速查表这样练一天抵三天。