返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C语言未定义行为分析
  • 697
分享到

C语言未定义行为分析

2023-06-17 08:06:39 697人浏览 泡泡鱼
摘要

这篇文章主要介绍“C语言未定义行为分析”,在日常操作中,相信很多人在C语言未定义行为分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C语言未定义行为分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!他在

这篇文章主要介绍“C语言未定义行为分析”,在日常操作中,相信很多人在C语言未定义行为分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C语言未定义行为分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

他在白板上写了几行代码,并问这个程序会输出什么?

#include <stdio.h>   int main(){     int i = 0;     int a[] = {10,20,30};       int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];     printf("%d\n", r);     return 0; }

看上去相当简单明了。我解释了操作符的优先顺序&mdash;&mdash;后缀操作比乘法先计算、乘法比加法先计算,并且乘法和加法的结合性都是从左到右,于是我抓出运算符号并开始写出算式。

int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++]; //    =    a[0]    + 2 * a[1]  + 3 * a[2]; //    =     10     +     40    +    90; //    = 140

我自鸣得意地写下答案后,我的同事回应了一个简单的“不”。我想了几分钟后,还是被难住了。我不太记得后缀操作符的结合顺序了。此外,我知道那个顺 序甚至  不会改变这里的值计算的顺序,因为结合规则只会应用于同级的操作符之间。但我想到了应该根据后缀操作符都从右到左求值的规则,尝试算一遍这条算式。看上去  相当简单明了。

int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++]; //    =    a[2]    + 2 * a[1]  + 3 * a[0]; //    =     30     +     40    +    30; //    = 100

我的同事再一次回答说,答案仍是错的。这时候我只好认输了,问他答案是什么。这段短小的样例代码原来是从他写过的更大的代码段里删减出来的。为了验   证他的问题,他编译并且运行了那个更大的代码样例,但是惊奇地发现那段代码没有按照他预想的运行。他删减了不需要的步骤后得到了上面的样例代码,用GCc   4.7.3编译了这段样例代码,结果输出了令人吃惊的结果:“60”。

这时我被迷住了。我记得,C语言里,函数参数的计算求值顺序是未定义的,所以我们以为后缀操作符只是遵照某个随机的、而非从左至右的顺序,计算的。   我们仍然确信后缀比加法和乘法拥有更高的操作优先级,所以很快证明我们自己,不存在我们可以计算i++的顺序,使得这三个数组元素一起加起来、乘起来得到  60。

现在我已对此入迷了。我的***个想法是,查看这段代码的反汇编代码,然后尝试查出它实际上发生了什么。我用调试符号(debugging symbols)编译了这段样例代码,用了objdump后很快得到了带注释的x86_64反汇编代码。

Disassembly of section .text:   0000000000000000 <main>: #include <stdio.h>   int main(){    0:   55                      push   %rbp    1:   48 89 e5                mov    %rsp,%rbp    4:   48 83 ec 20             sub    $0x20,%rsp     int i = 0;    8:   c7 45 e8 00 00 00 00    movl   $0x0,-0x18(%rbp)     int a[] = {10,20,30};    f:   c7 45 f0 0a 00 00 00    movl   $0xa,-0x10(%rbp)   16:   c7 45 f4 14 00 00 00    movl   $0x14,-0xc(%rbp)   1d:   c7 45 f8 1e 00 00 00    movl   $0x1e,-0x8(%rbp)     int r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];   24:   8b 45 e8                mov    -0x18(%rbp),%eax   27:   48 98                   cltq    29:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx   2d:   8b 45 e8                mov    -0x18(%rbp),%eax   30:   48 98                   cltq    32:   8b 44 85 f0             mov    -0x10(%rbp,%rax,4),%eax   36:   01 c0                   add    %eax,%eax   38:   8d 0c 02                lea    (%rdx,%rax,1),%ecx   3b:   8b 45 e8                mov    -0x18(%rbp),%eax   3e:   48 98                   cltq    40:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx   44:   89 d0                   mov    %edx,%eax   46:   01 c0                   add    %eax,%eax   48:   01 d0                   add    %edx,%eax   4a:   01 c8                   add    %ecx,%eax   4c:   89 45 ec                mov    %eax,-0x14(%rbp)   4f:   83 45 e8 01             addl   $0x1,-0x18(%rbp)   53:   83 45 e8 01             addl   $0x1,-0x18(%rbp)   57:   83 45 e8 01             addl   $0x1,-0x18(%rbp)     printf("%d\n", r);   5b:   8b 45 ec                mov    -0x14(%rbp),%eax   5e:   89 c6                   mov    %eax,%esi   60:   bf 00 00 00 00          mov    $0x0,%edi   65:   b8 00 00 00 00          mov    $0x0,%eax   6a:   e8 00 00 00 00          callq  6f <main+0x6f>     return 0;   6f:   b8 00 00 00 00          mov    $0x0,%eax }   74:   c9                      leaveq   75:   c3                      retq

***和***的几个指令只建立了堆栈结构,初始化变量的值,调用printf函数,还从main函数返回。所以我们实际上只需要关心从0&times;24到0&times;57之间的指令。那是令人关注的行为发生的地方。让我们每次查看几个指令。

24:   8b 45 e8                mov    -0x18(%rbp),%eax 27:   48 98                   cltq  29:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx

***的三个指令与我们预期的一致。首先,它把i(0)的值加载到eax寄存器,带符号扩展到64位,然后加载a[0]到edx寄存器。这里的乘以1的运算(1*)显然被编译器优化后去除了,但是一切看起来都正常。接下来的几个指令开始时也大致相同。

2d:   8b 45 e8                mov    -0x18(%rbp),%eax 30:   48 98                   cltq  32:   8b 44 85 f0             mov    -0x10(%rbp,%rax,4),%eax 36:   01 c0                   add    %eax,%eax 38:   8d 0c 02                lea    (%rdx,%rax,1),%ecx

***个mov指令把i的值(仍然是0)加载进eax寄存器,带符号扩展到64位,然后加载a[0]进eax寄存器。有意思的事情发生了&mdash;&mdash;我们再次 期待  i++在这三条指令之前已经运行过了,但也许***两条指令会用某种汇编的魔法来得到预期的结果(2*a[1])。这两条指令把eax寄存器的值自加了一  次,实际上执行了2*a[0]的操作,然后把结果加到前面的计算结果上,并存进ecx寄存器。此时指令已经求得了a[0] + 2 *   a[0]的值。事情开始看起来有一些奇怪了,然而再一次,也许某个编译器魔法在发生。

3b:   8b 45 e8                mov    -0x18(%rbp),%eax 3e:   48 98                   cltq  40:   8b 54 85 f0             mov    -0x10(%rbp,%rax,4),%edx 44:   89 d0                   mov    %edx,%eax

接下来这些指令开始看上去相当熟悉。他们加载i的值(仍然是0),带符号扩展至64位,加载a[0]到edx寄存器,然后拷贝edx里的值到eax。嗯,好吧,让我们在多看一些:

46:   01 c0                   add    %eax,%eax 48:   01 d0                   add    %edx,%eax 4a:   01 c8                   add    %ecx,%eax 4c:   89 45 ec                mov    %eax,-0x14(%rbp)

在这里把a[0]自加了3次,再加上之前的计算结果,然后存入到变量“r”。现在不可思议的事情&mdash;&mdash;我们的变量r现在包含了a[0] + 2 *   a[0] + 3 * a[0]。足够肯定的是,那就是程序的输出:“60”。但是那些后缀操作符上发生了什么?他们都在***:

4f:   83 45 e8 01             addl   $0x1,-0x18(%rbp) 53:   83 45 e8 01             addl   $0x1,-0x18(%rbp) 57:   83 45 e8 01             addl   $0x1,-0x18(%rbp)

看上去我们编译版本的代码完全错了!为什么后缀操作符被扔到***下、所有任务已经完成之后?随着我对现实的信仰减少,我决定直接去找本源。不,不是编译器的源代码&mdash;&mdash;那只是实现&mdash;&mdash;我抓起了C11语言规范。

这个问题处在后缀操作符的细节。在我们的案例中,我们在单个表达式里对数组下标执行了三次后缀自增。当计算后缀操作符时,它返回变量的初始值。把新   的值再分配回变量是一个副作用。结果是,那个副作用只被定义为只被付诸于各顺序点之间。参照标准的5.1.2.3章节,那里定义了顺序点的细节。但在我们  的例子中,我们的表达式展示了未定义行为。它完全取决于编译器对于 什么时候 给变量分配新值的副作用会执行 相对于表达式的其他部分。

到此,关于“C语言未定义行为分析”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

--结束END--

本文标题: C语言未定义行为分析

本文链接: https://lsjlt.com/news/287888.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • C语言未定义行为分析
    这篇文章主要介绍“C语言未定义行为分析”,在日常操作中,相信很多人在C语言未定义行为分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C语言未定义行为分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!他在...
    99+
    2023-06-17
  • C语言算法的定义及分析详解
    目录算法的定义算法和程序的区别算法程序算法的性质算法的表示算法的分析分析原则常用的复杂性函数算法分析基本法则非递归算法:总结算法的定义 算法是一系列良定义的计算步骤 算法和程序的区别...
    99+
    2024-04-02
  • C语言详细分析宏定义的使用
    目录一、C语言中函数的“缺陷”二、再次理解函数三、C语言中的宏四、宏与函数的不同五、编译器组成简介六、宏使用示例七、再论宏常量八、小结一、C语言中函数的&ld...
    99+
    2024-04-02
  • C语言中#define在多行宏定义出错的原因及分析
    目录C语言中#define在多行宏定义出错的原因1.第一种错误2.第二种错误使用#define宏定义的几个小技巧1.调试开关2.条件编译3.宏实现函数4.跨行宏定义5.防止头文件被重...
    99+
    2023-02-24
    C语言#define 多行宏定义出错 C语言多行宏定义
  • C语言自定义类型全解析
    目录前言结构体类型结构体的声明结构体变量的定义与初始化结构体的自引用结构体的访问结构体的传参传结构体传地址结构体的内存对齐(强烈建议观看)位段位段的声明位段的内存管理位段的跨平台性 ...
    99+
    2024-04-02
  • C语言宏定义的扩展定义讲解
    目录1. 常量宏定义2. 定义宏函数3. 宏定义和#号结合4. 宏定义和两个#结合5. 宏定义和do…while()的结合6. #ifdef…#else&...
    99+
    2022-12-27
    C语言宏定义 C语言宏定义扩展
  • C语言转义字符使用实例分析
    这篇文章主要介绍“C语言转义字符使用实例分析”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“C语言转义字符使用实例分析”文章能帮助大家解决问题。1.认识转义字符所有的ASCII码都可以用“”加数字(一...
    99+
    2023-06-17
  • C语言指针如何定义
    在C语言中,可以使用以下方法来定义指针:1. 在变量名前面添加一个星号(*),表示这是一个指针变量。例如:`int *ptr;` 表...
    99+
    2023-08-18
    C语言
  • c语言怎么定义方法
    在 c 语言中,方法是指函数的一种特殊形式,用于对对象进行操作。定义一个方法需要遵循以下步骤:声明方法:在类结构体内声明方法,格式为:class_name method_name(par...
    99+
    2024-05-21
    c语言
  • C语言详细分析宏定义与预处理命令的应用
    目录宏定义与预处理命令预处理命令 - 宏定义定义符号常量定义傻瓜表达式定义代码段预定义的宏函数 VS 宏定义预处理命令 - 条件式编译示例宏定义与预处理命令 预处理阶段:处理宏定义与...
    99+
    2024-04-02
  • Java自定义序列化行为的示例分析
    这篇文章给大家分享的是有关Java自定义序列化行为的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。正常情况下,一个类实现java序列化很简单,只需要implements Serializable接口即可,...
    99+
    2023-06-17
  • C++语言举例分析
    这篇文章主要介绍“C++语言举例分析”,在日常操作中,相信很多人在C++语言举例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C++语言举例分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!因为依赖开...
    99+
    2023-06-17
  • RedMonk语言排行分析
    本篇内容介绍了“RedMonk语言排行分析”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!知名软件行业分析公...
    99+
    2024-04-02
  • C++和C语言对比分析
    C++和C语言对比分析 C++和C语言都是广泛使用的编程语言,它们有着许多相似之处,同时也存在着一些显著的区别。本文将对这两种语言进行对比分析,从语法特点、面向对象编程、指针使用、标准...
    99+
    2024-04-02
  • C语言:自定义类型详解
    目录一、结构体1.结构体变量的定义及初始化2.结构体内存对齐3.为什么要内存对齐呢?二、位段1.什么是位段2.位段的内存分配三、枚举1.枚举的定义2.枚举的优点四、联合(共用体)1....
    99+
    2024-04-02
  • C语言如何自定义函数
    这篇文章主要介绍了C语言如何自定义函数的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇C语言如何自定义函数文章都会有所收获,下面我们一起来看看吧。先动手编写程序:#include <stdio.h...
    99+
    2023-06-16
  • C语言宏定义怎么使用
    这篇文章主要讲解了“C语言宏定义怎么使用”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“C语言宏定义怎么使用”吧!一、C语言中函数的“缺陷”实参和形参之间仅仅是值传递,因此,函数中无法直接改变...
    99+
    2023-06-30
  • C语言宏定义#define的使用
    目录无参宏定义定义形式 带参宏定义定义形式#和##运算 #运算 用法:##运算 用法:变参宏#ifndef 条件编译    ...
    99+
    2024-04-02
  • c语言string类型如何定义
    在C语言中,可以使用字符数组来表示字符串,并通过数组的最后一个元素设置为'\0'来表示字符串的结束。可以使用以下两种方式定义字符串:...
    99+
    2023-10-28
    c语言
  • c语言字符串怎么定义
    在C语言中,可以使用字符数组来定义字符串。例如: char str[20]; // 定义一个长度为20的字符数组,用于存储字符串 ...
    99+
    2024-02-29
    c语言
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作