u8国际,u8国际官方网站,u8国际网站最新,u8国际网站,u8国际网址,u8国际链接
c语言,未定义行为总结 :定义语言c语言未定义行为知乎c语言未定义行 为是什么c未定义行为意思 篇一:C语言中的未定义行为 C语言的初学者经常会问一些貌似“专业”的问题,比如 #includestdio.h main() { 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; 尽管在某些环境中数组和指针的行为非常相似,但它们是不同 的。在第一个声明中,filename 是一个字符数组的名字。尽管使 用数组的名字可以产生数组第一个元素的指针,但这个指针只有 在需要的时候才产生并且不会持续。在第二个声明中,filename 是一个指针的名字。这个指针可以指向程序员让它指向的任何地 方。如果程序员没有给它赋一个值,它将具有一个默认的 0 值 (NULL)([译注]实际上,在C 中一个为初始化的指针通常具有一 个随机的值,这是很危险的!)。 这两个声明以不同的方式使用存储区,它们不可能共存。 避免这种类型冲突的一个方法是使用像 lint 这样的工具(如果 可以的话)。为了在一个程序的不同编译单元之间检查类型冲突, 一些程序需要一次看到其所有部分。典型的编译器无法完成,但 lint 可以。 避免该问题的另一种方法是将外部声明放到包含文件 中。这时,一个外部对象的类型仅出现一次[7]。 4 语义缺陷 4.1 表达式求值顺序 一些C 运算符以一种已知的、特定的顺序对其操作数进行求值。 但另一些不能。例如,考虑下面的表达式: a b && c d C 语言定义规定a b 首先被求值。如果a 确实小于b,c d 必 须紧接着被求值以计算整个表达式的值。但如果a 大于或等于b, 则c d 根本不会被求值。 要对a b 求值,编译器对a 和b 的求值就会有一个先后。但在 一些机器上,它们也许是并行进行的。 C 中只有四个运算符 &&、、?:和,指定了求值顺序。&&和最先 对左边的操作数进行求值,而右边的操作数只有在需要的时候才 进行求值。而?:运算符中的三个操作数:a、b 和c,最先对a 进行 求值,之后仅对b 或c 中的一个进行求值,这取决于a 的值。,运 算符首先对左边的操作数进行求值,然后抛弃它的值,对右边的 操作数进行求值[8]。 C 中所有其它的运算符对操作数的求值顺序都是未定义的。事实 上,赋值运算符不对求值顺序做出任何保证。 出于这个原因, 下面这种将数组 x 中的前 n 个元素复制到数组 y 中的方法是不可 行的: i = 0; while(i n) y[i] = x[i++]; 其中的问题是y[i]的地址并不保证在i 增长之前被求值。在某些 实现中,这是可能的;但在另一些实现中却不可能。另一种情况 出于同样的原因会失败: i = 0; while(i n) y[i++] = x[i]; 而下面的代码是可以工作的: i = 0; while(i n) { y[i] = x[i]; i++; } 当然,这可以简写为: for(i = 0; i n; i++) y[i] = x[i]; 4.2 &&、和!运算符 4.3 下标从零开始 在很多语言中,具有 n 个元素的数组其元素的号码和它的下标 是从1 到n 严格对应的。但在C 中不是这样。 个具有 n 个元素的 C 数组中没有下标为 n 的元素,其中的元素 的下标是从0 到n - 1。因此从其它语言转到C 语言的程序员应该 特别小心地使用数组: int i, a[10]; for(i = 1; i = 10; i++) 篇三:c 语言笔试常见问题 1—程序的内存分配 1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数 值,局部变量的值等。其操作方式类似于数据结构中的栈。 2、堆区(heap) — 一般由程序员分配释放, 若程序员不释 放,程序结束时可能由 OS 回收 。注意它与数据结构中的堆是两 回事,分配方式倒是类似于链表,呵呵。 3、全局区(静态区)(static)—,全局变量和静态变量的存储 是放在一块的,初始化的全局变量和静态变量在一块区域, 未初 始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放 4、文字常量区—常量字符串就是放在这里的。 程序结束后由系 统释放 5、程序代码区—存放函数体的二进制代码。 二、例子程序 这是一个前辈写的,非常详细 //main.cpp int a = 0; 全局初始化区 char *p1; 全局未初始化区 main() { int b; 栈 char s[] = abc; 栈 char *p2; 栈 char *p3 = 123456; 123456\0 在常量区,p3 在栈上。 static int c =0; 全局(静态)初始化区 p1 = (char *)malloc(10); p2 = (char *)malloc(20); 分配得来得10 和20 字节的区域就在堆区。 strcpy(p1, 123456); 123456\0 放在常量区,编译器可能会将它与 p3 所指向的123456 优化成一个地方。 } 2、Const 和static 区别 对于C/C++语言来讲, const 就是只读的意思,只在声明中使用; static 一般有2 个作用,规定作用域和存储方式. 对于局部变量,static 规定其为静态存储方式,每次调用的初始值 为上一次调用的值,调用结束后存储空间不释放; 对于全局变量,如果以文件划分作用域的话,此变量只在当前文 件可见;对于static 函数也是在当前模块内函数可见. static const 应该就是上面两者的合集. 下面分别说明: 全局: const,只读的全局变量,其值不可修改. static,规定此全局变量只在当前模块(文件)中可见. static const,既是只读的,又是只在当前模块中可见的. 文件: 文件指针可当作一个变量来看,与上面所说类似. 函数: const,返回只读变量的函数. static,规定此函数只在当前模块可见. 类: const,一般不修饰类,(在VC6.0 中试了一下,修饰类没啥作用) static,C++中似乎没有静态类这个说法,一般还是拿类当特殊的变 量来看.C#中有静态类的详细说明,且用法与普通类大不相同. 3new delete 和malloc free 的区别 malloc 与 free 是 C++/C 语言的标准库函数,new/delete 是 C++ 的运算符。它们都可用于申请动态内存和释放内存。 对于非内部数据类型的对象而言,光用maloc/free 无法满足动 态对象的要求。对象在创建的同时要自动执行构造函数,对象在 消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是 运算符,不在编译器控制权限之内,不能够把执行构造函数和析 构函数的任务强加于malloc/free。 因此 C++语言需要一个能完成动态内存分配和初始化工作的运 算符new,以及一个能完成清理与释放内存工作的运算符delete。 注意new/delete 不是库函数。 4main() { int x=10,y=10,i; for (i=0;x8;y=++i) printf(%d %d ,x--,y); } 在循环语句for(表达式1;表达式2;表达式3)中,先执行表 达式一,再执行表达式二,如果表达式二成立,就进入循环,第 一次循环执行完后(本程序共两次循环),才执行表达式三(这是 表达式三第一次被执行),然后再执行表达式二,看其是否成立, 如果成立,就进行第二次循环。如此循环,表达式一只在第一次 循环时执行,以后不再执行,表达式三在第一次循环不执行,以 后的每次循环都执行。如果你要这是为什么,我只能说这是规定, 别的就不知道了。 5 什么是静态成员变量 在C++类的成员变量被声明为static(称为静态成员变量),意味 着它为该类的所有实例所共享,也就是说当某个类的实例修改了 该静态成员变量,其修改值为该类的其它所有实例所见。 比如在某个类A 中声明一个static int number;初始化为0。这个 number 就能被所有 A 的实例共用。在 A 的构造函数里加上 number++,在A 的析构函数里加上number--。那么每生成一个A 的实例,number 就加一,每销毁一个A 的实例,number 就减一, 这样,number 就可以记录程序生成了多少个A 的实例。 这只是静态成员的一种用法而已。 6 静态数据成员和静态成员函数在程序中是如何声明和定义的 class Foo { public: static int a; stataic void func(); } 静态数据成员和函数都是在声明前加static 静态成员必须要在类外初始化,无法在构造函数内初始化。新 标准的 C++也允许在生命静态数据成员的是后直接加等于号进行 初始化,但是大部分编译器不支持。所以最保险的办法就是在类 定义的外面再写: int Foo::a = 0; 注意,这时候不需要再static 了。 函数则很普通成员函数的声明以及实现没区别,唯一要注意的 是,静态函数是没有 this 指针的,因此不能访问任何非静态的其 他成员函数或成员变量,如果要访问需要传递 this 指针进去,比 如 class Foo { public: int a; static void func(Foo* ptrFoo) { a = 0; // 错误!!!a 不是静态变量,无法访问! ptrFoo-a= 0; //正确。 } void test() { // 非静态成员函数调用静态成员函数可以传递this 指针,让静 态成员函数通过他来访问 // 其他成员函数和成员变量。 Foo::func(this); } } 7 虚函数和纯虚函数区别 虚函数和纯虚函数 在面向对象的C++语言中,虚函数(virtual function)是一个非 常重要的概念。因为它充分体现了面向对象思想中的继承和多态 性这两大特性,在 C++语言里应用极广。比如在微软的 MFC 类库 中,你会发现很多函数都有 virtual 关键字,也就是说,它们都是 虚函数。难怪有人甚至称虚函数是C++语言的精髓。 那么,什么是虚函数呢,我们先来看看微软的解释: 虚函数是指一个类中你希望重载的成员函数,当你用一个基类 指针或引用指向一个继承类对象的时候,你调用一个虚函数,实 际调用的是继承类的版本。 ——摘自MSDN 这个定义说得不是很明白。MSDN 中还给出了一个例子,但是 它的例子也并不能很好的说明问题。我们自己编写这样一个例子: #i nclude stdio.h #i nclude conio.h class Parent { public: char data[20]; void Function1(); virtual void Function2(); // 这里声明Function2 是虚函数 }parent; void Parent::Function1() { printf(This is parent,function1\n); } void Parent::Function2()
@HASHKFK