Getting Started —— 从零开始理解 C++
目录
- 1.1 写一个最简单的 C++ 程序
- 1.2 第一次认识输入输出
- 1.3 注释是什么
- 1.4 控制流:让程序会"转弯"
- 1.5 初识类(Class)
- 1.6 书店综合程序
- 常用运算符速查表
1.1 写一个最简单的 C++ 程序
什么是函数?什么是 main?
每一个 C++ 程序都由一个或多个**函数(function)**组成。函数就像是一台机器:给它一些输入,它执行一些动作,然后返回一个结果。
操作系统(Windows/Linux/macOS)启动你的程序时,会自动去找一个名字叫 main 的函数,并从那里开始执行。所以 main 是程序的入口,有且只能有一个。
最简单的程序
- // 这是一个什么都不做、只是返回0的程序
- // int 表示返回类型(整数)
- // main 是函数名
- // () 是参数列表(这里为空,表示不需要任何参数)
- int main()
- {
- // { } 是函数体,所有要执行的代码写在里面
- return 0; // return 语句:结束函数,并向操作系统返回值 0
- // 返回 0 通常表示"程序成功运行"
- // 注意:每条语句必须以分号 ; 结尾!
- }
复制代码
https://godbolt.org/z/K3GGxPTdP
函数的四个组成部分
- int main () { return 0; }
- ^ ^ ^ ^
- 返回类型 函数名 参数列表 函数体
- (结果是整数) (叫main) (这里为空) (要执行的代码)
复制代码
返回值的含义
| 返回值 | 含义 |
|---|
| 0 | 成功 |
| 非 0(如 -1, 1) | 失败,具体含义由操作系统定义 |
怎么编译和运行?
写完代码保存为 prog1.cpp,然后在终端运行:
- # GCC 编译器(Linux/macOS)
- $ g++ -o prog1 prog1.cpp # 编译,生成可执行文件 prog1
- $ ./prog1 # 运行
- # 查看程序返回值(Linux/macOS)
- $ echo $?
- # Windows(Visual Studio 命令行)
- > cl /EHsc prog1.cpp # 编译,生成 prog1.exe
- > prog1 # 运行
- > echo %ERRORLEVEL% # 查看返回值
复制代码
建议使用 -Wall 开启所有警告:g++ -Wall -o prog1 prog1.cpp
1.2 第一次认识输入输出
C++ 的 IO 不是语言内置的
C++ 本身没有内置输入/输出命令。取而代之的是,标准库提供了 iostream 库。
使用前必须在文件开头声明:
- #include <iostream> // 告诉编译器:我要用 iostream 库
复制代码
四个标准 IO 对象
- cin → 标准输入(键盘) 读取用户输入
- cout → 标准输出(屏幕) 打印正常输出
- cerr → 标准错误(屏幕) 打印错误信息(不缓冲)
- clog → 标准日志(屏幕) 打印日志信息(缓冲)
复制代码
它们都属于命名空间 std,使用时要写 std::cout(:: 是作用域运算符)。
完整的两数求和程序
- #include <iostream> // 引入输入输出库
- int main()
- {
- // << 是输出运算符,把右边的值写入左边的输出流
- // std::endl 是操纵符:换行 + 冲刷缓冲区
- // 冲刷缓冲区:确保内容立刻显示在屏幕上,不在内存中等待
- std::cout << "Enter two numbers:" << std::endl;
- // 定义两个整型变量,初始化为 0
- // int 是内置类型,表示整数
- // 变量:有名字的存储空间,用来保存数据
- int v1 = 0, v2 = 0;
- // >> 是输入运算符,从左边的输入流读取数据,存入右边的变量
- // std::cin >> v1 >> v2 等价于:
- // std::cin >> v1; 先读第一个数存入 v1
- // std::cin >> v2; 再读第二个数存入 v2
- std::cin >> v1 >> v2;
- // 链式输出:每个 << 返回 std::cout 本身,所以可以连续写
- // v1 + v2 是表达式,会被自动计算成结果
- std::cout << "The sum of " << v1 << " and " << v2
- << " is " << v1 + v2 << std::endl;
- return 0;
- }
复制代码
https://godbolt.org/z/4Gq7arYr5
输出运算符的链式调用原理
- std::cout << "Enter two numbers:" << std::endl;
- 等价于:
- (std::cout << "Enter two numbers:") << std::endl;
- ^ ^
- 第一步:把字符串写入 cout,返回 cout 第二步:把 endl 写入 cout
复制代码
每个 << 运算符的返回值是其左操作数(即 std::cout),所以可以一直链下去。
endl 的作用
- std::endl = 换行符 '\n' + 冲刷缓冲区
- 为什么要冲刷缓冲区?
- - 程序崩溃时,缓冲区中还未输出的内容会丢失
- - 调试时加的 cout 语句,必须用 endl 或 std::flush 确保内容实际显示出来
复制代码
命名空间 std
所有标准库中的名字(cout, cin, endl 等)都在 std 命名空间里,必须用 std:: 前缀访问,防止与你自己定义的同名变量冲突。
1.3 注释是什么
注释是写给人看的说明文字,编译器完全忽略它。
两种注释格式
- // 这是单行注释:从 // 开始到本行末尾都是注释
- /*
- * 这是多行注释(块注释)
- * 从 /* 开始,到 */ 结束
- * 中间可以跨越多行
- * 注意:块注释不能嵌套!
- */
- int main()
- {
- // 好的注释:解释"为什么",而不是"做什么"
- // (代码本身已经说明了做什么)
- int x = 5; // 行尾注释:简短说明
- /*
- * 错误示范 —— 块注释不能嵌套:
- * /* 这里又开了一个注释 */ ← 这个 */ 会提前结束外层注释!
- */
- return 0;
- }
复制代码
https://godbolt.org/z/fj4xoos3c
注释嵌套的陷阱
- /* 外层注释开始
- /* 内层注释 */ ← 编译器在这里就认为注释结束了!
- 后面这段文字就变成了代码,导致编译错误
- */
复制代码
解决方法:注释掉一大段代码时,用单行注释 //:
- // /* 这样就安全了
- // * 整段都是单行注释
- // * 不存在嵌套问题
- // */
复制代码
1.4 控制流:让程序会"转弯"
正常情况下,程序从上到下顺序执行。控制流语句让程序能重复或选择执行某些代码。
1.4.1 while 循环
用途:只要条件为真,就反复执行一段代码。
语法结构:
求 1 到 10 的和:
- #include <iostream>
- int main()
- {
- int sum = 0; // 累加结果,初始为 0
- int val = 1; // 当前要加的数,从 1 开始
- // while 循环:每次检查条件 val <= 10
- // 条件为真(true)则执行循环体;条件为假(false)则退出循环
- while (val <= 10) // <= 是"小于等于"运算符
- {
- sum += val; // 等价于 sum = sum + val,把 val 累加到 sum
- ++val; // 前置递增运算符:val = val + 1,准备下一个数
- }
- // 循环结束后,val = 11(不满足 val <= 10),sum = 55
- std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
- return 0;
- }
复制代码
https://godbolt.org/z/jd6z7x1er
while 执行流程:
手动追踪前几次循环:
- 初始:sum=0, val=1
- 第1次:sum=0+1=1, val=2
- 第2次:sum=1+2=3, val=3
- 第3次:sum=3+3=6, val=4
- ...
- 第10次:sum=45+10=55, val=11
- 条件 11<=10 为假,退出循环
复制代码
1.4.2 for 循环
用途:当循环次数固定、或有明确的"初始化→条件→更新"模式时,for 比 while 更简洁。
语法结构:
同样求 1 到 10 的和:
- #include <iostream>
- int main()
- {
- int sum = 0;
- // for 循环三个部分:
- // int val = 1 → 初始化:创建 val 并赋值 1(只执行一次)
- // val <= 10 → 条件:每次循环前检查
- // ++val → 表达式:每次循环体执行完后运行
- for (int val = 1; val <= 10; ++val)
- {
- sum += val; // 循环体
- }
- // 注意:val 只在 for 循环内部存在,循环结束后无法使用
- std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl;
- return 0;
- }
复制代码
for 的执行顺序:
https://godbolt.org/z/jGsvKT9js
while vs for 对比
- while 版本: for 版本:
- int val = 1; for (int val = 1; val <= 10; ++val)
- while (val <= 10) { sum += val;
- sum += val;
- ++val;
- }
- - while:val 在循环外声明,循环后仍可用
- - for:val 在循环头声明,循环后不可用(作用域更小,更安全)
- - 两者功能完全等价,选哪个取决于代码风格和可读性
复制代码
1.4.3 读取未知数量的输入
有时不知道用户会输入多少个数,需要持续读取直到没有数据:
- #include <iostream>
- int main()
- {
- int sum = 0, value = 0;
- // 把 std::cin >> value 放在 while 条件里:
- // - 成功读取一个整数 → 条件为真,继续循环
- // - 遇到文件末尾(EOF)或非法输入 → 条件为假,退出循环
- while (std::cin >> value)
- {
- sum += value; // 把读到的值累加
- }
- std::cout << "Sum is: " << sum << std::endl;
- return 0;
- }
复制代码
https://godbolt.org/z/cMasn3j7n
如何输入"文件末尾"(EOF)?
- Windows:按 Ctrl + Z,然后回车
- Linux / macOS:按 Ctrl + D
复制代码
为什么 std::cin >> value 可以作为条件?
>> 运算符返回其左操作数(std::cin)。istream 对象(即 std::cin)在以下情况下变为"无效状态",在条件判断中被视为 false:
- 遇到文件末尾
- 读取了不合法的数据(比如期望读整数但输入了字母)
1.4.4 if 语句
用途:根据条件选择执行不同代码。
统计连续重复数字出现次数:
- #include <iostream>
- int main()
- {
- int currVal = 0; // 当前正在统计的数值
- int val = 0; // 每次新读入的数值
- // 外层 if:先读第一个数,如果读成功才继续
- // (处理输入为空的情况)
- if (std::cin >> currVal)
- {
- int cnt = 1; // currVal 已经出现了 1 次
- // 持续读取后续数字
- while (std::cin >> val)
- {
- if (val == currVal) // == 是等于运算符(注意不是赋值的 =)
- {
- ++cnt; // 相同,计数+1
- }
- else
- {
- // 不同:打印上一个数的统计结果
- std::cout << currVal << " occurs "
- << cnt << " times" << std::endl;
- currVal = val; // 切换到新的数
- cnt = 1; // 重置计数为 1
- }
- }
- // 循环结束后,最后一个数还没打印,在这里补上
- std::cout << currVal << " occurs "
- << cnt << " times" << std::endl;
- }
- // 如果一开始就没有输入,外层 if 的条件为假,直接跳到这里
- return 0;
- }
复制代码
https://godbolt.org/z/cr8b1jMMr
执行流程演示(输入 42 42 55 55 62):
- 读入 currVal = 42, cnt = 1
- 循环1:val=42,42==42,cnt=2
- 循环2:val=55,55!=42,输出"42 occurs 2 times",currVal=55,cnt=1
- 循环3:val=55,55==55,cnt=2
- 循环4:val=62,62!=55,输出"55 occurs 2 times",currVal=62,cnt=1
- EOF:退出循环
- 补充输出:"62 occurs 1 times"
复制代码
常见错误提醒:
- // 赋值 vs 等于的混淆!
- if (val = currVal) // 错误!这是赋值,不是比较!
- if (val == currVal) // 正确!这是判断是否相等
复制代码
1.5 初识类(Class)
原始源码已经从搜索结果中拼出来了,下面结合官方实现逐行解析:
Sales_item 类用到了运算符重载、友元函数、构造函数、成员函数这几个核心 C++ 特性。我们从零开始逐步拆解,最后给出完整可运行的代码。
整体结构一览
完整实现(含从零理解的注释)
- // ============================================================
- // Sales_item.h
- // C++ Primer 第5版教学用头文件(加详细中文注释)
- // ============================================================
- // 头文件卫士:防止同一个头文件被 #include 两次
- // 第一次包含时,SALESITEM_H 未定义 → 正常编译
- // 第二次包含时,SALESITEM_H 已定义 → 跳过整个文件
- #ifndef SALESITEM_H
- #define SALESITEM_H
- #include <iostream> // std::istream, std::ostream
- #include <string> // std::string
- // ============================================================
- // 类定义
- // ============================================================
- class Sales_item
- {
- // ---- 友元声明 ----
- // friend(友元):允许这些"外部函数"访问类的 private 成员
- // 它们不是成员函数,但可以直接读写 bookNo、units_sold、revenue
- friend std::istream& operator>>(std::istream&, Sales_item&);
- friend std::ostream& operator<<(std::ostream&, const Sales_item&);
- friend bool operator==(const Sales_item&, const Sales_item&);
- friend bool operator< (const Sales_item&, const Sales_item&);
- public:
- // ============================================================
- // 构造函数(Constructors)
- // 作用:创建对象时自动初始化数据成员
- // ============================================================
- // 1. 默认构造函数
- // Sales_item item; ← 这样声明时调用
- // = default 让编译器自动生成,等价于手写
- // Sales_item() : units_sold(0), revenue(0.0) {}
- Sales_item() = default;
- // 2. 从 string 构造
- // Sales_item item("0-201-70353-X");
- // 冒号后面是"初始化列表",直接初始化成员,比在函数体内赋值更高效
- Sales_item(const std::string& book)
- : bookNo(book), units_sold(0), revenue(0.0)
- {
- // bookNo = book, units_sold = 0, revenue = 0.0
- // 函数体为空,初始化已经在列表里完成了
- }
- // 3. 从输入流构造
- // Sales_item item(std::cin);
- // is >> *this 的意思:用 >> 运算符从 is 读取数据,存入"当前对象自己"
- // *this 是指向当前对象的指针解引用,即"这个对象本身"
- Sales_item(std::istream& is)
- {
- is >> *this; // 调用下面定义的 operator>>
- }
- public:
- // ============================================================
- // 公开成员函数(Public Member Functions)
- // ============================================================
- // isbn():返回书号
- // const 表示这个函数不会修改对象的任何数据成员(只读操作)
- std::string isbn() const { return bookNo; }
- // avg_price():计算平均售价 = 总收入 ÷ 销售册数
- // 要防止除以零的情况
- double avg_price() const
- {
- if (units_sold) // 如果 units_sold != 0
- return revenue / units_sold;
- else
- return 0; // 没有销售记录,返回 0
- }
- // operator+=:把另一条销售记录累加到本对象
- // 用法:total += trans; 等价于 total.operator+=(trans)
- // 返回 *this(即返回当前对象的引用),支持链式调用
- Sales_item& operator+=(const Sales_item& rhs)
- {
- units_sold += rhs.units_sold; // 册数相加
- revenue += rhs.revenue; // 收入相加
- return *this; // 返回自身引用
- // 注意:不修改 bookNo(ISBN 必须相同才能相加)
- }
- private:
- // ============================================================
- // 私有数据成员(Private Data Members)
- // 类外部不能直接访问,只能通过公开的成员函数或友元函数操作
- // ============================================================
- std::string bookNo; // ISBN 书号,如 "0-201-70353-X"
- unsigned units_sold = 0; // 销售册数(无符号整数,不能为负)
- double revenue = 0.0; // 总收入(单价 × 册数)
- };
- // ============================================================
- // 友元函数:operator+
- // 把两个 Sales_item 相加,返回一个新对象(不修改原来的两个对象)
- // 注意:这里不是成员函数,写在类定义外面
- // ============================================================
- inline Sales_item operator+(const Sales_item& lhs, const Sales_item& rhs)
- {
- Sales_item ret(lhs); // 用 lhs 拷贝初始化 ret(ret 是 lhs 的副本)
- ret += rhs; // 调用 operator+=,把 rhs 累加进 ret
- return ret; // 返回累加结果(新对象)
- }
- // ============================================================
- // 友元函数:operator>>(输入运算符)
- // 用法:std::cin >> item;
- // 从输入流读取:ISBN 册数 单价
- // 例如:0-201-70353-X 4 24.99
- // ============================================================
- inline std::istream& operator>>(std::istream& in, Sales_item& s)
- {
- double price = 0; // 单价(临时变量,revenue = units_sold * price)
- // 依次读取:书号、册数、单价
- in >> s.bookNo >> s.units_sold >> price;
- if (in) // 读取成功
- s.revenue = s.units_sold * price; // 计算总收入
- else
- s = Sales_item(); // 读取失败:重置为默认空对象
- return in; // 返回输入流,支持链式 cin >> a >> b
- }
- // ============================================================
- // 友元函数:operator<<(输出运算符)
- // 用法:std::cout << item;
- // 输出格式:ISBN 册数 总收入 均价
- // 例如:0-201-70353-X 4 99.96 24.99
- // ============================================================
- inline std::ostream& operator<<(std::ostream& out, const Sales_item& s)
- {
- out << s.isbn() << " " // 书号
- << s.units_sold << " " // 册数
- << s.revenue << " " // 总收入
- << s.avg_price(); // 均价(调用成员函数)
- return out; // 返回输出流,支持链式 cout << a << b
- }
- // ============================================================
- // 友元函数:operator==(相等运算符)
- // 两条记录 ISBN 相同 且 册数相同 且 收入相同 → 认为相等
- // ============================================================
- inline bool operator==(const Sales_item& lhs, const Sales_item& rhs)
- {
- return lhs.units_sold == rhs.units_sold
- && lhs.revenue == rhs.revenue
- && lhs.isbn() == rhs.isbn();
- }
- // operator!=(不等):直接用 == 取反
- inline bool operator!=(const Sales_item& lhs, const Sales_item& rhs)
- {
- return !(lhs == rhs);
- }
- // operator<:按 ISBN 字典序比较(用于排序)
- inline bool operator<(const Sales_item& lhs, const Sales_item& rhs)
- {
- return lhs.isbn() < rhs.isbn();
- }
- #endif // SALESITEM_H
复制代码
关键概念逐一解释
1. 头文件卫士(include guard)
- #ifndef SALESITEM_H ← 如果 SALESITEM_H 没有被定义过
- #define SALESITEM_H ← 现在定义它
- ... 类的全部内容 ...
- #endif ← 结束
复制代码
没有它,多个源文件都 #include "Sales_item.h" 时会导致类被定义两次,编译报错。
2. 初始化列表
- Sales_item(const std::string& book)
- : bookNo(book), units_sold(0), revenue(0.0)
- {}
复制代码
冒号 : 后面是初始化列表,格式是 成员名(初始值)。比在函数体里写 bookNo = book 效率更高,因为直接在构造时初始化,而不是先默认初始化再赋值。
3. 友元函数 vs 成员函数
- 成员函数: item.isbn() → 必须通过对象来调用
- 友元函数: operator>>(cin, item) → 普通函数,但可以访问 private 成员
复制代码
>> 和 << 之所以设计成友元而不是成员,是因为它们的左操作数是 cin/cout(istream/ostream类型),不是 Sales_item 对象。
4. *this 的含义
- Sales_item(std::istream& is)
- {
- is >> *this; // this 是指向当前对象的指针
- } // *
- ### 类是什么?
- **类(class)**是 C++ 中定义自己的数据类型的方式。类把"数据"和"对数据的操作"打包在一起。
- > 类比:`int` 是语言内置的整数类型,你可以定义自己的"书籍销售记录"类型。
- ### Sales_item 类
- 教材提供了一个 `Sales_item` 类,用来表示一本书的销售记录,包含:
- - ISBN(书的唯一编号)
- - 销售册数
- - 总收入
- - 平均售价
- 使用前需要包含其头文件:
- ```cpp
- #include "Sales_item.h" // 自定义头文件用双引号
- #include <iostream> // 标准库头文件用尖括号
复制代码
读写 Sales_item 对象
- #include <iostream>
- #include "Sales_item.h" // 包含 Sales_item 类的定义
- int main()
- {
- Sales_item book; // 定义一个 Sales_item 类型的对象,名字叫 book
- // >> 运算符:从标准输入读取一条记录存入 book
- // 输入格式:ISBN 销售册数 单价
- // 例如:0-201-70353-X 4 24.99
- std::cin >> book;
- // << 运算符:把 book 的信息打印到标准输出
- // 输出格式:ISBN 销售册数 总收入 平均售价
- // 例如:0-201-70353-X 4 99.96 24.99
- std::cout << book << std::endl;
- return 0;
- }
复制代码
https://godbolt.org/z/s8zcGj7s9
两个 Sales_item 相加
- #include <iostream>
- #include "Sales_item.h"
- int main()
- {
- Sales_item item1, item2; // 定义两个销售记录对象
- // 分别从输入读取两条记录
- std::cin >> item1 >> item2;
- // + 运算符:把两条记录合并(ISBN 必须相同)
- // 结果:ISBN相同,册数相加,收入相加
- std::cout << item1 + item2 << std::endl;
- return 0;
- }
复制代码
https://godbolt.org/z/a7q88a8Ka
示例输入与输出:
- 输入:
- 0-201-78345-X 3 20.00
- 0-201-78345-X 2 25.00
- 输出:
- 0-201-78345-X 5 110 22
- ^ ^
- 共5册 平均22元
复制代码
1.5.2 成员函数(Member Function)
成员函数是属于某个类的函数,用点运算符 . 来调用:
- 对象名.成员函数名(参数)
- item1.isbn()
- ^ ^
- 对象 函数名(这里无参数)
复制代码
检查两本书是否相同 ISBN 再相加:
- #include <iostream>
- #include "Sales_item.h"
- int main()
- {
- Sales_item item1, item2;
- std::cin >> item1 >> item2;
- // item1.isbn() 调用 item1 的 isbn 成员函数
- // 返回 item1 中存储的 ISBN 字符串
- // == 运算符比较两个 ISBN 是否相同
- if (item1.isbn() == item2.isbn())
- {
- std::cout << item1 + item2 << std::endl;
- return 0; // 成功
- }
- else
- {
- // cerr:标准错误输出,用于错误信息,不缓冲,立即显示
- std::cerr << "Data must refer to same ISBN" << std::endl;
- return -1; // 失败,返回非0
- }
- }
复制代码
https://godbolt.org/z/fWd1M87cr
1.6 书店综合程序
把前面学的所有内容综合起来,解决最初的问题:读取同一 ISBN 的所有销售记录,汇总后打印。
- #include <iostream>
- #include "Sales_item.h"
- int main()
- {
- // total:保存当前 ISBN 的累加结果
- Sales_item total;
- // 先读第一条记录:
- // 成功 → 执行大括号内的代码
- // 失败(没有输入)→ 执行 else 分支
- if (std::cin >> total)
- {
- Sales_item trans; // trans:每次读入的新记录
- // 持续读取剩余记录
- while (std::cin >> trans)
- {
- // 判断新记录和累计记录是否同一本书
- if (total.isbn() == trans.isbn())
- {
- total += trans; // 同一本书:累加到 total
- // += 等价于 total = total + trans
- }
- else
- {
- // 换了一本书:
- // 1. 打印上一本书的汇总
- std::cout << total << std::endl;
- // 2. 把 total 重置为新书的第一条记录
- total = trans;
- }
- }
- // 循环结束,最后一本书的汇总还没打印
- std::cout << total << std::endl;
- }
- else
- {
- // 一条记录都没有
- std::cerr << "No data?!" << std::endl;
- return -1;
- }
- return 0;
- }
复制代码
https://godbolt.org/z/6cfPaxdv4
程序逻辑流程图:
示例输入:
- 0-201-70353-X 4 24.99
- 0-201-70353-X 2 19.99
- 0-201-82470-1 3 15.00
- 0-201-82470-1 1 15.00
复制代码
示例输出:
- 0-201-70353-X 6 149.94 24.99
- 0-201-82470-1 4 60.00 15.00
复制代码
常用运算符速查表
| 运算符 | 名称 | 示例 | 含义 |
|---|
| = | 赋值 | x = 5 | 把 5 存入 x |
| == | 等于 | x == 5 | 判断 x 是否等于 5 |
| != | 不等于 | x != 5 | 判断 x 是否不等于 5 |
| < | 小于 | x < 5 | x 小于 5 |
| <= | 小于等于 | x <= 5 | x 小于或等于 5 |
| > | 大于 | x > 5 | x 大于 5 |
| >= | 大于等于 | x >= 5 | x 大于或等于 5 |
| ++ | 前置递增 | ++x | x = x + 1 |
| -- | 前置递减 | --x | x = x - 1 |
| += | 复合赋值 | x += 3 | x = 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++ 第二章:变量与基本类型 — 从零理解
目录
- 基本内置类型
- 变量
- 复合类型:引用与指针
- const 限定符
- 处理类型:auto 与 decltype
- 自定义数据结构
1. 基本内置类型
1.1 什么是类型?
类型告诉计算机两件事:
- 这块内存存的是什么(整数?小数?字符?)
- 可以对它做哪些操作(加减?比较?)
就像现实中,“3个苹果 + 2个苹果 = 5个苹果"和"3元钱 + 2元钱 = 5元钱”,用的是同一个"+“,但意义完全不同。C++ 的类型系统就是在管理这些"意义”。
1.2 算术类型一览
| 类型 | 含义 | 最小位数 | 举例 |
|---|
| bool | 布尔值(真/假) | — | true / false |
| char | 字符 | 8位 | 'A' |
| wchar_t | 宽字符 | 16位 | 支持国际字符 |
| char16_t | Unicode 16位字符 | 16位 | UTF-16 |
| char32_t | Unicode 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个比特组成一个字节,每个字节有一个地址:
- 地址 内容(8位)
- 736424 0 0 1 1 1 0 1 1
- 736425 0 0 0 1 1 0 1 1
- 736426 0 1 1 1 0 0 0 1
- 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 类型转换
当把一种类型的值赋给另一种类型时,会自动转换:
- #include <iostream>
- int main() {
- bool b = 42; // 非零值 -> true
- int i = b; // true -> 1
- i = 3.14; // 3.14 -> 3(小数部分截断)
- double pi = i; // 3 -> 3.0(整数转浮点)
- unsigned char c = -1; // -1 对 256 取模 -> 255
- // signed char c2 = 256; // 未定义行为!不要这样写
- std::cout << "b=" << b << "\n"; // 1
- std::cout << "i=" << i << "\n"; // 3
- std::cout << "pi=" << pi << "\n"; // 3
- std::cout << "c=" << (int)c << "\n"; // 255
- return 0;
- }
复制代码
https://godbolt.org/z/5fYTao6j8
转换规则记忆卡:
- 非bool -> bool: 0变false,其他变true
- bool -> 数字: true变1,false变0
- 浮点 -> 整数: 直接砍掉小数部分(不是四舍五入!)
- 整数 -> 浮点: 小数部分为0,可能损失精度
- 超出范围 -> 无符号: 对"类型能表示的个数"取余数
- 超出范围 -> 有符号: 未定义行为!(结果不可预测)
复制代码
1.5 无符号类型的陷阱
混合有符号和无符号运算时,int 会被强制转换为 unsigned!
- #include <iostream>
- int main() {
- unsigned u = 10;
- int i = -42;
- // -42 被转换为无符号,结果是一个很大的数
- std::cout << u + i << std::endl;
- // 假设 int 是 32 位: 10 + (2^32 - 42) = 4294967264
- // 危险的循环:u 永远不会小于 0!
- // for (unsigned u = 10; u >= 0; --u) // 死循环!
- // 正确写法:
- unsigned u2 = 11;
- while (u2 > 0) {
- --u2;
- std::cout << u2 << "\n";
- }
- return 0;
- }
复制代码
https://godbolt.org/z/P84asEern
1.6 字面量(Literals)
字面量就是"写死在代码里的值":
- #include <iostream>
- int main() {
- // 整数字面量
- int a = 20; // 十进制
- int b = 024; // 八进制(以0开头),值 = 20
- int c = 0x14; // 十六进制(以0x开头),值 = 20
- // 浮点字面量(默认 double)
- double d = 3.14159;
- double e = 3.14159E0; // 科学计数法: 3.14159 × 10^0
- float f = 1E-3F; // 单精度,值 = 0.001
- // 字符和字符串
- char ch = 'a'; // 单个字符,单引号
- const char* s = "Hello World!"; // 字符串,双引号
- // 转义序列
- std::cout << "换行:\n";
- std::cout << "制表符:\t结束\n";
- std::cout << "反斜杠:\\\n";
- std::cout << "Hi \x4dO\115!\n"; // Hi MOM!(用十六进制和八进制表示字符)
- // 带后缀的字面量
- unsigned long long x = 42ULL; // ULL后缀 = unsigned long long
- long double y = 3.14159L; // L后缀 = long double
- // 布尔字面量
- bool t = true;
- bool fa = false;
- // 指针字面量
- int* p = nullptr; // 空指针
- return 0;
- }
复制代码
https://godbolt.org/z/1T5zoea5b
常用转义序列:
| 转义 | 含义 |
|---|
| \n | 换行 |
| \t | 水平制表符(Tab) |
| \\ | 反斜杠本身 |
| \' | 单引号 |
| \" | 双引号 |
| \0 | 空字符(null) |
| \r | 回车 |
| \a | 响铃 |
2. 变量
2.1 什么是变量?
变量 = 有名字的内存区域 + 类型
类型决定了:
2.2 变量定义与初始化
- #include <iostream>
- #include <string>
- int main() {
- // 基本定义
- int sum = 0; // 定义 sum,初始值 0
- int value; // 定义 value,未初始化(危险!)
- int a = 0, b = 1; // 同时定义多个
- // 初始化的四种等价写法(对 int 来说)
- int x1 = 0; // 赋值初始化
- int x2 = {0}; // 列表初始化
- int x3{0}; // 列表初始化(推荐!)
- int x4(0); // 直接初始化
- // 列表初始化的好处:防止精度损失
- long double ld = 3.1415926536;
- // int bad{ld}; // 编译错误!会损失数据
- int ok(ld); // 合法,但会截断为 3(有警告)
- // 字符串
- std::string book("0-201-78345-X");
- std::cout << "sum=" << sum << "\n";
- std::cout << "book=" << book << "\n";
- return 0;
- }
复制代码
https://godbolt.org/z/YvoK5GYKh
重要区别:
- 初始化 = 创建变量的同时给它值
- 赋值 = 先创建变量,之后再改变它的值
这两件事在 C++ 里是不同的操作!
默认初始化规则:
- 定义在函数外(全局): 自动初始化为 0
- 定义在函数内(局部): 不初始化,值是随机垃圾!(危险)
- 类类型(如 string): 由类自己决定(string 默认是空字符串)
复制代码
2.3 声明与定义的区别
这是 C++ 分离编译的基础:
- 声明(declaration): 告诉编译器"有这个名字,类型是这个"
- 定义(definition): 声明 + 实际分配内存(真正创建变量)
复制代码- // 声明(不是定义):extern 关键字 + 没有初始值
- extern int i; // 只声明,不定义
- // 定义(也是声明)
- int j; // 定义 j,分配内存
- extern double pi = 3.1416; // 有初始值 = 定义,extern 被覆盖
- // 规则:
- // - 变量只能定义一次
- // - 可以声明多次
- // - 跨文件共享变量:在一个文件定义,其他文件声明
复制代码
2.4 标识符命名规则
| 规则 | 说明 |
|---|
| 由字母、数字、下划线组成 | 不能有空格或特殊字符 |
| 不能以数字开头 | 1abc 非法,abc1 合法 |
| 大小写敏感 | Abc 和 abc 是不同名字 |
| 不能用关键字 | int、class 等不能用 |
| 不能有连续两个下划线 | my__var 非法 |
| 不能以下划线+大写字母开头 | _Abc 非法 |
命名约定(非强制,但推荐):
- 变量名:小写,多词用下划线 student_loan 或驼峰 studentLoan
- 类名:首字母大写 Sales_item
- 名字要有意义,能说明用途
2.5 作用域
作用域决定了一个名字在哪些地方有效:
- #include <iostream>
- int reused = 42; // 全局作用域:整个程序都能用
- int main() {
- int unique = 0; // 块作用域:只在 main 函数内有效
- // 输出1:用的是全局 reused = 42
- std::cout << reused << " " << unique << "\n"; // 42 0
- int reused = 0; // 新的局部变量,遮蔽了全局 reused
- // 输出2:用的是局部 reused = 0
- std::cout << reused << " " << unique << "\n"; // 0 0
- // :: 是作用域运算符,:: 左边空 = 全局作用域
- // 输出3:明确要全局 reused = 42
- std::cout << ::reused << " " << unique << "\n"; // 42 0
- return 0;
- }
复制代码
https://godbolt.org/z/TM7591KoG
作用域的嵌套关系:
- 全局作用域
- └── main 函数的块作用域
- └── for 循环的块作用域
- └── ...
复制代码
建议: 变量在第一次使用的地方附近定义,不要在最前面一股脑全定义完。
3. 复合类型:引用与指针
复合类型= 基于另一种类型定义的类型。最重要的两种:引用和指针。
3.1 引用(Reference)
引用就是给已有变量起的别名,两个名字指向同一块内存:
- #include <iostream>
- int main() {
- int ival = 1024;
- int &refVal = ival; // refVal 是 ival 的别名(引用必须初始化!)
- // int &refVal2; // 错误!引用必须在定义时绑定
- // 对引用的操作 = 对原变量的操作
- refVal = 2; // 实际上是给 ival 赋值 2
- std::cout << ival; // 输出 2
- int ii = refVal; // 等价于 int ii = ival;
- std::cout << ii; // 输出 2
- // 可以定义多个引用(每个都要有 &)
- int i2 = 2048;
- int &r1 = ival, &r2 = i2; // r1 和 r2 都是引用
- // 引用的类型必须和被引用对象的类型完全匹配
- // double d = 3.14;
- // int &bad = d; // 错误!类型不匹配
- return 0;
- }
复制代码
https://godbolt.org/z/j3jKe5qro
引用 vs 普通变量:
- 普通变量: 有自己的内存地址,存储一个值
- 引用: 没有自己的内存(不是对象),只是另一个名字
- 一旦绑定就不能改变绑定对象
- 必须在定义时初始化
复制代码
3.2 指针(Pointer)
指针是一个存储地址的对象。它"指向"另一个对象:
- #include <iostream>
- int main() {
- int ival = 42;
- int *p = &ival; // p 是指针,存储 ival 的地址
- // & 在这里是"取地址运算符"
- // 读取指针指向的值:用 * 解引用
- std::cout << *p << "\n"; // 输出 42(* 是解引用运算符)
- // 通过指针修改原变量
- *p = 0;
- std::cout << ival << "\n"; // 输出 0,ival 被修改了!
- // 空指针:不指向任何对象
- int *p1 = nullptr; // 推荐写法(C++11)
- int *p2 = 0; // 也合法
- // int *p3 = NULL; // 旧式写法,不推荐
- // 检查指针是否为空
- if (p1) {
- // p1 非空时执行
- }
- if (!p1) {
- std::cout << "p1 是空指针\n";
- }
- // 指针和引用类型必须匹配
- double dval = 3.14;
- double *pd = &dval; // 合法
- // int *pi = &dval; // 错误!类型不匹配
- return 0;
- }
复制代码
https://godbolt.org/z/44fEef4zn
指针的四种状态:
- 1. 指向一个对象(有效)
- 2. 指向对象末尾的下一个位置(有效,但不能解引用)
- 3. 空指针 nullptr(有效,表示"不指向任何东西")
- 4. 无效指针(野指针)—— 危险!不能使用!
复制代码
& 和 * 的双重含义:
- int i = 42;
- int &r = i; // 声明中的 & = 定义引用
- int *p; // 声明中的 * = 定义指针
- p = &i; // 表达式中的 & = 取地址运算符
- *p = i; // 表达式中的 * = 解引用运算符
复制代码
指针 vs 引用对比:
| 特性 | 引用 | 指针 |
|---|
| 是否是对象 | 不是 | 是 |
| 必须初始化 | 是 | 否(但强烈建议!) |
| 能否改变绑定 | 不能 | 能(指向不同对象) |
| 能否为空 | 不能 | 能(nullptr) |
| 解引用语法 | 直接用名字 | 用 *p |
3.3 指针与引用的内存示意图
- ival: [ 42 ] 地址: 0x1000
- ^
- |(p 存储这个地址)
- p: [ 0x1000 ] 地址: 0x2000
- refVal: 就是 ival 本身的另一个名字,没有独立内存
复制代码
3.4 指针到指针(双重指针)
- #include <iostream>
- int main() {
- int ival = 1024;
- int *pi = &ival; // pi 指向 ival
- int **ppi = π // ppi 指向 pi(指针的指针)
- // 访问 ival 的三种方式:
- std::cout << ival << "\n"; // 直接访问:1024
- std::cout << *pi << "\n"; // 解引用一次:1024
- std::cout << **ppi << "\n"; // 解引用两次:1024
- return 0;
- }
复制代码
内存关系图:
- ppi -----> pi -----> ival
- [addr2] [addr1] [1024]
复制代码
3.5 void* 指针
void* 是特殊的指针,可以存储任何类型对象的地址,但不能直接解引用(因为不知道类型):
- #include <iostream>
- int main() {
- double obj = 3.14;
- void *pv = &obj; // 合法:void* 可以指向任何类型
- // *pv = 0; // 错误!不能解引用 void*
- // 只能:比较、传参、赋给另一个 void*
- return 0;
- }
复制代码
4. const 限定符
4.1 基本 const
const 让变量的值不可改变,因此必须在定义时初始化:
- #include <iostream>
- int main() {
- const int bufSize = 512; // 正确:有初始值
- // const int k; // 错误!没有初始值
- // bufSize = 256; // 错误!不能修改 const
- // const 对象可以参与大多数运算
- const int ci = 42;
- int j = ci; // 合法:把 ci 的值复制给 j(复制不会修改 ci)
- std::cout << bufSize << "\n"; // 512
- std::cout << j << "\n"; // 42
- return 0;
- }
复制代码
const 变量默认是文件内局部的:同一个 const 名字在不同文件里是独立的变量。
要跨文件共享 const,两边都要加 extern。
4.2 const 引用(对 const 的引用)
- #include <iostream>
- int main() {
- const int ci = 1024;
- const int &r1 = ci; // 合法:引用也是 const,不能通过 r1 修改 ci
- // r1 = 42; // 错误!不能通过 const 引用修改值
- // int &r2 = ci; // 错误!普通引用不能绑定到 const 对象
- // 特殊:const 引用可以绑定到非 const 对象、字面量、表达式
- int i = 42;
- const int &r3 = i; // 合法:const 引用绑定到普通 int
- const int &r4 = 42; // 合法:绑定到字面量(编译器创建临时变量)
- const int &r5 = r1 * 2; // 合法:绑定到表达式结果
- // r3 只是不能通过 r3 修改 i,但 i 本身还是可以改的
- i = 100;
- std::cout << r3 << "\n"; // 输出 100,r3 反映了 i 的最新值
- return 0;
- }
复制代码
4.3 指针与 const
这里有两种完全不同的东西:
a) 指向 const 对象的指针(底层 const): 指针本身可以改,但不能通过指针修改所指对象
- const double pi = 3.14;
- const double *cptr = π // 指针可以改变,但 *cptr 不能修改
- // *cptr = 42; // 错误!
- double dval = 2.72;
- cptr = &dval; // 合法:可以让指针指向另一个对象
复制代码
b) const 指针(顶层 const): 指针本身不能改(地址固定),但可以通过指针修改所指对象
- int errNumb = 0;
- int *const curErr = &errNumb; // const 在 * 后面 = 指针本身是 const
- // curErr = &errNumb; // 错误!不能改变指针存的地址
- *curErr = 1; // 合法:可以修改指向的 errNumb
复制代码
c) 指向 const 对象的 const 指针: 两者都不能改
- const double pi2 = 3.14159;
- const double *const pip = &pi2; // 指针本身是 const,指向的值也是 const
- // *pip = 2.72; // 错误!
- // pip = &pi2; // 错误!
复制代码
读懂复杂声明的技巧:从右往左读:
- int *const curErr = &errNumb;
- 读法:curErr → const(curErr本身是常量)→ *(是一个指针)→ int(指向int)
- 结论:curErr 是一个指向 int 的常量指针
复制代码
4.4 顶层 const 与底层 const
- 顶层 const (top-level const): 对象本身是 const(不能改自己)
- 底层 const (low-level const): 指向/引用的对象是 const(不能通过它改别人)
复制代码- #include <iostream>
- int main() {
- int i = 0;
- int *const p1 = &i; // 顶层 const:p1 本身不能改
- const int ci = 42; // 顶层 const:ci 本身不能改
- const int *p2 = &ci; // 底层 const:不能通过 p2 修改所指对象
- const int *const p3 = p2; // 左边底层,右边顶层
- // 复制时,顶层 const 被忽略
- i = ci; // 合法:复制 ci 的值,顶层 const 忽略
- p2 = p3; // 合法:底层 const 相同,顶层 const 忽略
- // 复制时,底层 const 不能忽略
- // int *p = p3; // 错误!p3 有底层 const,p 没有
- p2 = p3; // 合法:都有底层 const
- return 0;
- }
复制代码
4.5 constexpr:编译时常量
constexpr 让编译器在编译阶段就计算出值:
- #include <iostream>
- // constexpr 函数(值必须在编译期可知)
- constexpr int square(int x) { return x * x; }
- int main() {
- constexpr int mf = 20; // 20 是编译时常量
- constexpr int limit = mf + 1; // mf+1 也是编译时常量
- constexpr int s = square(5); // 合法:square 是 constexpr 函数
- // const int sz = get_size(); // 不是 constexpr,运行时才知道值
- std::cout << mf << "\n"; // 20
- std::cout << limit << "\n"; // 21
- std::cout << s << "\n"; // 25
- return 0;
- }
复制代码
const vs constexpr 的区别:
- const:值不可改,但可以在运行时确定
- constexpr:值在编译时就必须确定,是真正的编译期常量
5. 处理类型:auto 与 decltype
5.1 类型别名
给复杂类型起一个简洁的别名:
- #include <iostream>
- // 传统方式:typedef
- typedef double wages; // wages = double 的别名
- typedef wages base, *p; // base = double,p = double*
- // 新标准方式(推荐):using
- using SI = int; // SI = int 的别名
- int main() {
- wages hourly = 15.5; // 等价于 double hourly = 15.5
- SI x = 42; // 等价于 int x = 42
- // 注意:指针别名 + const 的陷阱
- typedef char *pstring; // pstring = char*(指向char的指针)
- const pstring cstr = 0; // cstr 是 const 指针(指向char),不是指向const char!
- // 不等价于 const char *cstr = 0; ← 这是指向const char的指针,不同!
- std::cout << hourly << "\n"; // 15.5
- std::cout << x << "\n"; // 42
- return 0;
- }
复制代码
5.2 auto:让编译器推断类型
当表达式类型复杂时,用 auto 让编译器自动推断:
- #include <iostream>
- int main() {
- int val1 = 10, val2 = 20;
- auto item = val1 + val2; // 编译器推断 item 是 int
- auto i = 0, *p = &i; // i 是 int,p 是 int*(类型一致)
- // auto sz = 0, pi = 3.14; // 错误!sz 是 int,pi 是 double,不一致
- // auto 对 const 的处理:
- int x = 0;
- const int ci = x;
- int &cr = x;
- auto b = ci; // b 是 int(顶层 const 被忽略)
- auto c = cr; // c 是 int(引用被忽略,cr 是 int 的别名)
- auto d = &x; // d 是 int*
- auto e = &ci; // e 是 const int*(底层 const 保留)
- // 要保留顶层 const,需要显式写出
- const auto f = ci; // f 是 const int
- // auto 推断引用类型
- auto &g = ci; // g 是 const int&(绑定到 ci)
- const auto &j = 42; // 合法:const 引用可以绑定到字面量
- std::cout << item << "\n"; // 30
- std::cout << b << "\n"; // 0
- std::cout << f << "\n"; // 0
- return 0;
- }
复制代码
auto 的推断规则总结:
- 引用初始化auto → 用被引用对象的类型
- 顶层 const → 被 auto 忽略
- 底层 const → 被 auto 保留
- 想保留顶层const → 写 const auto
- 想得到引用 → 写 auto&
复制代码
5.3 decltype:只获取类型,不计算值
有时想知道某个表达式的类型,但不想真正计算它:
- #include <iostream>
- int f() { return 42; }
- int main() {
- // decltype(f()) 得到 f 的返回类型 int,但不真正调用 f
- decltype(f()) sum = 0; // sum 是 int 类型
- const int ci = 0;
- int &cj = const_cast<int&>(ci); // 仅演示用
- // decltype 保留顶层 const 和引用(与 auto 不同!)
- decltype(ci) x = 0; // x 是 const int
- // decltype(cj) y = x; // y 是 int&(引用),必须初始化
- // decltype 与表达式:
- int i = 42, *p = &i;
- decltype(i) a; // a 是 int(变量 i 的类型)
- // decltype(*p) b; // b 是 int&(解引用返回引用类型)!必须初始化
- decltype(i+0) c; // c 是 int(表达式结果是右值)
- // 特殊:加括号 vs 不加括号
- decltype(i) e; // e 是 int
- // decltype((i)) d; // d 是 int&!变量加括号变成引用类型
- std::cout << sum << "\n";
- return 0;
- }
复制代码
decltype vs auto 的核心差异:
| 特性 | auto | decltype |
|---|
| 顶层 const | 忽略 | 保留 |
| 引用 | 忽略,用被引用对象类型 | 保留引用类型 |
| 需要初始值 | 是 | 否 |
| 加括号 | 不影响 | 变成引用类型 |
6. 自定义数据结构
6.1 定义结构体(struct)
结构体把相关数据组织在一起:
- // Sales_data.h 头文件内容
- #ifndef SALES_DATA_H // 头文件保护:如果还没定义这个宏
- #define SALES_DATA_H // 定义这个宏(防止重复包含)
- #include <string>
- struct Sales_data {
- // 数据成员(C++11 支持类内初始化)
- std::string bookNo; // ISBN 书号,默认空字符串
- unsigned units_sold = 0; // 销售数量,默认 0
- double revenue = 0.0; // 总收入,默认 0.0
- }; // 注意:结构体定义末尾必须有分号!
- #endif // 结束头文件保护
复制代码
头文件保护机制(#ifndef 防卫式声明):
- 第一次包含 Sales_data.h:
- SALES_DATA_H 未定义 → #ifndef 为真 → 处理内容 → 定义 SALES_DATA_H
- 第二次包含 Sales_data.h:
- SALES_DATA_H 已定义 → #ifndef 为假 → 跳过全部内容(避免重复定义)
复制代码
6.2 使用结构体
- #include <iostream>
- #include <string>
- // #include "Sales_data.h" // 实际项目中应单独放头文件
- // 为演示方便,这里直接内嵌定义
- struct Sales_data {
- std::string bookNo;
- unsigned units_sold = 0;
- double revenue = 0.0;
- };
- int main() {
- Sales_data data1, data2;
- // 读取第一条交易:ISBN 数量 单价
- double price = 0;
- std::cin >> data1.bookNo >> data1.units_sold >> price;
- data1.revenue = data1.units_sold * price; // 计算总收入
- // 读取第二条交易
- std::cin >> data2.bookNo >> data2.units_sold >> price;
- data2.revenue = data2.units_sold * price;
- // 检查是否是同一本书
- if (data1.bookNo == data2.bookNo) {
- unsigned totalCnt = data1.units_sold + data2.units_sold;
- double totalRevenue = data1.revenue + data2.revenue;
- std::cout << data1.bookNo << " "
- << totalCnt << " "
- << totalRevenue << " ";
- if (totalCnt != 0)
- std::cout << totalRevenue / totalCnt << "\n"; // 平均售价
- else
- std::cout << "(no sales)\n";
- return 0;
- } else {
- std::cerr << "Data must refer to the same ISBN\n";
- return -1;
- }
- }
复制代码
结构体的关键特性:
- 每个对象有自己独立的数据成员副本,修改 data1 不影响 data2
- 用 .(点运算符)访问成员:data1.bookNo
- 类内初始化:可以在定义时给成员初始值(C++11)
综合流程图
变量从定义到使用的完整流程
指针与引用的关系总览
const 的层次关系
- int i = 0;
- 顶层const 底层const
- (对象本身) (指向的对象)
- int *const p1 = &i; ← 顶层const,p1不能改,*p1能改
- const int ci = 42; ← 顶层const,ci不能改
- const int *p2 = &ci; ← 底层const,p2能改,*p2不能改
- const int *const p3 = p2; ← 两者都有
复制代码
典型错误汇总与正确写法
- #include <iostream>
- #include <string>
- int main() {
- // 错误1:引用未初始化
- // int &r; // 错误!
- int x = 0;
- int &r = x; // 正确
- // 错误2:用字面量初始化普通引用
- // int &r2 = 10; // 错误!
- const int &r2 = 10; // 正确:const 引用可以绑定字面量
- // 错误3:指针类型不匹配
- double d = 3.14;
- // int *p = &d; // 错误!
- double *p = &d; // 正确
- // 错误4:未初始化 const
- // const int k; // 错误!
- const int k = 0; // 正确
- // 错误5:修改 const 对象
- // k = 1; // 错误!
- // 错误6:auto 多变量类型不一致
- // auto sz = 0, pi = 3.14; // 错误!sz是int,pi是double
- auto sz = 0; // 正确:分开写
- auto pi = 3.14;
- // 错误7:使用未初始化的局部变量
- int val;
- // std::cout << val; // 危险!值未定义
- int val2 = 0; // 正确:显式初始化
- std::cout << val2 << "\n";
- // 错误8:对 const 对象解引用赋值
- const int ci = 42;
- const int *cp = &ci;
- // *cp = 0; // 错误!不能通过 const 指针修改值
- return 0;
- }
复制代码
关键概念速查表
| 概念 | 一句话理解 |
|---|
| 类型 | 告诉编译器内存里存的是什么,能做什么操作 |
| 变量 | 有名字的内存区域,有类型 |
| 引用 | 变量的别名,不是独立对象,必须初始化,不能重绑 |
| 指针 | 存地址的对象,可以为空,可以改变指向 |
| const | 值不可改,必须初始化 |
| 顶层 const | 对象本身不可改 |
| 底层 const | 指向/引用的对象不可改 |
| constexpr | 编译时就能算出来的常量 |
| auto | 让编译器推断类型 |
| decltype | 获取表达式的类型(不计算) |
| 作用域 | 名字有效的范围(花括号内) |
| 声明 | 告诉编译器名字存在 |
| 定义 | 真正分配内存 |
| 初始化 | 创建时给值 |
| 赋值 | 创建后改变值 |
C++ 第三章:字符串、向量和数组 — 从零理解
目录
- using 声明:告别 std:: 前缀
- string 类型:可变长度字符串
- vector 类型:可变大小的集合
- 迭代器:通用的元素访问机制
- 数组:固定大小的低级容器
- 多维数组
1. using 声明:告别 std:: 前缀
1.1 为什么需要 using 声明?
C++ 标准库的所有名字都放在 std 命名空间里。每次用 std::cin、std::cout 很繁琐。
using 声明让我们可以省略 std:: 前缀:
- #include <iostream>
- #include <string>
- // 每个名字单独声明一行
- using std::cin;
- using std::cout;
- using std::endl;
- using std::string;
- int main() {
- string s;
- cin >> s; // 直接用 cin,不用写 std::cin
- cout << s << endl;
- return 0;
- }
复制代码
https://godbolt.org/z/8evhxqxfa
规则:
- 每个 using 声明只引入一个名字
- 每条声明必须以分号结尾
- 头文件里不要写 using 声明(会污染包含该头文件的所有文件)
2. string 类型:可变长度字符串
2.1 初始化 string
- #include <iostream>
- #include <string>
- using std::string;
- using std::cout;
- using std::cin;
- using std::endl;
- int main() {
- // 六种初始化方式
- string s1; // 默认初始化:空字符串 ""
- string s2(s1); // 直接初始化:s2 是 s1 的副本
- string s3 = s1; // 拷贝初始化:等价于 s2(s1)
- string s4("hello"); // 直接初始化:s4 = "hello"
- string s5 = "hello"; // 拷贝初始化:等价于 s4("hello")
- string s6(10, 'c'); // 直接初始化:s6 = "cccccccccc"(10个c)
- cout << "s1='" << s1 << "'\n"; // 空
- cout << "s4='" << s4 << "'\n"; // hello
- cout << "s6='" << s6 << "'\n"; // cccccccccc
- return 0;
- }
复制代码
https://godbolt.org/z/boex7c1rj
直接初始化 vs 拷贝初始化:
- 用 = → 拷贝初始化(编译器把右边的值复制过来)
- 不用 =,直接用 () → 直接初始化
- 多个初始值必须用直接初始化(括号形式)
2.2 string 的常用操作
- #include <iostream>
- #include <string>
- using std::string;
- using std::cout;
- using std::cin;
- using std::endl;
- using std::getline;
- int main() {
- // ------- 读写 -------
- string word;
- // cin >> 读入:自动跳过开头的空白,遇到空白停止
- // 比如输入 " Hello World ",读入的是 "Hello"
- cin >> word;
- cout << word << endl;
- // getline:读取整行(包括空格),直到遇到换行符
- // 换行符被丢弃,不存入字符串
- string line;
- getline(cin, line);
- cout << line << endl;
- // ------- 基本操作 -------
- string s = "Hello, World!";
- cout << s.empty() << "\n"; // 0(false),因为 s 不空
- cout << s.size() << "\n"; // 13,字符个数
- // size() 返回的类型是 string::size_type(无符号整数)
- // 推荐用 auto 接收,避免有符号/无符号混用的陷阱
- auto len = s.size();
- // ------- 比较(字典序,大小写敏感)-------
- string s1 = "Hello";
- string s2 = "Hello World";
- string s3 = "Hiya";
- // s1 < s2:因为 s1 是 s2 的前缀,且 s1 更短
- cout << (s1 < s2) << "\n"; // 1(true)
- // s3 > s1:因为第一个不同字符 'i' > 'e'
- cout << (s3 > s1) << "\n"; // 1(true)
- // ------- 拼接 -------
- string a = "hello, ", b = "world\n";
- string c = a + b; // c = "hello, world\n"
- a += b; // a 变成 "hello, world\n"
- // 注意:string 字面量("...")不是 std::string!
- // 加号两边至少要有一个 std::string 对象
- string ok1 = s1 + ", "; // 合法:s1 是 string
- // string bad = "hello" + ", "; // 非法:两边都是字面量
- string ok2 = s1 + ", " + "world"; // 合法:先 s1+", " 得到 string,再加 "world"
- return 0;
- }
复制代码
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 循环(推荐,遍历每个字符)
- #include <iostream>
- #include <string>
- #include <cctype> // isalpha, ispunct, toupper 等函数
- using std::string;
- using std::cout;
- using std::endl;
- int main() {
- string s = "Hello World!!!";
- // 1. 统计标点符号个数
- // decltype(s.size()) 得到 string::size_type 类型(无符号整数)
- decltype(s.size()) punct_cnt = 0;
- for (auto c : s) { // c 是 char 的副本,修改 c 不影响 s
- if (ispunct(c))
- ++punct_cnt;
- }
- cout << punct_cnt << " punctuation characters\n"; // 3
- // 2. 把整个字符串转为大写
- // 用引用 &c,这样修改 c 就是修改 s 中对应的字符
- for (auto &c : s) {
- c = toupper(c); // toupper 把小写字母转为大写
- }
- cout << s << "\n"; // HELLO WORLD!!!
- return 0;
- }
复制代码
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) | 转为小写,非字母原样返回 |
方法二:下标访问(只处理部分字符)
- #include <iostream>
- #include <string>
- #include <cctype>
- using std::string;
- using std::cout;
- int main() {
- string s = "some string";
- // 只把第一个字符转大写
- if (!s.empty()) // 先检查非空,空字符串的 s[0] 是未定义行为!
- s[0] = toupper(s[0]);
- cout << s << "\n"; // Some string
- // 只把第一个单词转大写(遇到空白停止)
- // index 的类型用 decltype(s.size()) = string::size_type(无符号)
- for (decltype(s.size()) index = 0;
- index != s.size() && !isspace(s[index]); // && 短路求值:先检查不越界
- ++index)
- {
- s[index] = toupper(s[index]);
- }
- cout << s << "\n"; // SOME string
- // 随机访问:把数字0-15转为十六进制字符
- const string hexdigits = "0123456789ABCDEF";
- string result;
- string::size_type n;
- // 假设输入:12 0 5 15
- // 输出:C05F
- while (std::cin >> n) {
- if (n < hexdigits.size())
- result += hexdigits[n]; // 用数字做下标,直接取对应的十六进制字符
- }
- cout << "Hex: " << result << "\n";
- return 0;
- }
复制代码
https://godbolt.org/z/Ehene538f
安全警告: 下标不检查越界!
- 下标必须满足:
0
≤
index
<
s.size()
0 \leq \text{index} < \text{s.size()}
0≤index
- 用 string::size_type(无符号)做下标,可以保证不为负数
3. vector 类型:可变大小的集合
vector 是一个类模板,可以生成存储任意类型元素的容器。
- vector<int> 存 int
- vector<string> 存 string
- vector<double> 存 double
- vector<vector<int>> 存 vector<int>(嵌套)
复制代码
3.1 初始化 vector
- #include <iostream>
- #include <vector>
- #include <string>
- using std::vector;
- using std::string;
- using std::cout;
- int main() {
- // 空 vector(最常用的起点)
- vector<int> v1; // 0 个元素
- // 从另一个 vector 复制
- vector<int> v2(v1); // v2 是 v1 的副本
- vector<int> v3 = v1; // 等价于 v2(v1)
- // 指定元素个数和初始值
- vector<int> v4(10, -1); // 10 个元素,每个都是 -1
- vector<string> v5(10, "hi!"); // 10 个 "hi!"
- // 只指定个数,值自动初始化(int 为 0,string 为 "")
- vector<int> v6(10); // 10 个 0
- vector<string> v7(10); // 10 个空字符串
- // 列表初始化(花括号):直接指定每个元素的值
- vector<int> v8{1, 2, 3, 4, 5}; // 5 个元素
- vector<string> v9{"a", "an", "the"}; // 3 个字符串
- // 圆括号 vs 花括号的区别!
- vector<int> va(10); // 10 个元素,每个值为 0
- vector<int> vb{10}; // 1 个元素,值为 10
- vector<int> vc(10, 1); // 10 个元素,每个值为 1
- vector<int> vd{10, 1}; // 2 个元素,值分别为 10 和 1
- // string vector 的花括号:如果不能做元素值,就用来构造
- vector<string> ve{10}; // 10 个空字符串(10 不能是 string,退化为"个数")
- vector<string> vf{10, "hi"}; // 10 个 "hi"(同理)
- cout << "v4 size=" << v4.size() << "\n"; // 10
- cout << "vb size=" << vb.size() << "\n"; // 1
- cout << "vd size=" << vd.size() << "\n"; // 2
- return 0;
- }
复制代码
https://godbolt.org/z/311xsWMar
圆括号 vs 花括号记忆口诀:
- 圆括号 () → 构造(指定个数/值)
- 花括号 {} → 列表初始化(指定每个元素),如不可行才退化为构造
复制代码
3.2 向 vector 添加元素:push_back
- #include <iostream>
- #include <vector>
- #include <string>
- using std::vector;
- using std::string;
- using std::cout;
- using std::cin;
- int main() {
- // 从空 vector 开始,逐个添加
- vector<int> v; // 空 vector
- for (int i = 0; i < 100; ++i)
- v.push_back(i); // 在末尾追加元素,v 自动扩容
- // 结束后 v 有 100 个元素,值为 0..99
- // 读取未知个数的字符串
- vector<string> text;
- string word;
- while (cin >> word) {
- text.push_back(word); // 每读一个词就追加
- }
- cout << "共读入 " << text.size() << " 个词\n";
- return 0;
- }
复制代码
https://godbolt.org/z/3b85h7aEG
重要: vector 支持高效的运行时扩容。
推荐先定义空 vector,再用 push_back 添加,不要一开始就指定大小(除非所有元素值相同)。
禁忌: 范围 for 循环的循环体内不能 push_back!因为添加元素会让迭代失效。
3.3 vector 的其他操作
- #include <iostream>
- #include <vector>
- using std::vector;
- using std::cout;
- using std::cin;
- using std::endl;
- int main() {
- vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9};
- // 范围 for + 引用:修改每个元素
- for (auto &i : v)
- i *= i; // 每个元素变成自己的平方
- // 范围 for:只读
- for (auto i : v)
- cout << i << " ";
- cout << endl; // 1 4 9 16 25 36 49 64 81
- // 成员函数
- cout << v.empty() << "\n"; // 0(非空)
- cout << v.size() << "\n"; // 9
- // 下标访问(不能用下标添加新元素!)
- cout << v[0] << "\n"; // 1
- v[0] = 100; // 修改已有元素
- // 成绩分段统计例子
- vector<unsigned> scores(11, 0); // 11 个桶(0-9, 10-19, ..., 100),初始值全0
- unsigned grade;
- while (cin >> grade) {
- if (grade <= 100)
- ++scores[grade / 10]; // grade/10 得到桶的下标(0~10)
- }
- for (auto s : scores)
- cout << s << " ";
- cout << endl;
- return 0;
- }
复制代码
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] 只能访问已存在的元素,不能用来添加元素!
- vector<int> ivec; // 空 vector
- // ivec[0] = 42; // 错误!没有第0个元素
- ivec.push_back(42); // 正确!
复制代码
4. 迭代器:通用的元素访问机制
迭代器是比下标更通用的访问方式。所有容器都支持迭代器,但并非所有容器都支持下标。
4.1 迭代器的基本用法
- begin() → 指向第一个元素
- end() → 指向"末尾后一个位置"(不存在的元素,哨兵)
- 如果容器为空:begin() == end()
复制代码- +-------+-------+-------+-------+
- | [0] | [1] | [2] | [3] | (不存在)
- +-------+-------+-------+-------+
- ^ ^
- begin() end()
复制代码- #include <iostream>
- #include <string>
- #include <vector>
- #include <cctype>
- using std::string;
- using std::vector;
- using std::cout;
- int main() {
- string s("some string");
- // auto 让编译器推断迭代器类型
- auto b = s.begin(); // 指向第一个字符 's'
- auto e = s.end(); // 指向最后一个字符后面的位置
- // 用迭代器把第一个字符变大写
- if (s.begin() != s.end()) { // 先确认非空
- auto it = s.begin();
- *it = toupper(*it); // 解引用得到字符,修改它
- }
- cout << s << "\n"; // Some string
- // 用迭代器遍历,把第一个单词变大写(遇到空格停止)
- for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
- *it = toupper(*it);
- cout << s << "\n"; // SOME string
- // vector 也一样
- vector<int> v{1, 2, 3, 4, 5};
- for (auto it = v.begin(); it != v.end(); ++it)
- *it *= 2; // 每个元素乘2
- for (auto x : v)
- cout << x << " ";
- cout << "\n"; // 2 4 6 8 10
- return 0;
- }
复制代码
https://godbolt.org/z/55ja6dM1a
| 迭代器操作 | 含义 |
|---|
| *iter | 解引用,得到 iter 指向的元素 |
| iter->mem | 等价于 (*iter).mem,访问元素的成员 |
| ++iter | 移到下一个元素 |
| --iter | 移到上一个元素 |
| iter1 == iter2 | 两个迭代器是否相等 |
| iter1 != iter2 | 两个迭代器是否不等 |
4.2 迭代器类型:iterator vs const_iterator
- #include <iostream>
- #include <vector>
- #include <string>
- using std::vector;
- using std::string;
- using std::cout;
- int main() {
- vector<int> v{1, 2, 3};
- const vector<int> cv{4, 5, 6};
- // 普通 vector:begin() 返回 iterator(可读可写)
- vector<int>::iterator it1 = v.begin();
- *it1 = 100; // 合法,可以修改
- // const vector:begin() 返回 const_iterator(只读)
- vector<int>::const_iterator it2 = cv.begin();
- // *it2 = 100; // 错误!不能通过 const_iterator 修改
- // cbegin() / cend():强制返回 const_iterator(即使 vector 不是 const)
- auto it3 = v.cbegin(); // const_iterator,只读
- // *it3 = 100; // 错误!
- // 箭头运算符:简化"解引用后访问成员"
- vector<string> svec{"hello", "world"};
- auto it4 = svec.begin();
- cout << it4->size() << "\n"; // 等价于 (*it4).size(),输出 5
- return 0;
- }
复制代码
https://godbolt.org/z/Yb7Y8dxPb
选择哪种迭代器:
- 只读 → 用 const_iterator(通过 cbegin() / cend() 获取)
- 需要修改 → 用 iterator(通过 begin() / end() 获取)
4.3 迭代器算术(仅 vector 和 string 支持)
- #include <iostream>
- #include <vector>
- #include <string>
- using std::vector;
- using std::string;
- using std::cout;
- int main() {
- vector<int> vi{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
- // 迭代器加减整数
- auto mid = vi.begin() + vi.size() / 2; // 指向中间元素(vi[5])
- // 两个迭代器相减,得到距离(difference_type,有符号整数)
- auto dist = vi.end() - vi.begin(); // dist = 10
- // 比较迭代器
- auto it = vi.begin();
- if (it < mid)
- cout << "it 在 mid 之前\n";
- cout << "距离=" << dist << "\n"; // 10
- // 二分查找示例
- // 前提:text 必须是已排序的
- vector<string> text{"apple", "banana", "cherry", "date", "elderberry"};
- string sought = "cherry";
- auto beg = text.begin(), end = text.end();
- // 计算中间位置:(end - beg) / 2 得到距离,再加到 beg 上
- // 不能写 (beg + end) / 2,因为两个迭代器相加没有意义
- auto mid2 = text.begin() + (end - beg) / 2;
- while (mid2 != end && *mid2 != sought) {
- if (sought < *mid2)
- end = mid2; // 目标在左半部分,缩小范围
- else
- beg = mid2 + 1; // 目标在右半部分,缩小范围
- mid2 = beg + (end - beg) / 2; // 重新计算中间位置
- }
- if (mid2 != end)
- cout << "找到:" << *mid2 << "\n"; // 找到:cherry
- else
- cout << "未找到\n";
- return 0;
- }
复制代码
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 定义与初始化数组
- #include <iostream>
- #include <string>
- using std::string;
- using std::cout;
- int main() {
- // 维度必须是编译时常量(常量表达式)
- constexpr unsigned sz = 42; // constexpr = 编译时常量
- int arr[10]; // 10 个 int(函数内:未初始化)
- int *parr[sz]; // 42 个 int 指针
- // string bad[cnt]; // 错误!cnt 不是常量表达式
- // 列表初始化
- const unsigned n = 3;
- int ia1[n] = {0, 1, 2}; // 显式初始化 3 个元素
- int a2[] = {0, 1, 2}; // 维度由初始值个数推断,= 3
- int a3[5] = {0, 1, 2}; // 等价于 {0, 1, 2, 0, 0}(剩余补0)
- string a4[3] = {"hi", "bye"}; // 等价于 {"hi", "bye", ""}
- // 字符数组特殊初始化方式:字符串字面量
- char a5[] = {'C', '+', '+'}; // 维度3,没有 '\0'
- char a6[] = {'C', '+', '+', '\0'}; // 维度4,显式 null
- char a7[] = "C++"; // 维度4,自动补 '\0'
- // const char a8[6] = "Daniel"; // 错误!"Daniel" 有7个字符(含'\0'),放不下
- // 数组不能拷贝赋值
- int a[] = {0, 1, 2};
- // int a9[] = a; // 错误!不能用数组初始化另一个数组
- // a9 = a; // 错误!不能赋值
- // 复杂声明:从内向外读
- int *ptrs[10]; // ptrs 是数组,含10个 int 指针
- int (*Parray)[10] = &arr; // Parray 是指针,指向 含10个int 的数组
- int (&arrRef)[10] = arr; // arrRef 是引用,引用一个 含10个int 的数组
- cout << a7 << "\n"; // C++
- return 0;
- }
复制代码
https://godbolt.org/z/fTszjKjGh
读懂复杂数组声明的技巧:从内向外,从右向左
- int (*Parray)[10]
- 从 Parray 出发:
- (*Parray) → 先看 *,Parray 是一个指针
- [10] → 向右,它指向的是有10个元素的数组
- int → 向左,数组的元素类型是 int
- 结论:Parray 是指向"含10个int的数组"的指针
复制代码
5.2 访问数组元素
- #include <iostream>
- #include <cstddef> // size_t 定义在这里
- using std::cout;
- using std::cin;
- int main() {
- // size_t 是无符号整数类型,用来做下标最安全
- unsigned scores[11] = {}; // 11个桶,值初始化为 0
- unsigned grade;
- while (cin >> grade) {
- if (grade <= 100)
- ++scores[grade / 10];
- }
- // 范围 for 遍历(推荐)
- for (auto s : scores)
- cout << s << " ";
- cout << "\n";
- // 下标遍历
- for (size_t i = 0; i < 11; ++i)
- cout << scores[i] << " ";
- cout << "\n";
- return 0;
- }
复制代码
https://godbolt.org/z/h9qjoqTTE
5.3 指针与数组
核心规律:数组名在大多数表达式中自动转换为指向首元素的指针
- #include <iostream>
- #include <iterator> // begin, end 函数(C++11)
- using std::cout;
- using std::begin;
- using std::end;
- int main() {
- int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
- // 数组名 → 首元素指针
- int *p = arr; // 等价于 int *p = &arr[0]
- ++p; // 现在 p 指向 arr[1]
- // 计算"末尾后一个位置"的指针(用来当哨兵)
- int *e = &arr[10]; // 指向 arr[10](不存在的元素)
- // 遍历
- for (int *b = arr; b != e; ++b)
- cout << *b << " ";
- cout << "\n";
- // 更安全的写法:用标准库 begin/end(需要 <iterator>)
- int *beg = begin(arr); // 等价于 &arr[0]
- int *last = end(arr); // 等价于 &arr[10]
- for (int *p2 = beg; p2 != last; ++p2)
- cout << *p2 << " ";
- cout << "\n";
- // 指针算术
- int ia[] = {0, 2, 4, 6, 8};
- int *ip = ia;
- int *ip2 = ip + 4; // 指向 ia[4](值为8)
- cout << *ip2 << "\n"; // 8
- // 两指针相减 = 距离(类型是 ptrdiff_t,有符号)
- auto n = end(ia) - begin(ia); // n = 5
- cout << n << "\n";
- // 指针当下标(与数组下标等价)
- int *p3 = &ia[2]; // p3 指向 ia[2](值为4)
- cout << p3[1] << "\n"; // 等价于 *(p3+1) = ia[3] = 6
- cout << p3[-2] << "\n"; // 等价于 *(p3-2) = ia[0] = 0(下标可以是负数!)
- // auto 推断数组类型时,得到的是指针
- auto ia2(ia); // ia2 是 int*,不是数组!
- // decltype 不会转换
- decltype(ia) ia3 = {0,1,2,3,4}; // ia3 是 int[5],是数组
- return 0;
- }
复制代码
https://godbolt.org/z/147cq1zTj
迭代器与指针操作对照:
| 操作 | 迭代器(vector) | 指针(数组) |
|---|
| 首元素 | v.begin() | begin(arr) 或 arr |
| 末尾哨兵 | v.end() | end(arr) 或 &arr[n] |
| 下一个 | ++it | ++p |
| 解引用 | *it | *p |
| 加整数 | it + n | p + n |
| 相减 | it1 - it2(difference_type) | p1 - p2(ptrdiff_t) |
5.4 C 风格字符串(了解即可,不推荐使用)
C 风格字符串 = 以 '\0' 结尾的 char 数组。
- #include <iostream>
- #include <cstring> // strlen, strcmp, strcat, strcpy
- using std::cout;
- int main() {
- const char ca1[] = "A string example"; // 自动以 '\0' 结尾
- const char ca2[] = "A different string";
- // 错误的比较方式!比的是指针地址,不是字符串内容
- // if (ca1 < ca2) // 未定义行为!
- // 正确:用 strcmp
- if (strcmp(ca1, ca2) < 0)
- cout << "ca1 < ca2\n";
- // 字符串长度(不含 '\0')
- cout << strlen(ca1) << "\n"; // 16
- // C 风格字符串拼接(危险!必须手动管理内存)
- // 推荐用 std::string 代替
- // string 转 C 风格:c_str()
- std::string s = "Hello";
- const char *cp = s.c_str(); // 指向 "Hello\0" 的指针
- // 注意:s 改变后 cp 可能失效!需要时应复制这个数组
- return 0;
- }
复制代码
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 的互操作
- #include <iostream>
- #include <vector>
- #include <iterator>
- using std::vector;
- using std::cout;
- using std::begin;
- using std::end;
- int main() {
- int int_arr[] = {0, 1, 2, 3, 4, 5};
- // 用数组初始化 vector(传首指针和尾后指针)
- vector<int> ivec(begin(int_arr), end(int_arr)); // 全部6个元素
- vector<int> subVec(int_arr + 1, int_arr + 4); // int_arr[1], [2], [3]
- for (auto x : ivec)
- cout << x << " ";
- cout << "\n"; // 0 1 2 3 4 5
- for (auto x : subVec)
- cout << x << " ";
- cout << "\n"; // 1 2 3
- return 0;
- }
复制代码
https://godbolt.org/z/ThvsMvqcM
6. 多维数组
严格来说,C++ 没有多维数组,有的是数组的数组。
6.1 定义与初始化
- #include <iostream>
- #include <cstddef>
- using std::cout;
- using std::size_t;
- int main() {
- // 3行4列的二维数组
- int ia[3][4];
- // 读法:ia 是大小为3的数组,每个元素是大小为4的 int 数组
- // 带初始化(嵌套花括号,直观)
- int ib[3][4] = {
- {0, 1, 2, 3}, // 第0行
- {4, 5, 6, 7}, // 第1行
- {8, 9, 10, 11} // 第2行
- };
- // 等价写法(省略内层花括号)
- int ic[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};
- // 只初始化每行的第一个元素
- int id[3][4] = {{0}, {4}, {8}}; // 其余补0
- // 访问元素
- cout << ib[2][3] << "\n"; // 11(第2行第3列)
- // 引用绑定到某一行
- int (&row)[4] = ib[1]; // row 引用第1行(一个含4个int的数组)
- cout << row[0] << "\n"; // 4
- return 0;
- }
复制代码
https://godbolt.org/z/zEKEMxaoY
内存布局(行优先存储):
- ib[3][4] 在内存中:
- col0 col1 col2 col3
- row0 [ 0 | 1 | 2 | 3 ]
- row1 [ 4 | 5 | 6 | 7 ]
- row2 [ 8 | 9 | 10 | 11 ]
- ib[i][j] 的地址 = 首地址 + i*4*sizeof(int) + j*sizeof(int)
复制代码
6.2 遍历多维数组
- #include <iostream>
- #include <cstddef>
- using std::cout;
- using std::size_t;
- int main() {
- constexpr size_t rowCnt = 3, colCnt = 4;
- int ia[rowCnt][colCnt];
- // 方法一:下标遍历
- for (size_t i = 0; i != rowCnt; ++i)
- for (size_t j = 0; j != colCnt; ++j)
- ia[i][j] = i * colCnt + j; // 赋值为位置编号
- // 方法二:范围 for(外层必须用引用!)
- size_t cnt = 0;
- for (auto &row : ia) // row 是 int(&)[4],引用一整行
- for (auto &col : row) // col 是 int&
- col = cnt++;
- // 方法三:只读范围 for(外层仍需引用!)
- for (const auto &row : ia) // const 引用,防止修改
- for (auto col : row) // 这里 col 是值拷贝,不需要修改
- cout << col << " ";
- cout << "\n";
- // 为什么外层范围 for 必须用引用?
- // 如果不用引用:for (auto row : ia)
- // row 的类型会被推断为 int*(数组退化为指针),内层就无法遍历了!
- return 0;
- }
复制代码
https://godbolt.org/z/95WfPWffa
外层 for 必须用 & 的原因:
- for (auto row : ia) ← 错误!
- row 的类型被推断为 int*(数组名退化为指针)
- 内层 for (auto col : row) 就变成遍历一个指针,非法!
- for (auto &row : ia) ← 正确!
- row 的类型是 int(&)[4](对含4个int的数组的引用)
- 内层可以正常遍历
复制代码
6.3 多维数组与指针
- #include <iostream>
- using std::cout;
- using std::begin;
- using std::end;
- int main() {
- int ia[3][4] = {
- {0,1,2,3}, {4,5,6,7}, {8,9,10,11}
- };
- // ia 退化为指向第一行的指针,类型是 int(*)[4]
- int (*p)[4] = ia; // p 指向 ia[0](一个含4个int的数组)
- p = &ia[2]; // p 现在指向 ia[2]
- // 用 auto 简化(推荐)
- for (auto p = ia; p != ia + 3; ++p) { // p 是 int(*)[4]
- for (auto q = *p; q != *p + 4; ++q) // *p 是 int[4],退化为 int*
- cout << *q << " ";
- cout << "\n";
- }
- // 更简洁:用 begin/end
- for (auto p = begin(ia); p != end(ia); ++p) {
- for (auto q = begin(*p); q != end(*p); ++q)
- cout << *q << " ";
- cout << "\n";
- }
- // 类型别名简化
- using int_array = int[4]; // int_array 是"含4个int的数组"的别名
- // typedef int int_array[4]; // 等价的 typedef 写法
- for (int_array *p = ia; p != ia + 3; ++p) {
- for (int *q = *p; q != *p + 4; ++q)
- cout << *q << " ";
- cout << "\n";
- }
- return 0;
- }
复制代码
https://godbolt.org/z/x9do8a51f
多维数组遍历代码详解
完整代码
- #include <iostream>
- using std::cout;
- using std::begin;
- using std::end;
- int main() {
- int ia[3][4] = {
- {0,1,2,3}, {4,5,6,7}, {8,9,10,11}
- };
- // --- 方法一:auto 推断指针类型 ---
- int (*p)[4] = ia;
- p = &ia[2];
- // 这个auto 是int (*p)[4]
- for (auto p = ia; p != ia + 3; ++p) {
- for (auto q = *p; q != *p + 4; ++q)
- cout << *q << " ";
- cout << "\n";
- }
- // --- 方法二:begin / end 函数 ---
- for (auto p = begin(ia); p != end(ia); ++p) {
- for (auto q = begin(*p); q != end(*p); ++q)
- cout << *q << " ";
- cout << "\n";
- }
- // --- 方法三:类型别名 ---
- using int_array = int[4];
- // typedef int int_array[4]; // 等价写法
- for (int_array *p = ia; p != ia + 3; ++p) {
- for (int *q = *p; q != *p + 4; ++q)
- cout << *q << " ";
- cout << "\n";
- }
- return 0;
- }
复制代码
数组的内存结构
首先搞清楚 ia[3][4] 在内存里长什么样。
它是一个"含 3 个元素的数组",每个元素本身又是"含 4 个 int 的数组":
- col0 col1 col2 col3
- row0 [ 0 | 1 | 2 | 3 ] ia[0]
- row1 [ 4 | 5 | 6 | 7 ] ia[1]
- row2 [ 8 | 9 | 10 | 11 ] ia[2]
复制代码
在内存里是连续的一段:
- 地址: +0 +4 +8 +12 +16 +20 +24 +28 +32 +36 +40 +44
- 值: 0 1 2 3 4 5 6 7 8 9 10 11
- <------- ia[0] -------> <------- ia[1] -------> <------- ia[2] ------->
复制代码
每个 int 占 4 字节,每一行 4 个 int,共占 16 字节。
第一步:认识"指向数组的指针"
这里有一个很容易混淆的声明,先分清两种写法:
- int *p[4] -- p 是数组,含 4 个 int 指针(指针的数组)
- int (*p)[4] -- p 是指针,指向"含 4 个 int 的数组"(数组的指针)
复制代码
括号改变了优先级,让 * 先和 p 结合,所以 (*p) 是指针,[4] 说明它指向的是有 4 个元素的数组,int 是元素类型。
ia 是二维数组,它退化(decay)为指向第一行的指针:
- ia --> &ia[0] --> 指向 ia[0] 这个长度为4的数组
- 类型:int (*)[4]
复制代码
所以 int (*p)[4] = ia 就是让 p 指向 ia[0]。
- p ---> [ 0 | 1 | 2 | 3 ] ia[0]
复制代码
下一行 p = &ia[2] 让 p 跳到第三行:
- p ---> [ 8 | 9 | 10 | 11 ] ia[2]
复制代码
第二步:方法一 —— auto 推断指针类型
- for (auto p = ia; p != ia + 3; ++p) {
- for (auto q = *p; q != *p + 4; ++q)
- cout << *q << " ";
- cout << "\n";
- }
复制代码
外层循环
ia 退化为指向 ia[0] 的指针,auto 推断 p 的类型是 int (*)[4](指向含 4 个 int 的数组的指针)。
- ia + 3 --> 指向 ia[3],即第三行之后(不存在的位置,用作哨兵)
复制代码
循环条件 p != ia + 3:p 还没有越过最后一行就继续。
++p:p 向后移动一行(移动 16 字节,即 4 个 int 的大小)。
外层循环的执行过程:
- 第1次: p = ia -> p 指向 ia[0] -> 打印 0 1 2 3
- 第2次: p = ia + 1 -> p 指向 ia[1] -> 打印 4 5 6 7
- 第3次: p = ia + 2 -> p 指向 ia[2] -> 打印 8 9 10 11
- 第4次: p = ia + 3 -> p == ia+3,退出
复制代码
内层循环
- for (auto q = *p; q != *p + 4; ++q)
- 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] 为例:
- *p = ia[1] = {4, 5, 6, 7}
- q 初始指向 ia[1][0](值 4)
- 第1次: *q = 4, ++q -> q 指向 ia[1][1]
- 第2次: *q = 5, ++q -> q 指向 ia[1][2]
- 第3次: *q = 6, ++q -> q 指向 ia[1][3]
- 第4次: *q = 7, ++q -> q = *p + 4,退出
复制代码
完整流程图
- 外层 p 遍历每一行 内层 q 遍历每个元素
- ----------- -----------
- p -> ia[0] q -> ia[0][0]=0 -> ia[0][1]=1 -> ia[0][2]=2 -> ia[0][3]=3
- p -> ia[1] q -> ia[1][0]=4 -> ia[1][1]=5 -> ia[1][2]=6 -> ia[1][3]=7
- p -> ia[2] q -> ia[2][0]=8 -> ia[2][1]=9 -> ia[2][2]=10 -> ia[2][3]=11
- p == ia+3 退出
复制代码
第三步:方法二 —— begin / end 函数
- for (auto p = begin(ia); p != end(ia); ++p) {
- for (auto q = begin(*p); q != end(*p); ++q)
- cout << *q << " ";
- cout << "\n";
- }
复制代码
begin(ia) 和 end(ia) 是标准库函数(需要 ,这里通过 间接引入)。
它们对数组做的事情和容器的 begin() / end() 成员函数一样:
- begin(ia) --> 指向 ia[0] 的指针(等价于 &ia[0],类型 int(*)[4])
- end(ia) --> 指向 ia[3] 的指针(ia[3] 不存在,用作哨兵)
复制代码
对内层:
- begin(*p) --> 指向当前行第0个元素(类型 int*)
- end(*p) --> 指向当前行末尾后一位(类型 int*)
复制代码
这种写法的优点:不需要手写 ia + 3 或 *p + 4,让编译器通过数组的类型信息自动算出边界,不容易出错。
与方法一的对比:
- 方法一 方法二
- ------ ------
- p = ia p = begin(ia)
- p != ia + 3 p != end(ia) <- 更安全,不需要手写3
- q = *p q = begin(*p)
- q != *p + 4 q != end(*p) <- 更安全,不需要手写4
复制代码
第四步:方法三 —— 类型别名
- using int_array = int[4];
- for (int_array *p = ia; p != ia + 3; ++p) {
- for (int *q = *p; q != *p + 4; ++q)
- cout << *q << " ";
- cout << "\n";
- }
复制代码
using int_array = int[4] 给类型"含 4 个 int 的数组"起了别名 int_array。
效果等价于:
- typedef int int_array[4]; // 旧式写法,含义相同
复制代码
有了别名,int (*p)[4] 就可以写成 int_array *p,意思完全相同,但更直观:
- int (*p)[4] -- 读起来绕
- int_array *p -- 读起来:p 是指向 int_array 的指针
复制代码
内层循环的 int *q = *p 直接声明了类型,不依赖 auto,更明确:
- *p 的类型是 int_array(即 int[4]),退化为 int*
- 所以 q 是 int*,指向当前行的第一个 int
复制代码
输出结果
三种方法输出结果完全一致,每种方法都打印两遍(程序里写了三种,各输出一次):
- 0 1 2 3
- 4 5 6 7
- 8 9 10 11
- 0 1 2 3
- 4 5 6 7
- 8 9 10 11
- 0 1 2 3
- 4 5 6 7
- 8 9 10 11
复制代码
三种方法横向对比
| 对比项 | 方法一(auto) | 方法二(begin/end) | 方法三(类型别名) |
|---|
| 外层指针类型 | 自动推断 int(*)[4] | 自动推断 int(*)[4] | 显式 int_array* |
| 内层指针类型 | 自动推断 int* | 自动推断 int* | 显式 int* |
| 边界写法 | 手写 ia+3、*p+4 | begin/end 自动 | 手写 ia+3、*p+4 |
| 出错风险 | 手写数字容易错 | 最安全 | 手写数字容易错 |
| 可读性 | 中 | 高 | 高(类型名直观) |
| 推荐程度 | 一般 | 推荐 | 推荐 |
常见误区
误区1:忘记括号导致含义完全不同
- int *p[4] -- 含 4 个 int* 的数组(指针数组)
- int (*p)[4] -- 指向 int[4] 的指针(数组指针)
复制代码
误区2:误以为 *p 是 int
当 p 的类型是 int(*)[4] 时,*p 的类型是 int[4](一整行),不是 int。
在表达式里 int[4] 再次退化为 int*,指向该行的第一个元素。
误区3:两个指针相加
- mid = (begin(ia) + end(ia)) / 2 -- 非法!两个指针不能相加
- mid = begin(ia) + (end(ia) - begin(ia)) / 2 -- 正确
复制代码
两指针相减得到距离(ptrdiff_t,有符号整数),指针加整数才能得到新指针。
综合对比:string / vector / 数组
| 特性 | string | vector | 数组 |
|---|
| 大小 | 可变 | 可变 | 固定(编译时确定) |
| 元素类型 | 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++ 第五章:语句 — 从零理解
目录
- 简单语句
- 语句作用域
- 条件语句:if 与 switch
- 循环语句
- 跳转语句
- try 块与异常处理
1. 简单语句
1.1 表达式语句
在表达式后面加分号,就变成了"表达式语句"。语句执行后结果被丢弃:
- ival + 5; // 合法但无意义:加法结果没人用
- cout << ival; // 有意义:有"打印"这个副作用
复制代码
1.2 空语句(Null Statement)
只有一个分号,什么都不做:
什么时候用? 当语法上需要一条语句,但逻辑上不需要做任何事时。最常见的场景是循环体的工作已经在条件里完成了:
- #include <iostream>
- #include <string>
- using std::cin;
- using std::string;
- int main() {
- string s, sought = "hello";
- // 读入数据直到找到 sought 为止
- // 所有工作都在 while 条件里完成了,循环体不需要做任何事
- while (cin >> s && s != sought)
- ; // 空语句——循环体故意为空,加注释说明是刻意的
- return 0;
- }
复制代码
危险:多余的分号
- // 灾难!while 的循环体是那个空语句,++iter 永远不在循环里
- while (iter != svec.end()) ; // 这个分号就是循环体!
- ++iter; // 这句话在循环外,只执行一次
复制代码
1.3 块(复合语句)
用花括号括起来的一组语句,整体算一条语句:
- // while 只允许一条语句作为循环体
- // 用花括号把两条语句变成一条"块"
- while (val <= 10) {
- sum += val;
- ++val;
- }
复制代码
关键点:
- 块不以分号结尾(这点和普通语句不同)
- 块内部定义的变量,出了块就不能用了(作用域)
- 空块 {} 等价于空语句
2. 语句作用域
在 if、switch、while、for 的控制结构里定义的变量,只在该语句内部有效:
- #include <iostream>
- #include <vector>
- using std::vector;
- using std::cout;
- int main() {
- vector<int> v = {1, -2, 3, -4, 5};
- // i 在 while 条件里定义,只在 while 内有效
- // while (int i = get_num())
- // cout << i;
- // i = 0; // 错误!i 在 while 外面不存在
- // 如果循环结束后还需要用 beg,就要在外面定义
- auto beg = v.begin();
- while (beg != v.end() && *beg >= 0)
- ++beg;
- // beg 在这里仍然有效
- if (beg == v.end())
- cout << "所有元素都大于等于0\n";
- else
- cout << "第一个负数是:" << *beg << "\n";
- return 0;
- }
复制代码
规则: 控制结构内定义的变量必须初始化(因为它的值会立刻被该结构用到)。
3. 条件语句:if 与 switch
3.1 if 语句
条件为真执行语句A,为假执行语句B(else 分支可省略)。
- #include <iostream>
- #include <string>
- #include <vector>
- using std::cout;
- using std::string;
- using std::vector;
- int main() {
- const vector<string> scores = {"F", "D", "C", "B", "A", "A++"};
- int grade = 85;
- string lettergrade;
- // 基本 if-else:区分及格和不及格
- if (grade < 60)
- lettergrade = scores[0]; // 不及格:直接取 "F"
- else
- // (grade - 50) / 10:把分数段映射到下标
- // grade=60 -> (60-50)/10 = 1 -> "D"
- // grade=70 -> (70-50)/10 = 2 -> "C"
- // grade=100-> (100-50)/10= 5 -> "A++"
- lettergrade = scores[(grade - 50) / 10];
- // 嵌套 if:再根据个位数加 +/-
- if (grade < 60) {
- lettergrade = scores[0];
- } else {
- lettergrade = scores[(grade - 50) / 10];
- if (grade != 100) { // 100分已经是 A++,不需要再加
- if (grade % 10 > 7) // 个位数是 8 或 9
- lettergrade += '+';
- else if (grade % 10 < 3) // 个位数是 0、1 或 2
- lettergrade += '-';
- }
- }
- cout << grade << " 分 -> " << lettergrade << "\n";
- // 85 分 -> B+(85%10=5,不加符号,实际输出 B)
- // 改成 88 分 -> B+
- // 改成 61 分 -> D-
- return 0;
- }
复制代码
3.2 悬垂 else(Dangling else)
当 if 比 else 多时,else 和谁配对?
C++ 规则:else 总是和最近的、还没配对的 if 配对。
- // 看起来像这样(程序员的意图):
- if (grade % 10 >= 3)
- if (grade % 10 > 7)
- lettergrade += '+';
- else // 程序员以为这个 else 配外层 if
- lettergrade += '-';
- // 但实际上编译器理解为:
- if (grade % 10 >= 3) {
- if (grade % 10 > 7)
- lettergrade += '+';
- else // 这个 else 配内层 if!
- lettergrade += '-'; // 个位数 3~7 都会加 '-',这是 bug!
- }
复制代码
解决方法:用花括号强制指定配对关系
- if (grade % 10 >= 3) {
- if (grade % 10 > 7)
- lettergrade += '+';
- } // 花括号让内层 if 在这里结束
- else // 现在 else 只能配外层 if
- lettergrade += '-';
复制代码
悬垂 else 的配对规则可视化:
- if (A) <- 外层 if
- if (B) <- 内层 if
- X
- else <- else 配内层 if(C++ 规定)
- Y
复制代码
3.3 switch 语句
switch 适合从多个固定选项中选一个执行:
- #include <iostream>
- using std::cin;
- using std::cout;
- using std::endl;
- int main() {
- unsigned aCnt = 0, eCnt = 0, iCnt = 0, oCnt = 0, uCnt = 0;
- unsigned otherCnt = 0;
- char ch;
- while (cin >> ch) {
- switch (ch) {
- // 多个 case 共享同一段代码:统计所有元音
- case 'a': case 'A':
- ++aCnt;
- break; // break:跳出 switch,执行后面的代码
- case 'e': case 'E':
- ++eCnt;
- break;
- case 'i': case 'I':
- ++iCnt;
- break;
- case 'o': case 'O':
- ++oCnt;
- break;
- case 'u': case 'U':
- ++uCnt;
- break;
- default: // 没有任何 case 匹配时执行
- ++otherCnt;
- break;
- }
- }
- cout << "a: " << aCnt << "\n"
- << "e: " << eCnt << "\n"
- << "i: " << iCnt << "\n"
- << "o: " << oCnt << "\n"
- << "u: " << uCnt << "\n"
- << "其他: " << otherCnt << endl;
- return 0;
- }
复制代码
switch 的执行流程:
fall-through(穿透)是常见 bug:
- // 错误示例:忘记写 break
- switch (ch) {
- case 'a':
- ++aCnt; // 没有 break!
- case 'e':
- ++eCnt; // 没有 break!
- case 'i':
- ++iCnt; // 没有 break!
- // ...
- }
- // 如果 ch == 'a',则 aCnt、eCnt、iCnt 都会 +1!(连锁穿透)
复制代码
case 标签的规则:
- 必须是整型常量表达式(不能是变量,不能是浮点数)
- 不能有两个相同的 case 值
- case 标签本身只是一个"跳转目标",不是独立的作用域
switch 内定义变量的限制:
- switch (x) {
- case true:
- string s = "hello"; // 错误!如果跳到 false 分支,s 会被绕过但仍在作用域内
- int i = 0; // 错误!同上
- int j; // 可以:没有初始化,绕过不影响
- break;
- case false:
- j = 42; // 合法:j 在作用域内但未初始化
- // s 在作用域内但没被初始化——危险!
- }
- // 正确做法:用块限制变量的作用域
- switch (x) {
- case true: {
- string s = "hello"; // 合法:s 的作用域在块内
- break;
- }
- case false:
- // s 在这里不在作用域内,安全
- break;
- }
复制代码
| switch 要点 | 说明 |
|---|
| case 标签 | 必须是整型常量表达式 |
| break | 跳出 switch,必须显式写出 |
| fall-through | 没有 break 就继续执行下一个 case |
| default | 没有任何 case 匹配时执行 |
| 变量定义 | 初始化变量不能跨越 case,需用块 {} 限制作用域 |
4. 循环语句
4.1 while 循环
先检查条件,为真则执行语句,执行完再回来检查条件。
- #include <iostream>
- #include <vector>
- using std::cin;
- using std::cout;
- using std::vector;
- int main() {
- vector<int> v;
- int i;
- // 用途1:读取未知数量的数据
- while (cin >> i) // cin >> i 失败(EOF 或输入错误)时条件为 false
- v.push_back(i);
- // 用途2:需要在循环结束后使用控制变量
- auto beg = v.begin();
- while (beg != v.end() && *beg >= 0)
- ++beg;
- // 循环结束后 beg 仍然可用:要么 == end(),要么指向第一个负数
- if (beg == v.end())
- cout << "全是非负数\n";
- else
- cout << "第一个负数:" << *beg << "\n";
- return 0;
- }
复制代码
while 循环的执行流程:
- +-------+
- ---> | 检查条件 | --假--> 退出循环
- | +-------+
- | | 真
- | +--------+
- | | 执行循环体 |
- | +--------+
- | |
- +--------+
复制代码
注意: while 循环体内定义的变量,每次迭代都会被创建和销毁。
4.2 传统 for 循环
执行顺序:
初始化(仅一次)
→
条件
→
循环体
→
表达式
→
条件
→
⋯
\text{初始化(仅一次)} \to \text{条件} \to \text{循环体} \to \text{表达式} \to \text{条件} \to \cdots
初始化(仅一次)→条件→循环体→表达式→条件→⋯
- #include <iostream>
- #include <string>
- #include <vector>
- #include <cctype>
- using std::cout;
- using std::string;
- using std::vector;
- using std::cin;
- int main() {
- string s = "Hello World";
- // 经典 for:把第一个单词大写
- for (decltype(s.size()) index = 0;
- index != s.size() && !isspace(s[index]);
- ++index)
- {
- s[index] = toupper(s[index]);
- }
- // index 在 for 外面不可访问
- cout << s << "\n"; // HELLO World
- // for 头部定义多个变量(必须同类型)
- vector<int> v = {1, 2, 3, 4, 5};
- // 在末尾追加当前所有元素的副本(翻倍)
- // sz 记录原始大小,防止循环条件随着 push_back 而变化
- for (decltype(v.size()) i = 0, sz = v.size(); i != sz; ++i)
- v.push_back(v[i]);
- // v 现在是 {1,2,3,4,5,1,2,3,4,5}
- // 省略初始化:
- auto beg = v.begin();
- for ( ; beg != v.end() && *beg >= 0; ++beg)
- ; // 找第一个负数(本例没有)
- // 省略条件(等价于 while(true),循环体必须有退出机制)
- for (int i = 0; ; ++i) {
- if (i >= 5) break;
- cout << i << " ";
- }
- cout << "\n"; // 0 1 2 3 4
- // 省略表达式:
- for (int i; cin >> i; ) // 条件里读数据
- v.push_back(i);
- return 0;
- }
复制代码
for 循环四步骤详解(以实际例子为例):
- for (int i = 0; i < 5; ++i)
- cout << i;
- 步骤1: int i = 0 (只执行一次)
- 步骤2: 检查 i < 5 (true,继续)
- 步骤3: cout << i (执行循环体)
- 步骤4: ++i (更新 i)
- 回到步骤2...
- 当 i = 5 时,步骤2为 false,退出
复制代码
4.3 范围 for 循环
自动遍历序列中的每个元素,比传统 for 更简洁、不容易越界:
- #include <iostream>
- #include <vector>
- using std::cout;
- using std::vector;
- int main() {
- vector<int> v = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
- // 只读:auto 推断元素类型
- for (auto x : v)
- cout << x << " ";
- cout << "\n";
- // 读写:必须用引用 &,否则修改的是副本,原来的 v 不变
- for (auto &r : v)
- r *= 2; // 每个元素翻倍
- for (auto x : v)
- cout << x << " ";
- cout << "\n"; // 0 2 4 6 8 10 12 14 16 18
- // 范围 for 的等价传统 for 写法:
- // for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
- // auto &r = *beg;
- // r *= 2;
- // }
- // 注意:end() 的值被缓存了,所以循环体内不能 push_back!
- return 0;
- }
复制代码
为什么范围 for 内不能 push_back?
范围 for 在开始时缓存了 end() 的值。如果 push_back 导致 vector 重新分配内存,原来缓存的 end() 就失效了,行为未定义。
4.4 do-while 循环
先执行一次,再检查条件。保证循环体至少执行一次。注意 while 后面有分号!
- #include <iostream>
- #include <string>
- using std::cin;
- using std::cout;
- using std::string;
- int main() {
- string rsp; // 注意:条件里用到的变量必须在 do 外面定义!
- do {
- cout << "请输入两个数字:";
- int val1 = 0, val2 = 0;
- cin >> val1 >> val2;
- cout << val1 << " + " << val2 << " = " << (val1 + val2) << "\n";
- cout << "继续?(y/n): ";
- cin >> rsp;
- } while (!rsp.empty() && rsp[0] != 'n'); // 条件在这里,用了 rsp
- // ^ // do 外定义,这里才能用
- return 0;
- }
复制代码
三种循环对比:
| 循环 | 检查条件时机 | 最少执行次数 | 典型使用场景 |
|---|
| while | 先检查 | 0 次 | 读取未知数量的数据 |
| for | 先检查 | 0 次 | 步进式遍历,已知次数 |
| do-while | 后检查 | 1 次 | 至少做一次,如菜单选择 |
5. 跳转语句
5.1 break:跳出循环或 switch
break 终止最近一层的 while、do-while、for 或 switch,执行它后面的第一条语句。
- #include <iostream>
- #include <string>
- #include <vector>
- using std::cin;
- using std::cout;
- using std::string;
- using std::vector;
- int main() {
- string buf;
- vector<string> svec = {"hello", "-world end", "+test"};
- // break 只影响最近一层循环/switch
- for (auto &buf : svec) {
- switch (buf[0]) {
- case '-':
- for (auto it = buf.begin() + 1; it != buf.end(); ++it) {
- if (*it == ' ')
- break; // break #1:只跳出内层 for 循环
- cout << *it;
- }
- cout << "\n";
- break; // break #2:跳出 switch
- case '+':
- cout << "plus: " << buf.substr(1) << "\n";
- break; // break #3:跳出 switch
- }
- // break #2 和 #3 跳到这里
- }
- // break #1 跳到 for 循环的下一次迭代,不到这里
- return 0;
- }
复制代码
break 的作用范围示意:
- while (...) { <-- break #2 跳到这里之后(while 的下一次条件判断)
- switch (...) {
- case '-':
- for (...) { <-- break #1 跳到 for 之后
- if (...)
- break; // #1:只结束 for
- }
- break; // #2:结束 switch
- case '+':
- break; // #3:结束 switch
- }
- }
复制代码
5.2 continue:跳过本次迭代
continue 跳过当前迭代的剩余部分,直接开始下一次迭代。只能用在循环里(不能单独用在 switch 里,除非 switch 在循环里面)。
- #include <iostream>
- #include <string>
- using std::cin;
- using std::cout;
- using std::string;
- int main() {
- string buf;
- // 只处理以下划线开头的单词
- while (cin >> buf && !buf.empty()) {
- if (buf[0] != '_')
- continue; // 不以 '_' 开头:跳过,读下一个单词
- // 到这里说明 buf 以 '_' 开头
- cout << "处理:" << buf << "\n";
- }
- return 0;
- }
复制代码
break vs continue 的区别:
- while (condition) {
- if (some_check)
- break; // 完全退出 while
- if (other_check)
- continue; // 跳到 while 的条件判断,开始下一次迭代
- // 正常处理
- }
复制代码
5.3 goto:无条件跳转(不推荐使用)
goto 可以跳转到同一函数内的任意标签处。标签是"标识符 + 冒号"的形式:
- // goto 的典型危险:
- goto end;
- int ix = 10; // 错误!goto 跳过了 ix 的初始化,但 ix 还在作用域内
- end:
- ix = 42; // ix 未初始化就使用,行为未定义
- // 向后跳是允许的(会销毁并重新创建变量)
- begin:
- int sz = get_size();
- if (sz <= 0)
- goto begin; // 跳回 begin,sz 被销毁并重新初始化
复制代码
强烈建议不要使用 goto。它使程序流程难以理解和维护。上面的 goto begin 完全可以用循环替代:
- int sz;
- do { sz = get_size(); } while (sz <= 0);
复制代码
6. try 块与异常处理
6.1 什么是异常?
异常是程序运行时遇到的"意外情况":
- 数据库连接中断
- 用户输入非法数据
- 内存不足
异常处理让"发现问题的代码"和"处理问题的代码"分离。
6.2 三个组成部分
6.3 throw:抛出异常
- #include <iostream>
- #include <stdexcept> // runtime_error 定义在这里
- #include <string>
- using std::cin;
- using std::cout;
- using std::string;
- using std::runtime_error;
- struct Sales_item {
- string isbn() const { return bookNo; }
- string bookNo;
- };
- int main() {
- Sales_item item1, item2;
- // 假设已读入 item1 和 item2
- // 检查条件,不满足就抛出异常
- if (item1.isbn() != item2.isbn())
- throw runtime_error("Data must refer to same ISBN");
- // throw 之后的代码不会执行,控制权转移到最近的 catch
- cout << "ISBN 匹配,可以相加\n";
- return 0;
- }
复制代码
throw 后面跟的是一个异常对象,类型决定了哪个 catch 能捕获它。
6.4 try-catch:捕获异常
- #include <iostream>
- #include <stdexcept>
- #include <string>
- using std::cin;
- using std::cout;
- using std::endl;
- using std::string;
- using std::runtime_error;
- int main() {
- // 完整的 try-catch 示例:
- while (true) {
- try {
- // try 块:正常逻辑在这里
- // 如果出现问题,这里的代码会抛出异常
- int a, b;
- cout << "输入两个整数(除法):";
- cin >> a >> b;
- if (b == 0)
- throw runtime_error("除数不能为零"); // 抛出异常
- cout << a << " / " << b << " = " << (a / b) << "\n";
- break; // 正常完成,跳出 while
- } catch (runtime_error err) {
- // catch 块:处理 runtime_error 类型的异常
- // err.what() 返回初始化时传入的字符串
- cout << err.what() << "\n请重试?(y/n): ";
- char c;
- cin >> c;
- if (!cin || c == 'n')
- break; // 用户不想重试,退出
- // 如果用户回答 y,while 继续,再次执行 try 块
- }
- }
- return 0;
- }
复制代码
try-catch 的执行流程:
- try {
- 语句1;
- 语句2; <- 这里抛出异常
- 语句3; <- 不执行了
- }
- catch (ExceptionType e) {
- 处理异常; <- 控制权跳到这里
- }
- // 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_cast | | dynamic_cast 失败 |
使用规则:
- exception、bad_alloc、bad_cast:只能默认初始化,不能传字符串
- 其他类型:必须用 string 或 C 风格字符串初始化,不能默认初始化
- #include <stdexcept>
- #include <string>
- using std::runtime_error;
- using std::out_of_range;
- using std::string;
- // 完整示例:综合使用异常
- double safe_divide(double a, double b) {
- if (b == 0.0)
- throw runtime_error("除数为零"); // 必须提供字符串
- return a / b;
- }
- double safe_index(double arr[], int size, int i) {
- if (i < 0 || i >= size)
- throw out_of_range("下标越界"); // 必须提供字符串
- return arr[i];
- }
- int main() {
- try {
- double result = safe_divide(10.0, 0.0);
- } catch (runtime_error &e) {
- // e.what() 返回 "除数为零"
- // 接受引用避免拷贝,更高效
- }
- double arr[] = {1.0, 2.0, 3.0};
- try {
- double val = safe_index(arr, 3, 5); // 下标5越界
- } catch (out_of_range &e) {
- // e.what() 返回 "下标越界"
- }
- return 0;
- }
复制代码
综合完整示例
- // 综合运用本章所有知识点的完整程序
- #include <iostream>
- #include <vector>
- #include <string>
- #include <stdexcept>
- #include <cctype>
- using std::cin;
- using std::cout;
- using std::endl;
- using std::vector;
- using std::string;
- using std::runtime_error;
- // 统计文本中各类字符的数量
- int main() {
- unsigned vowelCnt = 0; // 元音
- unsigned digitCnt = 0; // 数字
- unsigned spaceCnt = 0; // 空白
- unsigned otherCnt = 0; // 其他
- char ch;
- string rsp;
- do {
- // do-while:至少执行一次,用于反复询问
- cout << "请输入一段文本(Ctrl+D/Z 结束):\n";
- // while:读取未知数量的字符
- while (cin.get(ch)) {
- // switch:根据字符类型分类统计
- switch (ch) {
- case 'a': case 'e': case 'i': case 'o': case 'u':
- case 'A': case 'E': case 'I': case 'O': case 'U':
- ++vowelCnt;
- break;
- case '0': case '1': case '2': case '3': case '4':
- case '5': case '6': case '7': case '8': case '9':
- ++digitCnt;
- break;
- case ' ': case '\t': case '\n':
- ++spaceCnt;
- break;
- default:
- ++otherCnt;
- break;
- }
- }
- // for:打印结果
- vector<string> labels = {"元音", "数字", "空白", "其他"};
- vector<unsigned> counts = {vowelCnt, digitCnt, spaceCnt, otherCnt};
- for (decltype(labels.size()) i = 0; i < labels.size(); ++i)
- cout << labels[i] << ": " << counts[i] << "\n";
- cout << "继续?(y/n): ";
- cin.clear(); // 清除 EOF 标志
- cin >> rsp;
- } while (!rsp.empty() && rsp[0] != 'n');
- return 0;
- }
复制代码
各语句速查表
| 语句 | 语法 | 要点 |
|---|
| 表达式语句 | expr; | 结果被丢弃,利用副作用 |
| 空语句 | ; | 需要语句但无事可做 |
| 块 | { ... } | 不以分号结尾 |
| if | if (c) s1 else s2 | else 配最近未匹配的 if |
| switch | switch (e) { case v: ... } | 需要 break,标签必须是整型常量 |
| while | while (c) s | 先判断再执行,0次或多次 |
| for | for (init; c; expr) s | 三部分都可省略 |
| 范围 for | for (d : seq) s | 循环体内不能改变序列大小 |
| do-while | do s while (c); | 先执行再判断,至少1次 |
| break | break; | 跳出最近的循环或 switch |
| continue | continue; | 跳过本次迭代,继续下一次 |
| goto | goto label; | 不推荐使用 |
| throw | throw expr; | 抛出异常,终止当前执行路径 |
| try-catch | try { } catch (T e) { } | 捕获并处理指定类型的异常 |