u8国际,u8国际官方网站,u8国际网站最新,u8国际网站,u8国际网址,u8国际链接
c语言,未定义行为总结:定义语言c语言未定义行为知乎c语言未定义行为是什么c未定义行为意思篇一:C语言中的未定义行为C语言的初学者经常会问一些貌似“专业”的问题,比如#includestdio.hmain(){inti=5;intj=++i+++i+++i;printf(%d\n,j);system(pause);}这样的问题实在不需要多做考虑,而且应该在实际编程实践中尽量避免。因为它们几乎都是”未指明的行为”或“由实现定义的行为”。另一方面,程序的错误或Bugs,通常是由于“未定义的行为”。C++Primer第四版中的解释:使用了未定义行为的程序都是错误的,即使程序能够运行,也只是巧合。未定义行为源于编译器不能检测到的程序错误或太麻烦以至无法检测的错误。不幸的是,含有未定义行为的程序在有些环境或编译器中可以正确执行,但并不能保证同一程序在不同编译器中甚至在当前编译器的后继版本中会继续正确运行,也不能保证程序在一组输入上可以正确运行且在另一组输入上也能正确运行。程序不应该依赖未定义行为。篇二:C语言缺陷与陷阱(学习笔记)C语言缺陷与陷阱(笔记)C语言像一把雕刻刀,锋利,并且在技师手中非常有用。和任何锋利的工具一样,C会伤到那些不能掌握它的人。本文介绍C语言伤害粗心的人的方法,以及如何避免伤害。第一部分研究了当程序被划分为记号时会发生的问题。第二部分继续研究了当程序的记号被编译器组合为声明、表达式和语句时会出现的问题。第三部分研究了由多个部分组成、分别编译并绑定到一起的C程序。第四部分处理了概念上的误解:当一个程序具体执行时会发生的事情。第五部分研究了我们的程序和它们所使用的常用库之间的关系。在第六部分中,我们注意到了我们所写的程序也许并不是我们所运行的程序;预处理器将首先运行。最后,第七部分讨论了可移植性问题:一个能在一个实现中运行的程序无法在另一个实现中运行的原因。词法分析器(lexicalanalyzer):检查组成程序的字符序列,并将它们划分为记号(token)一个记号是一个由一个或多个字符构成的序列,它在语言被编译时具有一个(相关地)统一的意义。C程序被两次划分为记号,首先是预处理器读取程序,它必须对程序进行记号划分以发现标识宏的标识符。通过对每个宏进行求值来替换宏调用,最后,经过宏替换的程序又被汇集成字符流送给编译器。编译器再第二次将这个流划分为记号。1.1=不是==:C语言则是用=表示赋值而用==表示比较。这是因为赋值的频率要高于比较,因此为其分配更短的符号。C还将赋值视为一个运算符,因此可以很容易地写出多重赋值(如a=b=c),并且可以将赋值嵌入到一个大的表达式中。1.2&和不是&&和1.3多字符记号C语言参考手册说明了如何决定:“如果输入流到一个给定的字符串为止已经被识别为记号,则应该包含下一个字符以组成能够构成记号的最长的字符串”“最长子串原则”1.4例外组合赋值运算符如+=实际上是两个记号。因此,a+/*strange*/=1和a+=1是一个意思。看起来像一个单独的记号而实际上是多个记号的只有这一个特例。特别地,p-a是不合法的。它和p-a不是同义词。另一方面,有些老式编译器还是将=+视为一个单独的记号并且和+=是同义词。1.5字符串和字符包围在单引号中的一个字符只是编写整数的另一种方法。这个整数是给定的字符在实现的对照序列中的一个对应的值。而一个包围在双引号中的字符串,只是编写一个有双引号之间的字符和一个附加的二进制值为零的字符所初始化的一个无名数组的指针的一种简短方法。使用一个指针来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替单引号也会得到一个警告消息(反之亦然)。但对于不检查参数类型的编译器却除外。由于一个整数通常足够大,以至于能够放下多个字符,一些C编译器允许在一个字符常量中存放多个字符。这意味着用yes代替yes将不会被发现。后者意味着“分别包含y、e、s和一个空字符的四个连续存储器区域中的第一个的地址”,而前者意味着“在一些实现定义的样式中表示由字符y、e、s联合构成的一个整数”。这两者之间的任何一致性都纯属巧合。2句法缺陷理解这些记号是如何构成声明、表达式、语句和程序的。2.1理解声明每个C变量声明都具有两个部分:一个类型和一组具有特定格式的、期望用来对该类型求值的表达式。float*g(),(*h)();表示*g()和(*h)()都是float表达式。由于()比*绑定得更紧密,*g()和*(g())表示同样的东西:g是一个返回指float指针的函数,而h是一个指向返回float的函数的指针。当我们知道如何声明一个给定类型的变量以后,就能够很容易地写出一个类型的模型(cast):只要删除变量名和分号并将所有的东西包围在一对圆括号中即可。float*g();声明g是一个返回float指针的函数,所以(float*())就是它的模型。(*(void(*)())0)();硬件会调用地址为0处的子程序(*0)();但这样并不行,因为*运算符要求必须有一个指针作为它的操作数。另外,这个操作数必须是一个指向函数的指针,以保证*的结果可以被调用。需要将0转换为一个可以描述“指向一个返回void的函数的指针”的类型。(Void(*)())0在这里,我们解决这个问题时没有使用typedef声明。通过使用它,我们可以更清晰地解决这个问题:typedefvoid(*funcptr)();//typedeffuncptrvoid(*)();指向返回void的函数的指针(*(funcptr)0)();//调用地址为0处的子程序2.2运算符并不总是具有你所想象的优先级 绑定得最紧密的运算符并不是真正的运算符:下标、函数调用 和结构选择。这些都与左边相关联。 接下来是一元运算符。它们具有真正的运算符中的最高优先级。 由于函数调用比一元运算符绑定得更紧密,你必须写(*p)()来调用 p 指向的函数;*p()表示p 是一个返回一个指针的函数。转换是一 元运算符,并且和其他一元运算符具有相同的优先级。一元运算 符是右结合的,因此*p++表示*(p++),而不是(*p)++。 在接下来 是真正的二元运算符。其中数学运算符具有最高的优先级,然后 是移位运算符、关系运算符、逻辑运算符、赋值运算符,最后是 条件运算符。需要记住的两个重要的东西是: 1. 所有的逻辑运算符具有比所有关系运算符都低的优先级。 2. 移位运算符比关系运算符绑定得更紧密比条件运算符更低的 优先级是有意义的。另外,所有的复合赋值运算符具有相同的优 先级并且是自右至左结合的 具有最低优先级的是逗号运算符。赋值是另一种运算符,通常 具有混合的优先级。 2.3 看看这些分号! 或者是一个空语句,无任何效果;或者编译器可能提出一个诊 断消息,可以方便除去掉它。一个重要的区别是在必须跟有一个 语句的 if 和 while 语句中。另一个因分号引起巨大不同的地方是 函数定义前面的结构声明的末尾,考虑下面的程序片段: struct foo { int x; } f() { ... } 在紧挨着 f 的第一个}后面丢失了一个分号。它的效果是声明了 一个函数 f,返回值类型是 struct foo,这个结构成了函数声明的 一部分。如果这里出现了分号,则 f 将被定义为具有默认的整型 返回值[5]。 2.4 switch 语句 C 中的 case 标签是真正的标签:控制流程可以无限制地进入到 一个case 标签中。 看看另一种形式,假设C 程序段看起来更像Pascal: switch(color) { case 1: printf (red); case 2: printf (yellow); case 3: printf (blue); } 并且假设 color 的值是 2。则该程序将打印 yellowblue,因为控 制自然地转入到下一个 printf()的调用。 这既是 C 语言 switch 语 句的优点又是它的弱点。说它是弱点,是因为很容易忘记一个 break 语句,从而导致程序出现隐晦的异常行为。说它是优点,是 因为通过故意去掉 break 语句,可以很容易实现其他方法难以实 现的控制结构。尤其是在一个大型的 switch 语句中,我们经常发 现对一个case 的处理可以简化其他一些特殊的处理。 2.5 函数调用 和其他程序设计语言不同,C 要求一个函数调用必须有一个参 数列表,但可以没有参数。因此,如果f 是一个函数, f(); 就是对该函数进行调用的语句,而 f; 什么也不做。它会作为函数地址被求值,但不会调用它[6]。 2.6 悬挂else 问题 一个else 总是与其最近的if 相关联。 3 连接 一个 C 程序可能有很多部分组成,它们被分别编译,并由一个 通常称为连接器、连接编辑器或加载器的程序绑定到一起。由于 编译器一次通常只能看到一个文件,因此它无法检测到需要程序 的多个源文件的内容才能发现的错误。 3.1 你必须自己检查外部类型 假设你有一个 C 程序,被划分为两个文件。其中一个包含如下 声明: int n; 而令一个包含如下声明: long n; 这不是一个有效的 C 程序,因为一些外部名称在两个文件中被 声明为不同的类型。然而,很多实现检测不到这个错误,因为编 译器在编译其中一个文件时并不知道另一个文件的内容。因此, 检查类型的工作只能由连接器(或一些工具程序如lint)来完成; 如果操作系统的连接器不能识别数据类型,C 编译器也没法过多 地强制它。 那么,这个程序运行时实际会发生什么?这有很多可能性: 1. 实现足够聪明,能够检测到类型冲突。则我们会得到一个诊 断消息,说明n 在两个文件中具有不同的类型。 2. 你所使用的实现将 int 和 long 视为相同的类型。典型的情况 是机器可以自然地进行32 位运算。在这种情况下你的程序或许能 够工作,好象你两次都将变量声明为 long(或 int)。但这种程序 的工作纯属偶然。 3. n 的两个实例需要不同的存储,它们以某种方式共享存储区, 即对其中一个的赋值对另一个也有效。这可能发生,例如,编译 器可以将 int 安排在 long 的低位。不论这是基于系统的还是基于 机器的,这种程序的运行同样是偶然。 4. n 的两个实例以另一种方式共享存储区,即对其中一个赋值的 效果是对另一个赋以不同的值。在这种情况下,程序可能失败。 这种情况发生的另一个例子出奇地频繁。程序的某一个文件包 含下面的声明: char filename[] = etc/passwd; 而另一个文件包含这样的声明: char *filename; 尽管在某些环境中数组和指针的行为非常相似,但它们是不同
@HASHKFK