1目录:2(1) 为什么要学习现代C++课程介绍3(2) 1第一章说明和真实开发环境的安装和使用4(3) 2理解第一个简单又重要的cpp程序5(4) 3Cpp程序生成过程中每种文件的作用6(5) 4代码到程序生成过程7(6) 5注释和cout详解8(7) 第一章的总结和作业在资源管理器里显示文件扩展名(避免把 .h/.cpp/.obj/.exe/.pdb 混淆)。
建议示例目录:
xxxxxxxxxx71ProjectDemo/2├─ include/3│ └─ add.h4├─ src/5│ ├─ add.cpp6│ └─ main.cpp7└─ build/ (编译产物放这里,便于清理)
源代码文件:.cpp(实现)、.h(声明/接口)
目标文件:.obj(MSVC)/ .o(GCC/Clang),每个 .cpp 独立编译生成一个
可执行文件:.exe(Windows)
调试符号:.pdb(MSVC),Linux/Clang 用可执行内的 -g 符号信息
项目/解决方案配置(VS 中常见):.vcxproj(项目)、.sln(解决方案,支持多项目)
预处理 → 编译(C++ → 汇编)→ 汇编(→ 目标文件)→ 链接(→ 可执行/库)→ 执行
预处理:展开 #include、宏替换,删除注释,条件编译
编译:把预处理结果翻译成汇编
汇编:把汇编变成机器码,得到 .obj/.o
链接:把多个目标文件和库合并,解决符号引用,生成 .exe/.dll/.lib
执行:操作系统加载程序和依赖的 DLL,跳到入口点(main)
#include 真相xxxxxxxxxx21// main.cpp2include "add.h" // 预处理阶段等价于把 add.h 的文本“复制粘贴”到这里核心结论
变量 = 一段内存(有类型/大小/地址/值/作用域/生命周期)。
使用 C++ 11 新增的 列表初始化 {} 赋值更安全,带类型检查,可防止“窄化”(精度丢失)。
整数除法会丢小数;想要小数,至少让一个操作数是浮点。
const(运行时常量) vs constexpr(编译期常量)要分清。
auto 能推导类型,但会丢掉顶层 const;需要常量性要显式写出。
来自源码的关键点
x1int y; // 未初始化(值不确定,禁止读取!)2int x{100}; // 安全的定义变量方法,带类型检查3cout << "x=" << x << '\n'; // 链式输出4
5cout << "x 的地址 = " << &x << '\n'; // 直接输出指针6cout << "sizeof(x) = " << sizeof(x) << " 字节\n";7
8long long bigint{0};9cout << "sizeof(bigint) = " << sizeof(bigint) << " 字节\n";输出(示例)(大小与地址因平台而异)
xxxxxxxxxx41x=1002x 的地址 = 0000009F9F5EF8743sizeof(x) = 4 字节4sizeof(bigint) = 8 字节
踩坑警告
int y; cout<<y; → 未定义行为,不要读未初始化变量。
xxxxxxxxxx41cout << 123 << '\n'; // 十进制2cout << 0123 << '\n'; // 八进制(以 0 开头) → 833cout << 0x123 << '\n'; // 十六进制(0x 开头) → 2914cout << 0b1010 << '\n'; // 二进制(0b 开头,C++14 起) → 10输出(示例):
xxxxxxxxxx411232833291410
👉 注意:
0123 是 八进制!初学者常误以为是 123。
0b 二进制字面量从 C++14 才开始支持。
xxxxxxxxxx51cout << 123U << '\n'; // unsigned int2cout << 123L << '\n'; // long3cout << 123LL << '\n'; // long long4cout << 123UL << '\n'; // unsigned long5cout << 123ULL << '\n'; // unsigned long long👉 后缀不区分大小写,但建议使用大写 U、L,更清晰。
xxxxxxxxxx11cout << 1'000'000 << '\n'; // 1000000,单引号用于分隔,提高可读性✅ 输出:
xxxxxxxxxx111000000
👉 这是 数字分隔符,只是给人看的,不影响数值。
xxxxxxxxxx51cout << 3.14 << '\n'; // double(默认)2cout << 3.14f << '\n'; // float(小写 f 或 F)3cout << 3.14L << '\n'; // long double(大写 L)4cout << 1.23e3 << '\n'; // 科学计数法:1.23 × 10³ → 12305cout << 1.23e-2 << '\n'; // 1.23 × 10⁻² → 0.0123输出:
xxxxxxxxxx513.1423.1433.144123050.0123
👉 e 或 E 表示 ×10 的幂,非常常见于科学计算。
xxxxxxxxxx41cout << 'A' << '\n'; // A2cout << static_cast<int>('A') << '\n'; // 65(ASCII 码)3cout << '\n'; // 换行字符4cout << '\t' << "Tab" << '\n'; // 制表符 Tab(跳到下一个对齐列)👉 字符字面量用 单引号 'A'
👉 它的底层其实是一个整数(ASCII 或 UTF-8 编码)
xxxxxxxxxx21cout << "Hello" << '\n'; // 普通字符串(const char[6])2cout << "Hello\nWorld" << '\n'; // 含转义字符👉 字符串字面量用 双引号 "..."
👉 编译时会自动在末尾加上 \0(字符串结束符)
xxxxxxxxxx61cout << true << " " << false << '\n'; // 默认输出 1 02
3cout << boolalpha; // 打开布尔字面量输出4cout << true << " " << false << '\n'; // 输出 true false5
6cout << noboolalpha; // 关闭输出:
xxxxxxxxxx211 02true false
👉 boolalpha 是一个 I/O 控制器(manipulator),切换布尔输出格式。
xxxxxxxxxx21int* p = nullptr; // 空指针,推荐2cout << "p = " << p << '\n';输出:
xxxxxxxxxx11p = 0
👉 nullptr 是类型安全的空指针,不要再用老式 NULL(那是 0 的宏定义)。
xxxxxxxxxx21cout << "line\n"; // ✅ 换行,不强制刷新(高频输出更高效)2cout << "line" << endl; // ✅ 换行+刷新缓冲区(频繁用会慢)👉 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 |
| 空指针 | nullptr | C++11 起引入 |
xxxxxxxxxx3512using namespace std;3
4int main() {5 // 整数6 cout << 123 << " " << 0123 << " " << 0x123 << " " << 0b1010 << '\n';7
8 // 后缀9 cout << 123U << " " << 123L << " " << 123LL << " " << 123ULL << '\n';10
11 // 数字分隔符12 cout << 1'000'000 << '\n';13
14 // 浮点数15 cout << 3.14 << " " << 3.14f << " " << 3.14L << '\n';16 cout << 1.23e3 << " " << 1.23e-2 << '\n';17
18 // 字符与字符串19 cout << 'A' << " " << static_cast<int>('A') << '\n';20 cout << "Hello\nWorld" << '\n';21
22 // 布尔23 cout << true << " " << false << '\n';24 cout << boolalpha << true << " " << false << '\n';25
26 // 空指针27 int* p = nullptr;28 cout << "p = " << p << '\n';29
30 // endl vs '\n'31 cout << "line\n";32 cout << "line" << endl;33
34 return 0;35}基础运算+自增自减
xxxxxxxxxx61int x{1+2}; // 32int y{3}; 3int z{ x*y + 10 }; // 3*3+10=194z += 10; z -= 5; z *= 4; z /= 2; // 复合赋值5z++; --z; // 后++/前--(单独一行时效果等同)6cout << "z = " << z << '\n';输出(示例)
xxxxxxxxxx11z = 48
⭐ 易混淆对比|前++ vs 后++
xxxxxxxxxx31int a=5;2int b = a++; // b = 5, a = 6 (先用后加)3int c = ++a; // a 先加到 7,再赋值;c = 7, a = 7整数除法会丢小数;想要小数,至少让一个操作数是浮点。
浮点数转整数小数位被直接扔掉,不会四舍五入
xxxxxxxxxx131int u;2u = 1.1; // -> 1 (小数直接丢弃:**不四舍五入**)3u = 1.9; // -> 14u = 2 / 3; // -> 0 (整除)5
6int x1{1}, x2{2};7float f2{0};8
9f2 = x1 / x2; // 0,再转 float 得 010f2 = x1 / (float)x2; // 0.5(先提升参与运算)11f2 = (float)(x1 / x2); // 0 (先整除,再整体转)12cout << "1/2 = " << 1 / 2 << '\n'; // 013cout << "1/2. = " << 1 / 2. << '\n'; // 0.5, “2.” 字面量为 float输出(示例)
xxxxxxxxxx211/2 = 021/2. = 0.5
易混淆对比|“什么时候是浮点除法?”
xxxxxxxxxx41double a = 1 / 2; // 0 -> 再转 double 得 02double b = 1.0 / 2; // 0.53double c = static_cast<double>(1) / 2; // 0.5 ✅ 推荐写法(意图清晰)4double d = (double)(1 / 2); // 0 ⚠️ 先整除,已来不及了⭐ 额外提醒
xxxxxxxxxx31float f1{3.3}; // ⚠️ 列表初始化禁止“窄化”,可能编译失败2float f2 = 3.3; // 允许(会从 double 转为 float,精度可能损失)3float f3{3.3f}; // ✅ 推荐:用 f 后缀给 float 字面量来自源码的作用域演示
xxxxxxxxxx111int gx{0}; // 全局:静态存储期(进入 main 前创建,进程结束释放)2
3int main() {4 int px{1}; // 局部:块作用域(进入块时创建,离开块时销毁)5 {6 int py{2};7 int px{555}; // 遮蔽外层同名变量(若外层也叫 px)8 cout << "gx=" << gx << ", px=" << px << ", py=" << py << '\n';9 }10 // cout << py; // ❌ py 已出作用域,不能访问11}输出(示例)
xxxxxxxxxx11gx=0, px=555, py=2
规则速记
作用域 = 一对 {};离开就“看不到/不存在”了。
就近原则:内层同名会 遮蔽 外层同名变量。
工程实践:尽量缩小变量作用域;能在小块里就别放外面。
const / constexpr:
作用:让人知道这是常量,不要去改动
constexpr编译期常量:必须是常量表达式
xxxxxxxxxx61const int cx{100}; // 运行时常量:只读,但不要求编译期可得2int t1 = 10;3const int cx2{ t1 + 10 }; // ✅ 可以:仍是运行时常量4
5constexpr int cex{300}; // 编译期常量:必须是常量表达式6// constexpr int cex2{ t1 * 10 }; // ❌ 错:t1 不是常量表达式能做什么?
constexpr 可用于需要编译期常量的场景:数组长度、模板非类型参数、case 标签等。
const 值不变,但可能直到运行时才确定(不能用在上面那些需要编译期常量的地方)。
来自源码的典型推导
xxxxxxxxxx111constexpr int cex{300};2auto a1 = 10; // int3auto d1 = 9.; // double4auto f1 = 8.f; // float5auto acex = cex; // int(变量 cex 的 const 被“剥落”,auto 推导常量是会变为普通变量)6
7constexpr auto c1 = cex; // constexpr int(保留编译期常量语义)8const auto c2 = a1; // const int(保留只读)9
10auto a4 = 14LL; // long long11auto a5 = (int)d1; // int(先显式转,再推导)输出(示例,类型注释)
xxxxxxxxxx31// a1: int, d1: double, f1: float2// acex: int(不是 const/constexpr!)3// c1: constexpr int, c2: const int易混淆对比|保留“常量性”
xxxxxxxxxx31auto v1 = cex; // int (❌ 常量性丢了)2const auto v2 = cex; // const int (✅ 只读)3constexpr auto v3 = cex;// constexpr int(✅ 编译期常量)关于 auto + 花括号 {}
auto 配合花括号可能触发 std::initializer_list 的推导规则(不同标准/编译器下体验不一致,容易困惑)。
新手建议:避免 auto x{...};,优先 auto x = ...; 或显式类型,以免出现意料之外的推导。
这章在讲:
位运算(逐位非/与/或)与逻辑运算(! && ||)的区别
短路求值对表达式副作用的影响
bool 与算术/位运算的互动(0/1、可参与 & | ~ 但不推荐)
if / else if / else 的写法与常见坑(多余分号、= 与 == 混淆、条件顺序)
std::string:初始化/赋值、长度/容量、截断、比较、数值转换、拼接、查找与替换
最小示例
xxxxxxxxxx111int x; 2cin >> x; // 从标准输入读取3if (x > 100) { // 分支14 cout << "x > 100\n";5} else if (x > 200) { // 分支2(此写法有逻辑问题,见下)6 cout << "x > 200\n";7} else if (x < 50) { // 分支38 cout << "x < 50\n";9} else { // 兜底10 cout << "else x = " << x << '\n';11}示例运行
输入 30 → x < 50
输入 150 → x > 100
输入 205 → 仍然是 x > 100(为什么没有走 >200? 见下)
易错点与修正
条件顺序:x > 100 放在最前,x > 200 永远到不了(因为 >200 也满足 >100)。
✅ 推荐:从“更严格/更特殊”的条件开始:
xxxxxxxxxx41if (x > 200) { ... }2else if (x > 100) { ... }3else if (x < 50) { ... }4else { ... }补充
对于简短的语句,可用:if () ________; 例如:if (f1 | t1) cout << "(f1|t1)true\n";
源码片段(关键坑点)
xxxxxxxxxx131if (x == 101)2 cout << "x==101\n";3if (x != 102) cout << "[!=102]";4if (x == 103); { // ⚠️ 多了一个分号!导致下面的大括号总会执行5 cout << "[103]";6}7
8if (x = 104) // ⚠️ 把 == 写成了 =9 cout << "[104]"; // 条件恒为 true,且副作用把 x 赋值为 10410
11if (105 == x) { // ✅ “常量在左”的写法可避免把 == 写成 = 的事故12 cout << "[105]";13}示例运行(假设输入 101)
xxxxxxxxxx31x > 1002x==1013[!=102][103][104]
解释:
[103]总会打印(多余分号导致的“悬空 if”)
[104]总会打印,且把x改成了 104(=赋值表达式的结果为 104,非 0 即真)
修正写法
xxxxxxxxxx71if (x == 103) { // ❌ 去掉多余分号2 cout << "[103]";3}4
5if (x == 104) { // ❌ 不要写成 =6 cout << "[104]";7}| 运算 | 符号 | 说明 | 示例 |
|---|---|---|---|
| 逐位非 | ~ | 每一位取反:0→1,1→0 | ~0 = 1 ~1=0 ~1010 = 0101 |
| 逐位与 | & | 两位都为 1 时结果才是 1 | 1&1 = 1 0&1 = 0 0&0=0 1101 & 1011 = 1001 |
| 逐位或 | | | 只要有一位是 1 就是 1 | 1|1=1 0|1=1 1|0=1 0|0=0 |
xxxxxxxxxx912char a = 0b10000001; // C++14 起支持 0b 前缀3char b = 0b00000001;4cout << "a:\t" << bitset<8>(a) << '\n';5cout << "~a:\t" << bitset<8>(~a) << '\n';6cout << "b:\t" << bitset<8>(b) << '\n';7cout << "~b:\t" << bitset<8>(~b) << '\n';8cout << "a&b:\t" << bitset<8>(a & b) << '\n';9cout << "a|b:\t" << bitset<8>(a | b) << '\n';示例运行
xxxxxxxxxx61a: 100000012~a: 011111103b: 000000014~b: 111111105a&b: 000000016a|b: 10000001
要点
~ 逐位取反;& 逐位与;| 逐位或。
用 bitset<8> 可直观看 8 位结果。
重要知识点: 非 0 则真 值为 0 → 被视为 false(假) 值不为 0 → 被视为 true(真)
| 值 | 判断结果 | bool 转换后 |
|---|---|---|
| 0 | 假 (false) | false (0) |
| 非 0 (如 5、-3) | 真 (true) | true (1) |
bool 与算术/位运算
xxxxxxxxxx61bool f1{false}, t1{true};2cout << "f1=" << f1 << ", t1=" << t1 << '\n'; // 输出 0 / 13
4cout << (f1 | t1) << '\n'; // 1 (位或)5cout << (f1 & t1) << '\n'; // 0 (位与)6cout << (!t1) << '\n'; // 0 (逻辑非)示例运行
xxxxxxxxxx41f1=0, t1=1213040
| 逻辑运算 | 常用符号 | 代用符号 | 作用 | 示例 | 结果 |
|---|---|---|---|---|---|
| 逻辑非 | ! | not | 取反(true → false,false → true) | !true / not false | false / true |
| 逻辑或 | || | or | 只要有一方为真 → 整体为真(短路:左真不算右) | false || true / false or true | true |
| 逻辑与 | && | and | 两方都为真 → 整体为真(短路:左假不算右) | true && true / true and true | true |
背景设定:
xxxxxxxxxx21bool f1 = false;2bool t1 = true;代码片段:
xxxxxxxxxx181// 逻辑运算符(代用运算符)2// 逻辑非 ! not3// 逻辑或 || or4// 逻辑与 && and5// 逻辑运算符 短路求值:如果通过第一个操作数就能得到结果,就不再求值第二个6if (f1 or t1) {7 cout << "f1 or t1" << endl;8}9if (f1 and t1) {10} else {11 cout << "f1 and t1 else" << endl;12}13if (not f1) {14 cout << "not f1" << endl;15}16if (not (f1 and t1)) {17 cout << "not (f1 and t1)" << endl;18}逐句解释:
if (f1 or t1)
左侧 f1=false,无法确定为真,继续计算右侧 t1=true,结果为 true → 打印 f1 or t1。
if (f1 and t1) ... else ...
and 的左侧 f1=false 已经足以确定整个表达式为 false,短路,右侧 t1 不再计算 → 走 else,打印 f1 and t1 else。
if (not f1)
not false == true → 打印 not f1。
if (not (f1 and t1))
括号里 f1 and t1 因 f1=false 短路 为 false,not false == true → 打印 not (f1 and t1)。
结论:
逻辑或
|| / or:左真右不算;左假要看右边。逻辑与
&& / and:左假右不算;左真才看右边。逻辑非
! / not:取反。 短路求值会跳过右侧表达式的求值与副作用。
|| / or vs |xxxxxxxxxx131// 验证短路求值2int x1{0};3x1 = 10;4if ((x1++) or (x1 += 2)) {5 cout << "(x1++) or (x1 += 2)" << endl;6}7cout << "x1 = " << x1 << endl;8
9x1 = 10;10if ((x1++) | (x1 += 2)) {11 cout << "(x1++) | (x1 += 2)" << endl;12}13cout << "x1 = " << x1 << endl;|| / or初始:x1 = 10
计算 ((x1++) or (x1 += 2))
先算左边 x1++:结果值为 10,然后 x1 自增为 11
在逻辑环境中,10 被视为 true → 短路,右侧 (x1 += 2) 完全不执行
条件为真 → 打印 "(x1++) or (x1 += 2)"
输出 x1 = 11
这一段的关键点:因为短路,x1 += 2 没有发生,副作用被跳过。
|初始:x1 = 10
计算 ((x1++) | (x1 += 2))
先算左边 x1++:结果为 10,自增后 x1 = 11
不短路,继续算右边 (x1 += 2):结果为 13,此时 x1 = 13
做按位或:10 (1010b) | 13 (1101b) = 15 (1111b) → 非零,条件为真
打印 "(x1++) | (x1 += 2)"
输出 x1 = 13
关键点:逐位或 | 总会计算两侧表达式,副作用都会发生。
结论(本段):
||/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 |
字符:char(如 'c'、'\n')。
字符串字面量:const char*;多个 char 组成;以 '\0' 结尾。
C++ 字符串:std::string;动态长度;值语义;易拷贝。
创建 / 赋值:构造、operator=。
长度 / 容量:size()、capacity()。
截断:substr(off[,count])。
判空 / 比较:empty()、==、!=。
查找 / 替换:find()、replace()、npos。
数字转换:stoi/stol/stoll/stof/stod/stold、to_string()。
拼接:+、+=。
GBK 与 UTF-8 的字节长度不同。
size() 统计字节,不统计“字符数”。
课堂建议:中文不直接写在源码;统一编码;必要时外部文件读入。
取长:size();截断:substr;
查找:find;替换:replace;
转数:sto*;转串:to_string;
判空:empty();拼接:+ / +=。
头文件:#include <string>
命名空间:using namespace std;
类型名:std::string
xxxxxxxxxx21string a{"test string 1"};2cout << a << "\n"; // 输出:test string 1xxxxxxxxxx31string b{a}; // 拷贝构造2a = "test string 1-2"; // 修改 a 不影响 b3cout << b << "\n"; // 输出:test string 1xxxxxxxxxx21string c; // 默认构造,空串2cout << c.empty() << "\n"; // 输出:1xxxxxxxxxx71string d{"hello"};2d = "world"; // 从字面量赋值3cout << d << "\n"; // 输出:world4
5string e{"x"};6e = d; // 从 string 赋值7cout << e << "\n"; // 输出:worldstring ≠ const char*。但可以用字面量给 string 赋值。
size() / length() ;容量:capacity()xxxxxxxxxx101string s{"123456789"};2cout << s.size() << "\n"; // 输出:93cout << s.length() << "\n"; // 输出:94cout << s.capacity() << "\n"; // ≥9(运行值:15)5
6/*7.capacity() 表示 string 底层当前分配的可用空间容量,即当前底层内存中可容纳的最大字符数(不含 '\0')8通常大于等于 .size(),9目的是提升性能,避免频繁内存重新分配。10*/size() 完全等价于 length() ,两者不含结尾 \0。
substr(off[, count])xxxxxxxxxx31string t{"123456789"};2cout << t.substr(3) << "\n"; // 从下标3:4567893cout << t.substr(1,3) << "\n"; // 从下标1(第2个字符)起取3个:234substr 返回新字符串,不改变原字符串
off 从 0 开始,因为字符串的下标是从 0 开始。
xxxxxxxxxx41string x; // 默认空串2if (x.empty()) cout << "empty\n"; // 推荐3if (x.size()==0) cout << "size == 0\n"; // 也可以4if (x == "") cout << "== \"\"\n"; // 也可以,输出:== ""xxxxxxxxxx61string a{"test"};2cout << (a=="test") << "\n"; // 13cout << (a!="test1") << "\n"; // 14
5string b{"if2"};6cout << (a==b) << "\n"; // 0,即 falseGBK:常见中文 2 字节。
UTF-8:2~4 字节不等。
size() 计的是字节数,不是“汉字个数”。
stoi)xxxxxxxxxx21auto i = stoi("1234");2cout << i + 1 << "\n"; // 1235xxxxxxxxxx11cout << stoll("-1231231231233") << "\n"; // 长整型如果失败会抛异常:invalid_argument 或 out_of_range。
stod)xxxxxxxxxx21double d = stod("123.5");2cout << d << "\n"; // 123.5xxxxxxxxxx31float f1 = stof("33.1f"); // 在 'f' 处停止2float f2 = stof("33.2");3cout << f1 << " " << f2 << "\n"; // 33.1 33.2这里 "33.1f" 是一个字符串,std::stof() 会从头开始尝试解析:
| 字符 | 动作 | 解析状态 |
|---|---|---|
'3' | ✅ 可作为数字 | 当前值:3 |
'3' | ✅ 可作为数字 | 当前值:33 |
'.' | ✅ 小数点 | 当前值:33. |
'1' | ✅ 小数位 | 当前值:33.1 |
'f' | ❌ 不是数字的一部分 → 🚫 停止解析 |
结果:
它成功解析出 "33.1" 部分;
'f' 之后的内容被忽略;
返回浮点数 33.1f。
所以,“在 'f' 处停止”的意思是:
std::stof()在扫描字符串"33.1f"时, 读到'f'发现这不是合法数字字符, 因此停止继续解析,并返回目前得到的数值33.1。
🟨 为什么不报错?
因为 std::stof() 只要求:
字符串开头部分能成功转成一个数值。
后面如果还有额外字符,只要前面那部分能转成数,函数就不会抛异常。
比如:
xxxxxxxxxx21cout << std::stof("123abc") << endl; // 输出 1232cout << std::stof("3.14xyz") << endl; // 输出 3.14只有以下情况才会抛异常:
字符串完全无法解析出数字(例如 "abc")
或数字太大超出 float 范围(例如 "1e1000")
to_string)xxxxxxxxxx31auto p = to_string(3.1415926);2cout << p << "\n"; // 3.141593(默认约6位)3cout << to_string(-19998) << "\n"; // -19998需要格式控制:用
std::format(C++20) 或ostringstream。
to_string)xxxxxxxxxx101string user="xcj";2string txt="login success!";3int tid=1023;4
5string log = user + ":" + txt + ":" + to_string(tid);6log = "[debug]" + log;7log += ";";8
9cout << log << "\n";10// 输出:[debug]xcj:login success!:1023;两侧至少一个是 string,否则 "a" + "b" 非法。
findxxxxxxxxxx41string s = "test for find [user] test";2
3auto p1 = s.find("[test]");4if (p1==string::npos) cout << "[test] not find\n"; // 未找到xxxxxxxxxx31string key = "[user]";2auto pos = s.find(key);3cout << pos << "\n"; // 14(位置可能随实现)substr(pos)xxxxxxxxxx11cout << s.substr(pos) << "\n"; // "[user] test"replace(pos, len, new_str)xxxxxxxxxx111auto bak = s; // 备份2
3auto ref = s.replace(4 pos, // 1 被替换字符的起始位置5 key.size(), // 2 要被替换掉的内容长度6 "xcj" // 3 要替换为的字符串7 );8 9// s 被原地修改10cout << bak << "\n"; // test for find [user] test11cout << ref << "\n"; // test for find xcj testreplace 返回被修改后的字符串引用。
替换后位置会变化;再次查找要重新 find。
len 是字节数;中文多字节时要谨慎。
需要“按字符替换”请先解码或用库。
类型转换指的是: 把一个变量的值从一种类型“转换”为另一种类型,以便进行计算、比较或赋值。
在 C++ 中,这种转换分为两大类:
| 分类 | 中文名称 | 触发方式 |
|---|---|---|
| 隐式转换 (Implicit Conversion) | 自动类型转换 | 编译器自动完成 |
| 显式转换 (Explicit Conversion) | 强制类型转换 | 程序员手动指定 |
编译器在需要时自动将一种类型转换为另一种类型,不需要你写任何额外代码。 也称为“自动类型提升(type promotion)”。
自动发生,无需人工干预
不会改变原变量类型,只在表达式中临时转换
可能会丢失精度(例如 int → float → double)
整型提升:char、short 自动提升为 int
算术转换:不同类型混合运算时,按精度高的转换
bool 自动转换:非零为 true,零为 false
xxxxxxxxxx912using namespace std;3
4int main() {5 int i = 5;6 double d = 2.5;7 double result = i + d; // int 自动转换为 double8 cout << result << endl; // 输出:7.59}📘说明: 这里的
i是 int,但在运算中被自动转成 double。
由程序员手动指定类型转换,也叫“强制类型转换”。
控制精度或避免自动转换引起的错误
明确告诉编译器“我就是要这么转!”
xxxxxxxxxx21int a = 10;2double b = (double)a; // C 风格| 写法 | 含义 |
|---|---|
static_cast<type>(expr) | 普通类型转换(安全) |
const_cast<type>(expr) | 去掉或添加 const 属性 |
reinterpret_cast<type>(expr) | 重新解释内存(危险) |
dynamic_cast<type>(expr) | 用于类的多态类型转换 |
xxxxxxxxxx812using namespace std;3
4int main() {5 double pi = 3.14159;6 int n = static_cast<int>(pi); // 显式转换:去掉小数部分7 cout << n << endl; // 输出:38}| 对比项 | 隐式转换 | 显式转换 |
|---|---|---|
| 触发方式 | 编译器自动完成 | 程序员手动写出 |
| 语法形式 | 无 | (type)expr 或 static_cast<type>(expr) |
| 控制程度 | 自动,灵活但不可控 | 明确,安全但需小心 |
| 是否可能丢精度 | 可能 | 取决于程序员意图 |
| 示例 | int + double 自动变 double | int n = (int)3.14; |
💡 “编译器主动叫隐式,程序员亲自上阵叫显式。”
隐式靠编译器帮你“自动提升”, 显式靠你“手动指定类型”,更安全、更可控。
用名字替代“魔法数字”。
编译期常量,零开销。
典型:日志级别、消息类型。
一个类型:枚举类型名。
一组常量:枚举值1 ... 枚举值5(它们是 常量,不是 变量)。
这些常量在 “非作用域枚举” 里会直接出现在外层作用域。
xxxxxxxxxx71enum 枚举类型名 {2 枚举值1, // 默认 0,从 0 开始3 枚举值2, // 1(每个加 1)4 枚举值3 = 100, // 显式赋值为 1005 枚举值4, // 101(从 100 继续 +1)6 枚举值5 // 1027};输出示例
xxxxxxxxxx21cout << "枚举值1 = " << 枚举值1 << "\n"; // 02cout << "枚举值4 = " << 枚举值4 << "\n"; // 101想存“枚举值”,变量类型应该是该枚举类型:
xxxxxxxxxx11枚举类型名 ev1{ 枚举值5 }; // ✅ 类型安全也可以用 auto(类型被推导成 枚举类型名):
xxxxxxxxxx11auto ev2 = 枚举值5; // ✅ 等价存成 int 虽能编过,但不推荐:
xxxxxxxxxx11int ev3 = 枚举值5; // ⚠️ 变量变成 int,丢失“这是哪个枚举”的信息enum 花括号里的名字不是普通变量,是枚举常量这些名字在非作用域枚举里,会直接“注入”到外层作用域,可直接用:
xxxxxxxxxx11cout << 枚举值1 << "\n"; // ✅ 会隐式转成 int 打印,比如 0但它们不是变量,不可以赋值、取地址:
xxxxxxxxxx21枚举值1 = 枚举值2; // ❌ 错:不能给枚举常量重新赋值2// &枚举值1; // ❌ 错:不是对象,不能取地址由于是常量,它们还能用于 switch、数组大小等需要编译期常量的地方。
xxxxxxxxxx111enum Status { PLAY, PAUSE, STOP }; // 0,1,22
3Status status{ STOP }; // 值 24int var1 = status; // ✅ 隐式转 int5if (status == 2) {6 cout << "2 STOP\n"; // 预期:2 STOP7}8status = PLAY;9if (status == PLAY) {10 cout << PLAY << " PLAY\n"; // 预期:0 PLAY11}枚举是整数,可以做大小比较。
enum class(C++11 及之后)xxxxxxxxxx61enum class LogLevel { DEBUG, INFO, ERROR, FATAL };2
3LogLevel level{ LogLevel::DEBUG }; // ✅ 必须加作用域4// cout << level; // ❌ 不能直接当整数打印5int s = static_cast<int>(level); // ✅ 显式转换6cout << s << "\n"; // 预期:0无法直接当做整数使用,防止混淆。
访问值必须加类型,减少命名冲突:LogLevel::DEBUG。
xxxxxxxxxx41LogLevel log_level = LogLevel::DEBUG;2if (level <= log_level) {3 cout << "记录日志\n"; // 预期:记录日志4}同一 enum class 之间可比较(按声明顺序)。
xxxxxxxxxx151enum Status { PLAY, PAUSE, STOP };2
3// 变量类型用枚举类型(推荐)4Status s = STOP; // ✅5if (s == STOP) {} // ✅6
7// 用 auto(也安全)8auto s2 = STOP; // ✅ 推导为 Status9
10// 直接存成 int(不推荐)11int v = STOP; // ⚠️ 易与普通 int 混用12
13// 把 int 赋给枚举(隐式不行;显式可以,但要自己保证范围)14Status s3 = 2; // ❌ 编译不过15Status s4 = static_cast<Status>(2); // ✅ 但请确保 2 合法xxxxxxxxxx61enum class LogLevel { DEBUG, INFO, ERROR, FATAL };2
3LogLevel lv = LogLevel::ERROR; // ✅4int i = lv; // ❌ 不能隐式转为 int5int j = static_cast<int>(lv); // ✅ 显式转换6// LogLevel::DEBUG 这类名字不会“裸露”在外层作用域目标:看懂
int main(int argc, char* argv[], char* env[])三个“入口参数”的含义、用法、易错点;能在终端或 VS 里传参与读取;能遍历/查询环境变量;会把字符串参数转换成数值。
int main()
不接收任何命令行参数。
int main(int argc, char* argv[])
最常用。argc 是参数个数(含程序名),argv 是字符串数组。
int main(int argc, char* argv[], char* env[])
多了 env(又写作 envp):环境变量数组;最后一个元素是 nullptr(哨兵)。
小结(一句一条): •
argc是数量; •argv[i]是第i个 C 字符串; •env[i]是第i条环境变量"KEY=VALUE",直到nullptr为止。
xxxxxxxxxx1312using namespace std;3
4int main(int argc, char* argv[], char* env[]) {5 cout << "argc = " << argc << endl;6 cout << "argv[0] = " << argv[0] << endl; // 通常是可执行文件路径/名称7
8 if (argc > 1) cout << argv[1] << endl;9 if (argc > 2) cout << argv[2] << endl;10 if (argc > 3) cout << argv[3] << endl;11
12 cout << env[0] << endl; 13}Linux / macOS:
xxxxxxxxxx11./test_main DEBUG 16 3.14
Windows(PowerShell/CMD):
xxxxxxxxxx11.\test_main.exe DEBUG 16 3.14
用引号包起来(跨平台通用):
xxxxxxxxxx11./test_main "Hello World" "C:\Program Files"
“项目属性 → 调试 → 命令参数”填:
xxxxxxxxxx11DEBUG 16 3.14
启动调试后,程序就能按 argv[1]、argv[2]… 读到。
易错:
argc至少为 1(argv[0]总是存在),你的自定义参数从argv[1]开始。
argv[0] 的差异(容易忽视)Linux 常见:"./test_main" 或绝对路径。
Windows 常见:"test_main.exe" 或带路径。
不要依赖它一定是绝对路径;想拿“程序所在目录”,请用平台 API(如 C++17 std::filesystem::current_path() 获取当前工作目录,但这不一定等于程序目录)。
下标越界:访问 argv[i] 前先判断 argc > i。
数值转换异常:stoi/stod 要包 try/catch。
含空格的参数丢失:请用引号 "..."。
把 env 当无限数组:必须以 nullptr 判断结束。
误以为 argv[0] 是绝对路径:并不保证。
switch 是什么?用“一个条件 → 多个离散分支”的场景。
比很多个 if-else if-else 更清晰,常能被编译器优化成跳转表。
最小示例
xxxxxxxxxx61int x = 2;2switch (x) {3 case 1: cout << "one\n"; break;4 case 2: cout << "two\n"; break;5 default: cout << "other\n";6}switch 能判断什么类型?条件表达式必须是 整型 或 枚举 enum 类型(或能隐式转为整型/枚举)。
不能直接用 std::string、double 等。
OK 示例
xxxxxxxxxx21char c = 'a'; // 整型家族2switch (c) { case 'a': break; default: break; }不 OK 示例(string 不支持)
xxxxxxxxxx21// std::string s = "hi";2// switch (s) { } // ❌ 编译不过case 后面是什么?编译期常量表达式:字面量、枚举值、constexpr、符合条件的 const。
不能重复,否则编译报错。
示例
xxxxxxxxxx111constexpr int Add = 1; // 编译期常量2const int Del = 2; // 只要满足编译期常量条件即可3enum class Op { Quit = 3 };4
5int cmd = 1;6switch (cmd) {7 case Add: cout << "Add\n"; break;8 case Del: cout << "Del\n"; break;9 case static_cast<int>(Op::Quit): cout << "Quit\n"; break;10 // case 1: ... // ❌ 与 Add 重复11}break 的作用执行到 break 就跳出整个 switch。
不写 break 会贯穿(fallthrough) 到后面的 case。
课堂对应代码片段
xxxxxxxxxx51switch (x) {2 case 0: cout << "case 0\n"; break;3 case 2: cout << "case 2\n"; break;4 default: cout << "default\n";5}演示贯穿(不推荐随意这么写)
xxxxxxxxxx91int n = 1;2switch (n) {3 case 1:4 cout << "1\n"; // 打印 15 // 没有 break,继续往下6 case 2:7 cout << "2\n"; // 紧接着打印 28 break;9}有意贯穿(C++17 可加标注)
xxxxxxxxxx91int n = 1;2switch (n) {3 case 1:4 cout << "merge with case 2\n";5 [[fallthrough]]; // 有意继续落到 case 26 case 2:7 cout << "handle 1&2\n";8 break;9}明确告诉编译器,这个 case 的“无 break”行为是有意的、不是忘写 break 的 bug,避免编译器 ⚠️Warning
default 的作用前面所有 case 都不匹配时进入。
可以放任何位置(一般放最后)。
可省略,但工程里通常保留兜底。
示例
xxxxxxxxxx41switch (x) {2 case 0: cout << "0\n"; break;3 default: cout << "not 0\n";4}如果 default 放在开头,而且没有加 break,会贯穿全部!
switch 内的“作用域”陷阱所有 case 共享同一作用域。
在某个 case 中定义新变量时,要用 {} 人为包一层,避免“跨初始化跳转”的编译错误。
课堂代码在 case 1 演示了这一点。
正确示例
xxxxxxxxxx121switch (x) {2 case 1: {3 int x1{100}; // 放到花括号里4 cout << "begin 1 " << x1 << "\n";5 cout << "case 1\n";6 break;7 }8 case 2:9 cout << "case 2\n"; break;10 default:11 cout << "default\n";12}错误示例(容易报错)
xxxxxxxxxx91switch (x) {2 case 1:3 int t = 42; // ❌ 可能跨初始化跳转4 cout << t;5 break;6 case 2:7 cout << "2";8 break;9}switch vs if-else 的选择离散常量判断(0/1/2、枚举项) → 首选 switch。
区间判断(<、>、范围) → 用 if-else。
对比示例
xxxxxxxxxx51int score = 85;2// 区间:用 if-else3if (score >= 90) cout << "A";4else if (score >= 80) cout << "B";5else cout << "C";“duplicate case value” → case 值重复。
“not a constant expression” → case 不是编译期常量。
“crosses initialization/跨初始化跳转” → 在 case 里定义变量但没用 {} 包起来。
“switch quantity not an integer” → 条件不是整型或枚举。
修复示例
xxxxxxxxxx51// 错误:case 用了运行期变量2int v = 10;3// case v: ... // ❌4constexpr int k = 10;5switch (x) { case k: /* ok */ break; default: break; }switch 适合离散值的多路分发。
case 必须是编译期常量,且不重复。
别忘记 break,否则会贯穿到下一个 case。
所有 case 同一作用域,在 case 内定义变量要用 {}。
与 enum class 搭配可读性、安全性更好。
C++17 的 初始化 switch 先了解,初学者不必急着用。
基本流程:
xxxxxxxxxx11初始化 → 判断条件 → 进入循环体(true 代码块) → 迭代表达式 → 再判断条件 → ...初始化:只执行一次,可声明新变量,也可给已有变量赋值。
条件:能转换为 bool 的表达式;为 false 时退出。
迭代:每一轮循环体结束后执行(即使本轮用 continue 提前跳过了“剩余语句”,仍会执行迭代表达式)。
三个位置都可以留空(留空即什么都不做):
xxxxxxxxxx11for ( ; ; ) { /* 无限循环 */ }基本语法:
xxxxxxxxxx41for([初始化];[条件];[迭代])2{3 [true 代码块]4}例:
xxxxxxxxxx41for (int i = 0; /*初始化*/ i < 10 /*条件*/; i++/*迭代*/) {2 // true 代码块3 cout << "i = " << i << endl;4}输出 0..9。
int i = 0 在循环内部作用域,循环结束即销毁
continue 与 breakxxxxxxxxxx51for (int i = 0; i < 100; i++) {2 if (i % 2 == 0) continue; // 跳到“迭代表达式”,然后再判断条件3 cout << i << " ";4 if (i > 50) break; // 直接跳出当前 for(直接跳到结束)5}这段会打印奇数到 51 为止:1 3 5 ... 49 51。
原因:51 被打印后才命中 i > 50 的 break。
常见用途:
continue:过滤不需要处理的元素(奇偶过滤、非法输入跳过等)。
break:提前结束(找到目标、超出阈值、收到退出信号等)。
xxxxxxxxxx61for (int i = 0; i < 3; i++) {2 for (int j = 0; j < 3; j++) {3 cout << i << "-" << j << " ";4 }5 cout << endl;6}打印一个 3 × 3 的网格坐标。
for(;;){})xxxxxxxxxx61int index = 0;2for (;;) {3 if (index > 10) break; // 条件由代码内控制4 cout << "index:" << index << endl;5 index++;6}for(;;) 与 while(true) 等价;一定要在体内设计退出条件,否则就是“死循环”。
continue 和 break 的精确语义continue(for 中):立刻跳到迭代表达式,再去判断条件,是否进入下一轮。
xxxxxxxxxx41for (int i = 0; i < 10; i++) {2 if (i % 3 == 0) continue; // 仍会执行 i++,不会卡住3 // ...4}对比:在
while循环中continue不会自动更新变量,容易造成无限循环,要手动更新。
break:终止当前最内层循环,流程接到循环后第一条语句。
在 C++ 中,for 语句的变量作用域(scope)取决于它是在循环语句中声明还是在外部声明。这点很重要,因为它决定了变量能否在循环结束后继续使用。下面分情况讲解。
当你在 for 的初始化部分中声明一个变量(如 int i = 0),它的作用域仅限于 for 循环内部,包括循环头和循环体。
也就是说,循环结束后,这个变量会被销毁,在外面访问它会报错:
xxxxxxxxxx61for (int i = 0; i < 3; ++i) 2{3 std::cout << i << std::endl;4}5
6std::cout << i; // ❌ 错误:i 不在此作用域中📘 这是 C++ 引入的一个改进(相较于早期 C 的写法): 让循环变量的生命周期尽可能短,避免命名冲突或误用。
如果你在 for 外面定义变量,再在 for 中使用它,那么它在循环结束后仍然存在:
xxxxxxxxxx912int main() {3 int i = 0;4 for (i = 100; i < 103; ++i) { // 注意这里是 `i = 100`,不是 `int i = 100`5 std::cout << i << std::endl;6 }7 std::cout << i << std::endl; // ✅ 合法,输出 1038 return 0;9}这里的 i 在整个函数中都有效。
xxxxxxxxxx61for (int i = 0; i < 3; ++i) {2 for (int i = 0; i < 2; ++i) {3 std::cout << i << ' ';4 }5 std::cout << std::endl;6}内层 for 的 i 会遮蔽外层的 i。
外层 i 和内层 i 是两个不同的变量,生命周期互不干扰。
| 声明位置 | 作用域 | 循环外能否访问 | 生命周期 |
|---|---|---|---|
for (int i = 0; ...) | 循环语句内部 | ❌ 否 | 循环结束即销毁 |
int i; for (i = 0; ...) | 整个函数或外层块 | ✅ 是 | 函数结束才销毁 |
xxxxxxxxxx31for (int i = 0; i < 5; ++i) {2 std::cout << i << std::endl;3}这里的 int i = 0 是声明 + 初始化:
它创建了一个新的局部变量 i;
同时把它初始化为 0;
它只在整个 for 循环语句块中有效;
离开循环后 i 会被销毁。
💬 这不是对外部变量赋值,而是一个独立的新变量。
即使外面也有一个 int i,这个新的 i 会遮蔽(shadow)外部的同名变量。
例如:
xxxxxxxxxx71int i = 100;2
3for (int i = 0; i < 3; ++i) {4 std::cout << i << " "; // 输出 0 1 25}6
7std::cout << i; // 输出 100🔸 内部的
i和外部的i是两个不同的变量,它们互不影响。
xxxxxxxxxx71int i = 100;2
3for (i = 0; i < 3; ++i) {4 std::cout << i << " ";5}6
7std::cout << i; // 输出 3这里的 i = 0 就是单纯地给已有变量赋值。
循环结束后,这个变量的值会被保留(变成 3)。
| 写法 | 是否创建新变量 | 是否影响外部同名变量 | 作用域 | 离开循环后还能用? |
|---|---|---|---|---|
for (int i = 0; ...) | ✅ 是 | ❌ 否(会遮蔽外部变量) | 仅限循环内 | ❌ 不能用 |
int i = 0; for (i = 0; ...) | ❌ 否 | ✅ 会影响外部变量 | 整个函数或块 | ✅ 仍可用 |
for (int i = 0; ...)→ 创建一个新的局部变量(仅循环内可见)
int i = 0; for (i = 0; ...)→ 对已有的外部变量赋值(循环外仍可见)
for 的第三个参数(迭代)其实可以是任何合法的表达式,不局限于 i++ 或 i += n 这种简单的形式。
我们来详细讲讲它的灵活性与边界。
xxxxxxxxxx11for (int i = 0; i < 10; ++i)经典写法。
xxxxxxxxxx11for (int i = 0; i < 100; i += 5)每次加 5。
xxxxxxxxxx31for (int i = 0, j = 10; i < j; ++i, --j) {2 std::cout << i << "," << j << std::endl;3}这里第三个部分是 ++i, --j,会先计算 ++i 再计算 --j。
xxxxxxxxxx11for (int i = 0; i < 10; i = i * 2 + 1)完全合法,i 的更新由一个算式决定。
xxxxxxxxxx31for (int i = 0; i < 10; updateIndex(i)) {2 std::cout << "i=" << i << std::endl;3}这里第三个表达式是函数调用,只要 updateIndex() 是个合法可调用函数即可。
它甚至可以修改全局变量或引用参数。
xxxxxxxxxx11for (int i = 0; i < 5; std::cout << "Next i=" << ++i << std::endl) {}也能这样用。虽然实际意义不大,但语法合法。
不能使用声明语句: 第三个部分必须是表达式,而不是声明。比如下面是错误的:
xxxxxxxxxx11for (int i = 0; i < 10; int x = i + 1) // ❌ 不行因为 int x = ...; 是声明语句,不是表达式。
复杂表达式要注意可读性: C++ 虽然允许任意表达式,但太复杂会让代码难懂。 实际工程中推荐保持第三个表达式“短而清晰”。
xxxxxxxxxx31for ( ; ; ) {2 // 无限循环3}只要条件为空,就默认永远为真。
for的第三个表达式非常自由: 只要是合法表达式,都可以放进去执行——递增、函数调用、复合算式、甚至多变量逗号表达式都行。 但出于可读性,推荐保持简洁(如++i、i += n等)。
while 循环的基本语法xxxxxxxxxx31while (条件表达式) {2 // true 代码块(循环体)3}执行流程:
xxxxxxxxxx31判断条件 → true → 执行循环体 → 再判断条件 → ...2 ↓3 → false → 跳出循环判断条件:每次循环前都会检查;
循环体:只有条件为 true 时才执行;
终止条件:当条件为 false 时退出循环。
xxxxxxxxxx51int i = 0;2while (i < 10) {3 cout << "i = " << i << endl;4 i++;5}输出:
xxxxxxxxxx41i = 02i = 13...4i = 9
⚠️ 注意:如果忘记写
i++,则i永远为 0,条件一直成立,会造成死循环。
while 与 for 的关系两者可以互相转换:
xxxxxxxxxx101// for 形式2for (int i = 0; i < 10; ++i)3 cout << i << endl;4
5// 等价 while 形式6int i = 0;7while (i < 10) {8 cout << i << endl;9 ++i;10}📘 总结区别:
| 特性 | for | while |
|---|---|---|
| 适用场景 | 已知循环次数 | 循环次数不确定 |
| 变量声明 | 可在循环头声明 | 一般在外部声明 |
| 条件检测 | 循环前检测 | 循环前检测 |
| 典型用途 | 遍历数组、固定步长循环 | 等待状态、条件控制、输入检测等 |
while(true) 无限循环xxxxxxxxxx31while (true) {2 cout << "Running..." << endl;3}这是一个死循环(永远不会结束)。
通常会搭配 break 手动退出:
xxxxxxxxxx51int i = 0;2while (true) {3 if (i > 5) break;4 cout << i++ << endl;5}输出:
0 1 2 3 4 5
continue 与 break 在 while 中的行为xxxxxxxxxx71int i = 0;2while (i < 10) {3 i++;4 if (i % 2 == 0) continue; // 跳到“条件判断”,不会执行后续语句5 if (i > 7) break; // 直接跳出循环6 cout << i << " ";7}输出:
xxxxxxxxxx111 3 5 7
🔍 分析:
continue:跳到 条件判断(不会自动更新变量!要手动更新)
break:直接退出循环。
while 与 do...while 的区别do...while 是 后判断循环,即先执行一次循环体,再判断条件。
xxxxxxxxxx51int i = 0;2do {3 cout << i << endl;4 i++;5} while (i < 5);执行顺序:
xxxxxxxxxx11执行循环体 → 判断条件 → 决定是否继续
✅ 结论:
while:先判断再执行 → 可能一次都不执行;
do...while:先执行一次再判断 → 至少执行一次。
| 错误类型 | 示例 | 问题 |
|---|---|---|
| 忘记更新变量 | while(i<10){ cout<<i; } | 死循环 |
| 条件写错 | while(i=10) | 赋值表达式,条件恒为真 |
| continue 用错 | while(...) { continue; i++; } | i++ 永远不执行,死循环 |
| 逻辑嵌套不清 | 多层 while 嵌套时 break/continue 指向最内层 | 需谨慎控制层级 |
while是 C++ 最基础的循环语句,适用于循环次数不确定的场景。 它每次都先判断条件,为真则执行,为假则退出。 若需要“至少执行一次”,请用do...while。 若循环次数已知或有规律,优先考虑for。
数组是 一组 相同类型 的数据,在内存中连续存放,有连续的空间。
基本语法:类型 变量名[元素个数]
例:
xxxxxxxxxx11int arr[5]; // 创建一个有5个int元素的数组,注意此时[]内的值代表元素个数,而不是下标👉 每个元素大小相同,排列连续。
👉 arr 这个名字本身代表“整个数组的首地址”。
数组名就是首地址,
arr == &arr[0]:xxxxxxxxxx31// 两者等价2cout << "arr1 = " << (long long)arr1 << endl;3cout << "&arr1[0] = " << (long long)&arr1[0] << endl;
C++ 的数组 下标从 0 开始。要访问或修改某个元素,用方括号:
xxxxxxxxxx41int arr[5];2arr[0] = 10; // 把第1个元素改成103arr[1] = 20; // 把第2个元素改成204cout << arr[0]; // 输出第1个元素⚠️ 注意:arr[5] 就 越界 了(此时 [] 内的 5 代表第 6 个元素(总共只有 5 个元素),因为下标从 0 开始),因为合法下标只有 0,1,2,3,4。
栈区数组空间(即[]里面的值)只能用编译期常量:
在现代 C++ 中,虽然
const是运行时常量,但是const int N = 5;会被视为编译期常量,因为编译器能提前知道数组的大小。 数组大小设置一定支持 constexpr 编译时常量,不支持无法在运行时确定的常量和变量。
xxxxxxxxxx21const int N = 5;2int nums[N]; // OK,长度在编译期确定const 与 constexpr、“编译期常量”的判定const ≠ “一定是运行期常量”。const 只表示“不可修改”(只读)。
是否是“编译期常量”,取决于其初始化表达式是否为常量表达式(constant expression)。
换句话说:
const int N = 5;—— 右边是字面量5,属于常量表达式,所以N在编译期就能确定;const int N = read();—— 右边依赖运行期输入,不是常量表达式,所以N不是编译期常量(只是个只读变量)。
编译器按标准对 常量表达式 做静态求值(constant folding / constant evaluation)。当它看到:
xxxxxxxxxx21const int N = 5;2// 或者 const int N = 2 + 3; 或者 const int N = SOME_ENUM;右侧完全由编译期可求值的东西组成(字面量、枚举值、另一个编译期常量等),于是它能在编译时直接得出 N == 5。这种 N 就可以用在需要常量表达式的语境里,
相反,如果初始化式不是常量表达式:
xxxxxxxxxx41int x; 2std::cin >> x;3const int N = x; // 运行期才能确定4int a[N]; // ❌ 标准 C++ 不允许(GCC/Clang 的 VLA 扩展除外)这时 N 只是“运行期只读”,不是编译期常量。
constexpr 的关系constexpr 保证对象/函数在语义上必须是编译期可求值的(若做不到就编译期报错)。
const 可能是编译期常量,也可能不是——看初始化式是否为常量表达式。
当你需要一个名字能出现在“常量表达式场合”(如数组长度、模板参数)时,用 constexpr 最稳妥;const 也可以,但前提是它的初始化明确是常量表达式。
结论一句话:
const 自身不等于“运行期常量”。当且仅当它用一个编译期可求值的表达式初始化时,它就成了“编译期常量”并可用于需要常量表达式的场合;constexpr 则是对此的强保证。
1️⃣ 全部手动赋值
xxxxxxxxxx41int a[3]; // 3个元素2a[0] = 1; // 为第1个元素赋值3a[1] = 2; // 为第2个元素赋值4a[2] = 3; // 为第3个元素赋值2️⃣ 定义时初始化
xxxxxxxxxx11int b[3] = {1, 2, 3}; // 所有元素都被赋予了指定的值3️⃣ 省略长度
xxxxxxxxxx21// 当不在[]设置元素个数的时候,编译器会根据初始化元素个数来设置数组大小2int c[] = {10, 20, 30, 40}; // 自动推算长度为44️⃣ 部分初始化
xxxxxxxxxx21int d[5] = {1, 2}; // 剩下的元素自动为0,即 {1, 2, 0, 0, 0}2int d[5] = {1}; // {1, 0, 0, 0, 0}5️⃣ 不赋值
xxxxxxxxxx11int arr1[4] // 此时值不确定,可能是任意值!有关字符数组:字符数组初始化会包含一个隐含的
'\0'结束符:xxxxxxxxxx41char arr7[] = "test string";2// 共 11 个字符,但结尾实际上还有一个隐含的 '\0' 结束符3cout << "sizeof(arr7) = " << sizeof(arr7) << endl;4// 输出:sizeof(arr7) = 12(包括 '\0')要注意一点,假如指定元素个数使用字符串数组,如下:
xxxxxxxxxx11char name[20]; // 最多存 19 个字符 + 结尾 '\0'此时最多存 19 个字符,因为结尾还需要留一个 '\0' 的位置!
最常见的方式是 for 循环:
xxxxxxxxxx41int arr[5] = {1, 2, 3, 4, 5};2for (int i = 0; i < 5; i++) {3 cout << arr[i] << " ";4}输出:
xxxxxxxxxx111 2 3 4 5
从 C++11 开始,还可以用范围 for 循环(更简洁):
xxxxxxxxxx31for (int x : arr) {2 cout << x << " ";3}计算数组元素个数的一种常见写法:
xxxxxxxxxx61cout << "sizeof(arr1) = " << sizeof(arr1) << endl;2for (int i = 0; i < sizeof(arr1)/sizeof(int); i++)3// `sizeof(int)` 获取 int 类型在此环境下所占用大小4{5cout << arr1[i] << ",";6}
数组在内存中是一整块连续空间,是地址连续的空间。
补充术语:
&:取地址符
*:解引用,取出指针指向的值
xxxxxxxxxx51cout << "arr1 = " << (long long)arr1 << endl; // 数组名就是首地址,&arr1[0] == arr12cout << "&arr1[0] = " << (long long)&arr1[0] << endl; // 同样是首地址3cout << "&arr1[1] = " << (long long)&arr1[1] << endl; // 比前一个地址大 4 个字节(int 大小)4cout << "&arr1[2] = " << (long long)&arr1[2] << endl;5cout << "&arr1[3] = " << (long long)&arr1[3] << endl;还可以用 指针偏移 的方式访问数组元素:
| 操作 | 含义 | 示例 |
|---|---|---|
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 的值(地址) |
xxxxxxxxxx41cout << "arr1 + 2 = " << (long long)(arr1 + 2) << endl; // 等于 &arr1[2],即从数组首地址偏移 2 个 int2cout << "arr1 + 3 = " << (long long)(arr1 + 3) << endl;3
4cout << "*(arr1 + 2) = " << *(arr1 + 2) << endl; // 等于 arr1[2]⚠️ 注意: 指针移动单位不是字节,而是“类型大小”。 例如:
xxxxxxxxxx21int *p = a;2p + 1 == &a[1]; // 地址增加 sizeof(int)| 类型 | 每个元素占多少字节 | p++ 实际加多少 |
|---|---|---|
int | 4 字节 | 地址 + 4 |
double | 8 字节 | 地址 + 8 |
char | 1 字节 | 地址 + 1 |
💬 特别说明
*p++ 的含义:
先取出 p 当前指向的值,然后让 p 向后移动一格。
相当于:
xxxxxxxxxx11*p++ == *(p++);C++ 中有三种常见的数组存储方式:
| 类型 | 定义方式 | 特点 |
|---|---|---|
| 栈区数组 | int a[10]; | 自动分配、作用域结束后自动释放 |
| 堆区数组 | new int[10]; | 动态申请,手动 delete[] 释放 |
| 容器数组(vector) | std::vector<int> a(10); | 自动管理内存,功能更强(推荐) |