41、算术与逻辑表达式的深入解析

算术与逻辑表达式的深入解析

1. 表达式求值顺序与序列点

在编写代码时,如果需要控制表达式内的求值顺序,就需要特别关注序列点。大多数语言保证在程序执行的某些特定点之前完成副作用的计算,这个特定点被称为序列点,语句结束处(分号)就是一个典型的序列点。

在C语言中,除了语句末尾的分号,以下运算符之间也定义了序列点:
| 运算符 | 示例 |
| ---- | ---- |
| 逗号运算符 | expression1, expression2 |
| 逻辑与运算符 | expression1 && expression2 |
| 逻辑或运算符 | expression1 || expression2 |
| 条件表达式运算符 | expression1 ? expression2 : expression3 |

C语言保证在这些示例中, expression1 中的所有副作用在计算 expression2 expression3 之前完成。对于条件表达式,C语言只会计算 expression2 expression3 中的一个,因此在条件表达式的一次执行中,只有一个子表达式的副作用会发生。

下面是一个C语言的示例,展示了序列点对程序操作的影响:

int array[6] = {0, 0, 0, 0, 0, 0};
int i;
// ...
i = 0;
array[i] = i++;

在这个例子中,C语言没有在赋值运算符上定义序列点,所以编译器可以选择在索引数组之前或之后使用 i 的值。最后一条语句在语义上可能等价于以下两种情况之一:

array[0] = i++;
// -or-
array[1] = i++;

为了控制数组的赋值,需要确保表达式的任何部分都不依赖于其他部分的副作用。如果想使用递增前的 i 值作为数组索引,可以这样写代码:

array [i] = i; //<-分号标记序列点
++i;

如果想使用递增后的 i 值作为数组索引,可以这样写:

++i;               //<-分号标记序列点
array[ i ] = i - 1;

也可以写成更易读的形式:

j = i;
++i;             //<-分号标记序列点
array[ i ] = j;

需要注意的是,序列点并不指定计算何时发生,它只是保证在越过序列点之前完成任何未完成的副作用计算。编译器可以在不产生副作用的情况下,自由地提前或推迟子表达式的计算。

由于语句结束(分号)是大多数语言中的序列点,一种控制副作用计算的方法是将复杂表达式手动分解为类似三地址码的语句序列。例如,对于Pascal中的一个表达式:

{ 在Pascal中结果未定义的语句 }
i := f(x) + g(x);
{ 具有明确定义语义的对应语句 }
temp1 := f(x);
temp2 := g(x);
i := temp1 + temp2;
{ 另一个版本,也具有明确定义但不同的语义 }
temp1 := g(x);
temp2 := f(x);
i := temp2 + temp1;

运算符的优先级和结合性并不控制表达式中计算的发生时间,它们只是控制编译器如何安排计算以产生结果。只要最终计算结果符合优先级和结合性的预期,编译器可以自由地以任何顺序和时间计算子组件。

2. 避免副作用带来的问题

由于副作用对代码的影响往往难以察觉,因此尽量减少程序受副作用问题的影响是个好主意。虽然完全消除副作用不太现实,但可以通过遵循以下简单规则来减少副作用的意外后果:
- 避免在程序流控制语句(如 if while do..until 等)的布尔表达式中放置副作用。
- 如果赋值运算符右侧存在副作用,尝试将副作用移到赋值之前或之后的单独语句中(取决于赋值语句是使用对象应用副作用之前还是之后的值)。
- 避免在同一语句中进行多次赋值,将它们拆分为单独的语句。
- 避免在同一表达式中调用多个可能产生副作用的函数。
- 编写函数时,避免对全局对象进行修改(即副作用)。
- 始终彻底记录副作用。对于函数,应在函数文档中记录副作用,并在每次调用该函数时也记录副作用。

3. 强制特定的求值顺序

运算符的优先级和结合性并不控制编译器何时计算子表达式。例如,对于表达式 X / Y * Z ,编译器可以自由地先计算 Z ,再计算 Y ,最后计算 X 。编译器只需要在计算 X / Y 之前计算 X Y 的值(顺序任意),并在计算 (X / Y) * Z 之前计算 X / Y 的值。

虽然编译器可以自由地以任何顺序计算子表达式,但通常会避免重新排列实际计算的顺序。例如,数学上 X / Y * Z Z * X / Y 是等价的,但在有限精度的计算机算术运算中,它们可能会产生不同的结果。考虑 X = 5 Y = 2 Z = 3 的情况:

X / Y * Z
= 5 / 2 * 3
= 2 * 3
= 6

Z * X / Y
= 3 * 5 / 2
= 15 / 2
= 7

因此,编译器在代数上重新排列表达式时会很谨慎。

整数算术有其自身的规则,实数代数的规则并不总是适用。同样,浮点算术也会受到舍入、截断、溢出或下溢等问题的影响,因此对浮点表达式应用任意的实数算术变换可能会引入计算误差。

一般来说,如果必须控制表达式的求值顺序和子组件的计算时间,唯一的选择是使用汇编语言。在汇编代码中,可以精确指定软件何时计算表达式的各个组件。对于非常精确的计算,当求值顺序会影响结果时,汇编语言可能是最安全的方法。

4. 短路求值

某些算术和逻辑运算符具有这样的特性:如果表达式的一个组件具有特定的值,那么整个表达式的值就可以自动确定,而无需考虑组成表达式的其余组件的值。一个典型的例子是乘法运算符。如果有表达式 A * B ,并且知道 A B 为零,那么就不需要计算另一个组件,因为结果已经为零。

虽然有一些算术运算可以采用短路求值,但检查短路求值的成本通常比完成计算的成本更高。例如,乘法可以使用短路求值来避免乘以零,但在实际程序中,乘以零的情况很少发生,因此在其他情况下与零比较的成本通常会超过避免乘以零所节省的成本。所以,很少会看到支持算术运算短路求值的语言系统。

5. 短路求值与布尔表达式

布尔表达式是可以从短路求值中受益的一种表达式类型。布尔表达式适合短路求值的原因有三个:
- 布尔表达式只产生两个结果, True False ,因此很有可能(假设随机分布,有50%的机会)出现短路“触发”值。
- 布尔表达式往往比较复杂。
- 布尔表达式在程序中频繁出现。

因此,许多编译器在处理布尔表达式时会使用短路求值。

考虑以下两个C语句:

A = B && C;
D = E || F;

如果 B False ,则无论 C 的值如何, A 都为 False 。同样,如果 E True ,则无论 F 的值如何, D 都为 True 。可以按以下方式计算 A D 的值:

A = B;
if( A )
{
    A = C;
}
D = E;
if( !D )
{
    D = F;
}

如果 C F 代表复杂的布尔表达式,并且 B 通常为 False E 通常为 True ,那么这段代码序列可能会运行得更快。如果编译器完全支持短路求值,就不需要手动编写这样的代码。

短路求值的相反情况是完全布尔求值。在完全布尔求值中,编译器会生成始终计算布尔表达式每个子组件的代码。一些语言(如C、C++、C# 和 Java)指定使用短路求值,少数语言(如Ada)允许程序员指定使用短路求值还是完全布尔求值,大多数语言(如Pascal)没有定义表达式是使用短路求值还是完全布尔求值,而是由实现者决定。

考虑以下常见的C语句:

if( ptr != NULL && *ptr != '\0' )
{
    // 处理ptr指向的字符串中的当前字符
}

如果使用完全布尔求值,这个例子可能会失败。当 ptr 变量包含 NULL 时,使用短路求值,程序不会计算子表达式 *ptr != '\0' ,因为程序知道结果总是 false ,控制会立即转移到 if 语句结束括号 } 之后的第一条语句。但如果使用完全布尔求值,程序会尝试解引用 ptr ,这可能会产生运行时错误。

短路求值和完全布尔求值在副作用方面也有语义差异。如果一个子表达式由于短路求值而不被执行,那么该子表达式不会产生任何副作用。这种行为既非常有用又存在内在危险,许多算法依赖于这个特性来正确运行。

6. 短路求值与完全布尔求值的对比实例分析

为了更清晰地理解短路求值和完全布尔求值的差异,我们可以通过一个具体的示例来进一步说明。假设有以下两个函数:

#include <stdio.h>

int func1() {
    printf("func1 is called.\n");
    return 0;
}

int func2() {
    printf("func2 is called.\n");
    return 1;
}

现在,我们使用这两个函数来构建布尔表达式,并分别观察短路求值和完全布尔求值的情况。

短路求值情况
int main() {
    if (func1() && func2()) {
        printf("Both functions return true.\n");
    } else {
        printf("At least one function returns false.\n");
    }
    return 0;
}

在这个例子中,由于 func1() 返回 0 (即 False ),根据短路求值的规则, func2() 不会被调用。运行这段代码,输出结果为:

func1 is called.
At least one function returns false.
完全布尔求值情况(假设编译器支持)

如果编译器采用完全布尔求值,即使 func1() 返回 False func2() 也会被调用。代码如下:

// 假设的完全布尔求值代码逻辑
int main() {
    int result1 = func1();
    int result2 = func2();
    if (result1 && result2) {
        printf("Both functions return true.\n");
    } else {
        printf("At least one function returns false.\n");
    }
    return 0;
}

运行这段代码,输出结果为:

func1 is called.
func2 is called.
At least one function returns false.

通过这个示例,我们可以清楚地看到短路求值和完全布尔求值在函数调用和副作用方面的差异。

7. 不同语言对布尔求值的规定总结

不同的编程语言对布尔表达式的求值方式有不同的规定,以下是一些常见语言的总结:
| 语言 | 布尔求值规定 |
| ---- | ---- |
| C、C++、C#、Java | 指定使用短路求值 |
| Ada | 允许程序员指定使用短路求值还是完全布尔求值 |
| Pascal | 未定义表达式是使用短路求值还是完全布尔求值,由实现者决定 |

在编写代码时,了解所使用语言的布尔求值规定非常重要,特别是当代码中包含可能产生副作用的子表达式时。

8. 利用短路求值优化代码

短路求值不仅可以提高代码的执行效率,还可以使代码更加健壮。例如,在检查数组索引是否越界时,可以利用短路求值来避免访问非法内存。以下是一个示例:

#include <stdio.h>

#define ARRAY_SIZE 10

int main() {
    int array[ARRAY_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int index = 15;

    if (index >= 0 && index < ARRAY_SIZE && array[index] > 5) {
        printf("The element at index %d is greater than 5.\n", index);
    } else {
        printf("Invalid index or element is not greater than 5.\n");
    }

    return 0;
}

在这个例子中,首先检查 index 是否大于等于 0 ,如果不满足这个条件,后面的 index < ARRAY_SIZE array[index] > 5 就不会被计算,从而避免了访问非法内存。

9. 总结

通过对算术与逻辑表达式的深入分析,我们了解了表达式求值顺序、序列点、副作用、短路求值等重要概念。这些概念对于编写高效、健壮的代码至关重要。

在实际编程中,我们应该遵循以下原则:
- 合理利用序列点来控制副作用的计算,避免因副作用导致的未定义行为。
- 尽量减少代码中副作用的使用,遵循避免副作用问题的规则。
- 在需要控制求值顺序时,考虑使用汇编语言,但也要权衡其可读性和可维护性。
- 了解所使用语言对布尔表达式求值的规定,充分利用短路求值的特性来优化代码。

通过掌握这些知识,我们可以更好地理解和控制代码的执行过程,提高代码的质量和性能。

下面是一个关于表达式求值相关概念的流程图,帮助大家梳理整个流程:

graph TD;
    A[表达式求值] --> B{是否有副作用};
    B -- 是 --> C{是否有序列点控制};
    C -- 是 --> D[按序列点顺序计算];
    C -- 否 --> E[可能产生未定义行为];
    B -- 否 --> F{是否需要控制求值顺序};
    F -- 是 --> G[考虑使用汇编语言];
    F -- 否 --> H[编译器自由安排计算];
    I[布尔表达式求值] --> J{是否使用短路求值};
    J -- 是 --> K[根据左侧结果决定是否计算右侧];
    J -- 否 --> L[计算所有子组件];

这个流程图展示了表达式求值过程中需要考虑的关键因素,以及布尔表达式求值的两种方式。希望通过上半部分和本部分的内容,大家能对算术与逻辑表达式有更深入的理解,并在实际编程中灵活运用这些知识。

本 PPT 介绍了制药厂房中供配电系统的总体概念设计要点,内容包括: 洁净厂房的特点及其对供配电系统的特殊要求; 供配电设计的一般原则依据的国家/行业标准; 从上级电网到工厂变电所、终端配电的总体结构模块化设计思路; 供配电范围:动力配电、照明、通讯、接地、防雷消防等; 动力配电中电压等级、接地系统形式(如 TN-S)、负荷等级可靠性、UPS 配置等; 照明的电源方式、光源选择、安装方式、应急备用照明要求; 通讯系统、监控系统在生产管理消防中的作用; 接地等电位连接、防雷等级防雷措施; 消防设施及其专用供电(消防泵、排烟风机、消防控制室、应急照明等); 常见高压柜、动力柜、照明箱等配电设备案例及部分设计图纸示意; 公司已完成的典型项目案例。 1. 工程背景总体框架 所属领域:制药厂房工程的公用工程系统,其中本 PPT 聚焦于供配电系统。 放在整个公用工程中的位置:给排水、纯化水/注射用水、气体热力、暖通空调、自动化控制等系统并列。 2. Part 01 供配电概述 2.1 洁净厂房的特点 空间密闭,结构复杂、走向曲折; 单相设备、仪器种类多,工艺设备昂贵、精密; 装修材料工艺材料种类多,对尘埃、静电等更敏感。 这些特点决定了:供配电系统要安全可靠、减少积尘、便于清洁和维护。 2.2 供配电总则 供配电设计应满足: 可靠、经济、适用; 保障人身财产安全; 便于安装维护; 采用技术先进的设备方案。 2.3 设计依据规范 引用了大量俄语标准(ГОСТ、СНиП、SanPiN 等)以及国家、行业和地方规范,作为设计的法规基础文件,包括: 电气设备、接线、接地、电气安全; 建筑物电气装置、照明标准; 卫生安全相关规范等。 3. Part 02 供配电总览 从电源系统整体结构进行总览: 上级:地方电网; 工厂变电所(10kV 配电装置、变压
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值