浅谈C/C++中的顺序点和副作用

简介:

一.副作用(side effect)

    表达式有两种功能:每个表达式都产生一个值( value ),同时可能包含副作用( side effect )。副作用是指改变了某些变量的值。

    如:

    1:20         //这个表达式的值是20;它没有副作用,因为它没有改变任何变量的值。

    2:x=5       // 这个表达式的值是5;它有一个副作用,因为它改变了变量x的值。

    3:x=y++     // 这个表达示有两个副作用,因为改变了两个变量的值。

    4:x=x++     // 这个表达式也有两个副作用,因为变量x的值发生了两次改变。

二.求值顺序点

    表达式求值规则的核心在于 顺序点( sequence point ) [ C99 6.5 Expressions 条款2 ] [ C++03 5 Expressions 概述 条款4 ]。

   顺序点的意思是在一系列步骤中的一个“结算”的点,语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。在C/C++中只有以下几种存在顺序点:

   1)分号;

   2)未重载的逗号运算符的左操作数赋值之后(即','处)

   3)未重载的'||'运算符的左操作数赋值之后(即'||'处);

   4)未重载的'&&'运算符的左操作数赋值之后(即"&&"处);

   5)三元运算符'? : '的左操作数赋值之后(即'?'处);

   6)在函数所有参数赋值之后但在函数第一条语句执行之前;

   7)在函数返回值已拷贝给调用者之后但在该函数之外的代码执行之前;

   8)每个基类和成员初始化之后;

   9)在每一个完整的变量声明处有一个顺序点,例如int i, j;中逗号和分号处分别有一个顺序点;

  10)for循环控制条件中的两个分号处各有一个顺序点。

  我们假定程序里有代码片段“...a[i]++ ... a[j] ...”,假定当时i与j的值恰好相等(a[i] 和a[j] 正好引用同一数组元素);假定a[i]++ 确实在a[j] 之前计算;再假定其间没有其他修改a[i] 的动作。在这些假定下,a[i]++ 对 a[i] 的修改能反映到 a[j] 的求值中吗?注意:由于 i 与 j 相等的问题无法静态判定,在目标代码里,这两个数组元素访问(对内存的访问)必然通过两段独立代码完成。现代计算机的计算都在寄存器里做,问题现在变成:在取 a[j] 值的代码执行之前,a[i] 更新的值是否已经被(从寄存器)保存到内存?程序语言通常都规定了执行中变量修改的最晚实现时刻(称为顺序点、序点或执行点)。程序执行中存在一系列顺序点(时刻),语言保证一旦执行到达一个顺序点,在此之前发生的所有修改(副作用)都必须实现(必须反应到随后对同一存储位置的访问中),在此之后的所有修改都还没有发生。在顺序点之间则没有任何保证

   在两个顺序点之间,子表达式求值和副作用的顺序是不同步的。如果代码的结果与求值和副作用发生顺序相关,称这样的代码有不确定的行为(unspecified behavior).而且,假如期间对一个内建类型执行一次以上的写操作,则是未定义行为. 

任意两个顺序点之间的副作用的发生顺序都是未定义的.

   如:

   x=x++;

   该表达式只有一个顺序点,在该顺序点之前有2个副作用,一个是自增,一个赋值,这两个副作用发生的顺序是未定义的,即自增运算和赋值运算哪一个先执行是没有被定义的(注意这个顺序跟运算符的优先级是无关的,注意理解运算符优先级的含义),这个执行次序交由编译器厂商去自行决定,因此对于不同的编译器可能会得出不同的结果。

复制代码
#include  < stdio.h >
#include 
< stdlib.h >

int  main( int  argc,  char * argv[])
{
int  i = 0 ;
int  m = ( ++ i) + ( ++ i) + ( ++ i) + ( ++ i);
printf(
" %d %d\n " ,m,i);
system(
" pause " );
return 0 ;
}
复制代码

   对于上述代码:

   在gcc编译器中运行得到的结果是 11 4

   而在Visual Studio 2008中运行得到的结果是 16 4

   因为对于

   int i=0;

   int m=(++i)+(++i)+(++i)+(++i);

  在两个分号之间有5个副作用,这5个副作用与子表达式的求值顺序是未定义的,对于不同的编译器会得出不同的结果。

  并且在这期间对i进行了不止一次的写操作,这也是一个未定义的行为,可能会引起任何后果。

  还比如:

  x[i]=i++;

  printf("%d %d\n",i++,i++);

  function(x,x++);

  这些都是未定义的行为。

  因此我们平时在写代码时,尽量不要写出这样风格不好的代码,因为它不仅会给程序带来不确定性,可能会引起任何后果(比如程序崩溃),而且对于代码的移植性来说是致命的打击。

  比如:

 x[i]=i++;

 可以用这段代码去代替:

 x[i]=i;

 i++;

 function(x,x++);-> function(x,x);x=x+1;

这样的代码才是风格良好的代码。

   尽量保证,在两个相邻顺序点之间同一个变量不可以被修改两次以上或者同时有读取和修改,否则,就会产生未定义的行为。


本文转载自海 子博客园博客,原文链接:http://www.cnblogs.com/dolphin0520/archive/2011/04/20/2022330.html如需转载自行联系原作者


相关文章
|
存储 安全 算法
一文理解UDS安全访问服务(0x27)
一文理解UDS安全访问服务(0x27)
一文理解UDS安全访问服务(0x27)
|
开发工具 git
Git出现MERGING状态
Git合并时有冲突,出现MERGING状态
7764 0
|
Android开发
autojs修改悬浮窗按钮点击事件
牙叔教程 简单易懂
1670 0
|
8月前
|
存储 人工智能
浙江大学与阿里云联合宣布共建人工智能通识课|阿里云云工开物合作动态
浙江大学与阿里云联合共建人工智能通识课,涵盖教育、法律、设计等多学科方向,将产业案例融入课程体系。阿里云开放大模型认证课程资源,提供云服务器、AI算力等支持,并通过“云工开物”计划为学生提供计算资源。双方还将发起“智能体创新大赛”,推动技术创新与人才培养。浙大是国内首批开展全校人工智能通识课的顶尖高校之一,2024年起“人工智能基础”成为全校本科生必修课。
|
SQL JavaScript 前端开发
基于若依ruoyi-nbcio支持flowable流程增加自定义业务表单(一)
基于若依ruoyi-nbcio支持flowable流程增加自定义业务表单(一)
1056 2
|
存储 人工智能 编译器
【AI系统】CPU 指令集架构
本文介绍了指令集架构(ISA)的基本概念,探讨了CISC与RISC两种主要的指令集架构设计思路,分析了它们的优缺点及应用场景。文章还简述了ISA的历史发展,包括x86、ARM、MIPS、Alpha和RISC-V等常见架构的特点。最后,文章讨论了CPU的并行处理架构,如SISD、SIMD、MISD、MIMD和SIMT,并概述了这些架构在服务器、PC及嵌入式领域的应用情况。
1077 5
|
安全 Java 关系型数据库
【开题报告】基于SpringBoot的农业电商服务系统的设计与实现
【开题报告】基于SpringBoot的农业电商服务系统的设计与实现
845 0
|
存储 编译器 C++
【C++从0到王者】第三十五站:面试官让手撕红黑树,我直接向他秀一手手撕map与set
【C++从0到王者】第三十五站:面试官让手撕红黑树,我直接向他秀一手手撕map与set
206 0
|
JavaScript 前端开发 Java
什么是深拷贝,什么是浅拷贝
什么是深拷贝,什么是浅拷贝
255 0