[C.C++] C++ primer 学习:C++ 入门:第一章

103 0
Honkers 昨天 16:37 来自手机 | 显示全部楼层 |阅读模式

Getting Started —— 从零开始理解 C++

目录

  1. 1.1 写一个最简单的 C++ 程序
  2. 1.2 第一次认识输入输出
  3. 1.3 注释是什么
  4. 1.4 控制流:让程序会"转弯"
  5. 1.5 初识类(Class)
  6. 1.6 书店综合程序
  7. 常用运算符速查表

1.1 写一个最简单的 C++ 程序

什么是函数?什么是 main?

每一个 C++ 程序都由一个或多个**函数(function)**组成。函数就像是一台机器:给它一些输入,它执行一些动作,然后返回一个结果。
操作系统(Windows/Linux/macOS)启动你的程序时,会自动去找一个名字叫 main 的函数,并从那里开始执行。所以 main 是程序的入口,有且只能有一个

最简单的程序

  1. // 这是一个什么都不做、只是返回0的程序
  2. // int 表示返回类型(整数)
  3. // main 是函数名
  4. // () 是参数列表(这里为空,表示不需要任何参数)
  5. int main()
  6. {
  7. // { } 是函数体,所有要执行的代码写在里面
  8. return 0; // return 语句:结束函数,并向操作系统返回值 0
  9. // 返回 0 通常表示"程序成功运行"
  10. // 注意:每条语句必须以分号 ; 结尾!
  11. }
复制代码

https://godbolt.org/z/K3GGxPTdP

函数的四个组成部分

  1. int main () { return 0; }
  2. ^ ^ ^ ^
  3. 返回类型 函数名 参数列表 函数体
  4. (结果是整数) (叫main) (这里为空) (要执行的代码)
复制代码

返回值的含义


返回值含义
0成功
非 0(如 -1, 1)失败,具体含义由操作系统定义

怎么编译和运行?

写完代码保存为 prog1.cpp,然后在终端运行:

  1. # GCC 编译器(Linux/macOS)
  2. $ g++ -o prog1 prog1.cpp # 编译,生成可执行文件 prog1
  3. $ ./prog1 # 运行
  4. # 查看程序返回值(Linux/macOS)
  5. $ echo $?
  6. # Windows(Visual Studio 命令行)
  7. > cl /EHsc prog1.cpp # 编译,生成 prog1.exe
  8. > prog1 # 运行
  9. > echo %ERRORLEVEL% # 查看返回值
复制代码

建议使用 -Wall 开启所有警告:g++ -Wall -o prog1 prog1.cpp

1.2 第一次认识输入输出

C++ 的 IO 不是语言内置的

C++ 本身没有内置输入/输出命令。取而代之的是,标准库提供了 iostream 库。
使用前必须在文件开头声明:

  1. #include <iostream> // 告诉编译器:我要用 iostream 库
复制代码

四个标准 IO 对象

  1. cin → 标准输入(键盘) 读取用户输入
  2. cout → 标准输出(屏幕) 打印正常输出
  3. cerr → 标准错误(屏幕) 打印错误信息(不缓冲)
  4. clog → 标准日志(屏幕) 打印日志信息(缓冲)
复制代码

它们都属于命名空间 std,使用时要写 std::cout(:: 是作用域运算符)。

完整的两数求和程序

  1. #include <iostream> // 引入输入输出库
  2. int main()
  3. {
  4. // << 是输出运算符,把右边的值写入左边的输出流
  5. // std::endl 是操纵符:换行 + 冲刷缓冲区
  6. // 冲刷缓冲区:确保内容立刻显示在屏幕上,不在内存中等待
  7. std::cout << "Enter two numbers:" << std::endl;
  8. // 定义两个整型变量,初始化为 0
  9. // int 是内置类型,表示整数
  10. // 变量:有名字的存储空间,用来保存数据
  11. int v1 = 0, v2 = 0;
  12. // >> 是输入运算符,从左边的输入流读取数据,存入右边的变量
  13. // std::cin >> v1 >> v2 等价于:
  14. // std::cin >> v1; 先读第一个数存入 v1
  15. // std::cin >> v2; 再读第二个数存入 v2
  16. std::cin >> v1 >> v2;
  17. // 链式输出:每个 << 返回 std::cout 本身,所以可以连续写
  18. // v1 + v2 是表达式,会被自动计算成结果
  19. std::cout << "The sum of " << v1 << " and " << v2
  20. << " is " << v1 + v2 << std::endl;
  21. return 0;
  22. }
复制代码

https://godbolt.org/z/4Gq7arYr5

输出运算符的链式调用原理

  1. std::cout << "Enter two numbers:" << std::endl;
  2. 等价于:
  3. (std::cout << "Enter two numbers:") << std::endl;
  4. ^ ^
  5. 第一步:把字符串写入 cout,返回 cout 第二步:把 endl 写入 cout
复制代码

每个 << 运算符的返回值是其左操作数(即 std::cout),所以可以一直链下去。

endl 的作用

  1. std::endl = 换行符 '\n' + 冲刷缓冲区
  2. 为什么要冲刷缓冲区?
  3. - 程序崩溃时,缓冲区中还未输出的内容会丢失
  4. - 调试时加的 cout 语句,必须用 endl 或 std::flush 确保内容实际显示出来
复制代码

命名空间 std

所有标准库中的名字(cout, cin, endl 等)都在 std 命名空间里,必须用 std:: 前缀访问,防止与你自己定义的同名变量冲突。

1.3 注释是什么

注释是写给人看的说明文字,编译器完全忽略它

两种注释格式

  1. // 这是单行注释:从 // 开始到本行末尾都是注释
  2. /*
  3. * 这是多行注释(块注释)
  4. * 从 /* 开始,到 */ 结束
  5. * 中间可以跨越多行
  6. * 注意:块注释不能嵌套!
  7. */
  8. int main()
  9. {
  10. // 好的注释:解释"为什么",而不是"做什么"
  11. // (代码本身已经说明了做什么)
  12. int x = 5; // 行尾注释:简短说明
  13. /*
  14. * 错误示范 —— 块注释不能嵌套:
  15. * /* 这里又开了一个注释 */ ← 这个 */ 会提前结束外层注释!
  16. */
  17. return 0;
  18. }
复制代码

https://godbolt.org/z/fj4xoos3c

注释嵌套的陷阱

  1. /* 外层注释开始
  2. /* 内层注释 */ ← 编译器在这里就认为注释结束了!
  3. 后面这段文字就变成了代码,导致编译错误
  4. */
复制代码

解决方法:注释掉一大段代码时,用单行注释 //:

  1. // /* 这样就安全了
  2. // * 整段都是单行注释
  3. // * 不存在嵌套问题
  4. // */
复制代码

1.4 控制流:让程序会"转弯"

正常情况下,程序从上到下顺序执行。控制流语句让程序能重复选择执行某些代码。

1.4.1 while 循环

用途:只要条件为真,就反复执行一段代码。
语法结构:

  1. while (条件)
  2. 语句或代码块
复制代码

求 1 到 10 的和:

  1. #include <iostream>
  2. int main()
  3. {
  4. int sum = 0; // 累加结果,初始为 0
  5. int val = 1; // 当前要加的数,从 1 开始
  6. // while 循环:每次检查条件 val <= 10
  7. // 条件为真(true)则执行循环体;条件为假(false)则退出循环
  8. while (val <= 10) // <= 是"小于等于"运算符
  9. {
  10. sum += val; // 等价于 sum = sum + val,把 val 累加到 sum
  11. ++val; // 前置递增运算符:val = val + 1,准备下一个数
  12. }
  13. // 循环结束后,val = 11(不满足 val <= 10),sum = 55
  14. std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
  15. return 0;
  16. }
复制代码

https://godbolt.org/z/jd6z7x1er
while 执行流程:

手动追踪前几次循环:

  1. 初始:sum=0, val=1
  2. 第1次:sum=0+1=1, val=2
  3. 第2次:sum=1+2=3, val=3
  4. 第3次:sum=3+3=6, val=4
  5. ...
  6. 第10次:sum=45+10=55, val=11
  7. 条件 11<=10 为假,退出循环
复制代码

1.4.2 for 循环

用途:当循环次数固定、或有明确的"初始化→条件→更新"模式时,for 比 while 更简洁。
语法结构:

  1. for (初始化语句; 条件; 表达式)
  2. 循环体
复制代码

同样求 1 到 10 的和:

  1. #include <iostream>
  2. int main()
  3. {
  4. int sum = 0;
  5. // for 循环三个部分:
  6. // int val = 1 → 初始化:创建 val 并赋值 1(只执行一次)
  7. // val <= 10 → 条件:每次循环前检查
  8. // ++val → 表达式:每次循环体执行完后运行
  9. for (int val = 1; val <= 10; ++val)
  10. {
  11. sum += val; // 循环体
  12. }
  13. // 注意:val 只在 for 循环内部存在,循环结束后无法使用
  14. std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
  15. return 0;
  16. }
复制代码

for 的执行顺序:

https://godbolt.org/z/jGsvKT9js

while vs for 对比

  1. while 版本: for 版本:
  2. int val = 1; for (int val = 1; val <= 10; ++val)
  3. while (val <= 10) { sum += val;
  4. sum += val;
  5. ++val;
  6. }
  7. - while:val 在循环外声明,循环后仍可用
  8. - for:val 在循环头声明,循环后不可用(作用域更小,更安全)
  9. - 两者功能完全等价,选哪个取决于代码风格和可读性
复制代码

1.4.3 读取未知数量的输入

有时不知道用户会输入多少个数,需要持续读取直到没有数据

  1. #include <iostream>
  2. int main()
  3. {
  4. int sum = 0, value = 0;
  5. // 把 std::cin >> value 放在 while 条件里:
  6. // - 成功读取一个整数 → 条件为真,继续循环
  7. // - 遇到文件末尾(EOF)或非法输入 → 条件为假,退出循环
  8. while (std::cin >> value)
  9. {
  10. sum += value; // 把读到的值累加
  11. }
  12. std::cout << "Sum is: " << sum << std::endl;
  13. return 0;
  14. }
复制代码

https://godbolt.org/z/cMasn3j7n
如何输入"文件末尾"(EOF)?

  1. Windows:按 Ctrl + Z,然后回车
  2. Linux / macOS:按 Ctrl + D
复制代码

为什么 std::cin >> value 可以作为条件?
>> 运算符返回其左操作数(std::cin)。istream 对象(即 std::cin)在以下情况下变为"无效状态",在条件判断中被视为 false:

  • 遇到文件末尾
  • 读取了不合法的数据(比如期望读整数但输入了字母)

1.4.4 if 语句

用途:根据条件选择执行不同代码。
统计连续重复数字出现次数:

  1. #include <iostream>
  2. int main()
  3. {
  4. int currVal = 0; // 当前正在统计的数值
  5. int val = 0; // 每次新读入的数值
  6. // 外层 if:先读第一个数,如果读成功才继续
  7. // (处理输入为空的情况)
  8. if (std::cin >> currVal)
  9. {
  10. int cnt = 1; // currVal 已经出现了 1 次
  11. // 持续读取后续数字
  12. while (std::cin >> val)
  13. {
  14. if (val == currVal) // == 是等于运算符(注意不是赋值的 =)
  15. {
  16. ++cnt; // 相同,计数+1
  17. }
  18. else
  19. {
  20. // 不同:打印上一个数的统计结果
  21. std::cout << currVal << " occurs "
  22. << cnt << " times" << std::endl;
  23. currVal = val; // 切换到新的数
  24. cnt = 1; // 重置计数为 1
  25. }
  26. }
  27. // 循环结束后,最后一个数还没打印,在这里补上
  28. std::cout << currVal << " occurs "
  29. << cnt << " times" << std::endl;
  30. }
  31. // 如果一开始就没有输入,外层 if 的条件为假,直接跳到这里
  32. return 0;
  33. }
复制代码

https://godbolt.org/z/cr8b1jMMr
执行流程演示(输入 42 42 55 55 62):

  1. 读入 currVal = 42, cnt = 1
  2. 循环1:val=42,42==42,cnt=2
  3. 循环2:val=55,55!=42,输出"42 occurs 2 times",currVal=55,cnt=1
  4. 循环3:val=55,55==55,cnt=2
  5. 循环4:val=62,62!=55,输出"55 occurs 2 times",currVal=62,cnt=1
  6. EOF:退出循环
  7. 补充输出:"62 occurs 1 times"
复制代码

常见错误提醒:

  1. // 赋值 vs 等于的混淆!
  2. if (val = currVal) // 错误!这是赋值,不是比较!
  3. if (val == currVal) // 正确!这是判断是否相等
复制代码

1.5 初识类(Class)

原始源码已经从搜索结果中拼出来了,下面结合官方实现逐行解析:
Sales_item 类用到了运算符重载友元函数构造函数成员函数这几个核心 C++ 特性。我们从零开始逐步拆解,最后给出完整可运行的代码。

整体结构一览

完整实现(含从零理解的注释)

  1. // ============================================================
  2. // Sales_item.h
  3. // C++ Primer 第5版教学用头文件(加详细中文注释)
  4. // ============================================================
  5. // 头文件卫士:防止同一个头文件被 #include 两次
  6. // 第一次包含时,SALESITEM_H 未定义 → 正常编译
  7. // 第二次包含时,SALESITEM_H 已定义 → 跳过整个文件
  8. #ifndef SALESITEM_H
  9. #define SALESITEM_H
  10. #include <iostream> // std::istream, std::ostream
  11. #include <string> // std::string
  12. // ============================================================
  13. // 类定义
  14. // ============================================================
  15. class Sales_item
  16. {
  17. // ---- 友元声明 ----
  18. // friend(友元):允许这些"外部函数"访问类的 private 成员
  19. // 它们不是成员函数,但可以直接读写 bookNo、units_sold、revenue
  20. friend std::istream& operator>>(std::istream&, Sales_item&);
  21. friend std::ostream& operator<<(std::ostream&, const Sales_item&);
  22. friend bool operator==(const Sales_item&, const Sales_item&);
  23. friend bool operator< (const Sales_item&, const Sales_item&);
  24. public:
  25. // ============================================================
  26. // 构造函数(Constructors)
  27. // 作用:创建对象时自动初始化数据成员
  28. // ============================================================
  29. // 1. 默认构造函数
  30. // Sales_item item; ← 这样声明时调用
  31. // = default 让编译器自动生成,等价于手写
  32. // Sales_item() : units_sold(0), revenue(0.0) {}
  33. Sales_item() = default;
  34. // 2. 从 string 构造
  35. // Sales_item item("0-201-70353-X");
  36. // 冒号后面是"初始化列表",直接初始化成员,比在函数体内赋值更高效
  37. Sales_item(const std::string& book)
  38. : bookNo(book), units_sold(0), revenue(0.0)
  39. {
  40. // bookNo = book, units_sold = 0, revenue = 0.0
  41. // 函数体为空,初始化已经在列表里完成了
  42. }
  43. // 3. 从输入流构造
  44. // Sales_item item(std::cin);
  45. // is >> *this 的意思:用 >> 运算符从 is 读取数据,存入"当前对象自己"
  46. // *this 是指向当前对象的指针解引用,即"这个对象本身"
  47. Sales_item(std::istream& is)
  48. {
  49. is >> *this; // 调用下面定义的 operator>>
  50. }
  51. public:
  52. // ============================================================
  53. // 公开成员函数(Public Member Functions)
  54. // ============================================================
  55. // isbn():返回书号
  56. // const 表示这个函数不会修改对象的任何数据成员(只读操作)
  57. std::string isbn() const { return bookNo; }
  58. // avg_price():计算平均售价 = 总收入 ÷ 销售册数
  59. // 要防止除以零的情况
  60. double avg_price() const
  61. {
  62. if (units_sold) // 如果 units_sold != 0
  63. return revenue / units_sold;
  64. else
  65. return 0; // 没有销售记录,返回 0
  66. }
  67. // operator+=:把另一条销售记录累加到本对象
  68. // 用法:total += trans; 等价于 total.operator+=(trans)
  69. // 返回 *this(即返回当前对象的引用),支持链式调用
  70. Sales_item& operator+=(const Sales_item& rhs)
  71. {
  72. units_sold += rhs.units_sold; // 册数相加
  73. revenue += rhs.revenue; // 收入相加
  74. return *this; // 返回自身引用
  75. // 注意:不修改 bookNo(ISBN 必须相同才能相加)
  76. }
  77. private:
  78. // ============================================================
  79. // 私有数据成员(Private Data Members)
  80. // 类外部不能直接访问,只能通过公开的成员函数或友元函数操作
  81. // ============================================================
  82. std::string bookNo; // ISBN 书号,如 "0-201-70353-X"
  83. unsigned units_sold = 0; // 销售册数(无符号整数,不能为负)
  84. double revenue = 0.0; // 总收入(单价 × 册数)
  85. };
  86. // ============================================================
  87. // 友元函数:operator+
  88. // 把两个 Sales_item 相加,返回一个新对象(不修改原来的两个对象)
  89. // 注意:这里不是成员函数,写在类定义外面
  90. // ============================================================
  91. inline Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs)
  92. {
  93. Sales_item ret(lhs); // 用 lhs 拷贝初始化 ret(ret 是 lhs 的副本)
  94. ret += rhs; // 调用 operator+=,把 rhs 累加进 ret
  95. return ret; // 返回累加结果(新对象)
  96. }
  97. // ============================================================
  98. // 友元函数:operator>>(输入运算符)
  99. // 用法:std::cin >> item;
  100. // 从输入流读取:ISBN 册数 单价
  101. // 例如:0-201-70353-X 4 24.99
  102. // ============================================================
  103. inline std::istream& operator>>(std::istream& in, Sales_item& s)
  104. {
  105. double price = 0; // 单价(临时变量,revenue = units_sold * price)
  106. // 依次读取:书号、册数、单价
  107. in >> s.bookNo >> s.units_sold >> price;
  108. if (in) // 读取成功
  109. s.revenue = s.units_sold * price; // 计算总收入
  110. else
  111. s = Sales_item(); // 读取失败:重置为默认空对象
  112. return in; // 返回输入流,支持链式 cin >> a >> b
  113. }
  114. // ============================================================
  115. // 友元函数:operator<<(输出运算符)
  116. // 用法:std::cout << item;
  117. // 输出格式:ISBN 册数 总收入 均价
  118. // 例如:0-201-70353-X 4 99.96 24.99
  119. // ============================================================
  120. inline std::ostream& operator<<(std::ostream& out, const Sales_item& s)
  121. {
  122. out << s.isbn() << " " // 书号
  123. << s.units_sold << " " // 册数
  124. << s.revenue << " " // 总收入
  125. << s.avg_price(); // 均价(调用成员函数)
  126. return out; // 返回输出流,支持链式 cout << a << b
  127. }
  128. // ============================================================
  129. // 友元函数:operator==(相等运算符)
  130. // 两条记录 ISBN 相同 且 册数相同 且 收入相同 → 认为相等
  131. // ============================================================
  132. inline bool operator==(const Sales_item& lhs, const Sales_item& rhs)
  133. {
  134. return lhs.units_sold == rhs.units_sold
  135. && lhs.revenue == rhs.revenue
  136. && lhs.isbn() == rhs.isbn();
  137. }
  138. // operator!=(不等):直接用 == 取反
  139. inline bool operator!=(const Sales_item& lhs, const Sales_item& rhs)
  140. {
  141. return !(lhs == rhs);
  142. }
  143. // operator<:按 ISBN 字典序比较(用于排序)
  144. inline bool operator<(const Sales_item& lhs, const Sales_item& rhs)
  145. {
  146. return lhs.isbn() < rhs.isbn();
  147. }
  148. #endif // SALESITEM_H
复制代码

关键概念逐一解释

1. 头文件卫士(include guard)

  1. #ifndef SALESITEM_H ← 如果 SALESITEM_H 没有被定义过
  2. #define SALESITEM_H ← 现在定义它
  3. ... 类的全部内容 ...
  4. #endif ← 结束
复制代码

没有它,多个源文件都 #include "Sales_item.h" 时会导致类被定义两次,编译报错。

2. 初始化列表

  1. Sales_item(const std::string& book)
  2. : bookNo(book), units_sold(0), revenue(0.0)
  3. {}
复制代码

冒号 : 后面是初始化列表,格式是 成员名(初始值)。比在函数体里写 bookNo = book 效率更高,因为直接在构造时初始化,而不是先默认初始化再赋值。

3. 友元函数 vs 成员函数

  1. 成员函数: item.isbn() → 必须通过对象来调用
  2. 友元函数: operator>>(cin, item) → 普通函数,但可以访问 private 成员
复制代码

>> 和 << 之所以设计成友元而不是成员,是因为它们的左操作数是 cin/cout(istream/ostream类型),不是 Sales_item 对象。

4. *this 的含义

  1. Sales_item(std::istream& is)
  2. {
  3. is >> *this; // this 是指向当前对象的指针
  4. } // *
  5. ### 类是什么?
  6. **类(class)**是 C++ 中定义自己的数据类型的方式。类把"数据"和"对数据的操作"打包在一起。
  7. > 类比:`int` 是语言内置的整数类型,你可以定义自己的"书籍销售记录"类型。
  8. ### Sales_item 类
  9. 教材提供了一个 `Sales_item` 类,用来表示一本书的销售记录,包含:
  10. - ISBN(书的唯一编号)
  11. - 销售册数
  12. - 总收入
  13. - 平均售价
  14. 使用前需要包含其头文件:
  15. ```cpp
  16. #include "Sales_item.h" // 自定义头文件用双引号
  17. #include <iostream> // 标准库头文件用尖括号
复制代码

读写 Sales_item 对象

  1. #include <iostream>
  2. #include "Sales_item.h" // 包含 Sales_item 类的定义
  3. int main()
  4. {
  5. Sales_item book; // 定义一个 Sales_item 类型的对象,名字叫 book
  6. // >> 运算符:从标准输入读取一条记录存入 book
  7. // 输入格式:ISBN 销售册数 单价
  8. // 例如:0-201-70353-X 4 24.99
  9. std::cin >> book;
  10. // << 运算符:把 book 的信息打印到标准输出
  11. // 输出格式:ISBN 销售册数 总收入 平均售价
  12. // 例如:0-201-70353-X 4 99.96 24.99
  13. std::cout << book << std::endl;
  14. return 0;
  15. }
复制代码

https://godbolt.org/z/s8zcGj7s9

两个 Sales_item 相加

  1. #include <iostream>
  2. #include "Sales_item.h"
  3. int main()
  4. {
  5. Sales_item item1, item2; // 定义两个销售记录对象
  6. // 分别从输入读取两条记录
  7. std::cin >> item1 >> item2;
  8. // + 运算符:把两条记录合并(ISBN 必须相同)
  9. // 结果:ISBN相同,册数相加,收入相加
  10. std::cout << item1 + item2 << std::endl;
  11. return 0;
  12. }
复制代码

https://godbolt.org/z/a7q88a8Ka
示例输入与输出:

  1. 输入:
  2. 0-201-78345-X 3 20.00
  3. 0-201-78345-X 2 25.00
  4. 输出:
  5. 0-201-78345-X 5 110 22
  6. ^ ^
  7. 共5册 平均22元
复制代码

1.5.2 成员函数(Member Function)

成员函数是属于某个类的函数,用点运算符 . 来调用:

  1. 对象名.成员函数名(参数)
  2. item1.isbn()
  3. ^ ^
  4. 对象 函数名(这里无参数)
复制代码

检查两本书是否相同 ISBN 再相加:

  1. #include <iostream>
  2. #include "Sales_item.h"
  3. int main()
  4. {
  5. Sales_item item1, item2;
  6. std::cin >> item1 >> item2;
  7. // item1.isbn() 调用 item1 的 isbn 成员函数
  8. // 返回 item1 中存储的 ISBN 字符串
  9. // == 运算符比较两个 ISBN 是否相同
  10. if (item1.isbn() == item2.isbn())
  11. {
  12. std::cout << item1 + item2 << std::endl;
  13. return 0; // 成功
  14. }
  15. else
  16. {
  17. // cerr:标准错误输出,用于错误信息,不缓冲,立即显示
  18. std::cerr << "Data must refer to same ISBN" << std::endl;
  19. return -1; // 失败,返回非0
  20. }
  21. }
复制代码

https://godbolt.org/z/fWd1M87cr

1.6 书店综合程序

把前面学的所有内容综合起来,解决最初的问题:读取同一 ISBN 的所有销售记录,汇总后打印。

  1. #include <iostream>
  2. #include "Sales_item.h"
  3. int main()
  4. {
  5. // total:保存当前 ISBN 的累加结果
  6. Sales_item total;
  7. // 先读第一条记录:
  8. // 成功 → 执行大括号内的代码
  9. // 失败(没有输入)→ 执行 else 分支
  10. if (std::cin >> total)
  11. {
  12. Sales_item trans; // trans:每次读入的新记录
  13. // 持续读取剩余记录
  14. while (std::cin >> trans)
  15. {
  16. // 判断新记录和累计记录是否同一本书
  17. if (total.isbn() == trans.isbn())
  18. {
  19. total += trans; // 同一本书:累加到 total
  20. // += 等价于 total = total + trans
  21. }
  22. else
  23. {
  24. // 换了一本书:
  25. // 1. 打印上一本书的汇总
  26. std::cout << total << std::endl;
  27. // 2. 把 total 重置为新书的第一条记录
  28. total = trans;
  29. }
  30. }
  31. // 循环结束,最后一本书的汇总还没打印
  32. std::cout << total << std::endl;
  33. }
  34. else
  35. {
  36. // 一条记录都没有
  37. std::cerr << "No data?!" << std::endl;
  38. return -1;
  39. }
  40. return 0;
  41. }
复制代码

https://godbolt.org/z/6cfPaxdv4
程序逻辑流程图:

示例输入:

  1. 0-201-70353-X 4 24.99
  2. 0-201-70353-X 2 19.99
  3. 0-201-82470-1 3 15.00
  4. 0-201-82470-1 1 15.00
复制代码

示例输出:

  1. 0-201-70353-X 6 149.94 24.99
  2. 0-201-82470-1 4 60.00 15.00
复制代码

常用运算符速查表


运算符名称示例含义
=赋值x = 5把 5 存入 x
==等于x == 5判断 x 是否等于 5
!=不等于x != 5判断 x 是否不等于 5
<小于x < 5x 小于 5
<=小于等于x <= 5x 小于或等于 5
>大于x > 5x 大于 5
>=大于等于x >= 5x 大于或等于 5
++前置递增++xx = x + 1
--前置递减--xx = x - 1
+=复合赋值x += 3x = x + 3
<<输出cout << x把 x 输出到 cout
>>输入cin >> x从 cin 读取值存入 x
.点运算符obj.func()访问对象的成员
::作用域std::cout访问 std 命名空间中的 cout
()调用运算符func()调用函数 func

编译常见错误类型


错误类型描述典型例子
语法错误违反 C++ 语法规则忘写 ;,括号不匹配
类型错误用错了数据类型把字符串传给需要整数的函数
声明错误使用了未声明的名字忘写 std::,变量名拼错

调试建议:

  • 从第一个错误开始修,因为一个错误往往引发连锁报错
  • 每修一小批错误就重新编译一次
  • 这个循环叫做编辑-编译-调试(edit-compile-debug)

知识体系总结

本文所有代码均使用 C++11 标准,编译命令:g++ -std=c++11 -Wall 文件名.cpp -o 输出文件

C++ 第二章:变量与基本类型 — 从零理解

目录

  1. 基本内置类型
  2. 变量
  3. 复合类型:引用与指针
  4. const 限定符
  5. 处理类型:auto 与 decltype
  6. 自定义数据结构

1. 基本内置类型

1.1 什么是类型?

类型告诉计算机两件事:

  1. 这块内存存的是什么(整数?小数?字符?)
  2. 可以对它做哪些操作(加减?比较?)
    就像现实中,“3个苹果 + 2个苹果 = 5个苹果"和"3元钱 + 2元钱 = 5元钱”,用的是同一个"+“,但意义完全不同。C++ 的类型系统就是在管理这些"意义”。

1.2 算术类型一览


类型含义最小位数举例
bool布尔值(真/假)true / false
char字符8位'A'
wchar_t宽字符16位支持国际字符
char16_tUnicode 16位字符16位UTF-16
char32_tUnicode 32位字符32位UTF-32
short短整数16位-32768 ~ 32767
int整数16位(通常32位)-2147483648 ~ 2147483647
long长整数32位比 int 大或相等
long long超长整数64位非常大的整数
float单精度浮点数6位有效数字3.14f
double双精度浮点数10位有效数字3.14159265
long double扩展精度浮点数10位有效数字特殊硬件用

内存里到底存的是什么?
计算机内存是一排排的"格子",每个格子只能放 0 或 1(一个比特位)。8个比特组成一个字节,每个字节有一个地址

  1. 地址 内容(8位)
  2. 736424 0 0 1 1 1 0 1 1
  3. 736425 0 0 0 1 1 0 1 1
  4. 736426 0 1 1 1 0 0 0 1
  5. 736427 0 1 1 0 0 1 0 0
复制代码

不同类型决定了"读几个字节"以及"怎么解读这些0和1"。

1.3 有符号与无符号

  • 有符号(signed):可以表示负数、零、正数,如 int
  • 无符号(unsigned):只能表示零和正数,如 unsigned int
    一个8位无符号类型:表示 0 0 0 到 255 255 255
    一个8位有符号类型:表示 − 128 -128 −128 到 127 127 127(现代机器)或 − 127 -127 −127 到 127 127 127(标准保证)

选哪种类型? 推荐:整数用 int,浮点用 double,字符用 char,不要随便混用有符号和无符号。

1.4 类型转换

当把一种类型的值赋给另一种类型时,会自动转换:

  1. #include <iostream>
  2. int main() {
  3. bool b = 42; // 非零值 -> true
  4. int i = b; // true -> 1
  5. i = 3.14; // 3.14 -> 3(小数部分截断)
  6. double pi = i; // 3 -> 3.0(整数转浮点)
  7. unsigned char c = -1; // -1 对 256 取模 -> 255
  8. // signed char c2 = 256; // 未定义行为!不要这样写
  9. std::cout << "b=" << b << "\n"; // 1
  10. std::cout << "i=" << i << "\n"; // 3
  11. std::cout << "pi=" << pi << "\n"; // 3
  12. std::cout << "c=" << (int)c << "\n"; // 255
  13. return 0;
  14. }
复制代码

https://godbolt.org/z/5fYTao6j8
转换规则记忆卡:

  1. 非bool -> bool: 0变false,其他变true
  2. bool -> 数字: true变1,false变0
  3. 浮点 -> 整数: 直接砍掉小数部分(不是四舍五入!)
  4. 整数 -> 浮点: 小数部分为0,可能损失精度
  5. 超出范围 -> 无符号: 对"类型能表示的个数"取余数
  6. 超出范围 -> 有符号: 未定义行为!(结果不可预测)
复制代码

1.5 无符号类型的陷阱

混合有符号和无符号运算时,int 会被强制转换为 unsigned!

  1. #include <iostream>
  2. int main() {
  3. unsigned u = 10;
  4. int i = -42;
  5. // -42 被转换为无符号,结果是一个很大的数
  6. std::cout << u + i << std::endl;
  7. // 假设 int 是 32 位: 10 + (2^32 - 42) = 4294967264
  8. // 危险的循环:u 永远不会小于 0!
  9. // for (unsigned u = 10; u >= 0; --u) // 死循环!
  10. // 正确写法:
  11. unsigned u2 = 11;
  12. while (u2 > 0) {
  13. --u2;
  14. std::cout << u2 << "\n";
  15. }
  16. return 0;
  17. }
复制代码

https://godbolt.org/z/P84asEern

1.6 字面量(Literals)

字面量就是"写死在代码里的值":

  1. #include <iostream>
  2. int main() {
  3. // 整数字面量
  4. int a = 20; // 十进制
  5. int b = 024; // 八进制(以0开头),值 = 20
  6. int c = 0x14; // 十六进制(以0x开头),值 = 20
  7. // 浮点字面量(默认 double)
  8. double d = 3.14159;
  9. double e = 3.14159E0; // 科学计数法: 3.14159 × 10^0
  10. float f = 1E-3F; // 单精度,值 = 0.001
  11. // 字符和字符串
  12. char ch = 'a'; // 单个字符,单引号
  13. const char* s = "Hello World!"; // 字符串,双引号
  14. // 转义序列
  15. std::cout << "换行:\n";
  16. std::cout << "制表符:\t结束\n";
  17. std::cout << "反斜杠:\\\n";
  18. std::cout << "Hi \x4dO\115!\n"; // Hi MOM!(用十六进制和八进制表示字符)
  19. // 带后缀的字面量
  20. unsigned long long x = 42ULL; // ULL后缀 = unsigned long long
  21. long double y = 3.14159L; // L后缀 = long double
  22. // 布尔字面量
  23. bool t = true;
  24. bool fa = false;
  25. // 指针字面量
  26. int* p = nullptr; // 空指针
  27. return 0;
  28. }
复制代码

https://godbolt.org/z/1T5zoea5b
常用转义序列:

转义含义
\n换行
\t水平制表符(Tab)
\\反斜杠本身
\'单引号
\"双引号
\0空字符(null)
\r回车
\a响铃

2. 变量

2.1 什么是变量?

变量 = 有名字的内存区域 + 类型
类型决定了:

  • 占多少内存
  • 能存什么值
  • 能做什么操作

2.2 变量定义与初始化

  1. #include <iostream>
  2. #include <string>
  3. int main() {
  4. // 基本定义
  5. int sum = 0; // 定义 sum,初始值 0
  6. int value; // 定义 value,未初始化(危险!)
  7. int a = 0, b = 1; // 同时定义多个
  8. // 初始化的四种等价写法(对 int 来说)
  9. int x1 = 0; // 赋值初始化
  10. int x2 = {0}; // 列表初始化
  11. int x3{0}; // 列表初始化(推荐!)
  12. int x4(0); // 直接初始化
  13. // 列表初始化的好处:防止精度损失
  14. long double ld = 3.1415926536;
  15. // int bad{ld}; // 编译错误!会损失数据
  16. int ok(ld); // 合法,但会截断为 3(有警告)
  17. // 字符串
  18. std::string book("0-201-78345-X");
  19. std::cout << "sum=" << sum << "\n";
  20. std::cout << "book=" << book << "\n";
  21. return 0;
  22. }
复制代码

https://godbolt.org/z/YvoK5GYKh

重要区别:

  • 初始化 = 创建变量的同时给它值
  • 赋值 = 先创建变量,之后再改变它的值

这两件事在 C++ 里是不同的操作!
默认初始化规则:

  1. 定义在函数外(全局): 自动初始化为 0
  2. 定义在函数内(局部): 不初始化,值是随机垃圾!(危险)
  3. 类类型(如 string): 由类自己决定(string 默认是空字符串)
复制代码

2.3 声明与定义的区别

这是 C++ 分离编译的基础:

  1. 声明(declaration): 告诉编译器"有这个名字,类型是这个"
  2. 定义(definition): 声明 + 实际分配内存(真正创建变量)
复制代码
  1. // 声明(不是定义):extern 关键字 + 没有初始值
  2. extern int i; // 只声明,不定义
  3. // 定义(也是声明)
  4. int j; // 定义 j,分配内存
  5. extern double pi = 3.1416; // 有初始值 = 定义,extern 被覆盖
  6. // 规则:
  7. // - 变量只能定义一次
  8. // - 可以声明多次
  9. // - 跨文件共享变量:在一个文件定义,其他文件声明
复制代码

2.4 标识符命名规则


规则说明
由字母、数字、下划线组成不能有空格或特殊字符
不能以数字开头1abc 非法,abc1 合法
大小写敏感Abc 和 abc 是不同名字
不能用关键字int、class 等不能用
不能有连续两个下划线my__var 非法
不能以下划线+大写字母开头_Abc 非法

命名约定(非强制,但推荐):

  • 变量名:小写,多词用下划线 student_loan 或驼峰 studentLoan
  • 类名:首字母大写 Sales_item
  • 名字要有意义,能说明用途

2.5 作用域

作用域决定了一个名字在哪些地方有效:

  1. #include <iostream>
  2. int reused = 42; // 全局作用域:整个程序都能用
  3. int main() {
  4. int unique = 0; // 块作用域:只在 main 函数内有效
  5. // 输出1:用的是全局 reused = 42
  6. std::cout << reused << " " << unique << "\n"; // 42 0
  7. int reused = 0; // 新的局部变量,遮蔽了全局 reused
  8. // 输出2:用的是局部 reused = 0
  9. std::cout << reused << " " << unique << "\n"; // 0 0
  10. // :: 是作用域运算符,:: 左边空 = 全局作用域
  11. // 输出3:明确要全局 reused = 42
  12. std::cout << ::reused << " " << unique << "\n"; // 42 0
  13. return 0;
  14. }
复制代码

https://godbolt.org/z/TM7591KoG
作用域的嵌套关系:

  1. 全局作用域
  2. └── main 函数的块作用域
  3. └── for 循环的块作用域
  4. └── ...
复制代码

建议: 变量在第一次使用的地方附近定义,不要在最前面一股脑全定义完。

3. 复合类型:引用与指针

复合类型= 基于另一种类型定义的类型。最重要的两种:引用指针

3.1 引用(Reference)

引用就是给已有变量起的别名,两个名字指向同一块内存:

  1. #include <iostream>
  2. int main() {
  3. int ival = 1024;
  4. int &refVal = ival; // refVal 是 ival 的别名(引用必须初始化!)
  5. // int &refVal2; // 错误!引用必须在定义时绑定
  6. // 对引用的操作 = 对原变量的操作
  7. refVal = 2; // 实际上是给 ival 赋值 2
  8. std::cout << ival; // 输出 2
  9. int ii = refVal; // 等价于 int ii = ival;
  10. std::cout << ii; // 输出 2
  11. // 可以定义多个引用(每个都要有 &)
  12. int i2 = 2048;
  13. int &r1 = ival, &r2 = i2; // r1 和 r2 都是引用
  14. // 引用的类型必须和被引用对象的类型完全匹配
  15. // double d = 3.14;
  16. // int &bad = d; // 错误!类型不匹配
  17. return 0;
  18. }
复制代码

https://godbolt.org/z/j3jKe5qro
引用 vs 普通变量:

  1. 普通变量: 有自己的内存地址,存储一个值
  2. 引用: 没有自己的内存(不是对象),只是另一个名字
  3. 一旦绑定就不能改变绑定对象
  4. 必须在定义时初始化
复制代码

3.2 指针(Pointer)

指针是一个存储地址的对象。它"指向"另一个对象:

  1. #include <iostream>
  2. int main() {
  3. int ival = 42;
  4. int *p = &ival; // p 是指针,存储 ival 的地址
  5. // & 在这里是"取地址运算符"
  6. // 读取指针指向的值:用 * 解引用
  7. std::cout << *p << "\n"; // 输出 42(* 是解引用运算符)
  8. // 通过指针修改原变量
  9. *p = 0;
  10. std::cout << ival << "\n"; // 输出 0,ival 被修改了!
  11. // 空指针:不指向任何对象
  12. int *p1 = nullptr; // 推荐写法(C++11)
  13. int *p2 = 0; // 也合法
  14. // int *p3 = NULL; // 旧式写法,不推荐
  15. // 检查指针是否为空
  16. if (p1) {
  17. // p1 非空时执行
  18. }
  19. if (!p1) {
  20. std::cout << "p1 是空指针\n";
  21. }
  22. // 指针和引用类型必须匹配
  23. double dval = 3.14;
  24. double *pd = &dval; // 合法
  25. // int *pi = &dval; // 错误!类型不匹配
  26. return 0;
  27. }
复制代码

https://godbolt.org/z/44fEef4zn
指针的四种状态:

  1. 1. 指向一个对象(有效)
  2. 2. 指向对象末尾的下一个位置(有效,但不能解引用)
  3. 3. 空指针 nullptr(有效,表示"不指向任何东西")
  4. 4. 无效指针(野指针)—— 危险!不能使用!
复制代码

& 和 * 的双重含义:

  1. int i = 42;
  2. int &r = i; // 声明中的 & = 定义引用
  3. int *p; // 声明中的 * = 定义指针
  4. p = &i; // 表达式中的 & = 取地址运算符
  5. *p = i; // 表达式中的 * = 解引用运算符
复制代码

指针 vs 引用对比:

特性引用指针
是否是对象不是
必须初始化否(但强烈建议!)
能否改变绑定不能能(指向不同对象)
能否为空不能能(nullptr)
解引用语法直接用名字用 *p

3.3 指针与引用的内存示意图

  1. ival: [ 42 ] 地址: 0x1000
  2. ^
  3. |(p 存储这个地址)
  4. p: [ 0x1000 ] 地址: 0x2000
  5. refVal: 就是 ival 本身的另一个名字,没有独立内存
复制代码

3.4 指针到指针(双重指针)

  1. #include <iostream>
  2. int main() {
  3. int ival = 1024;
  4. int *pi = &ival; // pi 指向 ival
  5. int **ppi = &pi; // ppi 指向 pi(指针的指针)
  6. // 访问 ival 的三种方式:
  7. std::cout << ival << "\n"; // 直接访问:1024
  8. std::cout << *pi << "\n"; // 解引用一次:1024
  9. std::cout << **ppi << "\n"; // 解引用两次:1024
  10. return 0;
  11. }
复制代码

内存关系图:

  1. ppi -----> pi -----> ival
  2. [addr2] [addr1] [1024]
复制代码

3.5 void* 指针

void* 是特殊的指针,可以存储任何类型对象的地址,但不能直接解引用(因为不知道类型):

  1. #include <iostream>
  2. int main() {
  3. double obj = 3.14;
  4. void *pv = &obj; // 合法:void* 可以指向任何类型
  5. // *pv = 0; // 错误!不能解引用 void*
  6. // 只能:比较、传参、赋给另一个 void*
  7. return 0;
  8. }
复制代码

4. const 限定符

4.1 基本 const

const 让变量的值不可改变,因此必须在定义时初始化

  1. #include <iostream>
  2. int main() {
  3. const int bufSize = 512; // 正确:有初始值
  4. // const int k; // 错误!没有初始值
  5. // bufSize = 256; // 错误!不能修改 const
  6. // const 对象可以参与大多数运算
  7. const int ci = 42;
  8. int j = ci; // 合法:把 ci 的值复制给 j(复制不会修改 ci)
  9. std::cout << bufSize << "\n"; // 512
  10. std::cout << j << "\n"; // 42
  11. return 0;
  12. }
复制代码

const 变量默认是文件内局部的:同一个 const 名字在不同文件里是独立的变量。
要跨文件共享 const,两边都要加 extern。

4.2 const 引用(对 const 的引用)

  1. #include <iostream>
  2. int main() {
  3. const int ci = 1024;
  4. const int &r1 = ci; // 合法:引用也是 const,不能通过 r1 修改 ci
  5. // r1 = 42; // 错误!不能通过 const 引用修改值
  6. // int &r2 = ci; // 错误!普通引用不能绑定到 const 对象
  7. // 特殊:const 引用可以绑定到非 const 对象、字面量、表达式
  8. int i = 42;
  9. const int &r3 = i; // 合法:const 引用绑定到普通 int
  10. const int &r4 = 42; // 合法:绑定到字面量(编译器创建临时变量)
  11. const int &r5 = r1 * 2; // 合法:绑定到表达式结果
  12. // r3 只是不能通过 r3 修改 i,但 i 本身还是可以改的
  13. i = 100;
  14. std::cout << r3 << "\n"; // 输出 100,r3 反映了 i 的最新值
  15. return 0;
  16. }
复制代码

4.3 指针与 const

这里有两种完全不同的东西:
a) 指向 const 对象的指针(底层 const): 指针本身可以改,但不能通过指针修改所指对象

  1. const double pi = 3.14;
  2. const double *cptr = &pi; // 指针可以改变,但 *cptr 不能修改
  3. // *cptr = 42; // 错误!
  4. double dval = 2.72;
  5. cptr = &dval; // 合法:可以让指针指向另一个对象
复制代码

b) const 指针(顶层 const): 指针本身不能改(地址固定),但可以通过指针修改所指对象

  1. int errNumb = 0;
  2. int *const curErr = &errNumb; // const 在 * 后面 = 指针本身是 const
  3. // curErr = &errNumb; // 错误!不能改变指针存的地址
  4. *curErr = 1; // 合法:可以修改指向的 errNumb
复制代码

c) 指向 const 对象的 const 指针: 两者都不能改

  1. const double pi2 = 3.14159;
  2. const double *const pip = &pi2; // 指针本身是 const,指向的值也是 const
  3. // *pip = 2.72; // 错误!
  4. // pip = &pi2; // 错误!
复制代码

读懂复杂声明的技巧:从右往左读:

  1. int *const curErr = &errNumb;
  2. 读法:curErr → const(curErr本身是常量)→ *(是一个指针)→ int(指向int)
  3. 结论:curErr 是一个指向 int 的常量指针
复制代码

4.4 顶层 const 与底层 const

  1. 顶层 const (top-level const): 对象本身是 const(不能改自己)
  2. 底层 const (low-level const): 指向/引用的对象是 const(不能通过它改别人)
复制代码
  1. #include <iostream>
  2. int main() {
  3. int i = 0;
  4. int *const p1 = &i; // 顶层 const:p1 本身不能改
  5. const int ci = 42; // 顶层 const:ci 本身不能改
  6. const int *p2 = &ci; // 底层 const:不能通过 p2 修改所指对象
  7. const int *const p3 = p2; // 左边底层,右边顶层
  8. // 复制时,顶层 const 被忽略
  9. i = ci; // 合法:复制 ci 的值,顶层 const 忽略
  10. p2 = p3; // 合法:底层 const 相同,顶层 const 忽略
  11. // 复制时,底层 const 不能忽略
  12. // int *p = p3; // 错误!p3 有底层 const,p 没有
  13. p2 = p3; // 合法:都有底层 const
  14. return 0;
  15. }
复制代码

4.5 constexpr:编译时常量

constexpr 让编译器在编译阶段就计算出值:

  1. #include <iostream>
  2. // constexpr 函数(值必须在编译期可知)
  3. constexpr int square(int x) { return x * x; }
  4. int main() {
  5. constexpr int mf = 20; // 20 是编译时常量
  6. constexpr int limit = mf + 1; // mf+1 也是编译时常量
  7. constexpr int s = square(5); // 合法:square 是 constexpr 函数
  8. // const int sz = get_size(); // 不是 constexpr,运行时才知道值
  9. std::cout << mf << "\n"; // 20
  10. std::cout << limit << "\n"; // 21
  11. std::cout << s << "\n"; // 25
  12. return 0;
  13. }
复制代码

const vs constexpr 的区别:

  • const:值不可改,但可以在运行时确定
  • constexpr:值在编译时就必须确定,是真正的编译期常量

5. 处理类型:auto 与 decltype

5.1 类型别名

给复杂类型起一个简洁的别名:

  1. #include <iostream>
  2. // 传统方式:typedef
  3. typedef double wages; // wages = double 的别名
  4. typedef wages base, *p; // base = double,p = double*
  5. // 新标准方式(推荐):using
  6. using SI = int; // SI = int 的别名
  7. int main() {
  8. wages hourly = 15.5; // 等价于 double hourly = 15.5
  9. SI x = 42; // 等价于 int x = 42
  10. // 注意:指针别名 + const 的陷阱
  11. typedef char *pstring; // pstring = char*(指向char的指针)
  12. const pstring cstr = 0; // cstr 是 const 指针(指向char),不是指向const char!
  13. // 不等价于 const char *cstr = 0; ← 这是指向const char的指针,不同!
  14. std::cout << hourly << "\n"; // 15.5
  15. std::cout << x << "\n"; // 42
  16. return 0;
  17. }
复制代码

5.2 auto:让编译器推断类型

当表达式类型复杂时,用 auto 让编译器自动推断:

  1. #include <iostream>
  2. int main() {
  3. int val1 = 10, val2 = 20;
  4. auto item = val1 + val2; // 编译器推断 item 是 int
  5. auto i = 0, *p = &i; // i 是 int,p 是 int*(类型一致)
  6. // auto sz = 0, pi = 3.14; // 错误!sz 是 int,pi 是 double,不一致
  7. // auto 对 const 的处理:
  8. int x = 0;
  9. const int ci = x;
  10. int &cr = x;
  11. auto b = ci; // b 是 int(顶层 const 被忽略)
  12. auto c = cr; // c 是 int(引用被忽略,cr 是 int 的别名)
  13. auto d = &x; // d 是 int*
  14. auto e = &ci; // e 是 const int*(底层 const 保留)
  15. // 要保留顶层 const,需要显式写出
  16. const auto f = ci; // f 是 const int
  17. // auto 推断引用类型
  18. auto &g = ci; // g 是 const int&(绑定到 ci)
  19. const auto &j = 42; // 合法:const 引用可以绑定到字面量
  20. std::cout << item << "\n"; // 30
  21. std::cout << b << "\n"; // 0
  22. std::cout << f << "\n"; // 0
  23. return 0;
  24. }
复制代码

auto 的推断规则总结:

  1. 引用初始化auto → 用被引用对象的类型
  2. 顶层 const → 被 auto 忽略
  3. 底层 const → 被 auto 保留
  4. 想保留顶层const → 写 const auto
  5. 想得到引用 → 写 auto&
复制代码

5.3 decltype:只获取类型,不计算值

有时想知道某个表达式的类型,但不想真正计算它:

  1. #include <iostream>
  2. int f() { return 42; }
  3. int main() {
  4. // decltype(f()) 得到 f 的返回类型 int,但不真正调用 f
  5. decltype(f()) sum = 0; // sum 是 int 类型
  6. const int ci = 0;
  7. int &cj = const_cast<int&>(ci); // 仅演示用
  8. // decltype 保留顶层 const 和引用(与 auto 不同!)
  9. decltype(ci) x = 0; // x 是 const int
  10. // decltype(cj) y = x; // y 是 int&(引用),必须初始化
  11. // decltype 与表达式:
  12. int i = 42, *p = &i;
  13. decltype(i) a; // a 是 int(变量 i 的类型)
  14. // decltype(*p) b; // b 是 int&(解引用返回引用类型)!必须初始化
  15. decltype(i+0) c; // c 是 int(表达式结果是右值)
  16. // 特殊:加括号 vs 不加括号
  17. decltype(i) e; // e 是 int
  18. // decltype((i)) d; // d 是 int&!变量加括号变成引用类型
  19. std::cout << sum << "\n";
  20. return 0;
  21. }
复制代码

decltype vs auto 的核心差异:

特性autodecltype
顶层 const忽略保留
引用忽略,用被引用对象类型保留引用类型
需要初始值
加括号不影响变成引用类型

6. 自定义数据结构

6.1 定义结构体(struct)

结构体把相关数据组织在一起:

  1. // Sales_data.h 头文件内容
  2. #ifndef SALES_DATA_H // 头文件保护:如果还没定义这个宏
  3. #define SALES_DATA_H // 定义这个宏(防止重复包含)
  4. #include <string>
  5. struct Sales_data {
  6. // 数据成员(C++11 支持类内初始化)
  7. std::string bookNo; // ISBN 书号,默认空字符串
  8. unsigned units_sold = 0; // 销售数量,默认 0
  9. double revenue = 0.0; // 总收入,默认 0.0
  10. }; // 注意:结构体定义末尾必须有分号!
  11. #endif // 结束头文件保护
复制代码

头文件保护机制(#ifndef 防卫式声明):

  1. 第一次包含 Sales_data.h:
  2. SALES_DATA_H 未定义 → #ifndef 为真 → 处理内容 → 定义 SALES_DATA_H
  3. 第二次包含 Sales_data.h:
  4. SALES_DATA_H 已定义 → #ifndef 为假 → 跳过全部内容(避免重复定义)
复制代码

6.2 使用结构体

  1. #include <iostream>
  2. #include <string>
  3. // #include "Sales_data.h" // 实际项目中应单独放头文件
  4. // 为演示方便,这里直接内嵌定义
  5. struct Sales_data {
  6. std::string bookNo;
  7. unsigned units_sold = 0;
  8. double revenue = 0.0;
  9. };
  10. int main() {
  11. Sales_data data1, data2;
  12. // 读取第一条交易:ISBN 数量 单价
  13. double price = 0;
  14. std::cin >> data1.bookNo >> data1.units_sold >> price;
  15. data1.revenue = data1.units_sold * price; // 计算总收入
  16. // 读取第二条交易
  17. std::cin >> data2.bookNo >> data2.units_sold >> price;
  18. data2.revenue = data2.units_sold * price;
  19. // 检查是否是同一本书
  20. if (data1.bookNo == data2.bookNo) {
  21. unsigned totalCnt = data1.units_sold + data2.units_sold;
  22. double totalRevenue = data1.revenue + data2.revenue;
  23. std::cout << data1.bookNo << " "
  24. << totalCnt << " "
  25. << totalRevenue << " ";
  26. if (totalCnt != 0)
  27. std::cout << totalRevenue / totalCnt << "\n"; // 平均售价
  28. else
  29. std::cout << "(no sales)\n";
  30. return 0;
  31. } else {
  32. std::cerr << "Data must refer to the same ISBN\n";
  33. return -1;
  34. }
  35. }
复制代码

结构体的关键特性:

  • 每个对象有自己独立的数据成员副本,修改 data1 不影响 data2
  • 用 .(点运算符)访问成员:data1.bookNo
  • 类内初始化:可以在定义时给成员初始值(C++11)

综合流程图

变量从定义到使用的完整流程

指针与引用的关系总览

const 的层次关系

  1. int i = 0;
  2. 顶层const 底层const
  3. (对象本身) (指向的对象)
  4. int *const p1 = &i; ← 顶层const,p1不能改,*p1能改
  5. const int ci = 42; ← 顶层const,ci不能改
  6. const int *p2 = &ci; ← 底层const,p2能改,*p2不能改
  7. const int *const p3 = p2; ← 两者都有
复制代码

典型错误汇总与正确写法

  1. #include <iostream>
  2. #include <string>
  3. int main() {
  4. // 错误1:引用未初始化
  5. // int &r; // 错误!
  6. int x = 0;
  7. int &r = x; // 正确
  8. // 错误2:用字面量初始化普通引用
  9. // int &r2 = 10; // 错误!
  10. const int &r2 = 10; // 正确:const 引用可以绑定字面量
  11. // 错误3:指针类型不匹配
  12. double d = 3.14;
  13. // int *p = &d; // 错误!
  14. double *p = &d; // 正确
  15. // 错误4:未初始化 const
  16. // const int k; // 错误!
  17. const int k = 0; // 正确
  18. // 错误5:修改 const 对象
  19. // k = 1; // 错误!
  20. // 错误6:auto 多变量类型不一致
  21. // auto sz = 0, pi = 3.14; // 错误!sz是int,pi是double
  22. auto sz = 0; // 正确:分开写
  23. auto pi = 3.14;
  24. // 错误7:使用未初始化的局部变量
  25. int val;
  26. // std::cout << val; // 危险!值未定义
  27. int val2 = 0; // 正确:显式初始化
  28. std::cout << val2 << "\n";
  29. // 错误8:对 const 对象解引用赋值
  30. const int ci = 42;
  31. const int *cp = &ci;
  32. // *cp = 0; // 错误!不能通过 const 指针修改值
  33. return 0;
  34. }
复制代码

关键概念速查表


概念一句话理解
类型告诉编译器内存里存的是什么,能做什么操作
变量有名字的内存区域,有类型
引用变量的别名,不是独立对象,必须初始化,不能重绑
指针存地址的对象,可以为空,可以改变指向
const值不可改,必须初始化
顶层 const对象本身不可改
底层 const指向/引用的对象不可改
constexpr编译时就能算出来的常量
auto让编译器推断类型
decltype获取表达式的类型(不计算)
作用域名字有效的范围(花括号内)
声明告诉编译器名字存在
定义真正分配内存
初始化创建时给值
赋值创建后改变值

C++ 第三章:字符串、向量和数组 — 从零理解

目录

  1. using 声明:告别 std:: 前缀
  2. string 类型:可变长度字符串
  3. vector 类型:可变大小的集合
  4. 迭代器:通用的元素访问机制
  5. 数组:固定大小的低级容器
  6. 多维数组

1. using 声明:告别 std:: 前缀

1.1 为什么需要 using 声明?

C++ 标准库的所有名字都放在 std 命名空间里。每次用 std::cin、std::cout 很繁琐。
using 声明让我们可以省略 std:: 前缀:

  1. #include <iostream>
  2. #include <string>
  3. // 每个名字单独声明一行
  4. using std::cin;
  5. using std::cout;
  6. using std::endl;
  7. using std::string;
  8. int main() {
  9. string s;
  10. cin >> s; // 直接用 cin,不用写 std::cin
  11. cout << s << endl;
  12. return 0;
  13. }
复制代码

https://godbolt.org/z/8evhxqxfa
规则:

  • 每个 using 声明只引入一个名字
  • 每条声明必须以分号结尾
  • 头文件里不要写 using 声明(会污染包含该头文件的所有文件)

2. string 类型:可变长度字符串

2.1 初始化 string

  1. #include <iostream>
  2. #include <string>
  3. using std::string;
  4. using std::cout;
  5. using std::cin;
  6. using std::endl;
  7. int main() {
  8. // 六种初始化方式
  9. string s1; // 默认初始化:空字符串 ""
  10. string s2(s1); // 直接初始化:s2 是 s1 的副本
  11. string s3 = s1; // 拷贝初始化:等价于 s2(s1)
  12. string s4("hello"); // 直接初始化:s4 = "hello"
  13. string s5 = "hello"; // 拷贝初始化:等价于 s4("hello")
  14. string s6(10, 'c'); // 直接初始化:s6 = "cccccccccc"(10个c)
  15. cout << "s1='" << s1 << "'\n"; // 空
  16. cout << "s4='" << s4 << "'\n"; // hello
  17. cout << "s6='" << s6 << "'\n"; // cccccccccc
  18. return 0;
  19. }
复制代码

https://godbolt.org/z/boex7c1rj
直接初始化 vs 拷贝初始化:

  • 用 = → 拷贝初始化(编译器把右边的值复制过来)
  • 不用 =,直接用 () → 直接初始化
  • 多个初始值必须用直接初始化(括号形式)

2.2 string 的常用操作

  1. #include <iostream>
  2. #include <string>
  3. using std::string;
  4. using std::cout;
  5. using std::cin;
  6. using std::endl;
  7. using std::getline;
  8. int main() {
  9. // ------- 读写 -------
  10. string word;
  11. // cin >> 读入:自动跳过开头的空白,遇到空白停止
  12. // 比如输入 " Hello World ",读入的是 "Hello"
  13. cin >> word;
  14. cout << word << endl;
  15. // getline:读取整行(包括空格),直到遇到换行符
  16. // 换行符被丢弃,不存入字符串
  17. string line;
  18. getline(cin, line);
  19. cout << line << endl;
  20. // ------- 基本操作 -------
  21. string s = "Hello, World!";
  22. cout << s.empty() << "\n"; // 0(false),因为 s 不空
  23. cout << s.size() << "\n"; // 13,字符个数
  24. // size() 返回的类型是 string::size_type(无符号整数)
  25. // 推荐用 auto 接收,避免有符号/无符号混用的陷阱
  26. auto len = s.size();
  27. // ------- 比较(字典序,大小写敏感)-------
  28. string s1 = "Hello";
  29. string s2 = "Hello World";
  30. string s3 = "Hiya";
  31. // s1 < s2:因为 s1 是 s2 的前缀,且 s1 更短
  32. cout << (s1 < s2) << "\n"; // 1(true)
  33. // s3 > s1:因为第一个不同字符 'i' > 'e'
  34. cout << (s3 > s1) << "\n"; // 1(true)
  35. // ------- 拼接 -------
  36. string a = "hello, ", b = "world\n";
  37. string c = a + b; // c = "hello, world\n"
  38. a += b; // a 变成 "hello, world\n"
  39. // 注意:string 字面量("...")不是 std::string!
  40. // 加号两边至少要有一个 std::string 对象
  41. string ok1 = s1 + ", "; // 合法:s1 是 string
  42. // string bad = "hello" + ", "; // 非法:两边都是字面量
  43. string ok2 = s1 + ", " + "world"; // 合法:先 s1+", " 得到 string,再加 "world"
  44. return 0;
  45. }
复制代码

https://godbolt.org/z/d9h1zG5qx

操作含义
s.empty()s 为空返回 true
s.size()返回字符数(类型是 string::size_type)
s[n]返回第 n 个字符的引用(从0开始)
s1 + s2拼接,返回新字符串
s1 = s2用 s2 替换 s1 的内容
s1 == s2相等判断(大小写敏感)
<, <=, >, >=字典序比较

2.3 字符串比较规则

比较使用字典序(与大小写有关):
规则1: 若一个串是另一个的前缀,短的小
"Hello" < "Hello World" \text{"Hello"} < \text{"Hello World"} "Hello"<"Hello World"
规则2: 若有不同字符,比较第一个不同位置的字符
"Hiya" > "Hello" \text{"Hiya"} > \text{"Hello"} "Hiya">"Hello"
(因为第2位 i > e i > e i>e)

2.4 处理字符串中的每个字符

方法一:范围 for 循环(推荐,遍历每个字符)

  1. #include <iostream>
  2. #include <string>
  3. #include <cctype> // isalpha, ispunct, toupper 等函数
  4. using std::string;
  5. using std::cout;
  6. using std::endl;
  7. int main() {
  8. string s = "Hello World!!!";
  9. // 1. 统计标点符号个数
  10. // decltype(s.size()) 得到 string::size_type 类型(无符号整数)
  11. decltype(s.size()) punct_cnt = 0;
  12. for (auto c : s) { // c 是 char 的副本,修改 c 不影响 s
  13. if (ispunct(c))
  14. ++punct_cnt;
  15. }
  16. cout << punct_cnt << " punctuation characters\n"; // 3
  17. // 2. 把整个字符串转为大写
  18. // 用引用 &c,这样修改 c 就是修改 s 中对应的字符
  19. for (auto &c : s) {
  20. c = toupper(c); // toupper 把小写字母转为大写
  21. }
  22. cout << s << "\n"; // HELLO WORLD!!!
  23. return 0;
  24. }
复制代码

https://godbolt.org/z/vx96dxez6

cctype 函数含义
isalpha(c)c 是字母
isdigit(c)c 是数字
isalnum(c)c 是字母或数字
isspace(c)c 是空白(空格、制表符、换行等)
ispunct(c)c 是标点符号
isupper(c)c 是大写字母
islower(c)c 是小写字母
toupper(c)转为大写,非字母原样返回
tolower(c)转为小写,非字母原样返回

方法二:下标访问(只处理部分字符)

  1. #include <iostream>
  2. #include <string>
  3. #include <cctype>
  4. using std::string;
  5. using std::cout;
  6. int main() {
  7. string s = "some string";
  8. // 只把第一个字符转大写
  9. if (!s.empty()) // 先检查非空,空字符串的 s[0] 是未定义行为!
  10. s[0] = toupper(s[0]);
  11. cout << s << "\n"; // Some string
  12. // 只把第一个单词转大写(遇到空白停止)
  13. // index 的类型用 decltype(s.size()) = string::size_type(无符号)
  14. for (decltype(s.size()) index = 0;
  15. index != s.size() && !isspace(s[index]); // && 短路求值:先检查不越界
  16. ++index)
  17. {
  18. s[index] = toupper(s[index]);
  19. }
  20. cout << s << "\n"; // SOME string
  21. // 随机访问:把数字0-15转为十六进制字符
  22. const string hexdigits = "0123456789ABCDEF";
  23. string result;
  24. string::size_type n;
  25. // 假设输入:12 0 5 15
  26. // 输出:C05F
  27. while (std::cin >> n) {
  28. if (n < hexdigits.size())
  29. result += hexdigits[n]; // 用数字做下标,直接取对应的十六进制字符
  30. }
  31. cout << "Hex: " << result << "\n";
  32. return 0;
  33. }
复制代码

https://godbolt.org/z/Ehene538f

安全警告: 下标不检查越界!

  • 下标必须满足: 0 ≤ index < s.size() 0 \leq \text{index} < \text{s.size()} 0≤index
  • 用 string::size_type(无符号)做下标,可以保证不为负数

3. vector 类型:可变大小的集合

vector 是一个类模板,可以生成存储任意类型元素的容器。

  1. vector<int> 存 int
  2. vector<string> 存 string
  3. vector<double> 存 double
  4. vector<vector<int>> 存 vector<int>(嵌套)
复制代码

3.1 初始化 vector

  1. #include <iostream>
  2. #include <vector>
  3. #include <string>
  4. using std::vector;
  5. using std::string;
  6. using std::cout;
  7. int main() {
  8. // 空 vector(最常用的起点)
  9. vector<int> v1; // 0 个元素
  10. // 从另一个 vector 复制
  11. vector<int> v2(v1); // v2 是 v1 的副本
  12. vector<int> v3 = v1; // 等价于 v2(v1)
  13. // 指定元素个数和初始值
  14. vector<int> v4(10, -1); // 10 个元素,每个都是 -1
  15. vector<string> v5(10, "hi!"); // 10 个 "hi!"
  16. // 只指定个数,值自动初始化(int 为 0,string 为 "")
  17. vector<int> v6(10); // 10 个 0
  18. vector<string> v7(10); // 10 个空字符串
  19. // 列表初始化(花括号):直接指定每个元素的值
  20. vector<int> v8{1, 2, 3, 4, 5}; // 5 个元素
  21. vector<string> v9{"a", "an", "the"}; // 3 个字符串
  22. // 圆括号 vs 花括号的区别!
  23. vector<int> va(10); // 10 个元素,每个值为 0
  24. vector<int> vb{10}; // 1 个元素,值为 10
  25. vector<int> vc(10, 1); // 10 个元素,每个值为 1
  26. vector<int> vd{10, 1}; // 2 个元素,值分别为 10 和 1
  27. // string vector 的花括号:如果不能做元素值,就用来构造
  28. vector<string> ve{10}; // 10 个空字符串(10 不能是 string,退化为"个数")
  29. vector<string> vf{10, "hi"}; // 10 个 "hi"(同理)
  30. cout << "v4 size=" << v4.size() << "\n"; // 10
  31. cout << "vb size=" << vb.size() << "\n"; // 1
  32. cout << "vd size=" << vd.size() << "\n"; // 2
  33. return 0;
  34. }
复制代码

https://godbolt.org/z/311xsWMar
圆括号 vs 花括号记忆口诀:

  1. 圆括号 () → 构造(指定个数/值)
  2. 花括号 {} → 列表初始化(指定每个元素),如不可行才退化为构造
复制代码

3.2 向 vector 添加元素:push_back

  1. #include <iostream>
  2. #include <vector>
  3. #include <string>
  4. using std::vector;
  5. using std::string;
  6. using std::cout;
  7. using std::cin;
  8. int main() {
  9. // 从空 vector 开始,逐个添加
  10. vector<int> v; // 空 vector
  11. for (int i = 0; i < 100; ++i)
  12. v.push_back(i); // 在末尾追加元素,v 自动扩容
  13. // 结束后 v 有 100 个元素,值为 0..99
  14. // 读取未知个数的字符串
  15. vector<string> text;
  16. string word;
  17. while (cin >> word) {
  18. text.push_back(word); // 每读一个词就追加
  19. }
  20. cout << "共读入 " << text.size() << " 个词\n";
  21. return 0;
  22. }
复制代码

https://godbolt.org/z/3b85h7aEG

重要: vector 支持高效的运行时扩容。
推荐先定义空 vector,再用 push_back 添加,不要一开始就指定大小(除非所有元素值相同)。
禁忌: 范围 for 循环的循环体内不能 push_back!因为添加元素会让迭代失效。

3.3 vector 的其他操作

  1. #include <iostream>
  2. #include <vector>
  3. using std::vector;
  4. using std::cout;
  5. using std::cin;
  6. using std::endl;
  7. int main() {
  8. vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};
  9. // 范围 for + 引用:修改每个元素
  10. for (auto &i : v)
  11. i *= i; // 每个元素变成自己的平方
  12. // 范围 for:只读
  13. for (auto i : v)
  14. cout << i << " ";
  15. cout << endl; // 1 4 9 16 25 36 49 64 81
  16. // 成员函数
  17. cout << v.empty() << "\n"; // 0(非空)
  18. cout << v.size() << "\n"; // 9
  19. // 下标访问(不能用下标添加新元素!)
  20. cout << v[0] << "\n"; // 1
  21. v[0] = 100; // 修改已有元素
  22. // 成绩分段统计例子
  23. vector<unsigned> scores(11, 0); // 11 个桶(0-9, 10-19, ..., 100),初始值全0
  24. unsigned grade;
  25. while (cin >> grade) {
  26. if (grade <= 100)
  27. ++scores[grade / 10]; // grade/10 得到桶的下标(0~10)
  28. }
  29. for (auto s : scores)
  30. cout << s << " ";
  31. cout << endl;
  32. return 0;
  33. }
复制代码

https://godbolt.org/z/4W4TqWEY8

操作含义
v.empty()是否为空
v.size()元素个数(类型是 vector::size_type)
v.push_back(t)在末尾添加值为 t 的元素
v[n]访问第 n 个元素(从0开始),不检查越界
v1 = v2用 v2 的元素替换 v1
v1 == v2元素个数和值都相等才相等
<, <=, >, >=字典序比较

陷阱: v[n] 只能访问已存在的元素,不能用来添加元素!

  1. vector<int> ivec; // 空 vector
  2. // ivec[0] = 42; // 错误!没有第0个元素
  3. ivec.push_back(42); // 正确!
复制代码

4. 迭代器:通用的元素访问机制

迭代器是比下标更通用的访问方式。所有容器都支持迭代器,但并非所有容器都支持下标。

4.1 迭代器的基本用法

  1. begin() → 指向第一个元素
  2. end() → 指向"末尾后一个位置"(不存在的元素,哨兵)
  3. 如果容器为空:begin() == end()
复制代码
  1. +-------+-------+-------+-------+
  2. | [0] | [1] | [2] | [3] | (不存在)
  3. +-------+-------+-------+-------+
  4. ^ ^
  5. begin() end()
复制代码
  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. #include <cctype>
  5. using std::string;
  6. using std::vector;
  7. using std::cout;
  8. int main() {
  9. string s("some string");
  10. // auto 让编译器推断迭代器类型
  11. auto b = s.begin(); // 指向第一个字符 's'
  12. auto e = s.end(); // 指向最后一个字符后面的位置
  13. // 用迭代器把第一个字符变大写
  14. if (s.begin() != s.end()) { // 先确认非空
  15. auto it = s.begin();
  16. *it = toupper(*it); // 解引用得到字符,修改它
  17. }
  18. cout << s << "\n"; // Some string
  19. // 用迭代器遍历,把第一个单词变大写(遇到空格停止)
  20. for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
  21. *it = toupper(*it);
  22. cout << s << "\n"; // SOME string
  23. // vector 也一样
  24. vector<int> v{1, 2, 3, 4, 5};
  25. for (auto it = v.begin(); it != v.end(); ++it)
  26. *it *= 2; // 每个元素乘2
  27. for (auto x : v)
  28. cout << x << " ";
  29. cout << "\n"; // 2 4 6 8 10
  30. return 0;
  31. }
复制代码

https://godbolt.org/z/55ja6dM1a

迭代器操作含义
*iter解引用,得到 iter 指向的元素
iter->mem等价于 (*iter).mem,访问元素的成员
++iter移到下一个元素
--iter移到上一个元素
iter1 == iter2两个迭代器是否相等
iter1 != iter2两个迭代器是否不等

4.2 迭代器类型:iterator vs const_iterator

  1. #include <iostream>
  2. #include <vector>
  3. #include <string>
  4. using std::vector;
  5. using std::string;
  6. using std::cout;
  7. int main() {
  8. vector<int> v{1, 2, 3};
  9. const vector<int> cv{4, 5, 6};
  10. // 普通 vector:begin() 返回 iterator(可读可写)
  11. vector<int>::iterator it1 = v.begin();
  12. *it1 = 100; // 合法,可以修改
  13. // const vector:begin() 返回 const_iterator(只读)
  14. vector<int>::const_iterator it2 = cv.begin();
  15. // *it2 = 100; // 错误!不能通过 const_iterator 修改
  16. // cbegin() / cend():强制返回 const_iterator(即使 vector 不是 const)
  17. auto it3 = v.cbegin(); // const_iterator,只读
  18. // *it3 = 100; // 错误!
  19. // 箭头运算符:简化"解引用后访问成员"
  20. vector<string> svec{"hello", "world"};
  21. auto it4 = svec.begin();
  22. cout << it4->size() << "\n"; // 等价于 (*it4).size(),输出 5
  23. return 0;
  24. }
复制代码

https://godbolt.org/z/Yb7Y8dxPb
选择哪种迭代器:

  • 只读 → 用 const_iterator(通过 cbegin() / cend() 获取)
  • 需要修改 → 用 iterator(通过 begin() / end() 获取)

4.3 迭代器算术(仅 vector 和 string 支持)

  1. #include <iostream>
  2. #include <vector>
  3. #include <string>
  4. using std::vector;
  5. using std::string;
  6. using std::cout;
  7. int main() {
  8. vector<int> vi{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  9. // 迭代器加减整数
  10. auto mid = vi.begin() + vi.size() / 2; // 指向中间元素(vi[5])
  11. // 两个迭代器相减,得到距离(difference_type,有符号整数)
  12. auto dist = vi.end() - vi.begin(); // dist = 10
  13. // 比较迭代器
  14. auto it = vi.begin();
  15. if (it < mid)
  16. cout << "it 在 mid 之前\n";
  17. cout << "距离=" << dist << "\n"; // 10
  18. // 二分查找示例
  19. // 前提:text 必须是已排序的
  20. vector<string> text{"apple", "banana", "cherry", "date", "elderberry"};
  21. string sought = "cherry";
  22. auto beg = text.begin(), end = text.end();
  23. // 计算中间位置:(end - beg) / 2 得到距离,再加到 beg 上
  24. // 不能写 (beg + end) / 2,因为两个迭代器相加没有意义
  25. auto mid2 = text.begin() + (end - beg) / 2;
  26. while (mid2 != end && *mid2 != sought) {
  27. if (sought < *mid2)
  28. end = mid2; // 目标在左半部分,缩小范围
  29. else
  30. beg = mid2 + 1; // 目标在右半部分,缩小范围
  31. mid2 = beg + (end - beg) / 2; // 重新计算中间位置
  32. }
  33. if (mid2 != end)
  34. cout << "找到:" << *mid2 << "\n"; // 找到:cherry
  35. else
  36. cout << "未找到\n";
  37. return 0;
  38. }
复制代码

https://godbolt.org/z/a97734437
为什么用 beg + (end - beg) / 2 而不是 (beg + end) / 2?
因为两个迭代器相加没有意义(地址+地址 = 什么?),但迭代器相减有意义(地址差 = 距离)。
中间迭代器 = beg + end − beg 2 \text{中间迭代器} = \text{beg} + \frac{\text{end} - \text{beg}}{2} 中间迭代器=beg+2end−beg​

迭代器算术含义
iter + n向前移动 n 步
iter - n向后移动 n 步
iter1 - iter2两迭代器间的距离(difference_type,有符号)
>, >=, <, <=比较两个迭代器的位置前后

5. 数组:固定大小的低级容器

数组与 vector 的最大区别:大小固定,不能动态扩容

5.1 定义与初始化数组

  1. #include <iostream>
  2. #include <string>
  3. using std::string;
  4. using std::cout;
  5. int main() {
  6. // 维度必须是编译时常量(常量表达式)
  7. constexpr unsigned sz = 42; // constexpr = 编译时常量
  8. int arr[10]; // 10 个 int(函数内:未初始化)
  9. int *parr[sz]; // 42 个 int 指针
  10. // string bad[cnt]; // 错误!cnt 不是常量表达式
  11. // 列表初始化
  12. const unsigned n = 3;
  13. int ia1[n] = {0, 1, 2}; // 显式初始化 3 个元素
  14. int a2[] = {0, 1, 2}; // 维度由初始值个数推断,= 3
  15. int a3[5] = {0, 1, 2}; // 等价于 {0, 1, 2, 0, 0}(剩余补0)
  16. string a4[3] = {"hi", "bye"}; // 等价于 {"hi", "bye", ""}
  17. // 字符数组特殊初始化方式:字符串字面量
  18. char a5[] = {'C', '+', '+'}; // 维度3,没有 '\0'
  19. char a6[] = {'C', '+', '+', '\0'}; // 维度4,显式 null
  20. char a7[] = "C++"; // 维度4,自动补 '\0'
  21. // const char a8[6] = "Daniel"; // 错误!"Daniel" 有7个字符(含'\0'),放不下
  22. // 数组不能拷贝赋值
  23. int a[] = {0, 1, 2};
  24. // int a9[] = a; // 错误!不能用数组初始化另一个数组
  25. // a9 = a; // 错误!不能赋值
  26. // 复杂声明:从内向外读
  27. int *ptrs[10]; // ptrs 是数组,含10个 int 指针
  28. int (*Parray)[10] = &arr; // Parray 是指针,指向 含10个int 的数组
  29. int (&arrRef)[10] = arr; // arrRef 是引用,引用一个 含10个int 的数组
  30. cout << a7 << "\n"; // C++
  31. return 0;
  32. }
复制代码

https://godbolt.org/z/fTszjKjGh
读懂复杂数组声明的技巧:从内向外,从右向左

  1. int (*Parray)[10]
  2. 从 Parray 出发:
  3. (*Parray) → 先看 *,Parray 是一个指针
  4. [10] → 向右,它指向的是有10个元素的数组
  5. int → 向左,数组的元素类型是 int
  6. 结论:Parray 是指向"含10个int的数组"的指针
复制代码

5.2 访问数组元素

  1. #include <iostream>
  2. #include <cstddef> // size_t 定义在这里
  3. using std::cout;
  4. using std::cin;
  5. int main() {
  6. // size_t 是无符号整数类型,用来做下标最安全
  7. unsigned scores[11] = {}; // 11个桶,值初始化为 0
  8. unsigned grade;
  9. while (cin >> grade) {
  10. if (grade <= 100)
  11. ++scores[grade / 10];
  12. }
  13. // 范围 for 遍历(推荐)
  14. for (auto s : scores)
  15. cout << s << " ";
  16. cout << "\n";
  17. // 下标遍历
  18. for (size_t i = 0; i < 11; ++i)
  19. cout << scores[i] << " ";
  20. cout << "\n";
  21. return 0;
  22. }
复制代码

https://godbolt.org/z/h9qjoqTTE

5.3 指针与数组

核心规律:数组名在大多数表达式中自动转换为指向首元素的指针

  1. #include <iostream>
  2. #include <iterator> // begin, end 函数(C++11)
  3. using std::cout;
  4. using std::begin;
  5. using std::end;
  6. int main() {
  7. int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  8. // 数组名 → 首元素指针
  9. int *p = arr; // 等价于 int *p = &arr[0]
  10. ++p; // 现在 p 指向 arr[1]
  11. // 计算"末尾后一个位置"的指针(用来当哨兵)
  12. int *e = &arr[10]; // 指向 arr[10](不存在的元素)
  13. // 遍历
  14. for (int *b = arr; b != e; ++b)
  15. cout << *b << " ";
  16. cout << "\n";
  17. // 更安全的写法:用标准库 begin/end(需要 <iterator>)
  18. int *beg = begin(arr); // 等价于 &arr[0]
  19. int *last = end(arr); // 等价于 &arr[10]
  20. for (int *p2 = beg; p2 != last; ++p2)
  21. cout << *p2 << " ";
  22. cout << "\n";
  23. // 指针算术
  24. int ia[] = {0, 2, 4, 6, 8};
  25. int *ip = ia;
  26. int *ip2 = ip + 4; // 指向 ia[4](值为8)
  27. cout << *ip2 << "\n"; // 8
  28. // 两指针相减 = 距离(类型是 ptrdiff_t,有符号)
  29. auto n = end(ia) - begin(ia); // n = 5
  30. cout << n << "\n";
  31. // 指针当下标(与数组下标等价)
  32. int *p3 = &ia[2]; // p3 指向 ia[2](值为4)
  33. cout << p3[1] << "\n"; // 等价于 *(p3+1) = ia[3] = 6
  34. cout << p3[-2] << "\n"; // 等价于 *(p3-2) = ia[0] = 0(下标可以是负数!)
  35. // auto 推断数组类型时,得到的是指针
  36. auto ia2(ia); // ia2 是 int*,不是数组!
  37. // decltype 不会转换
  38. decltype(ia) ia3 = {0,1,2,3,4}; // ia3 是 int[5],是数组
  39. return 0;
  40. }
复制代码

https://godbolt.org/z/147cq1zTj
迭代器与指针操作对照:

操作迭代器(vector)指针(数组)
首元素v.begin()begin(arr) 或 arr
末尾哨兵v.end()end(arr) 或 &arr[n]
下一个++it++p
解引用*it*p
加整数it + np + n
相减it1 - it2(difference_type)p1 - p2(ptrdiff_t)

5.4 C 风格字符串(了解即可,不推荐使用)

C 风格字符串 = 以 '\0' 结尾的 char 数组。

  1. #include <iostream>
  2. #include <cstring> // strlen, strcmp, strcat, strcpy
  3. using std::cout;
  4. int main() {
  5. const char ca1[] = "A string example"; // 自动以 '\0' 结尾
  6. const char ca2[] = "A different string";
  7. // 错误的比较方式!比的是指针地址,不是字符串内容
  8. // if (ca1 < ca2) // 未定义行为!
  9. // 正确:用 strcmp
  10. if (strcmp(ca1, ca2) < 0)
  11. cout << "ca1 < ca2\n";
  12. // 字符串长度(不含 '\0')
  13. cout << strlen(ca1) << "\n"; // 16
  14. // C 风格字符串拼接(危险!必须手动管理内存)
  15. // 推荐用 std::string 代替
  16. // string 转 C 风格:c_str()
  17. std::string s = "Hello";
  18. const char *cp = s.c_str(); // 指向 "Hello\0" 的指针
  19. // 注意:s 改变后 cp 可能失效!需要时应复制这个数组
  20. return 0;
  21. }
复制代码

https://godbolt.org/z/KE19oGf67

C 字符串函数(含义
strlen(p)字符串长度(不含 \0)
strcmp(p1, p2)比较,相等返回0,p1>p2返回正值
strcat(p1, p2)把 p2 追加到 p1 末尾,返回 p1
strcpy(p1, p2)把 p2 复制到 p1,返回 p1

5.5 数组与 vector 的互操作

  1. #include <iostream>
  2. #include <vector>
  3. #include <iterator>
  4. using std::vector;
  5. using std::cout;
  6. using std::begin;
  7. using std::end;
  8. int main() {
  9. int int_arr[] = {0, 1, 2, 3, 4, 5};
  10. // 用数组初始化 vector(传首指针和尾后指针)
  11. vector<int> ivec(begin(int_arr), end(int_arr)); // 全部6个元素
  12. vector<int> subVec(int_arr + 1, int_arr + 4); // int_arr[1], [2], [3]
  13. for (auto x : ivec)
  14. cout << x << " ";
  15. cout << "\n"; // 0 1 2 3 4 5
  16. for (auto x : subVec)
  17. cout << x << " ";
  18. cout << "\n"; // 1 2 3
  19. return 0;
  20. }
复制代码

https://godbolt.org/z/ThvsMvqcM

6. 多维数组

严格来说,C++ 没有多维数组,有的是数组的数组

6.1 定义与初始化

  1. #include <iostream>
  2. #include <cstddef>
  3. using std::cout;
  4. using std::size_t;
  5. int main() {
  6. // 3行4列的二维数组
  7. int ia[3][4];
  8. // 读法:ia 是大小为3的数组,每个元素是大小为4的 int 数组
  9. // 带初始化(嵌套花括号,直观)
  10. int ib[3][4] = {
  11. {0, 1, 2, 3}, // 第0行
  12. {4, 5, 6, 7}, // 第1行
  13. {8, 9, 10, 11} // 第2行
  14. };
  15. // 等价写法(省略内层花括号)
  16. int ic[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
  17. // 只初始化每行的第一个元素
  18. int id[3][4] = {{0}, {4}, {8}}; // 其余补0
  19. // 访问元素
  20. cout << ib[2][3] << "\n"; // 11(第2行第3列)
  21. // 引用绑定到某一行
  22. int (&row)[4] = ib[1]; // row 引用第1行(一个含4个int的数组)
  23. cout << row[0] << "\n"; // 4
  24. return 0;
  25. }
复制代码

https://godbolt.org/z/zEKEMxaoY
内存布局(行优先存储):

  1. ib[3][4] 在内存中:
  2. col0 col1 col2 col3
  3. row0 [ 0 | 1 | 2 | 3 ]
  4. row1 [ 4 | 5 | 6 | 7 ]
  5. row2 [ 8 | 9 | 10 | 11 ]
  6. ib[i][j] 的地址 = 首地址 + i*4*sizeof(int) + j*sizeof(int)
复制代码

6.2 遍历多维数组

  1. #include <iostream>
  2. #include <cstddef>
  3. using std::cout;
  4. using std::size_t;
  5. int main() {
  6. constexpr size_t rowCnt = 3, colCnt = 4;
  7. int ia[rowCnt][colCnt];
  8. // 方法一:下标遍历
  9. for (size_t i = 0; i != rowCnt; ++i)
  10. for (size_t j = 0; j != colCnt; ++j)
  11. ia[i][j] = i * colCnt + j; // 赋值为位置编号
  12. // 方法二:范围 for(外层必须用引用!)
  13. size_t cnt = 0;
  14. for (auto &row : ia) // row 是 int(&)[4],引用一整行
  15. for (auto &col : row) // col 是 int&
  16. col = cnt++;
  17. // 方法三:只读范围 for(外层仍需引用!)
  18. for (const auto &row : ia) // const 引用,防止修改
  19. for (auto col : row) // 这里 col 是值拷贝,不需要修改
  20. cout << col << " ";
  21. cout << "\n";
  22. // 为什么外层范围 for 必须用引用?
  23. // 如果不用引用:for (auto row : ia)
  24. // row 的类型会被推断为 int*(数组退化为指针),内层就无法遍历了!
  25. return 0;
  26. }
复制代码

https://godbolt.org/z/95WfPWffa
外层 for 必须用 & 的原因:

  1. for (auto row : ia) ← 错误!
  2. row 的类型被推断为 int*(数组名退化为指针)
  3. 内层 for (auto col : row) 就变成遍历一个指针,非法!
  4. for (auto &row : ia) ← 正确!
  5. row 的类型是 int(&)[4](对含4个int的数组的引用)
  6. 内层可以正常遍历
复制代码

6.3 多维数组与指针

  1. #include <iostream>
  2. using std::cout;
  3. using std::begin;
  4. using std::end;
  5. int main() {
  6. int ia[3][4] = {
  7. {0,1,2,3}, {4,5,6,7}, {8,9,10,11}
  8. };
  9. // ia 退化为指向第一行的指针,类型是 int(*)[4]
  10. int (*p)[4] = ia; // p 指向 ia[0](一个含4个int的数组)
  11. p = &ia[2]; // p 现在指向 ia[2]
  12. // 用 auto 简化(推荐)
  13. for (auto p = ia; p != ia + 3; ++p) { // p 是 int(*)[4]
  14. for (auto q = *p; q != *p + 4; ++q) // *p 是 int[4],退化为 int*
  15. cout << *q << " ";
  16. cout << "\n";
  17. }
  18. // 更简洁:用 begin/end
  19. for (auto p = begin(ia); p != end(ia); ++p) {
  20. for (auto q = begin(*p); q != end(*p); ++q)
  21. cout << *q << " ";
  22. cout << "\n";
  23. }
  24. // 类型别名简化
  25. using int_array = int[4]; // int_array 是"含4个int的数组"的别名
  26. // typedef int int_array[4]; // 等价的 typedef 写法
  27. for (int_array *p = ia; p != ia + 3; ++p) {
  28. for (int *q = *p; q != *p + 4; ++q)
  29. cout << *q << " ";
  30. cout << "\n";
  31. }
  32. return 0;
  33. }
复制代码

https://godbolt.org/z/x9do8a51f

多维数组遍历代码详解

完整代码

  1. #include <iostream>
  2. using std::cout;
  3. using std::begin;
  4. using std::end;
  5. int main() {
  6. int ia[3][4] = {
  7. {0,1,2,3}, {4,5,6,7}, {8,9,10,11}
  8. };
  9. // --- 方法一:auto 推断指针类型 ---
  10. int (*p)[4] = ia;
  11. p = &ia[2];
  12. // 这个auto 是int (*p)[4]
  13. for (auto p = ia; p != ia + 3; ++p) {
  14. for (auto q = *p; q != *p + 4; ++q)
  15. cout << *q << " ";
  16. cout << "\n";
  17. }
  18. // --- 方法二:begin / end 函数 ---
  19. for (auto p = begin(ia); p != end(ia); ++p) {
  20. for (auto q = begin(*p); q != end(*p); ++q)
  21. cout << *q << " ";
  22. cout << "\n";
  23. }
  24. // --- 方法三:类型别名 ---
  25. using int_array = int[4];
  26. // typedef int int_array[4]; // 等价写法
  27. for (int_array *p = ia; p != ia + 3; ++p) {
  28. for (int *q = *p; q != *p + 4; ++q)
  29. cout << *q << " ";
  30. cout << "\n";
  31. }
  32. return 0;
  33. }
复制代码

数组的内存结构

首先搞清楚 ia[3][4] 在内存里长什么样。
它是一个"含 3 个元素的数组",每个元素本身又是"含 4 个 int 的数组":

  1. col0 col1 col2 col3
  2. row0 [ 0 | 1 | 2 | 3 ] ia[0]
  3. row1 [ 4 | 5 | 6 | 7 ] ia[1]
  4. row2 [ 8 | 9 | 10 | 11 ] ia[2]
复制代码

在内存里是连续的一段:

  1. 地址: +0 +4 +8 +12 +16 +20 +24 +28 +32 +36 +40 +44
  2. 值: 0 1 2 3 4 5 6 7 8 9 10 11
  3. <------- ia[0] -------> <------- ia[1] -------> <------- ia[2] ------->
复制代码

每个 int 占 4 字节,每一行 4 个 int,共占 16 字节。

第一步:认识"指向数组的指针"

  1. int (*p)[4] = ia;
复制代码

这里有一个很容易混淆的声明,先分清两种写法:

  1. int *p[4] -- p 是数组,含 4 个 int 指针(指针的数组)
  2. int (*p)[4] -- p 是指针,指向"含 4 个 int 的数组"(数组的指针)
复制代码

括号改变了优先级,让 * 先和 p 结合,所以 (*p) 是指针,[4] 说明它指向的是有 4 个元素的数组,int 是元素类型。
ia 是二维数组,它退化(decay)为指向第一行的指针:

  1. ia --> &ia[0] --> 指向 ia[0] 这个长度为4的数组
  2. 类型:int (*)[4]
复制代码

所以 int (*p)[4] = ia 就是让 p 指向 ia[0]。

  1. p ---> [ 0 | 1 | 2 | 3 ] ia[0]
复制代码

下一行 p = &ia[2] 让 p 跳到第三行:

  1. p ---> [ 8 | 9 | 10 | 11 ] ia[2]
复制代码

第二步:方法一 —— auto 推断指针类型

  1. for (auto p = ia; p != ia + 3; ++p) {
  2. for (auto q = *p; q != *p + 4; ++q)
  3. cout << *q << " ";
  4. cout << "\n";
  5. }
复制代码

外层循环

  1. auto p = ia
复制代码

ia 退化为指向 ia[0] 的指针,auto 推断 p 的类型是 int (*)[4](指向含 4 个 int 的数组的指针)。

  1. ia + 3 --> 指向 ia[3],即第三行之后(不存在的位置,用作哨兵)
复制代码

循环条件 p != ia + 3:p 还没有越过最后一行就继续。
++p:p 向后移动一行(移动 16 字节,即 4 个 int 的大小)。
外层循环的执行过程:

  1. 第1次: p = ia -> p 指向 ia[0] -> 打印 0 1 2 3
  2. 第2次: p = ia + 1 -> p 指向 ia[1] -> 打印 4 5 6 7
  3. 第3次: p = ia + 2 -> p 指向 ia[2] -> 打印 8 9 10 11
  4. 第4次: p = ia + 3 -> p == ia+3,退出
复制代码

内层循环

  1. for (auto q = *p; q != *p + 4; ++q)
  2. cout << *q << " ";
复制代码

*p 解引用 p,得到 p 当前指向的那一行,类型是 int[4](一个含 4 个 int 的数组)。
数组在表达式中再次退化为指向首元素的指针,所以 auto q = *p 推断 q 的类型是 int*,指向当前行的第 0 个元素。
*p + 4:当前行首指针加 4,得到当前行末尾的下一个位置(哨兵)。
*q:解引用 q,得到 q 当前指向的 int 值。
以 p 指向 ia[1] 为例:

  1. *p = ia[1] = {4, 5, 6, 7}
  2. q 初始指向 ia[1][0](值 4)
  3. 第1次: *q = 4, ++q -> q 指向 ia[1][1]
  4. 第2次: *q = 5, ++q -> q 指向 ia[1][2]
  5. 第3次: *q = 6, ++q -> q 指向 ia[1][3]
  6. 第4次: *q = 7, ++q -> q = *p + 4,退出
复制代码

完整流程图

  1. 外层 p 遍历每一行 内层 q 遍历每个元素
  2. ----------- -----------
  3. p -> ia[0] q -> ia[0][0]=0 -> ia[0][1]=1 -> ia[0][2]=2 -> ia[0][3]=3
  4. p -> ia[1] q -> ia[1][0]=4 -> ia[1][1]=5 -> ia[1][2]=6 -> ia[1][3]=7
  5. p -> ia[2] q -> ia[2][0]=8 -> ia[2][1]=9 -> ia[2][2]=10 -> ia[2][3]=11
  6. p == ia+3 退出
复制代码

第三步:方法二 —— begin / end 函数

  1. for (auto p = begin(ia); p != end(ia); ++p) {
  2. for (auto q = begin(*p); q != end(*p); ++q)
  3. cout << *q << " ";
  4. cout << "\n";
  5. }
复制代码

begin(ia) 和 end(ia) 是标准库函数(需要 ,这里通过 间接引入)。
它们对数组做的事情和容器的 begin() / end() 成员函数一样:

  1. begin(ia) --> 指向 ia[0] 的指针(等价于 &ia[0],类型 int(*)[4])
  2. end(ia) --> 指向 ia[3] 的指针(ia[3] 不存在,用作哨兵)
复制代码

对内层:

  1. begin(*p) --> 指向当前行第0个元素(类型 int*)
  2. end(*p) --> 指向当前行末尾后一位(类型 int*)
复制代码

这种写法的优点:不需要手写 ia + 3 或 *p + 4,让编译器通过数组的类型信息自动算出边界,不容易出错。
与方法一的对比:

  1. 方法一 方法二
  2. ------ ------
  3. p = ia p = begin(ia)
  4. p != ia + 3 p != end(ia) <- 更安全,不需要手写3
  5. q = *p q = begin(*p)
  6. q != *p + 4 q != end(*p) <- 更安全,不需要手写4
复制代码

第四步:方法三 —— 类型别名

  1. using int_array = int[4];
  2. for (int_array *p = ia; p != ia + 3; ++p) {
  3. for (int *q = *p; q != *p + 4; ++q)
  4. cout << *q << " ";
  5. cout << "\n";
  6. }
复制代码

using int_array = int[4] 给类型"含 4 个 int 的数组"起了别名 int_array。
效果等价于:

  1. typedef int int_array[4]; // 旧式写法,含义相同
复制代码

有了别名,int (*p)[4] 就可以写成 int_array *p,意思完全相同,但更直观:

  1. int (*p)[4] -- 读起来绕
  2. int_array *p -- 读起来:p 是指向 int_array 的指针
复制代码

内层循环的 int *q = *p 直接声明了类型,不依赖 auto,更明确:

  1. *p 的类型是 int_array(即 int[4]),退化为 int*
  2. 所以 q 是 int*,指向当前行的第一个 int
复制代码

输出结果

三种方法输出结果完全一致,每种方法都打印两遍(程序里写了三种,各输出一次):

  1. 0 1 2 3
  2. 4 5 6 7
  3. 8 9 10 11
  4. 0 1 2 3
  5. 4 5 6 7
  6. 8 9 10 11
  7. 0 1 2 3
  8. 4 5 6 7
  9. 8 9 10 11
复制代码

三种方法横向对比


对比项方法一(auto)方法二(begin/end)方法三(类型别名)
外层指针类型自动推断 int(*)[4]自动推断 int(*)[4]显式 int_array*
内层指针类型自动推断 int*自动推断 int*显式 int*
边界写法手写 ia+3、*p+4begin/end 自动手写 ia+3、*p+4
出错风险手写数字容易错最安全手写数字容易错
可读性高(类型名直观)
推荐程度一般推荐推荐

常见误区

误区1:忘记括号导致含义完全不同

  1. int *p[4] -- 含 4 个 int* 的数组(指针数组)
  2. int (*p)[4] -- 指向 int[4] 的指针(数组指针)
复制代码

误区2:误以为 *p 是 int
当 p 的类型是 int(*)[4] 时,*p 的类型是 int[4](一整行),不是 int。
在表达式里 int[4] 再次退化为 int*,指向该行的第一个元素。
误区3:两个指针相加

  1. mid = (begin(ia) + end(ia)) / 2 -- 非法!两个指针不能相加
  2. mid = begin(ia) + (end(ia) - begin(ia)) / 2 -- 正确
复制代码

两指针相减得到距离(ptrdiff_t,有符号整数),指针加整数才能得到新指针。

综合对比:string / vector / 数组


特性stringvector数组
大小可变可变固定(编译时确定)
元素类型char任意任意
下标越界检查
拷贝/赋值支持支持不支持
迭代器支持支持用指针模拟
push_back不适用支持不支持
推荐程度优先使用优先使用低级,谨慎使用

关键概念速查


概念一句话理解
using 声明引入命名空间中的单个名字,避免每次写 std::
string可变长字符序列,支持 +、size()、[]、比较运算
cin >>读一个单词(跳过空白,遇空白停止)
getline读一整行(保留空格,丢弃换行符)
string::size_type无符号整数,用来存储字符串大小
vector可变大小的元素序列,用 push_back 添加
vector模板实例化,T 是元素类型
迭代器指向容器元素的"智能指针",begin()/end() 获取
const_iterator只读迭代器,通过 cbegin()/cend() 获取
difference_type两迭代器相减的结果类型(有符号)
数组固定大小,编译时确定,退化为指针
size_t用于数组下标的无符号整数类型(
ptrdiff_t两指针相减的结果类型(有符号,
begin(arr) / end(arr)获取数组首/尾后指针(
C 风格字符串null 结尾的 char 数组,危险,用 string 代替
c_str()string 转 C 风格字符串指针(只读)

C++ 第五章:语句 — 从零理解

目录

  1. 简单语句
  2. 语句作用域
  3. 条件语句:if 与 switch
  4. 循环语句
  5. 跳转语句
  6. try 块与异常处理

1. 简单语句

1.1 表达式语句

在表达式后面加分号,就变成了"表达式语句"。语句执行后结果被丢弃:

  1. ival + 5; // 合法但无意义:加法结果没人用
  2. cout << ival; // 有意义:有"打印"这个副作用
复制代码

1.2 空语句(Null Statement)

只有一个分号,什么都不做:

  1. ; // 空语句
复制代码

什么时候用? 当语法上需要一条语句,但逻辑上不需要做任何事时。最常见的场景是循环体的工作已经在条件里完成了:

  1. #include <iostream>
  2. #include <string>
  3. using std::cin;
  4. using std::string;
  5. int main() {
  6. string s, sought = "hello";
  7. // 读入数据直到找到 sought 为止
  8. // 所有工作都在 while 条件里完成了,循环体不需要做任何事
  9. while (cin >> s && s != sought)
  10. ; // 空语句——循环体故意为空,加注释说明是刻意的
  11. return 0;
  12. }
复制代码

危险:多余的分号

  1. // 灾难!while 的循环体是那个空语句,++iter 永远不在循环里
  2. while (iter != svec.end()) ; // 这个分号就是循环体!
  3. ++iter; // 这句话在循环外,只执行一次
复制代码

1.3 块(复合语句)

用花括号括起来的一组语句,整体算一条语句:

  1. // while 只允许一条语句作为循环体
  2. // 用花括号把两条语句变成一条"块"
  3. while (val <= 10) {
  4. sum += val;
  5. ++val;
  6. }
复制代码

关键点:

  • 块不以分号结尾(这点和普通语句不同)
  • 块内部定义的变量,出了块就不能用了(作用域)
  • 空块 {} 等价于空语句

2. 语句作用域

在 if、switch、while、for 的控制结构里定义的变量,只在该语句内部有效:

  1. #include <iostream>
  2. #include <vector>
  3. using std::vector;
  4. using std::cout;
  5. int main() {
  6. vector<int> v = {1, -2, 3, -4, 5};
  7. // i 在 while 条件里定义,只在 while 内有效
  8. // while (int i = get_num())
  9. // cout << i;
  10. // i = 0; // 错误!i 在 while 外面不存在
  11. // 如果循环结束后还需要用 beg,就要在外面定义
  12. auto beg = v.begin();
  13. while (beg != v.end() && *beg >= 0)
  14. ++beg;
  15. // beg 在这里仍然有效
  16. if (beg == v.end())
  17. cout << "所有元素都大于等于0\n";
  18. else
  19. cout << "第一个负数是:" << *beg << "\n";
  20. return 0;
  21. }
复制代码

规则: 控制结构内定义的变量必须初始化(因为它的值会立刻被该结构用到)。

3. 条件语句:if 与 switch

3.1 if 语句

  1. if (条件)
  2. 语句A
  3. else
  4. 语句B
复制代码

条件为真执行语句A,为假执行语句B(else 分支可省略)。

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. using std::cout;
  5. using std::string;
  6. using std::vector;
  7. int main() {
  8. const vector<string> scores = {"F", "D", "C", "B", "A", "A++"};
  9. int grade = 85;
  10. string lettergrade;
  11. // 基本 if-else:区分及格和不及格
  12. if (grade < 60)
  13. lettergrade = scores[0]; // 不及格:直接取 "F"
  14. else
  15. // (grade - 50) / 10:把分数段映射到下标
  16. // grade=60 -> (60-50)/10 = 1 -> "D"
  17. // grade=70 -> (70-50)/10 = 2 -> "C"
  18. // grade=100-> (100-50)/10= 5 -> "A++"
  19. lettergrade = scores[(grade - 50) / 10];
  20. // 嵌套 if:再根据个位数加 +/-
  21. if (grade < 60) {
  22. lettergrade = scores[0];
  23. } else {
  24. lettergrade = scores[(grade - 50) / 10];
  25. if (grade != 100) { // 100分已经是 A++,不需要再加
  26. if (grade % 10 > 7) // 个位数是 8 或 9
  27. lettergrade += '+';
  28. else if (grade % 10 < 3) // 个位数是 0、1 或 2
  29. lettergrade += '-';
  30. }
  31. }
  32. cout << grade << " 分 -> " << lettergrade << "\n";
  33. // 85 分 -> B+(85%10=5,不加符号,实际输出 B)
  34. // 改成 88 分 -> B+
  35. // 改成 61 分 -> D-
  36. return 0;
  37. }
复制代码

3.2 悬垂 else(Dangling else)

当 if 比 else 多时,else 和谁配对?
C++ 规则:else 总是和最近的、还没配对的 if 配对。

  1. // 看起来像这样(程序员的意图):
  2. if (grade % 10 >= 3)
  3. if (grade % 10 > 7)
  4. lettergrade += '+';
  5. else // 程序员以为这个 else 配外层 if
  6. lettergrade += '-';
  7. // 但实际上编译器理解为:
  8. if (grade % 10 >= 3) {
  9. if (grade % 10 > 7)
  10. lettergrade += '+';
  11. else // 这个 else 配内层 if!
  12. lettergrade += '-'; // 个位数 3~7 都会加 '-',这是 bug!
  13. }
复制代码

解决方法:用花括号强制指定配对关系

  1. if (grade % 10 >= 3) {
  2. if (grade % 10 > 7)
  3. lettergrade += '+';
  4. } // 花括号让内层 if 在这里结束
  5. else // 现在 else 只能配外层 if
  6. lettergrade += '-';
复制代码

悬垂 else 的配对规则可视化:

  1. if (A) <- 外层 if
  2. if (B) <- 内层 if
  3. X
  4. else <- else 配内层 if(C++ 规定)
  5. Y
复制代码

3.3 switch 语句

switch 适合从多个固定选项中选一个执行:

  1. #include <iostream>
  2. using std::cin;
  3. using std::cout;
  4. using std::endl;
  5. int main() {
  6. unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
  7. unsigned otherCnt = 0;
  8. char ch;
  9. while (cin >> ch) {
  10. switch (ch) {
  11. // 多个 case 共享同一段代码:统计所有元音
  12. case 'a': case 'A':
  13. ++aCnt;
  14. break; // break:跳出 switch,执行后面的代码
  15. case 'e': case 'E':
  16. ++eCnt;
  17. break;
  18. case 'i': case 'I':
  19. ++iCnt;
  20. break;
  21. case 'o': case 'O':
  22. ++oCnt;
  23. break;
  24. case 'u': case 'U':
  25. ++uCnt;
  26. break;
  27. default: // 没有任何 case 匹配时执行
  28. ++otherCnt;
  29. break;
  30. }
  31. }
  32. cout << "a: " << aCnt << "\n"
  33. << "e: " << eCnt << "\n"
  34. << "i: " << iCnt << "\n"
  35. << "o: " << oCnt << "\n"
  36. << "u: " << uCnt << "\n"
  37. << "其他: " << otherCnt << endl;
  38. return 0;
  39. }
复制代码

switch 的执行流程:

fall-through(穿透)是常见 bug:

  1. // 错误示例:忘记写 break
  2. switch (ch) {
  3. case 'a':
  4. ++aCnt; // 没有 break!
  5. case 'e':
  6. ++eCnt; // 没有 break!
  7. case 'i':
  8. ++iCnt; // 没有 break!
  9. // ...
  10. }
  11. // 如果 ch == 'a',则 aCnt、eCnt、iCnt 都会 +1!(连锁穿透)
复制代码

case 标签的规则:

  • 必须是整型常量表达式(不能是变量,不能是浮点数)
  • 不能有两个相同的 case 值
  • case 标签本身只是一个"跳转目标",不是独立的作用域
    switch 内定义变量的限制:
  1. switch (x) {
  2. case true:
  3. string s = "hello"; // 错误!如果跳到 false 分支,s 会被绕过但仍在作用域内
  4. int i = 0; // 错误!同上
  5. int j; // 可以:没有初始化,绕过不影响
  6. break;
  7. case false:
  8. j = 42; // 合法:j 在作用域内但未初始化
  9. // s 在作用域内但没被初始化——危险!
  10. }
  11. // 正确做法:用块限制变量的作用域
  12. switch (x) {
  13. case true: {
  14. string s = "hello"; // 合法:s 的作用域在块内
  15. break;
  16. }
  17. case false:
  18. // s 在这里不在作用域内,安全
  19. break;
  20. }
复制代码

switch 要点说明
case 标签必须是整型常量表达式
break跳出 switch,必须显式写出
fall-through没有 break 就继续执行下一个 case
default没有任何 case 匹配时执行
变量定义初始化变量不能跨越 case,需用块 {} 限制作用域

4. 循环语句

4.1 while 循环

  1. while (条件)
  2. 语句
复制代码

先检查条件,为真则执行语句,执行完再回来检查条件。

  1. #include <iostream>
  2. #include <vector>
  3. using std::cin;
  4. using std::cout;
  5. using std::vector;
  6. int main() {
  7. vector<int> v;
  8. int i;
  9. // 用途1:读取未知数量的数据
  10. while (cin >> i) // cin >> i 失败(EOF 或输入错误)时条件为 false
  11. v.push_back(i);
  12. // 用途2:需要在循环结束后使用控制变量
  13. auto beg = v.begin();
  14. while (beg != v.end() && *beg >= 0)
  15. ++beg;
  16. // 循环结束后 beg 仍然可用:要么 == end(),要么指向第一个负数
  17. if (beg == v.end())
  18. cout << "全是非负数\n";
  19. else
  20. cout << "第一个负数:" << *beg << "\n";
  21. return 0;
  22. }
复制代码

while 循环的执行流程:

  1. +-------+
  2. ---> | 检查条件 | --假--> 退出循环
  3. | +-------+
  4. | | 真
  5. | +--------+
  6. | | 执行循环体 |
  7. | +--------+
  8. | |
  9. +--------+
复制代码

注意: while 循环体内定义的变量,每次迭代都会被创建和销毁。

4.2 传统 for 循环

  1. for (初始化语句; 条件; 表达式)
  2. 语句
复制代码

执行顺序:
初始化(仅一次) → 条件 → 循环体 → 表达式 → 条件 → ⋯ \text{初始化(仅一次)} \to \text{条件} \to \text{循环体} \to \text{表达式} \to \text{条件} \to \cdots 初始化(仅一次)→条件→循环体→表达式→条件→⋯

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. #include <cctype>
  5. using std::cout;
  6. using std::string;
  7. using std::vector;
  8. using std::cin;
  9. int main() {
  10. string s = "Hello World";
  11. // 经典 for:把第一个单词大写
  12. for (decltype(s.size()) index = 0;
  13. index != s.size() && !isspace(s[index]);
  14. ++index)
  15. {
  16. s[index] = toupper(s[index]);
  17. }
  18. // index 在 for 外面不可访问
  19. cout << s << "\n"; // HELLO World
  20. // for 头部定义多个变量(必须同类型)
  21. vector<int> v = {1, 2, 3, 4, 5};
  22. // 在末尾追加当前所有元素的副本(翻倍)
  23. // sz 记录原始大小,防止循环条件随着 push_back 而变化
  24. for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i)
  25. v.push_back(v[i]);
  26. // v 现在是 {1,2,3,4,5,1,2,3,4,5}
  27. // 省略初始化:
  28. auto beg = v.begin();
  29. for ( ; beg != v.end() && *beg >= 0; ++beg)
  30. ; // 找第一个负数(本例没有)
  31. // 省略条件(等价于 while(true),循环体必须有退出机制)
  32. for (int i = 0; ; ++i) {
  33. if (i >= 5) break;
  34. cout << i << " ";
  35. }
  36. cout << "\n"; // 0 1 2 3 4
  37. // 省略表达式:
  38. for (int i; cin >> i; ) // 条件里读数据
  39. v.push_back(i);
  40. return 0;
  41. }
复制代码

for 循环四步骤详解(以实际例子为例):

  1. for (int i = 0; i < 5; ++i)
  2. cout << i;
  3. 步骤1: int i = 0 (只执行一次)
  4. 步骤2: 检查 i < 5 (true,继续)
  5. 步骤3: cout << i (执行循环体)
  6. 步骤4: ++i (更新 i)
  7. 回到步骤2...
  8. 当 i = 5 时,步骤2为 false,退出
复制代码

4.3 范围 for 循环

  1. for (声明 : 序列)
  2. 语句
复制代码

自动遍历序列中的每个元素,比传统 for 更简洁、不容易越界:

  1. #include <iostream>
  2. #include <vector>
  3. using std::cout;
  4. using std::vector;
  5. int main() {
  6. vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
  7. // 只读:auto 推断元素类型
  8. for (auto x : v)
  9. cout << x << " ";
  10. cout << "\n";
  11. // 读写:必须用引用 &,否则修改的是副本,原来的 v 不变
  12. for (auto &r : v)
  13. r *= 2; // 每个元素翻倍
  14. for (auto x : v)
  15. cout << x << " ";
  16. cout << "\n"; // 0 2 4 6 8 10 12 14 16 18
  17. // 范围 for 的等价传统 for 写法:
  18. // for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
  19. // auto &r = *beg;
  20. // r *= 2;
  21. // }
  22. // 注意:end() 的值被缓存了,所以循环体内不能 push_back!
  23. return 0;
  24. }
复制代码

为什么范围 for 内不能 push_back?
范围 for 在开始时缓存了 end() 的值。如果 push_back 导致 vector 重新分配内存,原来缓存的 end() 就失效了,行为未定义。

4.4 do-while 循环

  1. do
  2. 语句
  3. while (条件);
复制代码

先执行一次,再检查条件。保证循环体至少执行一次。注意 while 后面有分号!

  1. #include <iostream>
  2. #include <string>
  3. using std::cin;
  4. using std::cout;
  5. using std::string;
  6. int main() {
  7. string rsp; // 注意:条件里用到的变量必须在 do 外面定义!
  8. do {
  9. cout << "请输入两个数字:";
  10. int val1 = 0, val2 = 0;
  11. cin >> val1 >> val2;
  12. cout << val1 << " + " << val2 << " = " << (val1 + val2) << "\n";
  13. cout << "继续?(y/n): ";
  14. cin >> rsp;
  15. } while (!rsp.empty() && rsp[0] != 'n'); // 条件在这里,用了 rsp
  16. // ^ // do 外定义,这里才能用
  17. return 0;
  18. }
复制代码

三种循环对比:

循环检查条件时机最少执行次数典型使用场景
while先检查0 次读取未知数量的数据
for先检查0 次步进式遍历,已知次数
do-while后检查1 次至少做一次,如菜单选择

5. 跳转语句

5.1 break:跳出循环或 switch

break 终止最近一层的 while、do-while、for 或 switch,执行它后面的第一条语句。

  1. #include <iostream>
  2. #include <string>
  3. #include <vector>
  4. using std::cin;
  5. using std::cout;
  6. using std::string;
  7. using std::vector;
  8. int main() {
  9. string buf;
  10. vector<string> svec = {"hello", "-world end", "+test"};
  11. // break 只影响最近一层循环/switch
  12. for (auto &buf : svec) {
  13. switch (buf[0]) {
  14. case '-':
  15. for (auto it = buf.begin() + 1; it != buf.end(); ++it) {
  16. if (*it == ' ')
  17. break; // break #1:只跳出内层 for 循环
  18. cout << *it;
  19. }
  20. cout << "\n";
  21. break; // break #2:跳出 switch
  22. case '+':
  23. cout << "plus: " << buf.substr(1) << "\n";
  24. break; // break #3:跳出 switch
  25. }
  26. // break #2 和 #3 跳到这里
  27. }
  28. // break #1 跳到 for 循环的下一次迭代,不到这里
  29. return 0;
  30. }
复制代码

break 的作用范围示意:

  1. while (...) { <-- break #2 跳到这里之后(while 的下一次条件判断)
  2. switch (...) {
  3. case '-':
  4. for (...) { <-- break #1 跳到 for 之后
  5. if (...)
  6. break; // #1:只结束 for
  7. }
  8. break; // #2:结束 switch
  9. case '+':
  10. break; // #3:结束 switch
  11. }
  12. }
复制代码

5.2 continue:跳过本次迭代

continue 跳过当前迭代的剩余部分,直接开始下一次迭代。只能用在循环里(不能单独用在 switch 里,除非 switch 在循环里面)。

  1. #include <iostream>
  2. #include <string>
  3. using std::cin;
  4. using std::cout;
  5. using std::string;
  6. int main() {
  7. string buf;
  8. // 只处理以下划线开头的单词
  9. while (cin >> buf && !buf.empty()) {
  10. if (buf[0] != '_')
  11. continue; // 不以 '_' 开头:跳过,读下一个单词
  12. // 到这里说明 buf 以 '_' 开头
  13. cout << "处理:" << buf << "\n";
  14. }
  15. return 0;
  16. }
复制代码

break vs continue 的区别:

  1. while (condition) {
  2. if (some_check)
  3. break; // 完全退出 while
  4. if (other_check)
  5. continue; // 跳到 while 的条件判断,开始下一次迭代
  6. // 正常处理
  7. }
复制代码

5.3 goto:无条件跳转(不推荐使用)

goto 可以跳转到同一函数内的任意标签处。标签是"标识符 + 冒号"的形式:

  1. // goto 的典型危险:
  2. goto end;
  3. int ix = 10; // 错误!goto 跳过了 ix 的初始化,但 ix 还在作用域内
  4. end:
  5. ix = 42; // ix 未初始化就使用,行为未定义
  6. // 向后跳是允许的(会销毁并重新创建变量)
  7. begin:
  8. int sz = get_size();
  9. if (sz <= 0)
  10. goto begin; // 跳回 begin,sz 被销毁并重新初始化
复制代码

强烈建议不要使用 goto。它使程序流程难以理解和维护。上面的 goto begin 完全可以用循环替代:

  1. int sz;
  2. do { sz = get_size(); } while (sz <= 0);
复制代码

6. try 块与异常处理

6.1 什么是异常?

异常是程序运行时遇到的"意外情况":

  • 数据库连接中断
  • 用户输入非法数据
  • 内存不足
    异常处理让"发现问题的代码"和"处理问题的代码"分离。

6.2 三个组成部分

6.3 throw:抛出异常

  1. #include <iostream>
  2. #include <stdexcept> // runtime_error 定义在这里
  3. #include <string>
  4. using std::cin;
  5. using std::cout;
  6. using std::string;
  7. using std::runtime_error;
  8. struct Sales_item {
  9. string isbn() const { return bookNo; }
  10. string bookNo;
  11. };
  12. int main() {
  13. Sales_item item1, item2;
  14. // 假设已读入 item1 和 item2
  15. // 检查条件,不满足就抛出异常
  16. if (item1.isbn() != item2.isbn())
  17. throw runtime_error("Data must refer to same ISBN");
  18. // throw 之后的代码不会执行,控制权转移到最近的 catch
  19. cout << "ISBN 匹配,可以相加\n";
  20. return 0;
  21. }
复制代码

throw 后面跟的是一个异常对象,类型决定了哪个 catch 能捕获它。

6.4 try-catch:捕获异常

  1. #include <iostream>
  2. #include <stdexcept>
  3. #include <string>
  4. using std::cin;
  5. using std::cout;
  6. using std::endl;
  7. using std::string;
  8. using std::runtime_error;
  9. int main() {
  10. // 完整的 try-catch 示例:
  11. while (true) {
  12. try {
  13. // try 块:正常逻辑在这里
  14. // 如果出现问题,这里的代码会抛出异常
  15. int a, b;
  16. cout << "输入两个整数(除法):";
  17. cin >> a >> b;
  18. if (b == 0)
  19. throw runtime_error("除数不能为零"); // 抛出异常
  20. cout << a << " / " << b << " = " << (a / b) << "\n";
  21. break; // 正常完成,跳出 while
  22. } catch (runtime_error err) {
  23. // catch 块:处理 runtime_error 类型的异常
  24. // err.what() 返回初始化时传入的字符串
  25. cout << err.what() << "\n请重试?(y/n): ";
  26. char c;
  27. cin >> c;
  28. if (!cin || c == 'n')
  29. break; // 用户不想重试,退出
  30. // 如果用户回答 y,while 继续,再次执行 try 块
  31. }
  32. }
  33. return 0;
  34. }
复制代码

try-catch 的执行流程:

  1. try {
  2. 语句1;
  3. 语句2; <- 这里抛出异常
  4. 语句3; <- 不执行了
  5. }
  6. catch (ExceptionType e) {
  7. 处理异常; <- 控制权跳到这里
  8. }
  9. // catch 完成后,从这里继续
复制代码

如果没有匹配的 catch:

6.5 标准异常类


异常类头文件用途
exception最通用的异常基类,只说明"发生了异常"
runtime_error只能在运行时检测到的问题
range_error运行时:结果超出有意义的范围
overflow_error运行时:计算上溢
underflow_error运行时:计算下溢
logic_error程序逻辑错误
domain_error逻辑:参数值没有对应结果
invalid_argument逻辑:参数不合适
length_error逻辑:对象超过最大允许大小
out_of_range逻辑:使用了超出有效范围的值
bad_alloc内存分配失败
bad_castdynamic_cast 失败

使用规则:

  • exception、bad_alloc、bad_cast:只能默认初始化,不能传字符串
  • 其他类型:必须用 string 或 C 风格字符串初始化,不能默认初始化
  1. #include <stdexcept>
  2. #include <string>
  3. using std::runtime_error;
  4. using std::out_of_range;
  5. using std::string;
  6. // 完整示例:综合使用异常
  7. double safe_divide(double a, double b) {
  8. if (b == 0.0)
  9. throw runtime_error("除数为零"); // 必须提供字符串
  10. return a / b;
  11. }
  12. double safe_index(double arr[], int size, int i) {
  13. if (i < 0 || i >= size)
  14. throw out_of_range("下标越界"); // 必须提供字符串
  15. return arr[i];
  16. }
  17. int main() {
  18. try {
  19. double result = safe_divide(10.0, 0.0);
  20. } catch (runtime_error &e) {
  21. // e.what() 返回 "除数为零"
  22. // 接受引用避免拷贝,更高效
  23. }
  24. double arr[] = {1.0, 2.0, 3.0};
  25. try {
  26. double val = safe_index(arr, 3, 5); // 下标5越界
  27. } catch (out_of_range &e) {
  28. // e.what() 返回 "下标越界"
  29. }
  30. return 0;
  31. }
复制代码

综合完整示例

  1. // 综合运用本章所有知识点的完整程序
  2. #include <iostream>
  3. #include <vector>
  4. #include <string>
  5. #include <stdexcept>
  6. #include <cctype>
  7. using std::cin;
  8. using std::cout;
  9. using std::endl;
  10. using std::vector;
  11. using std::string;
  12. using std::runtime_error;
  13. // 统计文本中各类字符的数量
  14. int main() {
  15. unsigned vowelCnt = 0; // 元音
  16. unsigned digitCnt = 0; // 数字
  17. unsigned spaceCnt = 0; // 空白
  18. unsigned otherCnt = 0; // 其他
  19. char ch;
  20. string rsp;
  21. do {
  22. // do-while:至少执行一次,用于反复询问
  23. cout << "请输入一段文本(Ctrl+D/Z 结束):\n";
  24. // while:读取未知数量的字符
  25. while (cin.get(ch)) {
  26. // switch:根据字符类型分类统计
  27. switch (ch) {
  28. case 'a': case 'e': case 'i': case 'o': case 'u':
  29. case 'A': case 'E': case 'I': case 'O': case 'U':
  30. ++vowelCnt;
  31. break;
  32. case '0': case '1': case '2': case '3': case '4':
  33. case '5': case '6': case '7': case '8': case '9':
  34. ++digitCnt;
  35. break;
  36. case ' ': case '\t': case '\n':
  37. ++spaceCnt;
  38. break;
  39. default:
  40. ++otherCnt;
  41. break;
  42. }
  43. }
  44. // for:打印结果
  45. vector<string> labels = {"元音", "数字", "空白", "其他"};
  46. vector<unsigned> counts = {vowelCnt, digitCnt, spaceCnt, otherCnt};
  47. for (decltype(labels.size()) i = 0; i < labels.size(); ++i)
  48. cout << labels[i] << ": " << counts[i] << "\n";
  49. cout << "继续?(y/n): ";
  50. cin.clear(); // 清除 EOF 标志
  51. cin >> rsp;
  52. } while (!rsp.empty() && rsp[0] != 'n');
  53. return 0;
  54. }
复制代码

各语句速查表


语句语法要点
表达式语句expr;结果被丢弃,利用副作用
空语句;需要语句但无事可做
{ ... }不以分号结尾
ifif (c) s1 else s2else 配最近未匹配的 if
switchswitch (e) { case v: ... }需要 break,标签必须是整型常量
whilewhile (c) s先判断再执行,0次或多次
forfor (init; c; expr) s三部分都可省略
范围 forfor (d : seq) s循环体内不能改变序列大小
do-whiledo s while (c);先执行再判断,至少1次
breakbreak;跳出最近的循环或 switch
continuecontinue;跳过本次迭代,继续下一次
gotogoto label;不推荐使用
throwthrow expr;抛出异常,终止当前执行路径
try-catchtry { } catch (T e) { }捕获并处理指定类型的异常

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

中国红客联盟公众号

联系站长QQ:5520533

admin@chnhonker.com
Copyright © 2001-2026 Discuz Team. Powered by Discuz! X3.5 ( 粤ICP备13060014号 )|天天打卡 本站已运行