返回博客

深度辨析:C++与JS中的赋值与求值顺序——从两道LeetCode经典题说起

2026-03-242 min 阅读
C++JavaScript

在刷题时,我们经常要在数组中交换元素。面对“交换元素并移动指针”的需求,不同语言的底层机制决定了我们能怎么写代码。今天我们借着两道 LeetCode 经典题,彻底把 C++ 和 JavaScript 的等号赋值顺序函数传参计算顺序弄清楚。

一、 原题重现

我们先来看两道大家非常熟悉的题目:

1. LeetCode 189. 轮转数组

题目大意:给定一个数组,将数组中的元素向右轮转 k 个位置。 核心解法:三次翻转法。在这个过程中,我们需要实现一个 reverse 交换逻辑。

  • JavaScript 解法片段(解构赋值 + 行内自增减)
    const reverse = function(nums, begin, end){
        while(begin < end){
            [nums[begin++], nums[end--]] = [nums[end], nums[begin]];
        }
    }
  • C++ 解法片段(swap 函数 + 行内自增减)
    void reverse(vector<int>& nums, int begin, int end){
        while(begin < end){
            swap(nums[begin++], nums[end--]);
        }
    }

2. LeetCode 75. 颜色分类 (荷兰国旗问题)

题目大意:原地对包含 0、1、2 的数组进行排序。 核心解法:双指针法。

  • JavaScript 解法片段(解构赋值 + 行内自增减)
    if(nums[i] == 0){
        [nums[i++], nums[l++]] = [nums[l], nums[i]];
    }
  • C++ 解法片段(拆分书写)
    if(nums[i] == 0){
        swap(nums[i], nums[l]);
        l++;
        i++;
    }

在 C++ 的《颜色分类》解法中,我们没有使用行内的 swap(nums[i++], nums[l++]),这仅仅是代码风格的选择。但如果真的写成一行,或者用等号赋值,语言底层到底是从左向右算,还是从右向左算?这就是我们今天要讲的核心。


二、 等号赋值:从左向右,还是从右向左?

赋值操作符 = 本身是右结合的(即 a = b = c 等价于 a = (b = c)),但这说的是“结合性”。当我们讨论 LHS = RHS(左侧 = 右侧)时,求值顺序(先计算左边还是右边)在 C++ 和 JS 中截然不同。

1. JavaScript 的赋值:严格的“先左后右”与解构魔法

在 JavaScript 中,普通赋值操作的求值顺序是严格的:先计算左侧(LHS),再计算右侧(RHS),最后赋值

  • 普通赋值a[i++] = i

    1. JS 引擎先看左边:确定 a[i++] 的内存引用。此时 i 被返回旧值(假设为 0),随后 i 自增变为 1。左侧锁定了对 a[0] 的引用。
    2. 再看右边:计算 i 的值。因为刚才 i 已经自增了,所以此时右侧的 i 是 1。
    3. 最后赋值:将 1 赋给 a[0]
  • 解构赋值(重点)[nums[i++], nums[l++]] = [nums[l], nums[i]] 解构赋值打破了普通赋值的直觉!它的规范要求:必须先将等号右侧(RHS)完整地计算并打包,然后再从左到右依次处理左侧(LHS)的赋值目标。 假设 i=1, l=0

    1. 先彻底计算右侧:此时任何变量都没自增,右侧打包为一个临时数组 [nums[0], nums[1]]
    2. 再从左到右处理左侧
      • 计算第一个目标 nums[i++]:锁定 nums[1] 的引用,i 变为 2。把临时数组的第 0 项(原 nums[0])赋给它。
      • 计算第二个目标 nums[l++]:锁定 nums[0] 的引用,l 变为 1。把临时数组的第 1 项(原 nums[1])赋给它。 结论:正是因为 JS 解构赋值先求值右侧的特性,我们才能安全地把 ++ 写在左边,而不用担心右侧取到自增后的脏数据。

2. C++ 的赋值:C++17 带来的“先右后左”

C++ 的普通赋值 E1 = E2,在不同版本的标准中表现不同:

  • C++17 之前:先计算左边还是右边是未指定(Unspecified)的。编译器可以自由决定。如果你写出 a[i++] = i,这会导致经典的未定义行为(UB, Undefined Behavior),因为你修改了 i,同时又在同一个表达式的另一处访问了 i,且没有顺序保证。
  • C++17 之后:标准明确规定了赋值表达式的右侧(RHS)必须在左侧(LHS)之前求值。即先算 E2,再算 E1。 但即便如此,为了代码的可读性和跨版本安全性,在 C++ 中极力避免在等号两边对同一个变量进行自增/自减混用,老老实实拆成多行才是正道。

三、 函数传参顺序:swap(nums[begin++], nums[end--])

在《189. 轮转数组》的 C++ 解法中,我们用到了 swap 函数传参。这又涉及到了函数参数的计算顺序

当调用 func(A, B) 时,A 和 B 谁先计算?

  • C++ 中的函数参数:求值顺序是未指定/不确定顺序的。编译器可以先算 begin++,也可以先算 end--

    • 为什么这里是安全的? 因为 beginend 是两个毫不相干的独立变量。无论谁先算,都不会干扰另一个变量的值。加上后缀 ++ 的特性(先返回副本再自增),传给 swap 的引用必然是原位置的元素。
    • 反面教材:如果你写 swap(nums[i++], nums[i++]),这就再次触发了未定义行为,因为你无法确定左边的 i 是先加还是右边的先加。
  • JavaScript 中的函数参数:严格从左到右求值。 如果你在 JS 里写一个类似的 swapFunc(nums[i++], nums[l++]),引擎必定先计算第一个参数,导致 i 自增,然后再计算第二个参数。


四、 核心对比总结

为了方便记忆,你可以直接参考这张总结表:

场景 JavaScript C++ (C++17及以后) C++ (C++17之前)
普通赋值 A = B 先左后右 (先定左侧引用,再算右侧值) 先右后左 (右侧 B 优先计算) 未指定 (可能导致 UB)
解构赋值 先右后左 (完整打包右侧,再逐个分配给左侧) N/A (C++17 有结构化绑定,但不适用此场景) N/A
函数传参 f(A, B) 从左向右严格求值 不确定顺序,但不会交错 未指定

理解了这些底层逻辑,你就会明白为什么在 JS 里可以用解构赋值秀一行流,而在 C++ 里我们往往更倾向于把逻辑拆分得清清楚楚。代码不仅仅是写给机器看的,掌握底层顺序,是为了在炫技和工程规范之间找到最安全的平衡点。


评论

0/1000