C陷阱与缺陷整理二

1.在C语言中,我们没有办法将一个数组作为函数参数传递,如果我们使用数组名作为参数,这个时候数组名立刻会被转换为指向该数组的第一个元素的指针。

关于这一点的理解可以向前深入一步,比如定义的数组为int a[3],那么a作为参数传递之后会变为int *类型;如果定义的数组为int a[3][4],那么a作为参数传递之后被变为int (*)[4];如果定义的数组为int a[3][4][5],那么a作为参数传递之后会变为int (*)[4][5];后续的以此类推。为什么可以这样呢?因为C语言中的多维数组都是利用一维数组仿真出来的,即一维数组的每一个元素都可以是别的类型的数据单元,即便这个数据单元又是另一个数组,然而根据上面的观点,一维数组a在被作为参数传递的时候会自动退化为指向该一维数组的第一个单元的指针,所以如果第一个单元是一个一维数组,那么a就退化为一个一维数组指针,如果a的第一个单元是一个二维数组,那么a就退化为一个二维数组指针,所以上面的结论是不难得出的。

2.看下面的代码片段输出会是多少?

void print(int b[])

{

printf("%d", sizeof(b));

}

int main(void)

{

int a[4];

print(a);

return 0;

}

分析:要弄清楚这段代码片段的输出,还是要清楚函数调用时候数组的传递过程,上面第一点已经说过了,在传递参数的时候数组已经自动被退化为指向其第一个单元的指针,所以在函数传递的过程中相当于出现了这样的一个赋值的过程,int b[] = a或者更清楚一些int b[] = &a[0],但是这样的语句编译器会认为是一个错误的语法!但是实际中我们经常可能会这样来使用却并没有报错,这是因为编译器在这里会将b强制做一次退化,退化为一个int *的指针类型。所以上面的程序片段输出内容显而易见,输出的就是一个int类型的指针变量的大小,也就是4(32位系统)。

3.main函数参数的两种形式

int main(int argc, char *argv[])

int main(int argc, char **argv)

需要注意的是,前一种写法强调的重点在于argv是一个指向某数组的起始元素的指针,该数组的元素为字符指针类型。

4.以下的这种写法:

char *p = "xyz";

p[0] = ‘A‘;

编译的器件可能不会产生问题,但是运行的时候很可能会碰到类似于某地址不能为written这种提示,K&RC中对这种修改行为的说明是:试图修改字符串常量的行为是未定义的。虽然有些编译器允许这样的行为,但是这种写法是不值得提倡的。

5.除了一个重要的例外情况,在C语言中将一个整数转换为一个指针,最后得到的结果都取决于具体的C编译器实现。这个特殊情况就是常数0,编译器保证由0转换而来的指针不等于任何有效的指针,出于代码文档化的考虑,常数0这个值经常用一个符号来代替:

#define NULL 0

需要记住的是当常数0被转换为指针使用时,这个指针绝对不能被解除引用(解除引用即是使用(*p)这类取该地址中内容的操作),换句话说,当我们将0赋值为一个指针变量时,绝对不能企图使用该指针所指向的内存中所存储的内容。

6.C语言中“不对称边界”的好处

在C语言中定义了一个数组int a[10]之后,数组的下标0~9为合法的下标,而下标10已经超出了数组的范围。这样做的好处是什么呢?

第一个好处,请看下面的一个例子:

for(i = 0; i < 10; i++)

a[i] = *p++;

如果用户给出了begin(0)和end(10)的范围之后要求对这之间的单元进行操作,如果用户给定的begin和end是相同的话,上面这种写法完全可以避免出现错误。同时要操作的单元个数可以通过end-begin简单的就算出来,这样做的前提就是用户给出的begin和end都是遵守C语言的“不对称边界”使用方法。而如果不使用不对称边界时候(这时候数组的下标为1~10合法)的诸如代码:

for(i = 1; i <= 10; i++)

a[i] = *p++;

才可以完成对数组的初始化或者遍历等操作,这样写之后,实际操作的单元个数为10-1+1=10个,这样的计算过程如果程序员在编程的时候忘了加上一个1那么很容易造成程序的bug。同时如果将1和10换成begin和end变量的话,那么用户在调用这个函数的时候传递的begin和end值就算是同一个值,这段代码也会操作到数组中的a[begin]值,这个也会造成调用者使用的困难。

第二个好处是我们可以将&a[10]来作为一个判断条件,作为缓冲区或者数组操作完成的一个标志,这在实际编程中也是相当方便的。虽然对a[10]的值进行操作是属于非法的行为,但是在ANSI中明确规定了&a[10]这种操作是合法的。

7.在大多数的C语言实现中,--n >= 0至少与等效的n-- > 0一样快,甚至在某些C实现中还要更快,第一个表达式--n >= 0的计算是首先从n中减去1,然后将结果与0比较;第二个表达式的计算则首先保存n,然后从n中减去1,最后比较保存值与0的大小。

8.C语言中只有四个运算符(&&、||、?:、,)存在规定的求值顺序,运算符&&和运算符||首先对左侧的操作数求值,只有在需要的时候才会对右侧的操作数求值。运算符?:有三个操作数,在a?b:c中,操作数a首先被求值,根据a的值在求操作数b或者c的值(b和c只有一个表达式会被计算)。而逗号运算符,首先对左侧的操作数求值,然后该值被“丢弃”,在对右侧操作数求值,整个表达式的值是最右侧表达式的值。

逗号运算符举例:a = (1, 2, 3);

a最后被赋值为3。

注意:分隔函数参数的逗号并非逗号运算符,例如:f(x, y)中的求值顺序是未定义的,而在函数g((x,y))中却是确定的先x后y的顺序,在后一个例子中,函数g只有一个参数,这个参数的值就是括号中逗号运算符的值。

注意:在C语言中其他所有运算符对其操作数求值的顺序是未定义的。特别地,赋值运算符并不保证任何求值顺序。如果在一个表达式中出现对同一变量的多次使用中出现了++或者--等操作后果有时是不可预计的。例如:

y[i] = x[i++];

9.逻辑运算符的结果是一个逻辑值,即真(1)或假(0),而逻辑判断的时候通常约定将0视作假,非0视作真。所以!10表达式的值为假(0),因为10非0在进行非运算的时候被视作真,真的非即为假。

10.C语言中存在两类整数算术运算,有符号数与无符号数运算。无符号数运算中没有溢出的说法,然而有符号数操作就可能会发生溢出的情况,当一个运算的结果发生“溢出”时,作出任何假设都是不安全的。当碰到可能溢出的情况应该采取的方法是将两个操作数a和b都强制转换为无符号整数:

if((unsigned)a + (unsigned)b > INT_MAX)

complain();

此处的INT_MAX是一个已定义常量,代表可能的最大整数值。ANSI C标准在<limits.h>中定义了INT_MAX;如果在其它的C语言实现上,读者可能需要自己重新定义这个值。

时间: 2024-10-11 07:24:41

C陷阱与缺陷整理二的相关文章

C陷阱和缺陷整理四

1.assert宏的定义 #define assert(e) \ ((void)((e) || _assert_error(__FILE__, __LINE__))) 库里面对这个宏做了这样的定义,当宏参数(或表达式)e为真的时候由||运算符的运算规则会执行_assert_error(__FILE__, __LINE__)从而打印一条报警信息.所以整个表达式的最终会变为(void)0或者(void)1这种形式,这种形式确实有点奇怪? 系统这样定义的目的是当一个值被转换为void类型之后,没有一个

读书笔记--C陷阱与缺陷(二)

第二章 1. 理解函数声明 书中分析了复杂的类型声明方式,也说明了使用typedef声明会更好理解,推荐大家使用typedef进行函数声明. 书中类型分析一层一层挖掘,让读者可以理解多层嵌套的类型含义,有时间的读者可以去看看,笔者不再重复. 既然书中推荐使用typedef进行函数声明,我们就来研究下typedef: typedef主要用于定义一种类型/结构体的别名. 从字面上看和C的宏定义 #define 挺像的,但是define只是简单的参数替换,如果不注意括号很容易产生预期之外的错误. 在指

C陷阱与缺陷整理一

1.词法分析中的"贪心法" C语言的某些符号,例如/.*和=,只有一个字符长,称为单字符符号.而C语言中的其他符号,例如/*和==,以及标识符等都包含了多个字符,称为多字符符号.当C编译器读入一个字符'/'后又跟了一个字符'*',那么编译器就必须做出判断:是将其作为两个分别的符号对待,还是合起来作为一个符号来对待.C语言对这个问题的解决方案可以归纳为一个很简单的规则:每一个符号应该包含尽可能多的字符.也就是说,编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可

C陷阱与缺陷整理三

1.大多数C语言的实现都通过函数main的返回值来告诉操作系统该函数的执行是成功还是失败.典型的处理方案是,返回值为0代表程序执行成功,返回值非0则表示程序执行失败.如果一个程序的main函数并不返回任何值,那么有可能看上去执行失败.所以建议我们的C程序的main函数应该如下编写: int main() { return 0; } 当然如果main函数需要接受参数的话将参数声明加上更加完美. 2.一个C程序可能是由多个分别编译的部分组成,这些不同的部分通过一个通常叫做连接器的程序合并为一个整体.

阅读《C陷阱与缺陷》的知识增量

看完<C陷阱与缺陷>,忍不住要重新翻一下,记录一下与自己的惯性思维不符合的地方.记录的是知识的增量,是这几天的流量,而不是存量. 这本书是在ASCI C/C89订制之前写的,有些地方有疏漏. 第一章 词法陷阱 1.3 C语言中解析符号时使用贪心策略,如x+++++y将被解析为x++ ++ +y,并编译出错. 1.5 单引号引起的一个字符代表一个对应的整数,对于采用ASCII字符集的编译器而言,'a'与0141.97含义一致. 练习1.1 嵌套注释(如/*/**/*/)只在某些C编译器中允许,如

《C陷阱与缺陷》读书笔记

<C陷阱与缺陷>读书笔记 1.编译器中的词法分析器负责将程序分解为一个个符号.C语言中,符号之间的空白 (包括Space ,Tab , Enter) 都将被忽略,但一个符号的中间不能有空白,否则可能被解释成为另一个或几个符号. 2.编译器将程序分解成符号的方法是从左到右逐个字符读入,如果该字符可能会组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分:如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已经不再可能组成一个有意义的

C陷阱与缺陷 读书笔记

C陷阱与缺陷 1.  typedef用法: ①    定义一种类型别名,而不是简单的宏替换: char *pa,pb;(注意:pb并没有定义为指针,虽然你可能想这么定义它) typedef char* PCHAR PCHAR pa, pb; ②   用在旧的C代码中,帮助struct.以前的代码中,声明struct新对象时,必须带上struct,即形式为:struc结构名对象名,如: struct tagPOINT1 { Int x; Int y; }; struct tagPOINT1 p1;

读书笔记--C陷阱与缺陷(七)

第七章 1.null指针并不指向任何对象,所以只用于赋值和比较运算,其他使用目的都是非法的. 误用null指针的后果是未定义的,根据编译器各异. 有的编译器对内存位置0只读,有的可读写. 书中给出了一种判断编译器如何处理内存0的代码: 1 #include <stdio.h> 2 int main() 3 { 4 5 char *p; 6 p=NULL; 7 printf("location 0 contains: %d\n", *p); 8 9 return 0; 10

AJAX学习整理二之简单实例

做了几个简单的实例,加载txt文本内容.加载xml文件内容,把xml文本内容转换成html表格显示.废话不多说,直接贴代码: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/html"> <head>     <title>通过ajax获取文本内容</title>     <meta charset="utf-8">     <scr