自在学

我们与你共同进步

  • 分类课程
  • 文章
  • 工作台
  • 订阅

  • 关于我们
  • 隐私政策
  • 使用条款

探索

  • 分类课程
  • 文章
  • 工作台
  • 订阅

网站信息

  • 关于我们
  • 隐私政策
  • 使用条款

加入社区

自在学学习社区微信二维码

微信扫码,交流学习

株洲市自在学教育科技有限公司© 2025 - 2026 版权所有

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

湘公网安备43020302000292号|湘ICP备2025148919号-1
分类课程工作台文章订阅
分类课程工作台文章价格

C++

  1. 01C++入门
  2. 02变量与基本类型
  3. 03特殊类型: 字符串、vector 与数组
  4. 04表达式
  5. 05语句
  6. 06函数
  7. 07类与对象
  8. 08输入与输出
  9. 09顺序容器
  10. 10泛型算法
  11. 11关联容器
  12. 12动态内存管理
  13. 13拷贝控制
  14. 14运算符重载与类型转换
  15. 15面向对象编程
  16. 16模板与泛型编程
  17. 17专用库设施
  18. 18高级技术与实用工具
正在加载课程章节内容
课程编程C++表达式

C++ 表达式:从分组、类型到求值顺序

下面这行代码看起来像是在计算平均值:

cpp
int total{11};
int count{4};
double average = total / count;

average 最终却是 2.0,不是 2.75。问题不在赋值目标,而在右侧表达式:total 和 count 都是 int,所以除法在赋值前已经完成了整数运算。等结果再转换成 double,被舍去的小数部分不会回来。

表达式里的许多错误都长这样:代码符合语法,编译器也能生成程序,但程序员误判了分组、类型或执行顺序。读一个表达式时,可以固定问四件事:

  1. 运算符和操作数怎样分组?
  2. 每个子表达式的结果类型是什么?
  3. 结果标识对象,还是一个临时计算值?
  4. 求值过程中会修改哪些状态?

这四个问题会贯穿后面的例子。所有完整程序都按 C++17 编写;如果某段代码专门展示编译错误或未定义行为,会明确标出,不应拿去运行。


先看清表达式产生了什么

表达式是一段可以被求值的代码。字面量 42 是表达式,变量名 price 是表达式,price * count、函数调用 read() 和赋值 count = 5 也都是表达式。

运算符说明要做什么,操作数提供参与运算的内容。操作数本身还可以是表达式,因此复杂表达式是一层套一层的结构:

cpp
(price + shipping) * quantity

最外层是乘法。它的左操作数是括号内的加法表达式,右操作数是 quantity。

每次读表达式,都可以做一张“四项清单”:

问题count + 1++count
算出的值原值加一递增后的值
结果类型通常是 int与 count 相同
值类别纯右值左值
副作用无修改 count
C++ 表达式四问模型对比图:++count 与 count + 1 的值、类型、值类别和副作用
两个表达式都得到原值加一;内置前置 ++ 的结果是标识 count 对象的左值,并修改 count,而 count + 1 是不标识对象的纯右值,也不修改 count。

“纯右值”和“左值”稍后会专门解释。此处先抓住重点:两个表达式都能算出值,只有第二个会改动对象。

表达式后面加分号,就可以形成表达式语句:

cpp
count = 5;
++count;
print_total();

这些语句分别产生赋值、递增和函数调用的效果。反过来,下面两条语句虽然合法,却把结果直接丢掉了:

cpp
1 + 2;
count * 10;

如果表达式既没有保存结果,也没有可观察的副作用,它通常没有实际作用,编译器也可能给出警告。

初始化和赋值也要分开理解:

cpp
int score{80};  // 初始化:对象刚建立时取得第一个值
score = 95;     // 赋值:替换对象已有的值

赋值运算本身仍然有结果,所以可以链式写:

cpp
int left{0};
int right{0};
left = right = 7;

这段代码会把两个对象都设为 7。它能成立,是因为 right = 7 求值后还能把赋值结果交给外层赋值。不过,能写成一个表达式不等于总该这样写;出现不同业务动作时,分成两条语句更容易检查。

常量表达式是可以在编译期求值的表达式:

cpp
constexpr int minutes_per_hour{60};
constexpr int seconds_per_minute{60};
constexpr int seconds_per_hour = minutes_per_hour * seconds_per_minute;

seconds_per_hour 的初始化不依赖运行时输入,编译器可以直接得到 3600。如果一个值要到运行时才能取得、但初始化后不再修改,则可以用 const 保存。

表达式的类型、值类别和副作用是三件独立的事。只知道“结果等于多少”还不够;赋值是否合法、引用能否绑定、程序状态是否改变,都依赖另外两项。

小节测试

1
下列哪一项是表达式,但通常没有副作用?
2
在 C++ 中,赋值只能是一种语句,不能出现在更大的表达式里。

运算符先形成表达式树

同一个表达式里出现多个运算符时,编译器先按语法规则确定分组。最可靠的心智模型不是“从左往右扫”,而是画出一棵表达式树。

2 + 3 * 4 的结构是:

text
      +
     / \
    2   *
       / \
      3   4

乘法节点位于更深的子树中,因此整个表达式按 2 + (3 * 4) 分组。

C++ 表达式 2 + 3 * 4 与 (2 + 3) * 4 的表达式树对比,展示运算符优先级、括号分组及结果 14 和 20
乘法优先级使 2 + 3 * 4 先计算乘法得到 14;括号把加法变成更深的子表达式后,(2 + 3) * 4 得到 20。

运算符可以按操作数数量区分:

形式例子说明
一元-value、!ready、++index一个操作数
二元a + b、x == y、ok && ready两个操作数
三元condition ? a : b三个操作数

同一个符号还可能承担不同角色。-value 是一元取负,left - right 是二元减法;*p 是解引用,a * b 是乘法。编译器根据语法位置和操作数类型判断含义。

优先级决定不同运算符谁结合得更紧。日常代码最常见的层级如下,从上到下逐渐降低:

层级常见运算符分组示例
后缀与访问()、[]、.、->、后缀 ++items[i]
一元前缀 ++、--、!、*、&!ready
乘除余*、/、%a + (b * c)
加减+、-(a + b) < c
移位<<、>>(x + 1) << 2
关系<、<=、>、>=(a + b) < c
相等==、!=(a < b) == flag
逻辑与、逻辑或&&、`
条件?:ok ? a : b
赋值=、+=、*= 等x = (y + 1)
逗号运算符,(first(), second())

结合性只在同一优先级连续出现时决定分组方向。加减乘除通常左结合:

cpp
10 - 5 - 3        // (10 - 5) - 3,结果是 2

赋值和条件运算符通常右结合:

cpp
a = b = 3         // a = (b = 3)

下面的完整程序同时展示优先级、结合性和一个常见陷阱:

cpp
#include <iostream>
 
int main() {
    std::cout << std::boolalpha;
    std::cout << "2 + 3 * 4 = " << 2 + 3 * 4 << '\n';
    std::cout << "(2 + 3) * 4 = " << (2 + 3) * 4 << '\n';
    std::cout << "10 - 5 - 3 = " << 10 - 5 - 3 << '\n';
 
    int a{0};
    int b{0};
    a = b = 3;
    std::cout << "a = " << a << ", b = " << b << '\n';
 
    std::cout << "0 < 5 < 3 = " << ((0 < 5) < 3) << '\n';
}

输出:

text
2 + 3 * 4 = 14
(2 + 3) * 4 = 20
10 - 5 - 3 = 2
a = 3, b = 3
0 < 5 < 3 = true

最后一行并不是说 5 < 3 成立。表达式按 (0 < 5) < 3 分组,第一次比较得到 true,参与数值比较时相当于 1,于是又计算 1 < 3。区间判断要明确写成:

cpp
bool inside = (0 < value) && (value < 3);

复合赋值也不是纯粹的文本缩写。target += value 大致表达 target = target + value,但 target 只求值一次。若左侧包含下标递增或函数调用,机械展开可能改变行为。更好的做法是让赋值目标本身保持简单。

括号应表达意图,不必包住每一个普通算术步骤。a + b * c 已是熟悉结构;混合比较、逻辑运算或业务条件时,(age >= 18) && has_id 会更容易确认。

优先级和结合性只决定表达式怎样分组。它们没有承诺所有子表达式在运行时按同样的先后顺序求值;这两个问题必须分开分析。

下面的观察器把完整括号式、表达式树和逐步求值放在同一幅画面中。切换案例时,先预测根节点是哪个运算符,再用树结构检查分组是否正确。

小节测试

3
下列分组判断哪些正确?
4
若要让 2 + 3 先计算,再乘以 4,应写成 ____。

类型会沿着子表达式流动

表达式的结果类型不是由接收结果的变量倒推出来的。编译器先决定各个子表达式怎样运算、产生什么类型,最后才把结果用于初始化或赋值。

这正是开头平均值错误的原因:

cpp
double average = total / count;

右侧的两个操作数都是 int,所以 total / count 先得到 int。左侧的 double 只负责接收已经算完的结果。

内置算术常经历两步:先做整数提升,再把两边转换成适合共同计算的类型。

输入组合典型处理
bool、char、short 参与普通算术通常先提升到 int
int 与 double 一起运算int 转成 double
两个整数相除仍做整数除法
同等级有符号与无符号整数混合有符号值可能转成无符号值

下面的程序把几个结果放在一起观察:

cpp
#include <iostream>
 
int main() {
    int total{11};
    int count{4};
    char letter{'A'};
 
    auto integer_average = total / count;
    auto precise_average = static_cast<double>(total) / count;
    auto next_code = letter + 1;
 
    std::cout << "integer average = " << integer_average << '\n';
    std::cout << "precise average = " << precise_average << '\n';
    std::cout << "next code = " << next_code << '\n';
}

输出:

text
integer average = 2
precise average = 2.75
next code = 66

letter + 1 的结果不是 char,而是整数提升后的 int。在使用 ASCII 的环境中,'A' 的编码是 65,所以示例输出为 66;可移植代码不应把所有字符编码细节都硬编码成 ASCII 数值。

有符号和无符号整数混用更隐蔽:

cpp
int debt{-1};
unsigned int credit{1};
bool smaller = debt < credit;  // 结果通常与直觉相反

比较前,debt 会被转换为 unsigned int,负数变成该无符号类型中的一个很大值。因此循环下标、容器长度和差值运算应尽量使用协调的类型,不要等警告出现后再用强制转换掩盖问题。

窄化发生在结果装进目标时

从 double 到 int 会向零截断小数部分;从较大范围的整数类型到较小范围的类型,也可能无法保留原值。以下两行能编译,但可能丢信息:

cpp
double price{19.95};
int whole = price;       // 得到 19

花括号初始化会把潜在窄化变成编译错误:

cpp
int whole{price};        // 编译错误:可能窄化

如果丢弃小数确实是设计要求,可以明确表达:

cpp
int whole = static_cast<int>(price);

static_cast 让审查者看见转换意图,但它不会自动检查范围,也不会改成四舍五入。若业务要求“最近整数”,应选用相应的舍入函数;若目标值可能越界,应先检查范围。

T{value} 与 static_cast<T>(value) 因此表达两种不同态度:前者要求初始化不能窄化,后者声明程序员接受这次转换。

常量表达式也遵守相同类型规则

编译期求值不会改变算术规则:

cpp
constexpr double bad_factor = 9 / 5;      // 1.0
constexpr double good_factor = 9.0 / 5;   // 1.8

bad_factor 的目标虽然是 double,右侧仍先做整数除法。只要让一个操作数成为浮点类型,整个除法才会转为浮点运算。

显式转换不是“安全通行证”。它只把转换写得明显。目标范围、截断方式和业务含义仍需由代码检查。

小节测试

5
int total{11}; int count{4}; 要得到 2.75,哪种写法正确?
6
关于窄化和显式转换,下列哪些说法正确?

有限表示会带来精度和溢出问题

数学整数可以无限增长,内置整数类型却只能使用固定数量的位。实数可以有无限小数位,浮点类型也只能保存有限精度。表达式语法完全正确,结果仍可能因为表示范围而偏离数学答案。

浮点数首先带来舍入误差。下面的程序在常见的 IEEE 754 double 环境中会把误差显示出来:

cpp
#include <iomanip>
#include <iostream>
 
int main() {
    double sum = 0.1 + 0.2;
 
    std::cout << std::setprecision(17);
    std::cout << "sum = " << sum << '\n';
    std::cout << std::boolalpha
              << "sum == 0.3: " << (sum == 0.3) << '\n';
}

在本例环境中的输出:

text
sum = 0.30000000000000004
sum == 0.3: false

这不是随机误差。0.1、0.2 和 0.3 都需要用有限二进制位近似表示,几次近似不一定落在同一个最终位模式上。处理测量值或计算结果时,应根据问题允许的误差比较,而不是默认用 == 判断所有浮点结果。

整数溢出需要分两类:

  • 有符号整数溢出是未定义行为,不能依赖某台机器出现的“回绕结果”。
  • 无符号整数按模运算回绕,最大值再加一会得到 0,但“有定义”不表示符合业务要求。

一个很容易漏掉的问题是:转换发生得太晚。

cpp
int width{1'000'000};
int height{1'000'000};
 
long long unsafe_area = width * height;       // 先在 int 中相乘,可能溢出
long long safe_area = 1LL * width * height;   // 从第一个乘法起使用 long long

unsafe_area 的左侧类型救不了右侧已经发生的溢出。1LL 让第一个乘法就进入 long long,后续操作也沿用更宽类型。

C++ 表达式转换时机示意:double 19.95 转 int 向零截断为 19,int 相乘后赋给 long long 仍可能溢出,而 1LL 让乘法从一开始使用 long long
转换时机决定结果:目标类型是 long long 不代表乘法会自动在 long long 中进行;使用 1LL 才能让提升发生在第一次乘法之前。

更宽类型仍然有上限。处理外部输入时,可以在乘法前检查:

cpp
#include <limits>
 
bool multiplication_fits(long long left, long long right) {
    if (left < 0 || right < 0) {
        return false;  // 这个简化版本只接受非负数
    }
    if (right == 0) {
        return true;
    }
    return left <= std::numeric_limits<long long>::max() / right;
}

把检查写成除法,是为了避免“用一次可能溢出的乘法去检查乘法是否溢出”。真实项目还要根据是否允许负数补齐其他分支。

浮点转整数也有两个问题:小数部分会向零截断;若截断后的值超出目标整数可表示范围,则不能依赖结果。整数转浮点虽然能表示更大的数量级,也可能丢失低位精度。

可用 std::numeric_limits<T> 查询当前实现的范围和性质,不要把某台机器上的 int 位数当成所有环境的保证。

实践中可以按这个顺序检查数值表达式:

  1. 先估计合理数量级。
  2. 确认运算开始时的类型,而不只看接收结果的类型。
  3. 在危险运算前做边界检查。
  4. 为边界值和刚刚越界的值保留测试。

小节测试

7
long long result = int_a * int_b; 一定会使用 long long 完成乘法。
8
头文件 <limits> 中,用来查询类型 T 数值性质的模板是 std::____<T>。

值类别说明结果是否标识对象

表达式除了有类型,还具有值类别。值类别回答的是:这个结果是否关联到一个有身份、可定位的对象,以及它可以怎样被使用。

入门阶段先掌握三类:

值类别常见例子直观含义
左值 lvaluex、*pointer、array[i]标识一个仍然存在的对象
纯右值 prvalue42、x + 1、按值返回的结果计算出一个值,常用于初始化
将亡值 xvalue明确把对象视为可移交资源的表达式仍标识对象,但资源可被复用

本文只需知道将亡值属于现代分类的一部分;资源移动和右值引用有各自的使用场景,不需要为了写普通算术表达式提前引入。

变量名 x 是左值,因为它标识一个具体对象。x + 1 是纯右值,因为加法产生的是计算结果,不是名为某物的持久对象。

这会直接影响赋值:

cpp
int x{10};
x = 20;       // 正确:x 是可修改左值
x + 1 = 20;   // 编译错误:加法结果不是可修改左值

“能放在赋值号左边”只是助记法,并不完整:

cpp
const int limit{100};

表达式 limit 仍是左值,因为它标识对象;但它受 const 保护,不是可修改左值,所以不能给它重新赋值。

引用绑定能把区别显示得更清楚:

cpp
int value{10};
int* pointer{&value};
 
int& first = value;           // 绑定左值
int& second = *pointer;       // 解引用结果也是左值
const int& third = value + 1; // const 引用可以绑定临时结果

下面的程序展示引用如何继续指向原对象:

cpp
#include <iostream>
 
int main() {
    int value{10};
    int& alias = value;
    const int& temporary = value + 1;
 
    alias += 5;
 
    std::cout << "value = " << value << '\n';
    std::cout << "alias = " << alias << '\n';
    std::cout << "temporary = " << temporary << '\n';
}

输出:

text
value = 15
alias = 15
temporary = 11

alias 不是一份独立副本,对它赋值就是修改 value。temporary 绑定的是计算 value + 1 时产生的临时结果;这个结果在绑定发生时是 11,不会随着 value 后续变成 15 而重新计算。

值类别属于表达式,不是对象永远不变的标签。一个对象可以通过不同表达式被不同方式看待。日常代码里,先能区分“标识对象的表达式”和“单纯计算值的表达式”,就足以解释大多数赋值与引用问题。

左值不等于“可修改”。const 对象名也是左值;真正能作为普通赋值目标的是可修改左值。

小节测试

9
设 int x{3}; int* p{&x};,下列哪些表达式通常是左值?
10
const int limit{10}; 中,表达式 limit 不是左值,因为不能给它赋值。

求值顺序与副作用必须单独分析

表达式树回答“怎样分组”,求值顺序回答“运行时先算哪一部分”。只有把两者分开,才能正确分析带副作用的代码。

副作用是求值过程中对程序状态造成的改变,常见情况包括:

  • 赋值或递增修改对象;
  • 输入操作改变流状态并写入对象;
  • 输出操作产生可观察文本;
  • 函数调用修改参数引用、成员或外部资源。

对没有副作用的 a + b * c,操作数先后通常不影响结果。把 a、b、c 换成会修改同一状态的函数调用,顺序就可能决定行为。

C++17 中,常用规则可以整理成下表:

形式已保证的求值关系
left && right先左后右;左侧为假时不求右侧
`left
condition ? yes : no先条件,再只求一个分支
内置 left, right先左后右,结果取右侧
target = source右操作数先于左操作数求值
left + right两侧没有先后保证
function(a(), b())两个实参谁先算没有指定
C++ 求值顺序与短路规则对比:普通表达式的操作数先后未指定,而 &&、|| 和条件运算符按规则只求值必要分支
优先级只决定表达式分组,不决定普通操作数的运行先后;&&、|| 与 ?: 则会根据已得到的条件,只求值必要的部分。

函数实参在 C++17 中各自完整求值,不会彼此交错,但先后仍可能是 a() 再 b(),也可能相反。如果顺序影响结果,就不该把两个调用塞进同一参数列表。

下面的程序只使用有明确顺序的规则:

cpp
#include <iostream>
 
bool trace(const char* name, bool result) {
    std::cout << name << '\n';
    return result;
}
 
int main() {
    bool first = trace("A", false) && trace("B", true);
    bool second = trace("C", true) || trace("D", false);
 
    std::cout << std::boolalpha;
    std::cout << "first = " << first << '\n';
    std::cout << "second = " << second << '\n';
}

输出:

text
A
C
first = false
second = true

B 和 D 都没有输出,说明对应右操作数没有求值。

下面这段则不要运行:

cpp
int i{0};
int result = i++ + i++;  // 未定义行为:+ 的两侧未定序地修改同一对象

一次运行给出的数字没有解释价值。编译器可以优化、重排,甚至基于“程序不会触发未定义行为”的前提生成完全出乎直觉的结果。

另一个常见问题没有未定义行为,却仍然不稳定:

cpp
consume(read_left(), read_right());

如果两个读取动作必须按顺序发生,应明确拆开:

先执行 auto left = read_left();,把第一个动作及其结果固定在一条完整语句中。

再执行 auto right = read_right();,让第二个动作明确发生在第一条语句之后。

最后调用 consume(left, right);。此时参数求值顺序不再影响前两个读取动作。

最实用的规则是:一个表达式里若修改了某个对象,就不要再安排另一个依赖其新旧值的动作。让每条语句只承担一个明显的状态修改,调试时也更容易观察中间值。

小节测试

11
下列哪个说法正确?
12
哪些改写能减少求值顺序带来的风险?

用短路、条件和逗号表达明确选择

有些运算符不仅产生值,还明确控制哪些子表达式会被执行。正确使用它们,可以把前置条件和简单选择写得很紧凑。

逻辑与 && 常用于逐层保护:

cpp
if (pointer != nullptr && pointer->ready()) {
    use(*pointer);
}

只有指针非空,右侧成员访问才会发生。类似地,检查下标时可以写:

cpp
if (index < values.size() && values[index] > 0) {
    // 此处访问 values[index] 才有前面的范围保护
}

逻辑或 || 常用于后备条件:左侧已经为真时,右侧不再计算。

短路适合保护“只有条件满足才合法”的读取,但不要把必须发生的业务动作偷偷放在右侧:

cpp
saved || save();  // 若保存是必要动作,这种写法会掩盖意图

清楚的 if 更合适。

条件运算符在表达式层面二选一:

cpp
const char* label = score >= 60 ? "pass" : "retry";

它先求条件,只求值选中的分支。不过,两个分支都必须在语法和类型上有效,编译器还要为整个条件表达式确定类型:

cpp
auto value = ready ? 1 : 2.5;

不论选中哪一边,value 都推导为 double,因为 int 分支会转换到与 double 分支协调的类型。

条件运算符适合短小、对称、只负责选值的场景。分支里若要记录日志、修改多个对象或处理错误,应改用 if。嵌套条件运算符虽然右结合,但很难快速确认分支对应关系:

cpp
// 能写,但不利于维护
const char* level = score >= 90 ? "A" : score >= 60 ? "B" : "C";

内置逗号运算符会先求左侧,再求右侧,并把右侧结果作为整体结果:

cpp
int result = (log_start(), compute());

括号很重要,因为逗号运算符优先级最低。它最常见的合理位置是 for 的更新部分;下面假设 size > 0:

cpp
for (std::size_t left{0}, right{size - 1};
     left < right;
     ++left, --right) {
    // 两端向中间移动
}

参数列表里的逗号、声明多个变量时的逗号是分隔符,并不是这里的逗号运算符:

cpp
print(x, y);      // 分隔两个实参
int a{1}, b{2};   // 分隔两个声明项

下面的程序把三种选择规则放在一起:

cpp
#include <iostream>
 
int calls{0};
 
int probe(int value) {
    ++calls;
    std::cout << "probe " << value << '\n';
    return value;
}
 
int main() {
    bool ready{false};
 
    int selected = ready ? probe(10) : probe(20);
    bool allowed = false && (probe(30) > 0);
    int last = (probe(1), probe(2));
 
    std::cout << std::boolalpha;
    std::cout << "selected = " << selected << '\n';
    std::cout << "allowed = " << allowed << '\n';
    std::cout << "last = " << last << '\n';
    std::cout << "calls = " << calls << '\n';
}

输出:

text
probe 20
probe 1
probe 2
selected = 20
allowed = false
last = 2
calls = 3

probe(10) 没有被条件表达式选中,probe(30) 被逻辑与短路,逗号表达式则按顺序执行了 probe(1) 和 probe(2)。

提交代码前,可以沿着下面的判断顺序检查每个非平凡表达式:

  1. 不看运行结果,先用括号或表达式树确认分组。
  2. 从最内层开始标出结果类型,尤其检查整数除法和有符号/无符号混用。
  3. 找出所有赋值、递增、输入输出和可能改状态的函数。
  4. 只依据语言明确保证的求值顺序,不依据某次运行。
  5. 条件选择超过一行意图时,改写为命名变量或 if。

下面的实验室把“子表达式先算出什么类型”与“哪些分支真正执行”放在同一组执行轨迹中。先比较三种 11 / 4 的转换位置,再查看乘法范围风险和短路日志。

小节测试

13
表达式 false && side_effect() 中,side_effect 被调用的次数是 ____。
14
auto value = condition ? 1 : 2.5; 中,value 的类型是什么?

综合测试

15
int a{9}; int b{5}; double x = a / b;,x 的值是多少?
16
下列哪些表达式包含明确的短路或分支选择规则?
17
表达式 x < y < z 与 (x < y) && (y < z) 含义相同。
18
要让 int width 与 int height 从第一次乘法起就在 long long 中计算,可写成 ____ * width * height。
19
下列哪个表达式是纯右值?
20
审查带副作用的表达式时,哪些做法可靠?
21
static_cast<int>(value) 会自动确认 value 位于 int 的范围内。
22
内置逗号表达式 (first(), second()) 的整体结果来自 ____。
上一章特殊类型: 字符串、vector 与数组下一章语句