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 是程序的入口,有且只能有一个

最简单的程序

// 这是一个什么都不做、只是返回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 循环

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

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

val <= 10?

sum += val
++val

输出 sum

结束

手动追踪前几次循环:

初始: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 循环

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

for (初始化语句; 条件; 表达式)
    循环体

同样求 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 的执行顺序:

开始

初始化
int val = 1

条件
val <= 10?

执行循环体
sum += val

表达式
++val

退出循环

结束

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 类

数据成员
bookNo isbn
units_sold 销量
revenue 总收入

构造函数
默认构造
从string构造
从istream构造

成员函数
isbn 获取书号
avg_price 均价
operator+= 累加

友元函数
operator>> 输入
operator<< 输出
operator== 比较

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

// ============================================================
// 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/coutistream/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
程序逻辑流程图:

是 同一本书

否 换书了

开始

读取 total
成功?

输出 No data?!
return -1

结束

读取 trans
成功?

输出最后一条
total

return 0

total.isbn()
== trans.isbn()?

total += trans
(累加)

输出 total
total = trans
(切换新书)

示例输入:

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++ 入门第一章

程序结构

main 函数

返回类型 int

函数体 curly braces

分号结尾

输入输出

include iostream

std::cout 输出

std::cin 输入

std::cerr 错误输出

endl 换行加冲刷

命名空间 std

注释

"单行 //"

"多行 /* */"

不能嵌套

控制流

while 条件循环

for 计数循环

if/else 条件分支

读取未知数量输入

类 Class

Sales_item

对象 object

运算符重载

本文所有代码均使用 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_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 合法
大小写敏感 Abcabc 是不同名字
不能用关键字 intclass 等不能用
不能有连续两个下划线 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 = &pi;   // 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 = &pi;   // 指针可以改变,但 *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)

综合流程图

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

没有,全局

没有,局部

没有

程序员写 int x = 42

是否有初始值?

初始化为给定值

初始化为0

未定义的垃圾值
危险!

变量可以安全使用

使用该变量
行为未定义

是否有 const?

值不可改变

值可以被赋值修改

指针与引用的关系总览

指针 ptr

指向

ptr
存地址 0x1000

ival: 42
地址 0x1000

引用 ref

别名,无独立地址

ref

ival: 42

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++ 第三章:字符串、向量和数组 — 从零理解

目录

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

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

1.1 为什么需要 using 声明?

C++ 标准库的所有名字都放在 std 命名空间里。每次用 std::cinstd::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()} 0index<s.size()
  • 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<T>::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+2endbeg

迭代器算术 含义
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 - it2difference_type p1 - p2ptrdiff_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 字符串函数(<cstring> 含义
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] = ia;

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

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";
}

外层循环

auto p = ia

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) 是标准库函数(需要 <iterator>,这里通过 <iostream> 间接引入)。
它们对数组做的事情和容器的 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 / 数组

需要存储字符序列?

长度固定?

需要存储对象序列?

char 数组
固定大小,低级

std::string
推荐!可变长,安全

长度固定?

其他类型

内置数组
固定大小,低级

std::vector
推荐!可变大小,安全


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

关键概念速查


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

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

目录

  1. 简单语句
  2. 语句作用域
  3. 条件语句:if 与 switch
  4. 循环语句
  5. 跳转语句
  6. 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. 语句作用域

ifswitchwhilefor 的控制结构里定义的变量,只在该语句内部有效:

#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 语句

if (条件)
    语句A
else
    语句B

条件为真执行语句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)

ifelse 多时,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 的执行流程:

没有

没有

计算 switch 表达式的值

与 case 'a' 匹配?

执行 case 'a' 代码

有 break?

跳出 switch

继续执行下一个 case 的代码
fall-through 穿透!

与 case 'e' 匹配?

执行 case 'e' 代码

...更多 case...

有 default?

执行 default 代码

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 循环

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 循环

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 (声明 : 序列)
    语句

自动遍历序列中的每个元素,比传统 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 循环

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 终止最近一层whiledo-whileforswitch,执行它后面的第一条语句。

#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 三个组成部分

throw 表达式
抛出异常

异常对象
携带错误信息

try 块
包裹可能出错的代码

catch 子句
捕获并处理异常

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:

找到

没找到

找到

没找到

throw 抛出异常

在当前函数查找匹配的 catch

执行 catch 块

当前函数退出
沿调用链向上找

在调用者函数查找 catch

继续向上...

所有调用链都没有匹配的 catch

调用 terminate 函数
程序终止

6.5 标准异常类


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

使用规则:

  • exceptionbad_allocbad_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) { } 捕获并处理指定类型的异常

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐