c语言中的顺序点

限时加码!20+主流AI编程工具免费用 购周边加赠Coding Plan Lite,Claude Code、Cursor等即刻畅享,学习进阶更高效! 阅读详情

http://bbs.csdn.net/topics/370153775


经常可以在一些讨论组里看到下面的提问:“谁知道下面C语句给n赋什么值?”
m = 1; n = m+++m++;


最近有位不相识的朋友发email给我,问为什么在某个C++系统里,下面表达式打印出两个4,而不是4和5:
a = 4; cout << a++ << a;
C++ 不是规定 << 操作左结合吗?是C++ 书上写错了,还是这个系统的实现有问题?


      要弄清这些,需要理解的一个问题是:如果程序里某处修改了一个变量(通过赋值、增量/减量操作等),什么时候从该变量能够取到新值?有人可能说,“这算什么问题!我修改了变量,再从这个变量取值,取到的当然是修改后的值!”其实事情并不这么简单。
      C/C++ 语言是“基于表达式的语言”,所有计算(包括赋值)都在表达式里完成。“x = 1;”就是表达式“x = 1”后加表示语句结束的分号。要弄清程序的意义,首先要理解表达式的意义,也就是:1)表达式所确定的计算过程;2)它对环境(可以把环境看作当时可用的所有变量)的影响。如果一个表达式(或子表达式)只计算出值而不改变环境,我们就说它是引用透明的,这种表达式早算晚算对其他计算没有影响(不改变计算的环境。当然,它的值可能受到其他计算的影响)。如果一个表达式不仅算出一个值,还修改了环境,就说这个表达式有副作用(因为它多做了额外的事)。a++ 就是有副作用的表达式。这些说法也适用于其他语言里的类似问题。
      现在问题变成:如果C/C++ 程序里的某个表达式(部分)有副作用,这种副作用何时才能实际体现到使用中?为使问题更清楚,我们假定程序里有代码片段“...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] 更新的值是否已经被(从寄存器)保存到内存?如果了解语言在这方面的规定,这个问题的答案就清楚了。
     程序语言通常都规定了执行中变量修改的最晚实现时刻(称为顺序点、序点或执行点)。程序执行中存在一系列顺序点(时刻),语言保证一旦执行到达一个顺序点,在此之前发生的所有修改(副作用)都必须实现(必须反应到随后对同一存储位置的访问中),在此之后的所有修改都还没有发生。在顺序点之间则没有任何保证。对C/C++ 语言这类允许表达式有副作用的语言,顺序点的概念特别重要。
     现在上面问题的回答已经很清楚了:如果在a[i]++ 和a[j] 之间存在一个顺序点,那么就能保证a[j] 将取得修改之后的值;否则就不能保证。


C/C++语言定义(语言的参考手册)明确定义了顺序点的概念。顺序点位于:
   1. 每个完整表达式结束时。完整表达式包括变量初始化表达式,表达式语句,return语句的表达式,以及条件、循环和switch语句的控制表达式(for头部有三个控制表达式);
  2. 运算符 &&、||、?: 和逗号运算符的第一个运算对象计算之后;
  3. 函数调用中对所有实际参数和函数名表达式(需要调用的函数也可能通过表达式描述)的求值完成之后(进入函数体之前)。
假设时刻ti和ti+1是前后相继的两个顺序点,到了ti+1,任何C/C++ 系统(VC、BC等都是C/C++系统)都必须实现ti之后发生的所有副作用。当然它们也可以不等到时刻ti+1,完全可以选择在时段 [t, ti+1] 之间的任何时刻实现在此期间出现的副作用,因为C/C++ 语言允许这些选择。


     前面讨论中假定了a[i]++ 在a[i] 之前做。在一个程序片段里a[i]++ 究竟是否先做,还与它所在的表达式确定的计算过程有关。我们都熟悉C/C++ 语言有关优先级、结合性和括号的规定,而出现多个运算对象时的计算顺序却常常被人们忽略。看下面例子:
(a + b) * (c + d) fun(a++, b, a+5)
这里“*”的两个运算对象中哪个先算?fun及其三个参数按什么顺序计算?对第一个表达式,采用任何计算顺序都没关系,因为其中的子表达式都是引用透明的。第二个例子里的实参表达式出现了副作用,计算顺序就非常重要了。少数语言明确规定了运算对象的计算顺序(Java规定从左到右),C/C++ 则有意不予规定,既没有规定大多数二元运算的两个对象的计算顺序(除了&&、|| 和 ,),也没有规定函数参数和被调函数的计算顺序。在计算第二个表达式时,首先按照某种顺序算fun、a++、b和a+5,之后是顺序点,而后进入函数执行。


      不少书籍在这些问题上有错(包括一些很流行的书)。例如说C/C++ 先算左边(或右边),或者说某个C/C++ 系统先计算某一边。这些说法都是错误的!一个C/C++ 系统可以永远先算左边或永远先算右边,也可以有时先算左边有时先算右边,或在同一表达式里有时先算左边有时先算右边。不同系统可能采用不同的顺序(因为都符合语言标准);同一系统的不同版本完全可以采用不同方式;同一版本在不同优化方式下,在不同位置都可能采用不同顺序。因为这些做法都符合语言规范。在这里还要注意顺序点的问题:即使某一边的表达式先算了,其副作用也可能没有反映到内存,因此对另一边的计算没有影响。
回到前面的例子:“谁知道下面C语句给n赋什么值?”
m = 1; n = m++ +m++;
正确回答是:不知道!语言没有规定它应该算出什么,结果完全依赖具体系统在具体上下文中的具体处理。其中牵涉到运算对象的求值顺序和变量修改的实现时刻问题。对于:

cout << a++ << a;
我们知道它是
(cout.operator <<(a++)).operator << (a);的简写。

      先看外层函数调用,这里需要算出所用函数(由加下划线的一段得到),还需要计算a的值。语言没有规定哪个先算。如果真的先算函数,这一计算中出现了另一次函数调用,在被调函数体执行前有一个顺序点,那时a++的副作用就会实现。如果是先算参数,求出a的值4,而后计算函数时的副作用当然不会改变它(这种情况下输出两个4)。当然,这些只是假设,实际应该说的是:这种东西根本不该写,讨论其效果没有意义。

      有人可能说,为什么人们设计 C/C++时不把顺序规定清楚,免去这些麻烦?C/C++ 语言的做法完全是有意而为,其目的就是允许编译器采用任何求值顺序,使编译器在优化中可以根据需要调整实现表达式求值的指令序列,以得到效率更高的代码。像Java那样严格规定表达式的求值顺序和效果,不仅限制了语言的实现方式,还要求更频繁的内存访问(以实现副作用),这些可能带来可观的效率损失。应该说,在这个问题上,C/C++和Java的选择都贯彻了它们各自的设计原则,各有所获(C/C++ 潜在的效率,Java更清晰的程序行为),当然也都有所失。还应该指出,大部分程序设计语言实际上都采用了类似C/C++的规定。
      讨论了这么多,应该得到什么结论呢?C/C++ 语言的规定告诉我们,任何依赖于特定计算顺序、依赖于在顺序点之间实现修改效果的表达式,其结果都没有保证。程序设计中应该贯彻的规则是:如果在任何“完整表达式”(形成一段由顺序点结束的计算)里存在对同一“变量”的多个引用,那么表达式里就不应该出现对这一“变量”的副作用。否则就不能保证得到预期结果。注意:这里的问题不是在某个系统里试一试的问题,因为我们不可能试验所有可能的表达式组合形式以及所有可能的上下文。这里讨论的是语言,而不是某个实现。总而言之,绝不要写这种表达式,否则我们或早或晚会某种环境中遇到麻烦。


      后记:去年参加一个学术会议,看到有同行写文章讨论某个C系统里表达式究竟按什么顺序求值,并总结出一些“规律”。从讨论中了解到某“程序员水平考试”出了这类题目。这使我感到很不安。今年给一个教师学习班讲课,发现许多专业课教师也对这一基本问题也不甚明了,更觉得问题确实严重。因此整理出这篇短文供大家参考。
     后后记:4年多过去了,许多新的和老的教科书仍然在不厌其烦地讨论在C语言里原本并无意义的问题(如本文所指出的)。希望学习和使用C语言的人不要陷入其中。。

【C语言复习(二十八)】函数本质、顺序 函数本质 1、函数的意义 大概也就是模块化程序设计吧.... 在面向过程是一种以过程为中心的编程思想,首先将复杂的问题分解为一个个容易解决的问题,分解过后的问题可以按照步骤一步步完成,每一步都使用一个独立的函数来实现,函数是面向过程在C语言中的体现。   2、函数的声明和定义 函数的声明和定义是不一样的,函数声明只是预先告诉编译器有这么一个东西存在,只是还没有具体定义;而函数定义是具体 阅读详情

相关推荐

C语言顺序结构和选择结构注意

顺序结构 1.赋值表达式 (1)给多个变量赋同一个值,可以使用连续等号的方法: int m, n; m = n = 5; (2)n /= m + 1等价于n = n/(m + 1) 2.scanf函数的使用 数据类型 格式符 举例 int %d scanf(&amp;quot;%d&amp;quot;, &amp;amp;amp;n); long long %l64d scan

usuyitakumi的博客 3339

C语言中的顺序

C语言中的顺序

Stay Hungry,Stay Foolish!!! 4090

【数据结构】栈的知识总结--关于栈的定义和基本操作;C语言实现栈;顺序栈;链栈;共享栈;栈的易错的总结

关于栈的定义和基本操作;C语言实现栈;顺序栈;链栈;共享栈;栈的易错的总结

小人物,大梦想! 3013

c语言中的求值顺序

求值顺序 表达式求值规则的核心在于  “顺序( sequence point )”     顺序的意思是在一系列步骤中的一个“结算”的语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。在C/C++中只有以下几种存在顺序:     1)分号;     2)未重载的逗号运算符的左操作数赋值之后(即','处)     3)未重载的'||'运算符的左操作数赋值之后(即'||

shenqixiaozi1234的专栏 1264

C语言中的和副作用

在C99标准文件讲到了序。这里给出一些例子和说明。

Sefank的博客 775

: 什么是序? 简而言之,序就是这么一个位置,在它之前所有的副作用已经发生,在它之后的所有副作用仍未开始,而两个序之间所有的表达式或者代码执行的顺序是未定义的! 序规则1: 在前一个序和后一个序之前,也就是两个序之间,一个值最多只能被写一次; 这里,在两个序之间,a被写了两次,因此,这种行为属于未定义。   序规则2: 进一步说,先前的值应该是

situzhuge的专栏 1407

C语言中的副作用、序列以及复杂表达式的求值顺序

系统的介绍了C语言中的副作用、序列、复杂表达式求值顺序相关内容,帮助读者深入理解C语言

Royi的学习博客 633

【C语言进阶剖析】44、函数参数的求值顺序与程序中的顺序

1 函数的参数 函数参数在本质上与局部变量相同在栈上分配空间 函数参数的初始值是函数调用时的实参值 如下图所示,函数调用时,函数参数就被压入栈中,函数返回时,栈中数据弹出。 函数参数的求值顺序依赖于编译器的实现 下面看一个实例分析,看一下函数参数的求值顺序到底是怎样的。 // 44-1.c #include<stdio.h> int func(int i, int j) { ...

桑凯旋的博客 2315

C语言参数顺序,C语言中的顺序

C语言1、函数参数的求值顺序依赖于编译器,例如f(a,a++);是先求a++还是求a不一定2、C语言中的大多数运算符对其操作数的求值顺序也依赖于编译器警告int i = f() * g();这里先求f()还是先求g()不一定,所以不能写出要先实现f(),在实现g()的函数;程序中的顺序定义:指程序执行过程中修改变量值的最晚时刻。有哪些顺序1、每个完整表达式结束后,即分号后面2、&&...

weixin_28951347的博客 714

c语言 乱序,C语言中的顺序

C语言1、函数参数的求值顺序依赖于编译器,例如f(a,a++);是先求a++还是求a不一定2、C语言中的大多数运算符对其操作数的求值顺序也依赖于编译器警告int i = f() * g();这里先求f()还是先求g()不一定,所以不能写出要先实现f(),在实现g()的函数;程序中的顺序定义:指程序执行过程中修改变量值的最晚时刻。有哪些顺序1、每个完整表达式结束后,即分号后面2、&&...

weixin_31299543的博客 417

C 语言 中关于 顺序(Sequence points) 的问题

     关于顺序(sequence point),在C标准中有解释,不过很晦涩。     The sequence points laid down in the Standard are the following:The point of calling a function, after evaluating its arguments.The end of the first operand of the && operator.The end of the first operand

1028

c语言中副作用和顺序的理解

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34162695的博客 265

关于C语言中表达式运算顺序(优先级、结合性、副作用、序列)的讨论以及c语言未定义行为

关于C语言中表达式运算顺序(优先级、结合性、副作用、序列)的讨论以及c语言未定义行为

m0_52727650的博客 3592

c语言中的 顺序

http://blog.csdn.net/scorpio16/archive/2007/04/08/1556235.aspx        看下面两条语句: int i = 3;int k=(++i)+(++i)+(++i);         执行后k的值是多少?不同的编译器有不同的结果,可能是16(gcc),也可能是18(vc)。为什么会这样呢?        在C++标准中有一个很重要的概

张同光 (Tongguang Zhang):Hello everyone ! 563

用c语言定义顺序中的最大与最小值易错

** 用c语言定义顺序中的最大与最小值易错 #源代码 #include<stdio.h> #include<malloc.h> struct node { int x; node *next; }; void init(node &L) { L=(node)malloc(sizeof(int)); L->next=NULL; } void creat...

qq_44337718的博客 743

C语言的本质(8)——副作用与顺序

C 语言中,术语副作用是指对数据对象或者文件的修改。例如以下语句var = 99;的副作用是把 var 的值修改成 99。对表达式求值也可能产生副作用,例如: se = 100对这个表达式求值所产生的副作用就是 se 的值被修改成 100。序列是指程序运行中的一个特殊的时间,在该之前的所有副作用已经结束,并且后续的副作用还没发生。 C 语句结束标志——分号(;)是序列。也就是说

尹成的技术博客 3639

C语言语法的嵌入式工程化解析:翻译单元、结构体对齐与顺序

C语言语法不仅是编程规则,更是编译器生成可靠机器码的基础契约。其核心概念如翻译单元(Translation Unit)定义了模块化编译边界,结构体对齐机制直接影响硬件访问安全与内存布局,而顺序(sequence point)则严格约束表达式求值次序,防止未定义行为。在嵌入式开发中,这些底层语法特性直接决定代码的可移植性、实时确定性和内存安全性。典型应用场景包括STM32寄存器操作、FreeRTOS任务管理、CAN/UART协议栈实现以及中断服务程序设计。本文聚焦C标准语法在真实工程中的落地逻辑,深入剖析声

weixin_28809949的博客 248

程序中的顺序

程序中的顺序 * 程序中存在一定的顺序 * 顺序指的是执行过程中修改变量值的最晚时刻 * 在程序达到顺序的时候,之前所做的一切操作必须反映到后续的访问中 c语言中的顺序 1 每个完整表达式结束时 2 &&, ||, ?:,以及逗号表达式的每个运算对象计算之后 3 函数调用中对所有实际参数的求值完成之后(进入函数体之前) /* 1&amp...

dingxiu8587的博客 149
上一篇: c语言要点摘录(面试题分析以及疑点分析)(完)
下一篇: printf的参数类型
gexueyuan
博客等级 码龄19年 92粉丝 140原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值