《现代 C++ 零基础到工程实战》课堂笔记

第一章 大部分人被打倒在这里-C++环境和程序分析

0. 准备与目录

1. 常见文件与含义

2. 编译流水线(必须理解的五步)

预处理 → 编译(C++ → 汇编)→ 汇编(→ 目标文件)→ 链接(→ 可执行/库)→ 执行

3. ⭐ 理解 #include 真相


第二章 C++变量就是内存-它的一生是怎样的

(8) 第二章介绍

核心结论


(9) cpp变量分析

来自源码的关键点

输出(示例)(大小与地址因平台而异)

踩坑警告


(10) 变量代码演示

🟦 1. 基本整数字面量

输出(示例)

👉 注意:


🟧 2. 整数字面量后缀(类型标识)

👉 后缀不区分大小写,但建议使用大写 UL,更清晰。


🟨 3. 数字分隔符(C++14 起)

✅ 输出:

👉 这是 数字分隔符,只是给人看的,不影响数值。


🟩 4. 浮点数字面量

输出

👉 eE 表示 ×10 的幂,非常常见于科学计算。


🟦 5. 字符字面量(Character)

👉 字符字面量用 单引号 'A' 👉 它的底层其实是一个整数(ASCII 或 UTF-8 编码)


🟧 6. 字符串字面量(String)

👉 字符串字面量用 双引号 "..." 👉 编译时会自动在末尾加上 \0(字符串结束符)


🟨 7. 布尔字面量(Boolean)

输出:

👉 boolalpha 是一个 I/O 控制器(manipulator),切换布尔输出格式。


🟩 8. 空指针与 nullptr(C++11 起)

输出:

👉 nullptr 是类型安全的空指针,不要再用老式 NULL(那是 0 的宏定义)。


📝 9. endl vs '\n'

👉 endl 等价于 '\n' + 强制 flush 在日志、交互式输出中有用,但在循环中频繁使用会降低性能。


⚡ 小结表

字面量类型示例说明
整数123, 0123, 0xFF, 0b1010十进制/八进制/十六进制/二进制
整数后缀123U, 123L, 123LL, 123ULL指定类型
数字分隔符1'000'000仅增强可读性
浮点数3.14, 1.23e3, 3.14f, 3.14L支持科学计数法
字符'A', '\n', '\t'单个字符,底层是整数
字符串"Hello", "line\n"双引号包裹
布尔true, false + boolalpha 控制默认输出 1/0
空指针nullptrC++11 起引入

🧪 完整演示代码


(11) 变量的算术运算

基础运算+自增自减

输出(示例)

⭐ 易混淆对比|前++ vs 后++


(12) cpp浮点数计算和转化

  1. 整数除法会丢小数想要小数,至少让一个操作数是浮点

  2. 浮点数转整数小数位被直接扔掉,不会四舍五入

输出(示例)

易混淆对比|“什么时候是浮点除法?”

⭐ 额外提醒


(13) 变量的作用域和生命周期

来自源码的作用域演示

输出(示例)

规则速记


(14) cpp运行时和编译时常量

const / constexpr

作用:让人知道这是常量,不要去改动 constexpr编译期常量:必须是常量表达式

能做什么?


(15) cpp的auto自动推导类型

来自源码的典型推导

输出(示例,类型注释)

易混淆对比|保留“常量性”

关于 auto + 花括号 {}


第三章 C++开始逻辑了-有了分歧怎么办

(17) 第三章介绍

这章在讲:


(18) 逻辑判断if语法分析

最小示例

示例运行

易错点与修正


(19) if代码演示和常见错误

源码片段(关键坑点)

示例运行(假设输入 101

解释:

  • [103] 总会打印(多余分号导致的“悬空 if”)

  • [104] 总会打印,且把 x 改成了 104(= 赋值表达式的结果为 104,非 0 即真)

修正写法


(20) 算术运算逐位非与或(位运算)

运算符号说明示例
逐位非~每一位取反:0→1,1→0~0 = 1 ~1=0 ~1010 = 0101
逐位与&两位都为 1 时结果才是 11&1 = 1 0&1 = 0 0&0=0 1101 & 1011 = 1001
逐位或|只要有一位是 1 就是 11|1=1 0|1=1 1|0=1 0|0=0

示例运行

要点


(21) bool类型的算数运算

重要知识点: 非 0 则真 值为 0 → 被视为 false(假) 值不为 0 → 被视为 true(真)

判断结果bool 转换后
0假 (false)false (0)
非 0 (如 5、-3)真 (true)true (1)

bool 与算术/位运算

示例运行


(22) cpp逻辑运算和数学运算处理逻辑区别(短路)

1) 逻辑运算符与短路

逻辑运算常用符号代用符号作用示例结果
逻辑非!not取反(true → false,false → true)!true / not falsefalse / true
逻辑或||or只要有一方为真 → 整体为真(短路:左真不算右)false || true / false or truetrue
逻辑与&&and两方都为真 → 整体为真(短路:左假不算右)true && true / true and truetrue

背景设定

代码片段:

逐句解释:

结论:

  • 逻辑或 || / or:左真右不算;左假要看右边。

  • 逻辑与 && / and:左假右不算;左真才看右边。

  • 逻辑非 ! / not:取反。 短路求值跳过右侧表达式的求值与副作用。


2) ”逻辑或“ 和 ”逐位或“ 的差异:|| / or vs |

2.1 逻辑或(有短路) || / or

这一段的关键点:因为短路,x1 += 2 没有发生,副作用被跳过

2.2 逐位或(无短路) |

关键点:逐位或 | 总会计算两侧表达式,副作用都会发生

结论(本段):

  • ||/or逻辑或,有短路;

  • |按位或没有短路,两边一定都会求值; 在使用逻辑判断时,优先使用 逻辑或,效率更高

补充:比较运算符

运算符含义示例说明
==等于a == b判断 a 与 b 是否相等
!=不等于a != b判断 a 与 b 是否不相等
<小于a < b判断 a 是否小于 b
>大于a > b判断 a 是否大于 b
<=小于或等于a <= b判断 a 是否小于或等于 b
>=大于或等于a >= b判断 a 是否大于或等于 b

(23) c++ string 功能分析(综述)

1) 基础对象

2) 本章 API 地图(与上面小节对应)

3) 编码与实践

4) 一句话复盘


(24) string 代码示例分析赋值

1) 头文件与基础

2) 初始化

3) 赋值

4) 小坑 & 提示


(25) string 字符串长度、截断和比较

1) 长度:size() / length() ;容量:capacity()

2) 截断:substr(off[, count])

3) 判空

4) 比较

5) 中文与编码


(26) string 数字转换与拼接

1) 字符串 → 整数(stoi

2) 字符串 → 浮点(stod

这里 "33.1f" 是一个字符串std::stof() 会从头开始尝试解析:

字符动作解析状态
'3'✅ 可作为数字当前值:3
'3'✅ 可作为数字当前值:33
'.'✅ 小数点当前值:33.
'1'✅ 小数位当前值:33.1
'f'不是数字的一部分 → 🚫 停止解析 

结果:

所以,“在 'f' 处停止”的意思是:

std::stof() 在扫描字符串 "33.1f" 时, 读到 'f' 发现这不是合法数字字符, 因此停止继续解析,并返回目前得到的数值 33.1


🟨 为什么不报错?

因为 std::stof() 只要求:

字符串开头部分能成功转成一个数值。

后面如果还有额外字符,只要前面那部分能转成数,函数就不会抛异常

比如:

只有以下情况才会抛异常:

3) 数字 → 字符串(to_string

需要格式控制:用 std::format(C++20) 或 ostringstream

5) 拼接:+、+=(数值先 to_string


(27) string 的查找和替换

1) 查找:find

2) 定位后查看:substr(pos)

3) 替换:replace(pos, len, new_str)

4) 小心中文


补充:隐式转换显式转换

🧩 一、什么是类型转换?

类型转换指的是: 把一个变量的值从一种类型“转换”为另一种类型,以便进行计算、比较或赋值。

在 C++ 中,这种转换分为两大类:

分类中文名称触发方式
隐式转换 (Implicit Conversion) 自动类型转换编译器自动完成
显式转换 (Explicit Conversion) 强制类型转换程序员手动指定

🪄 二、隐式类型转换(Implicit Conversion)

🔹 概念

编译器在需要时自动将一种类型转换为另一种类型,不需要你写任何额外代码。 也称为“自动类型提升(type promotion)”。

🔹 特点

🔹 常见规则

  1. 整型提升charshort 自动提升为 int

  2. 算术转换:不同类型混合运算时,按精度高的转换

  3. bool 自动转换:非零为 true,零为 false

🔹 示例

📘说明: 这里的 i 是 int,但在运算中被自动转成 double。


🧠 三、显式类型转换(Explicit Conversion)

🔹 概念

程序员手动指定类型转换,也叫“强制类型转换”。

🔹 使用场景

🔹 常见写法

1️⃣ C风格强制转换
2️⃣ C++ 新式强制转换(推荐)
写法含义
static_cast<type>(expr)普通类型转换(安全)
const_cast<type>(expr)去掉或添加 const 属性
reinterpret_cast<type>(expr)重新解释内存(危险)
dynamic_cast<type>(expr)用于类的多态类型转换
🔹 示例

🧾 四、两者对比总结

对比项隐式转换显式转换
触发方式编译器自动完成程序员手动写出
语法形式(type)exprstatic_cast<type>(expr)
控制程度自动,灵活但不可控明确,安全但需小心
是否可能丢精度可能取决于程序员意图
示例int + double 自动变 doubleint n = (int)3.14;

🧩 五、小结口诀

💡 “编译器主动叫隐式,程序员亲自上阵叫显式。”

隐式靠编译器帮你“自动提升”, 显式靠你“手动指定类型”,更安全、更可控。

(28) 枚举enum类型和新特性分析

1) 为什么用枚举?


2) 枚举声明会产生两类实体

这些常量在 “非作用域枚举” 里会直接出现在外层作用域。


3) 非作用域枚举(C++11 之前)

3.1 定义与自增规则(默认从 0 开始自增,若被新赋值从新值继续自增

输出示例

3.2 变量类型该如何写

3.3 enum 花括号里的名字不是普通变量,是枚举常量

3.4 与整型的关系(允许隐式转换)


4) 强作用域枚举 enum class(C++11 及之后)

4.1 定义与访问

4.2 比较与应用(日志门槛)


5) “能 / 不能”速查(把边界感再夯实)


(30) 入口函数 main 参数传递和环境变量获取

目标:看懂 int main(int argc, char* argv[], char* env[]) 三个“入口参数”的含义、用法、易错点;能在终端或 VS 里传参与读取;能遍历/查询环境变量;会把字符串参数转换成数值


1. 三种常见的 main 形态(思维导图对应)

小结(一句一条): argc 是数量; argv[i] 是第 i 个 C 字符串; env[i] 是第 i 条环境变量 "KEY=VALUE",直到 nullptr 为止。


2. 代码骨架


3. 如何“传参”(三种情景)

3.1 终端/控制台

3.2 带空格的参数

3.3 Visual Studio(本地调试)

易错:argc 至少为 1argv[0] 总是存在),你的自定义参数从 argv[1] 开始。


4. argv[0] 的差异(容易忽视)


5. 常见坑 & 排错清单

(31) switch高效条件判断分析和代码示例

1. switch 是什么?

最小示例


2. switch 能判断什么类型?

OK 示例

不 OK 示例(string 不支持)


3. case 后面是什么?

示例


4. break 的作用

课堂对应代码片段

演示贯穿(不推荐随意这么写)

有意贯穿(C++17 可加标注)


5. default 的作用

示例


6. switch 内的“作用域”陷阱

正确示例

错误示例(容易报错


7. switch vs if-else 的选择

对比示例


8. 常见报错与排查

修复示例


9. 小结(一条一句话)

第四章 C++批量处理任务开始了

(35) cpp的for循环语法分析

1)执行模型

基本流程:

三个位置都可以留空(留空即什么都不做):

基本语法:

例:


2)常见用法示例

2.1 continuebreak

常见用途:

  • continue:过滤不需要处理的元素(奇偶过滤、非法输入跳过等)。

  • break:提前结束(找到目标、超出阈值、收到退出信号等)。

2.2 嵌套 for(尽量不超过三层)

2.3 “无限循环”( for(;;){}


3)continuebreak 的精确语义


4)关于初始化变量的作用域

在 C++ 中,for 语句的变量作用域(scope)取决于它是在循环语句中声明还是在外部声明。这点很重要,因为它决定了变量能否在循环结束后继续使用。下面分情况讲解。


🔒 一、在 for 内声明的变量:局部作用域

当你在 for 的初始化部分中声明一个变量(如 int i = 0),它的作用域仅限于 for 循环内部,包括循环头和循环体。

也就是说,循环结束后,这个变量会被销毁,在外面访问它会报错:

📘 这是 C++ 引入的一个改进(相较于早期 C 的写法) 让循环变量的生命周期尽可能短,避免命名冲突或误用。


🌍 二、在外部声明的变量:广泛作用域

如果你在 for 外面定义变量,再在 for 中使用它,那么它在循环结束后仍然存在:

这里的 i 在整个函数中都有效。


🔄 四、嵌套 for 的作用域示例


✅ 总结表格

声明位置作用域循环外能否访问生命周期
for (int i = 0; ...)循环语句内部❌ 否循环结束即销毁
int i; for (i = 0; ...)整个函数或外层块✅ 是函数结束才销毁

5) 初始化部分到底是“声明变量”还是“给已有变量赋值”,要分两种情况看。


🌱 一、两种情况对比

✅ 情况 1:声明新变量

这里的 int i = 0声明 + 初始化

💬 这不是对外部变量赋值,而是一个独立的新变量 即使外面也有一个 int i,这个新的 i遮蔽(shadow)外部的同名变量

例如:

🔸 内部的 i 和外部的 i 是两个不同的变量,它们互不影响。


✅ 情况 2:对外部变量赋值

这里的 i = 0 就是单纯地给已有变量赋值 循环结束后,这个变量的值会被保留(变成 3)。


⚙️ 二、总结对比表

写法是否创建新变量是否影响外部同名变量作用域离开循环后还能用?
for (int i = 0; ...)✅ 是❌ 否(会遮蔽外部变量)仅限循环内❌ 不能用
int i = 0; for (i = 0; ...)❌ 否✅ 会影响外部变量整个函数或块✅ 仍可用

💡 小结一句话

  • for (int i = 0; ...) → 创建一个新的局部变量(仅循环内可见)

  • int i = 0; for (i = 0; ...) → 对已有的外部变量赋值(循环外仍可见)


6) ”迭代“参数的进阶语法

for 的第三个参数(迭代)其实可以是任何合法的表达式,不局限于 i++i += n 这种简单的形式。 我们来详细讲讲它的灵活性与边界。


🔬 一、可以写的类型举例

✅ 1. 普通递增递减

经典写法。


✅ 2. 步长改变

每次加 5。


✅ 3. 多变量同时更新(逗号表达式)

这里第三个部分是 ++i, --j,会先计算 ++i 再计算 --j


✅ 4. 复杂计算表达式

完全合法,i 的更新由一个算式决定。


✅ 5. 带函数调用

这里第三个表达式是函数调用,只要 updateIndex() 是个合法可调用函数即可。

它甚至可以修改全局变量或引用参数。


✅ 6. 带逻辑或输出操作

也能这样用。虽然实际意义不大,但语法合法


⚠️ 二、注意事项

  1. 不能使用声明语句 第三个部分必须是表达式,而不是声明。比如下面是错误的:

    因为 int x = ...; 是声明语句,不是表达式。

  2. 复杂表达式要注意可读性 C++ 虽然允许任意表达式,但太复杂会让代码难懂。 实际工程中推荐保持第三个表达式“短而清晰”。


✨ 三、甚至可以什么都不写(无线循环

只要条件为空,就默认永远为真。


✅ 四、总结一句话

for 的第三个表达式非常自由: 只要是合法表达式,都可以放进去执行——递增、函数调用、复合算式、甚至多变量逗号表达式都行。 但出于可读性,推荐保持简洁(如 ++ii += n 等)。

 

(37) while循环流程控制

🧭 一、while 循环的基本语法

执行流程:


✅ 示例 1:打印 0 到 9

输出:

⚠️ 注意:如果忘记写 i++,则 i 永远为 0,条件一直成立,会造成死循环


🧩 二、whilefor 的关系

两者可以互相转换:

📘 总结区别:

特性forwhile
适用场景已知循环次数循环次数不确定
变量声明可在循环头声明一般在外部声明
条件检测循环前检测循环前检测
典型用途遍历数组、固定步长循环等待状态、条件控制、输入检测等

🌀 三、while(true) 无限循环

这是一个死循环(永远不会结束)。

通常会搭配 break 手动退出:

输出: 0 1 2 3 4 5


🧱 四、continuebreak 在 while 中的行为

输出:

🔍 分析:


📋 五、whiledo...while 的区别

do...while后判断循环,即先执行一次循环体,再判断条件。

执行顺序:

✅ 结论:

  • while先判断再执行 → 可能一次都不执行;

  • do...while先执行一次再判断 → 至少执行一次。


🔄 六、常见错误与注意事项

错误类型示例问题
忘记更新变量while(i<10){ cout<<i; }死循环
条件写错while(i=10)赋值表达式,条件恒为真
continue 用错while(...) { continue; i++; }i++ 永远不执行,死循环
逻辑嵌套不清多层 while 嵌套时 break/continue 指向最内层需谨慎控制层级

✅ 七、总结

while 是 C++ 最基础的循环语句,适用于循环次数不确定的场景。 它每次都先判断条件,为真则执行,为假则退出。 若需要“至少执行一次”,请用 do...while 若循环次数已知或有规律,优先考虑 for

(41) 栈区数组

🧠 一、什么是数组(Array)

数组是 一组 相同类型 的数据,在内存中连续存放,有连续的空间

基本语法类型 变量名[元素个数]

例:

👉 每个元素大小相同,排列连续。 👉 arr 这个名字本身代表“整个数组的首地址”。

数组名就是首地址,arr == &arr[0]


📍 二、下标与访问

C++ 的数组 下标从 0 开始。要访问或修改某个元素,用方括号:

⚠️ 注意arr[5]越界 了(此时 [] 内的 5 代表第 6 个元素(总共只有 5 个元素),因为下标从 0 开始),因为合法下标只有 0,1,2,3,4


🧩 三、数组的定义与长度来源

栈区数组空间(即[]里面的值)只能用编译期常量

在现代 C++ 中,虽然 const 是运行时常量,但是 const int N = 5; 会被视为编译期常量,因为编译器能提前知道数组的大小。 数组大小设置一定支持 constexpr 编译时常量,不支持无法在运行时确定的常量和变量。

扩展constconstexpr、“编译期常量”的判定

换句话说:

const int N = 5; —— 右边是字面量 5,属于常量表达式,所以 N 在编译期就能确定 const int N = read(); —— 右边依赖运行期输入,不是常量表达式,所以 N 不是编译期常量(只是个只读变量)。

为什么编译器“知道”是否是运行期常量?

编译器按标准对 常量表达式 做静态求值(constant folding / constant evaluation)。当它看到:

右侧完全由编译期可求值的东西组成(字面量、枚举值、另一个编译期常量等),于是它能在编译时直接得出 N == 5。这种 N 就可以用在需要常量表达式的语境里,

相反,如果初始化式不是常量表达式

这时 N 只是“运行期只读”,不是编译期常量

constexpr 的关系

结论一句话: const 自身不等于“运行期常量”。当且仅当它用一个编译期可求值的表达式初始化时,它就成了“编译期常量”并可用于需要常量表达式的场合;constexpr 则是对此的强保证。


🧱 四、数组的初始化方法

1️⃣ 全部手动赋值

2️⃣ 定义时初始化

3️⃣ 省略长度

4️⃣ 部分初始化

5️⃣ 不赋值

有关字符数组:字符数组初始化会包含一个隐含的 '\0' 结束符:

要注意一点,假如指定元素个数使用字符串数组,如下:

此时最多存 19 个字符,因为结尾还需要留一个 '\0' 的位置!


🧮 五、遍历数组

最常见的方式是 for 循环:

输出:

从 C++11 开始,还可以用范围 for 循环(更简洁):

计算数组元素个数的一种常见写法


📦 六、内存与地址

数组在内存中是一整块连续空间,是地址连续的空间。

补充术语

还可以用 指针偏移 的方式访问数组元素:

操作含义示例
p + 1 / p += 1指针向后移动一个元素不是一个字节,对于 int 元素一般 4 个字节)如果 p 指向 a[0],那么 p + 1 指向 a[1]
p - 1 / p -= 1指针向前移动一个元素如果 p 指向 a[3],那么 p - 1 指向 a[2]
p++ / ++p向后移动一格(效果与上面相同) 
p-- / --p向前移动一格 
表达式作用是否修改 p
p + 1计算“p 向后一个元素的地址”,但不改变 p 本身❌ 不改变
p++让 p 自己向后移动一个元素✅ 改变了 p 的值(地址)

⚠️ 注意: 指针移动单位不是字节,而是“类型大小”。 例如:

类型每个元素占多少字节p++ 实际加多少
int4 字节地址 + 4
double8 字节地址 + 8
char1 字节地址 + 1

💬 特别说明


🧭 七、数组在不同内存区域的形式

C++ 中有三种常见的数组存储方式:

类型定义方式特点
栈区数组int a[10];自动分配、作用域结束后自动释放
堆区数组new int[10];动态申请,手动 delete[] 释放
容器数组(vector)std::vector<int> a(10);自动管理内存,功能更强(推荐)