简介:这份C++讲义笔记面向零基础入门者与需要系统梳理语法体系的开发者,覆盖从编写第一个程序到核心概念、进阶技巧的完整学习路径。内容围绕C++初识、注释、变量与常量、关键字、标识符命名规则、整型与浮点型等数据类型展开,并延伸至指针、数组、结构体、类、运算符重载、模板与异常处理等进阶主题,配合大量代码示例与表格对比,便于边学边练。资源包共1个PDF文件,大小约5.07MB,以图文讲义形式呈现,结构按章节递进,适合作为课堂笔记或自学参考。目前已有25964人学习下载,说明其内容经过较多读者验证。对于希望打牢C++基础、理解内存与面向对象思想、逐步过渡到复杂设计模式与算法的读者,这份笔记能提供清晰的知识框架与可查阅的语法速查价值。
1. 从三份讲义到一条可复现的 C++ 学习路径
很多人学 C++ 的路径是断裂的:入门看一份教程,核心看另一份,提高又换一套,结果三份讲义之间的知识缝隙没人补,写出来的代码能跑但经不起追问。我见过一个模拟项目,某开发者把入门讲义里的vector用法背得滚瓜烂熟,却在核心讲义讲对象生命周期时卡住,因为他不知道vector扩容时元素会被移动或拷贝——这正是入门与核心之间的那道缝。所谓「c++入门,核心,提高讲义笔记」,本质是把三个阶段的知识点串成一条线:入门解决语法能不能写,核心解决对象怎么活,提高解决性能怎么榨。这篇笔记面向的是已经能写简单 C++ 程序、但一遇到模板、移动语义、内存布局就发怵的从业者,也适合想系统重刷一遍基础的人。下面按这条线拆开讲,每一步都落到能编译、能验证的代码上。
2. 入门讲义:把语法写成能编译的最小闭环
2.1 从「能跑」到「能解释」:变量、类型与控制流的最小验证
入门讲义最容易犯的错是只给语法不给验证。我一般会让初学者写一个最小闭环:定义变量、做一次类型转换、跑一个循环、打印结果,然后逐行解释为什么。下面这段代码覆盖了入门阶段最核心的几个点,每个点都能单独拿出来追问。
#include <iostream> #include <string> #include <vector> int main() { // 1. 基本类型与初始化:区分拷贝初始化与直接初始化 int a = 42; // 拷贝初始化 int b(42); // 直接初始化 int c{42}; // 列表初始化,窄化转换会报错 // 2. 引用与 const:引用必须绑定,const 引用可绑临时量 const int& ref = a; // ref 是 a 的别名 // int& bad = 42; // 错误:非常量引用不能绑临时量 // 3. 控制流与作用域:for 循环里的变量生命周期 std::vector<int> nums{1, 2, 3, 4, 5}; int sum = 0; for (int n : nums) { // 范围 for,n 是拷贝 sum += n; } // 4. 字符串与输入输出 std::string name; std::cout << "sum=" << sum << ", a=" << a << std::endl; return 0; }这段代码的逻辑说明:列表初始化int c{42}在入门阶段就要强调,因为它能挡住int c{3.14}这种窄化转换,而拷贝初始化会静默截断。参数说明上,const int& ref绑定a之后,a变了ref也跟着变,这是引用和指针的第一个认知分水岭。范围 for 里的n是拷贝,如果元素是大对象,入门阶段就该养成写const auto&的习惯,虽然这时候还不讲移动语义,但习惯先立住。
控制流部分,入门讲义常忽略switch的 fallthrough 和break缺失,我一般会让学生故意写一个漏break的例子,看输出怎么串行,比背规则管用。函数部分,入门阶段要讲清声明与定义分离,头文件里放声明、源文件里放定义,这是后面多文件编译的基础。
2.2 函数、头文件与编译单元:入门阶段就要建立的工程感
入门讲义如果只在一个main.cpp里打转,到了核心阶段一拆多文件就懵。我一般会在入门末尾加一个最小多文件例子:一个add.h放声明,一个add.cpp放定义,一个main.cpp调用。
// add.h #pragma once int add(int x, int y); // add.cpp #include "add.h" int add(int x, int y) { return x + y; } // main.cpp #include <iostream> #include "add.h" int main() { std::cout << add(3, 4) << std::endl; return 0; }编译命令用g++ main.cpp add.cpp -o demo,这里的关键是理解编译单元:每个.cpp单独编译成目标文件,链接器再把它们拼起来。#pragma once是头文件保护的一种写法,另一种是#ifndef宏,入门阶段两种都见一下,知道它们解决的是同一个问题——防止头文件被重复包含导致重定义。
参数说明上,add的形参是值传递,入门阶段先建立「值传递是拷贝」的概念,后面核心讲义讲引用传递和指针传递时才有对照。常见坑是头文件里写了函数定义而不是声明,多个编译单元包含同一个头文件时链接期报重定义,这个坑在入门阶段踩一次,比后面项目里踩强。
3. 核心讲义:对象生命周期、拷贝控制与 RAII
3.1 构造函数、析构函数与拷贝控制的三角关系
核心讲义的分水岭是「对象什么时候生、什么时候死、死的时候谁负责」。我一般用一个带资源的类来串:构造函数拿资源,析构函数放资源,拷贝构造和拷贝赋值决定资源怎么复制。
#include <cstring> #include <iostream> class Buffer { public: explicit Buffer(std::size_t size) : size_(size), data_(new char[size]) { std::cout << "ctor size=" << size_ << std::endl; } // 拷贝构造:深拷贝 Buffer(const Buffer& other) : size_(other.size_), data_(new char[other.size_]) { std::memcpy(data_, other.data_, size_); std::cout << "copy ctor" << std::endl; } // 拷贝赋值:先释放自己的,再深拷贝 Buffer& operator=(const Buffer& other) { if (this == &other) return *this; // 自赋值保护 delete[] data_; size_ = other.size_; data_ = new char[size_]; std::memcpy(data_, other.data_, size_); std::cout << "copy assign" << std::endl; return *this; } ~Buffer() { delete[] data_; std::cout << "dtor size=" << size_ << std::endl; } private: std::size_t size_; char* data_; };逻辑说明:这个类故意用裸指针管理资源,是为了把拷贝控制的四个函数(构造、拷贝构造、拷贝赋值、析构)都显式写出来。参数说明上,explicit挡住Buffer b = 10;这种隐式转换,operator=返回Buffer&是为了支持链式赋值。自赋值保护if (this == &other)在拷贝赋值里必须有,否则a = a会先delete[]自己的数据再拷贝已经释放的内存,直接翻车。
核心讲义还要讲清「三法则」和「五法则」:如果需要一个自定义析构函数,通常也需要拷贝构造和拷贝赋值;C++11 之后再加上移动构造和移动赋值。这里先不展开移动,下一章提高部分再补。常见误用是拷贝赋值里忘了释放旧资源,导致内存泄漏,或者忘了自赋值保护导致悬空指针。
3.2 RAII 与智能指针:把资源管理从手写变成类型系统的事
手写new/delete在核心阶段就要逐步替换成 RAII。我一般会先让学生用std::unique_ptr重写上面的Buffer,感受一下析构函数不用自己写是什么体验。
#include <memory> #include <iostream> class Buffer { public: explicit Buffer(std::size_t size) : size_(size), data_(std::make_unique<char[]>(size)) { std::cout << "ctor size=" << size_ << std::endl; } // 拷贝构造需要手动深拷贝,因为 unique_ptr 不可拷贝 Buffer(const Buffer& other) : size_(other.size_), data_(std::make_unique<char[]>(other.size_)) { std::copy(other.data_.get(), other.data_.get() + size_, data_.get()); } // 移动构造:直接转移所有权 Buffer(Buffer&& other) noexcept : size_(other.size_), data_(std::move(other.data_)) { other.size_ = 0; std::cout << "move ctor" << std::endl; } private: std::size_t size_; std::unique_ptr<char[]> data_; };逻辑说明:std::make_unique<char[]>在 C++14 之后可用,它把new[]和unique_ptr绑在一起,析构时自动delete[]。参数说明上,移动构造标记noexcept很重要,因为std::vector扩容时只有在移动构造不抛异常的情况下才会用移动而不是拷贝,否则为了强异常安全会退化成拷贝。other.size_ = 0是把源对象置为有效但空的状态,避免移动后源对象析构时重复释放。
RAII 的核心思想是「资源获取即初始化」,把资源的生命周期绑定到对象的生命周期上。核心讲义要讲清unique_ptr独占所有权、shared_ptr共享所有权、weak_ptr打破循环引用。常见坑是用shared_ptr管理数组需要自定义删除器,或者两个对象互相shared_ptr导致循环引用内存泄漏,这时候weak_ptr就是后悔药。
4. 提高讲义:模板、移动语义与性能边界
4.1 模板实例化与编译期计算:把运行时开销前移
提高讲义的第一道坎是模板。很多人会用std::vector<int>,但自己写一个模板函数就发怵。我一般从函数模板入手,再过渡到类模板和编译期计算。
#include <iostream> #include <type_traits> // 函数模板:编译期推导 T template <typename T> T maxValue(T a, T b) { return a > b ? a : b; } // 类模板:固定大小数组,编译期确定大小 template <typename T, std::size_t N> class FixedArray { public: T& operator[](std::size_t i) { return data_[i]; } constexpr std::size_t size() const { return N; } private: T data_[N]; }; // 编译期计算:constexpr 函数 constexpr int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); } int main() { std::cout << maxValue(3, 7) << std::endl; FixedArray<int, 5> arr; arr[0] = 42; static_assert(factorial(5) == 120, "factorial check"); return 0; }逻辑说明:函数模板maxValue在调用时根据实参推导T,如果两个实参类型不同会推导失败,需要显式指定maxValue<int>(3, 7.0)。类模板FixedArray的N是编译期常量,data_[N]是栈上数组,没有堆分配。constexpr函数factorial在编译期就能算出结果,static_assert用来在编译期验证。
参数说明上,std::size_t N作为非类型模板参数,必须是编译期常量。提高讲义要讲清模板实例化是编译期行为,每个不同的T和N组合都会生成一份代码,这就是代码膨胀的来源。常见坑是模板定义放在.cpp里导致链接错误,因为模板需要在头文件里可见才能实例化,或者显式实例化。
4.2 移动语义与完美转发:把拷贝变成转移
移动语义是提高讲义的核心。我一般会用一个std::vector<std::string>的例子来展示移动和拷贝的差别,再用std::move和std::forward讲清左值右值。
#include <iostream> #include <string> #include <vector> #include <utility> void process(const std::string& s) { std::cout << "copy: " << s << std::endl; } void process(std::string&& s) { std::cout << "move: " << s << std::endl; } template <typename T> void wrapper(T&& arg) { // 完美转发:保持 arg 的值类别 process(std::forward<T>(arg)); } int main() { std::string a = "hello"; wrapper(a); // 左值,走拷贝 wrapper(std::string("world")); // 右值,走移动 wrapper(std::move(a)); // 显式移动,走移动 return 0; }逻辑说明:process有两个重载,const std::string&接受左值,std::string&&接受右值。wrapper是转发函数,T&&是转发引用(也叫万能引用),std::forward<T>(arg)根据T的推导结果决定转发成左值还是右值。参数说明上,std::move本身不移动任何东西,它只是把左值转成右值引用,真正的移动发生在移动构造或移动赋值里。
提高讲义要讲清移动构造和移动赋值的实现要点:源对象要被置为有效但可析构的状态,移动操作要标记noexcept。常见坑是对const对象调用std::move会退化成拷贝,因为const T&&无法绑定到T&&移动构造。另一个坑是在返回局部变量时画蛇添足写return std::move(local);,这会阻止 NRVO(返回值优化),反而多一次移动。
5. 避坑与排查:三份讲义之间最容易断的五个点
5.1 现象:入门能跑,核心一加类就链接错误
原因:头文件里写了函数定义或全局变量定义,多个编译单元包含后链接期重定义。解决:头文件只放声明,定义放.cpp;全局变量用extern声明,定义放一个.cpp;模板和inline函数可以放头文件。
5.2 现象:拷贝赋值后程序随机崩溃
原因:拷贝赋值没有自赋值保护,或者没有释放旧资源。解决:拷贝赋值先判断this == &other,再释放旧资源、分配新资源、拷贝数据;或者直接用拷贝并交换惯用法。
5.3 现象:std::vector扩容后元素内容错乱
原因:元素类型没有正确实现拷贝构造或移动构造,扩容时用了默认的浅拷贝。解决:检查元素类型的拷贝控制和移动控制,确保深拷贝或正确转移所有权;移动构造标记noexcept让vector优先用移动。
5.4 现象:模板代码在头文件里编译通过,链接时找不到符号
原因:模板定义放在.cpp里,其他编译单元看不到定义无法实例化。解决:模板定义放头文件;或者在.cpp里显式实例化需要的类型,并在头文件里extern template声明。
5.5 现象:std::move之后源对象还能用,但值变了
原因:移动构造或移动赋值把源对象的资源转移走了,源对象处于有效但未指定状态。解决:移动后不要再依赖源对象的值,只能对它赋值或析构;如果确实需要保留,用拷贝而不是移动。
6. 把三份讲义压成一张可执行的检查表
提高讲义的最后一层,是把入门、核心、提高的知识点压成一张能在代码审查时逐条过的检查表。我一般会按「资源、拷贝、移动、模板、异常」五个维度列,每条都对应一个能编译验证的小例子。
| 维度 | 检查项 | 验证方式 |
|---|---|---|
| 资源 | 是否用 RAII 管理裸资源 | 搜索new/delete是否成对出现在同一类 |
| 拷贝 | 拷贝构造和拷贝赋值是否深拷贝 | 构造两个对象,改一个看另一个是否受影响 |
| 移动 | 移动构造是否标记noexcept | static_assert(std::is_nothrow_move_constructible_v<T>) |
| 模板 | 模板定义是否在头文件可见 | 多编译单元包含同一头文件,链接是否通过 |
| 异常 | 析构函数是否可能抛异常 | 析构函数标记noexcept,内部捕获所有异常 |
这张表的价值在于把「讲义里讲过」变成「代码里查过」。我自己的习惯是每写一个带资源的类,先过一遍拷贝和移动的四个函数,再看析构是否noexcept,最后用static_assert把移动构造的noexcept钉死。这个习惯帮我省掉了至少两次线上内存泄漏的排查时间。入门讲义让你能写,核心讲义让你写对,提高讲义让你写快,而这张检查表让你写完之后还能睡得着。希望帮到你。
本文还有配套的精品资源,点击获取