简介C黑马程序员学习资料面向具备C语言基础、希望系统掌握C面向对象与STL应用的编程学习者兼顾课程复习与项目实战。压缩包约64.14MB页面文件总数显示为0且类型明细暂未提供实际内容以课程文档手册、PDF电子书与实验项目源码为主附有面试指南、LeetCode刷题手册、背包问题九讲、算法图解等辅助资料。核心价值在于三个由简到繁的实验项目通讯录管理系统、职工管理系统、基于STL的演讲比赛流程管理系统可帮助读者把类与对象、继承多态、容器算法等知识落到完整功能模块中并借助刷题与面试材料补充算法训练。已有1697人学习下载适合用作课程配套练习、项目复盘与求职准备。1. 从「能写」到「写对」C 学习地图长什么样很多人对 C 的第一印象是语法碎、细节多于是把 C 学习路径排成一条长长的队列变量、循环、函数、类、STL、模板、并发……队列走完了一合上课程资料还是写不出东西。梳理下来会发现真正的分水岭不在语法数量而在两个心智模型内存里的对象怎么生、怎么死编译期和运行期各自发生了什么。这套 C 课程资源对应的知识地图大致分三层底层是内存与指针中层是面向对象与标准库顶层是模板、智能指针与并发再往上才是项目实战把这些串起来。它适合已经能跑通小例子、但缺一张全局图的人按层补比从头刷一遍快得多也更适合拿来做阶段复习的对照表。2. 基础语法的三层地基从变量、指针到内存布局C 的基础语法看起来最没技术含量实际上后面所有的坑都埋在这里。对象多大、放在哪、什么时候销毁这三个问题答不上来写出来的代码就只能靠运气跑。这一章把语法拆成三块地基类型与布局、指针与引用、内存分区与生命周期再补一个容易被忽略的存储期问题。复习时不要只背语法点要能对着一段代码说出每个变量在哪、活多久、谁负责释放。2.1 类型与 sizeof编译器眼里的对象有多大基本类型的大小不是背出来的是查出来的。C 标准只规定最小范围和相对大小具体宽度跟平台和编译器有关所以看到「int 一定是 4 字节」这种说法要留个心眼。64 位平台上常见的对应关系如下类型常见大小64 位备注char1恰好 1 字节标准保证short2至少 16 位int4最常见但不保证long4 或 8Windows 多为 4类 Unix 多为 8long long8至少 64 位float / double4 / 8浮点精度不同指针832 位平台为 4比大小更容易错的是结构体大小。编译器为了对齐会在成员之间插入填充字节所以结构体大小往往大于成员大小之和。下面这段代码可以直接观察填充位置#include cstddef #include iostream struct A { char c; // 1 字节 int i; // 需要 4 字节对齐c 后面补 3 字节 short s; // 2 字节 }; // 常见结果12而不是 7 struct B { char c; short s; int i; }; // 调整顺序后常见结果为 8 int main() { std::cout sizeof(A) sizeof(B) \n; std::cout offsetof(A, i) \n; // 查看 i 的起始偏移 }sizeof是编译期运算符不产生运行时代码offsetof来自cstddef用来查看成员偏移只对标准布局类型可靠。对齐规则可以记成一句话每个成员相对结构体起始地址的偏移必须是该成员对齐数的整数倍结构体总大小还要补齐到最大对齐数的整数倍。改成员顺序就能省内存这是实际项目里最便宜的一档优化。复习时把#pragma pack和alignas放到一起对照就能明白「手动改对齐」和「让编译器按规则对齐」是两回事。2.2 指针与引用写法不同语义也不同指针和引用常被当成同一件事的两种写法实际差别不小。引用必须初始化绑定后不能改绑语言层面没有「空引用」这个概念指针可以为空、可以改指向、可以做算术运算。把两者的差异列成表比死记定义有效维度指针引用是否必须初始化否是能否为空能语言层面不能能否改绑能不能sizeof 结果指针本身大小被引用对象大小是否支持算术支持不支持典型用途可选对象、数组遍历函数传参、避免拷贝const和指针组合是另一个高频翻车点读法是看const离谁近int a 1, b 2; int* p a; *p 10; // 可以改指向的内容 p b; // 也可以改指向 int r a; // 引用必须初始化 r 20; // 改的是 a 本身 const int* p1 a; // 指向常量的指针不能改 *p1 int* const p2 a; // 常量指针不能改 p2 本身 const int* const p3 a; // 两者都不能改从右往左读p1是「指向 const int 的指针」p2是「const 的指针指向 int」。函数参数里更推荐const T既避免拷贝又不允许修改需要表达「可能没有」时用指针需要表达「一定有」时用引用。这个约定一旦统一接口可读性会明显提升。2.3 内存分区与 new/delete生命周期从哪开始C 程序的内存常被分成几块栈区存放局部变量和函数调用信息由编译器自动管理堆区由new/malloc申请、delete/free释放全局和静态区存放全局变量与静态变量常量区存放字符串字面量等只读数据代码区存放机器指令。这五块不是考试名词而是判断「这个对象能不能返回、会不会泄漏」的依据。int g 0; // 全局数据区程序结束时销毁 static int sg 1; // 静态区生命周期同程序 void f() { int local 2; // 栈函数返回即失效 int* heap new int(3); // 堆需手动 delete const char* s abc; // 字面量在只读常量区不能改 delete heap; // 成对释放释放后建议置空 heap nullptr; }new表达式做两件事分配内存、调用构造函数delete做两件事调用析构函数、释放内存。所以new[]必须配delete[]混用属于未定义行为小例子可能不崩换个编译器就出问题。返回局部变量的指针或引用本质是返回一块已经失效的栈内存编译器不一定报错运行时才出鬼。这也是后面 RAII 和智能指针要解决的问题把「谁释放」交给对象的生命周期而不是交给人的记性。2.4 存储期与 static 的三个位置static在 C 里有三种常见用法含义完全不同混着记必然出错。修饰局部变量时它改变的是存储期变量从栈搬到静态区函数退出不销毁只在第一次执行到声明处初始化。修饰全局变量或函数时它改变的是链接性只在当前源文件可见避免命名冲突。修饰类成员时它表示这个成员属于类而不是某个对象静态成员变量要在类外定义静态成员函数没有this指针。void counter() { static int n 0; // 只初始化一次跨调用保持 n; } class Config { public: static int version; // 声明 static int get() { return version; } // 无 this }; int Config::version 1; // 类外定义否则链接错误复习时把「存储期」和「链接性」两个词分开局部static管的是活多久全局static管的是谁能看见类内static管的是属于谁。三条线理清后面看单例模式、看头文件里的全局对象就不会被「为什么链接报重复定义」卡住。3. 面向对象与 STL把代码组织成可复用的模块语法过关之后代码规模一上来问题就从「写不写得出来」变成「改不改得动」。面向对象解决的是对象关系和生命周期STL 解决的是数据结构和算法复用。这两块在课程里通常挨着讲但复习时要分开类与对象模型考的是「编译器替你生成了什么」STL 考的是「什么场景选什么容器」。把这两条线各自理清再看项目代码会顺很多。3.1 类的构造与析构初始化列表与拷贝控制构造函数最容易忽略的是初始化顺序成员按声明顺序初始化跟初始化列表里写的顺序无关。初始化列表也不是「赋值」而是直接构造对const成员、引用成员和没有默认构造函数的成员类型来说它是唯一选择。class Buffer { public: explicit Buffer(std::size_t n) : data_(new char[n]), size_(n) {} // 拷贝构造深拷贝避免两个对象指向同一块堆内存 Buffer(const Buffer o) : data_(new char[o.size_]), size_(o.size_) { std::copy(o.data_, o.data_ size_, data_); } // 拷贝赋值先分配再释放保证异常安全 Buffer operator(const Buffer o) { if (this ! o) { // 必须处理自赋值 char* nd new char[o.size_]; std::copy(o.data_, o.data_ o.size_, nd); delete[] data_; data_ nd; size_ o.size_; } return *this; } ~Buffer() { delete[] data_; } private: char* data_; std::size_t size_; };这段代码对应「三法则」只要需要自定义析构函数、拷贝构造、拷贝赋值中的任何一个通常三个都要写。C11 之后扩展成「五法则」再加上移动构造和移动赋值。C11 之后更推荐「零法则」用std::vector、std::string、智能指针管理资源让编译器生成的版本就够用手写拷贝控制的场景越少越好。自赋值检查、先分配后释放、返回*this引用这三处是拷贝赋值的手写模板漏一个就可能在边界条件下崩。3.2 继承与多态vtable 不是玄学多态的实现机制常被当成黑匣子。常见实现是每个含虚函数的类有一张虚函数表对象里存一个指向该表的指针调用虚函数时通过表找到实际函数地址。知道这一点就能解释几个常见现象虚函数调用比普通函数多一次间接寻址对象里会多一个指针大小的开销构造函数期间虚函数不会派发到派生类。struct Base { virtual void f() { std::cout Base\n; } virtual ~Base() default; // 基类指针删除派生类对象时必须虚析构 }; struct Derived : Base { void f() override { std::cout Derived\n; } // override 让编译器检查签名 ~Derived() override { std::cout ~Derived\n; } };重写要满足几个条件函数名相同、参数列表相同、返回类型兼容协变、const属性一致。override的价值在于把「以为重写了其实没有」这种错误变成编译错误final用来禁止继续重写或继承。虚析构是最容易漏的一条通过基类指针delete派生类对象时如果基类析构不是虚函数派生类的析构不会执行堆上的资源就漏了。构造函数不能是虚函数因为构造时对象类型还没确定析构函数可以是虚函数而且基类通常应该写成虚析构。3.3 STL 容器选型先看场景再看复杂度容器选型的顺序是先明确访问模式再看复杂度最后看内存和迭代器稳定性。把常用容器放一张表里对照比逐个背接口有效容器底层结构随机访问中间插入删除查找典型场景vector连续数组O(1)O(n)无序 O(n)默认首选尾部增删多deque分段连续O(1)O(n)无序 O(n)头尾都要插入list双向链表不支持O(1)O(n)频繁中间插删、不要求随机访问set / map红黑树不支持O(log n)O(log n)需要有序遍历、范围查询unordered_map哈希表不支持平均 O(1)平均 O(1)只做键值查找不要求顺序vector的扩容是均摊 O(1)但扩容瞬间会让所有迭代器、指针、引用失效这是后面迭代器一节的重点。map的迭代顺序稳定且有序unordered_map的迭代顺序不保证不能依赖它做输出顺序。哈希容器在冲突严重时性能会退化键类型要提供合适的哈希函数。选型时还有一条实用经验不确定就先用vector真的测出瓶颈再换过早换容器往往换来的是更复杂的代码和更隐蔽的 bug。3.4 迭代器失效循环里最容易翻车的地方迭代器失效是 STL 里最集中的踩坑区。vector扩容后原有迭代器全部失效insert/erase之后被操作位置及其后的迭代器通常失效map和list只有被删除的那个节点失效其他迭代器仍然有效。删除元素时必须接住返回值std::vectorint v{1, 2, 3, 4}; for (auto it v.begin(); it ! v.end(); ) { if (*it % 2 0) { it v.erase(it); // erase 返回下一个有效位置不能写 it } else { it; } }如果写成v.erase(it); it;it已经失效再自增就是未定义行为。另一个高频错误是在范围for循环里删除元素循环变量背后是迭代器删完继续走同样会失效。需要边遍历边删时用「接返回值」的写法或者先记录要删的键循环结束后统一删。复习时把这条和「扩容导致全部失效」一起记基本能覆盖大部分线上问题。4. 进阶三件套模板、智能指针与并发基础和中层过关后课程通常会进入模板、资源管理、并发这三块。它们各自的难点不一样模板难在编译期的心智模型智能指针难在所有权设计并发难在不确定性。这三块也是项目代码里出现频率最高的「看起来懂、写起来错」的区域复习时要以能写出最小可运行示例为标准而不是看懂为止。4.1 模板把类型当参数模板的本质是把类型参数化让编译器在实例化时生成具体代码。函数模板负责算法复用类模板负责数据结构复用特化负责对特定类型走不同实现。可变参数模板配合折叠表达式可以写出参数个数不定的函数。template typename T T maxOf(const T a, const T b) { return a b ? b : a; } // C17 折叠表达式把参数包展开成 (((a b) c) 0) template typename... Args auto sum(Args... args) { return (args ... 0); } // 类模板示例固定容量缓冲区 template typename T, std::size_t N class FixedBuffer { public: void push(const T v) { if (size_ N) data_[size_] v; } std::size_t size() const { return size_; } private: T data_[N]{}; std::size_t size_ 0; };模板定义必须放在头文件里因为编译器在实例化时需要看到完整定义放到.cpp里通常会在链接阶段报「undefined reference」。这是模板最经典的易漏点报错信息还常常指向调用处容易误判成别的问题。另一个理解点是模板 errors 的阅读顺序从报错的第一行「实例化自哪里」开始看往往比看最后一行更有用。复习模板时重点不是背语法而是能说清「实例化发生在编译期」「每个类型生成一份代码」「特化优先于泛化」这三句话。4.2 智能指针与 RAII所有权写清楚泄漏就少一半RAII 的核心是把资源释放绑定到对象析构上对象在作用域结束时一定析构资源就一定被释放。智能指针是这套思想的标准实现。三种指针的区别不在接口而在所有权语义指针所有权拷贝开销典型场景unique_ptr独占禁止只能移动与裸指针相当默认首选作用域内独占资源shared_ptr共享允许引用计数控制块 原子计数多方共享生命周期weak_ptr不拥有允许不增加计数打破循环引用、观察者#include memory struct Node { std::shared_ptrNode next; std::weak_ptrNode prev; // 弱引用不增加计数避免循环引用 }; // 使用侧 auto a std::make_sharedNode(); auto b std::make_sharedNode(); a-next b; b-prev a; // 若这里也用 shared_ptr两个节点计数永不归零make_shared通常只分配一次内存把对象和控制块放在一起比shared_ptrT(new T)少一次分配也更异常安全。weak_ptr不能直接解引用要用lock()提升为shared_ptr提升失败说明对象已销毁。循环引用是shared_ptr最典型的泄漏原因排查时看「对象该销毁却没销毁」优先检查双向关系和回调里持有的强引用。4.3 移动语义std::move 不搬东西std::move的名字容易误导它本身不移动任何内存只是把表达式转换成右值引用让编译器有机会选中移动构造或移动赋值。真正的资源转移发生在移动构造/移动赋值的实现里偷走源对象的指针再把源对象置为可安全析构的状态。class Buf { public: Buf(Buf o) noexcept : p_(o.p_), n_(o.n_) { o.p_ nullptr; o.n_ 0; } // 偷指针并置空源 Buf operator(Buf o) noexcept { if (this ! o) { delete[] p_; // 先释放自身资源 p_ o.p_; n_ o.n_; // 再接管 o.p_ nullptr; o.n_ 0; } return *this; } private: char* p_ nullptr; std::size_t n_ 0; };移动构造标noexcept很关键容器扩容时如果移动构造可能抛异常标准库会选择拷贝而不是移动以保证强异常安全。移动之后源对象处于「有效但未指定」状态只能析构或重新赋值不能假设它还是原值。复习时把这三句话记住std::move是类型转换、移动是成员函数实现的、noexcept影响容器行为。4.4 并发基础线程、锁与条件变量并发部分的难点不是 API而是「不确定」。同样的代码跑一百次都对第一百零一次可能就崩。入门阶段先掌握四件事线程的创建与 join、互斥锁保护共享数据、条件变量做线程间通知、原子变量做简单计数。#include condition_variable #include mutex #include queue #include thread std::mutex m; std::condition_variable cv; std::queueint q; void producer() { { std::lock_guardstd::mutex lk(m); // 作用域锁离开自动解锁 q.push(1); } cv.notify_one(); } void consumer() { std::unique_lockstd::mutex lk(m); // 谓词版 wait被唤醒后先检查条件避免伪唤醒 cv.wait(lk, [] { return !q.empty(); }); int v q.front(); q.pop(); }wait必须配谓词否则可能被伪唤醒后在队列为空时继续往下走。lock_guard适合简单加锁unique_lock适合需要配合条件变量或手动解锁的场景。死锁的常见成因是加锁顺序不一致统一「按地址或按 ID 排序加锁」能解决大部分问题。atomic适合单变量计数多个变量之间要保持一致时还是得用锁。5. 易错点复盘那些看着会、写起来就错的地方复习到这一层知识点已经不是问题问题在「以为自己会」。下面这些点有个共同特征小例子不报错规模一上来就出问题靠肉眼 review 很难发现。每条按「易漏点 → 为什么漏 → 怎么补」写建议对照自己的代码逐条过一遍。5.1 内存与对象类编译器不报错运行才出鬼成员初始化顺序与初始化列表不一致。为什么漏初始化列表写起来像执行顺序读代码时很容易按书写顺序理解。怎么补牢记「按声明顺序初始化」开启-Wreorder一类警告写初始化列表时按声明顺序排。有裸指针却不写拷贝控制。为什么漏编译器生成的拷贝构造在没用到时完全够用一旦对象被拷贝就变成两个对象共享同一块内存析构时重复释放。怎么补类里出现裸指针、文件句柄、锁这类资源就按三/五法则手写或者直接用智能指针和标准容器把裸资源换掉。new[] 配 delete、释放后不置空。为什么漏小规模测试往往不崩或者崩在别的位置误导排查方向。怎么补申请和释放成对写在一个函数里释放后立刻置nullptr有条件就跑一遍地址 sanitizer 或内存检查工具让问题在开发期暴露。返回局部变量的引用或指针。为什么漏函数能编译、能返回甚至偶尔能读到正确值因为栈内存还没被覆盖。怎么补返回值语义用值返回或智能指针确实要返回内部对象时确认生命周期长于调用方静态分析工具能抓出一部分。基类析构没写虚函数。为什么漏不通过基类指针删除时完全正常。怎么补只要类可能被继承并通过基类指针管理析构就写成virtual或者把基类析构设为 protected 禁止这样用。5.2 库与并发类编译能过逻辑不对迭代器失效后继续使用。为什么漏失效的迭代器不一定是空指针解引用可能读到旧数据看起来「还能用」。怎么补删除统一用erase的返回值接住容器扩容前先 reserve范围 for 里不删元素。模板定义放在源文件里。为什么漏编译单个文件能过链接时才报错报错信息还指向调用处。怎么补模板实现放头文件确实要分离时用显式实例化把用到的类型列全。shared_ptr 循环引用。为什么漏引用计数不为零不会报错只是内存慢慢涨短时间测不出来。怎么补双向关系里一端用weak_ptr回调里避免长期持有强引用怀疑泄漏时打印引用计数观察。条件变量不加谓词。为什么漏大多数时候唤醒时机正好伪唤醒或惊群只是偶发。怎么补一律用cv.wait(lk, pred)的谓词版本唤醒后重新检查条件。用 map 的 operator[] 做查询。为什么漏键不存在时它不报错而是插入一个默认值逻辑悄悄改变还可能让容器变大。怎么补查询用find、count或 C20 的contains只有确实要插入或修改时才用operator[]。6. 复习清单把整张地图压缩成一张自检表复习最怕的是「从头再看一遍」效率低还容易产生已经掌握的错觉。更实用的做法是把知识地图压缩成一张自检表每个模块只问自己几个能立刻验证的问题答不上来再回去补那一段。下面这张表可以直接抄下来每轮复习打一次勾模块自检问题合格标准类型与布局结构体成员顺序换一下大小会变吗能说出对齐规则并手算出一个例子指针与引用const int*和int* const分别限制什么能不看资料写出四种组合内存分区局部变量、new 出来的对象、字符串字面量各在哪能判断一个指针指向的区域和生命周期类与对象拷贝赋值为什么要先分配再释放、要判自赋值能默写出五法则的骨架多态为什么基类析构要虚、vtable 大致怎么工作能解释不虚析构时的具体后果STLvector 扩容后哪些东西失效能写出边遍历边删除的正确写法模板模板定义为什么放头文件能说出实例化发生在什么阶段智能指针shared_ptr 循环引用怎么破能写出 weak_ptr 的正确用法并发condition_variable 为什么必须带谓词能写出不会伪唤醒的生产者消费者工程习惯编译警告开没开、内存检查跑没跑至少有一轮是带警告和检查工具构建的用这张表时建议分三轮。第一轮只看问题、不看答案把答不上来的标红这一轮暴露的是盲区第二轮针对标红项回到对应章节只补那一段不要顺带重刷整章第三轮合上所有资料挑三个模块口述一遍能讲清楚才算过。口述是个很好用的检验方式因为「看得懂」和「讲得出」之间差着一层真正的理解讲卡壳的地方往往就是你以为会、其实没会的地方。再补一个具体技巧给自己建一个错题本只记三样东西——错误代码的最小复现、当时的错误判断、正确的判断依据。比如「vector 范围 for 里 erase 导致崩溃」这条记下复现代码和「迭代器失效」四个字就够了不用抄整段教材。复习到后期这本错题本比任何资料都贴身因为它记录的是你自己的盲区而不是别人的知识大纲。最后说个血泪教训。我早期复习 C 的习惯是把课程从头到尾再看一遍看完感觉很踏实一写代码还是卡在对象生命周期和迭代器失效上。后来改成每轮复习先默写对象从构造到析构的完整路径再默写一遍容器操作后的失效规则才真正把那些零散的知识点连成了线。从那以后我每次复习 C都强制先走一遍这张自检表再动键盘哪怕只花二十分钟。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?