读《C陷阱与缺陷》

摘要:
《C陷阱与缺陷》里面介绍了一些自己不知道和以前理解不深的东东,现总结如下:1.词法分析的陷阱y=x/*p;/*p指向除数*/上述语句的本意是:用x除以指针p所指向的值,然后把商赋给y;但是/*被编译器理解为一段注释的开始,编译器将不断地读入字符,直到*/出现为止。这是C语言将数组的下表定义为从0开始的引起的一个弊端。但是C语言数组的这种不对称边界会对程序设计的简化带来许多方便之处:I.数组的长度就是上届与下届之差。

《C陷阱与缺陷》里面介绍了一些自己不知道和以前理解不深的东东,现总结如下:

1.词法分析的陷阱(本书第9页)

y = x/*p;            /* p指向除数 */

上述语句的本意是:用x除以指针p所指向的值,然后把商赋给y;但是/*被编译器理解为一段注释的开始,编译器将不断地读入字符,直到*/出现为止。也就是说该语句实际的执行效果只是将x的值赋给y而已;

可以将上面的语句重写成如下格式:

y = x / *p           /*p指向除数 */

【备注】:我们的项目组中,明确规定在运算符与变量之间必须添加空格,就是为了避免上面的错误;

2.运算符优先级(本书第22页)

关于运算符优先级,我们需要记住两点:
I.任何一个逻辑运算符的优先级低于任何一个关系运算符;
II.移位运算符的优先级比算术运算符要低,但是比关系运算符要高;

3.数组的边界计算与不对称边界(本书第45页)

int i, a[10];

for (i=1; i<=10; i++)
    a[i] = 0;

上述代码的错误非常明显,数组a中并不存在a[10]这个元素,而在for循环中却引用了这个变量。如果用来编译这段程序的编译器按照内存地址递减的方式来给变量分配内存,那么内存中数组a之后的一个字(word)实际上是分配给了整型变量i。此时,本来循环计数器i的值是10,循环体内将并不存在的a[10]设置为0,实际上却是将计数器i的值设置为0,这就是陷入了一个死循环(本人使用gcc测试了一下,运行该程序确实为以死循环)。这是C语言将数组的下表定义为从0开始的引起的一个弊端。

但是C语言数组的这种不对称边界会对程序设计的简化带来许多方便之处:
I.数组的长度就是上届与下届之差。
II.如果数组的取值范围为空,那么上界等于下界;
III.即使数组的取值范围为空,上界也永远不可能小于下界;

这种不对称边界的思考方式使用,是把上界视作某序列中第一个被占用的元素,而把下界视作序列中第一个被释放的元素。当处理各种不同的类型的缓冲区时,这种看待问题的方式就特别有用。例如,考虑这样一个函数,该函数的功能是将长度无规律的输入数据送到缓冲区(即一块能够容纳N个字符的内存)中去,每当这块内存被“填满”时,就将缓冲区的内容写出。因此,该函数就可以这样实现:

#define N 1024
static charbuffer[N];           
static char *bufptr;             /*指向缓冲区的当前位置 */

void bufwrite (char *p, intn)
{
        bufptr = &buffer[0];

       while (--n >= 0)
       {
            if (bufptr == &buffer[N])
                flushbuffer();    /*清空缓冲区 */
            
            *bufptr++ = *p++;
        }
}

虽然不存在buffer[N]这个元素,但是我们并不需要引用这个元素,而只需要引用这个元素的地址,并且这个地址在我们遇到的所有C语言实现中又是“千真万确”存在的。而且ANSI C标准明确允许这种做法:数组中实际不存在的“溢界”元素的地址卫浴数组所占内存之后,这个地址可以用于进行赋值和比较。当然,如果要引用该元素,那就是非法的了。

4.数组和指针的区别(本书第36页)

int a[10];
int *p = a;

上述两个声明,sizeof(a) =10*sizeof(int),而sizeof(p)=4。

5.关于scanf的参数越界(本书第76页)

#include <stdio.h>

intmain ()
{
    inti;
    charc;
    for (i=0; i<5; i++)
    {
        scranf("%d", &c);
        printf("%d ", i);
    }

    printf("\n");

    return 0;          
}

上段代码问题关键在于,这里c被声明为char类型,而不是int类型。当scanf读入一个整数,应该传递给它一个指向整数的指针。而程序中scanf函数得到的却是一个指向字符的指针,scanf函数并不能分辨这种情况,它只是将这个指向字符的指针作为指向整数的指针而接受,并且在指针指向的位置存储一个整数。因为整数所占的存储空间要大于字符所占的存储空间,所以c附近的内存将被覆盖。c附近的内存中存储的内容是由编译器决定的,本例中它存放的是整数i的低端部分。因此,每次读入一个数值到c时,都会将i的低端部分覆盖为0,而i的高端部分本来就是0,相当于i每次被重新设置为0,循环将一直进行下去。

5.文件的读写顺序(本书第85页)

如果要同时进行输入和输出操作,必须在其中插入fseek函数的调用。如fwrite之后如想立即fread,则必须在fread之前fseek一下,及时fseek只是将指针偏移了0个字节。如下所示的代码:

while (fread((char *)&rec, sizeof(rec), 1, fp) == 1)
{
    /*对rec执行某些操作 */

    if ( /*rec必须被重新写入 */)
    {
        fseek(fp, -(long)sizeof(rec), 1);
        fwrite( (char *)&rec, sizeof(rec), 1, fp);
        fseek(fp, 0L, 1);
    }
}

/*第二个fseek函数看上去什么也没做,但它改变了文件的状态,使得文件现在可以正常地进行读取了 */

6.尽量不要使用宏来定义新的数据类型(本书第101页)

#define T1 struct foo *typedef struct foo *T2;

T1 a, b;
T2 a,b;

考虑以上代码,第一个声明被扩展为:struct foo *a, b; 在这个语句中a被定义为一个指向结构的指针,而b却被定义为一个结构(而不是指针)。而第二个声明,则将a和b都定义为了指向结构的指针,因为第二种T2的行为完全与一个真实的类型相同。

免责声明:文章转载自《读《C陷阱与缺陷》》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇转载:数据库 ' 库名' 已打开,并且一次只能有一个用户访问。 (Microsoft SQL Server,错误: 924)Websphere6.1安装说明下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

MATLAB的基本元素

MALTAB程序的基本数据单元是数组,MATLAB 的变量名必须以字母开头,后面可以跟字母,数字和下划线(_).只有前31个字符是有效的;如果超过了31 个字符,基余的字符将被忽略。如果声明两个变量,两变量名只有第32 个字符不同,那么MATLAB 将它们当作同一变量对待。要注意的是:确保你所声明的变量名前31 个字符是独一无二的。否则,MATLAB 将无...

【数据结构与算法】用go语言实现数组结构及其操作

引言   数组结构是一种很常见的数据结构,并且在大部分编程语言中都存在,这些语言都提供了现成的可以立马就能使用的数组这种数据结构。为了更好的理解数组,这边文章就是来实现数组。 数组的特点   1.内存中数据之间紧密排列在一起。   2.新增元素需要开辟内存空间用以存放新的元素。   3.新增元素时候,如果新增的元素在其他元素中间,那么该新增元素的位置后面所...

11 libubox

1 libubox 主要提供事件循环,二进制块格式处理、linux链表实现和一些JSON辅助主要包含3个软件包:libubox、jshn、libblobmsg-json 1.1 libubox 1、提供多种基础通用功能接口。如:链表、平衡二叉树、二进制块处理、MD5等2、提供多种sock接口封装3、提供事件驱动机制及任务管理功能 utils.h...

C++中点操作符和箭头操作符

区别 C++中对于类来说,对于其中的成员,用点操作符.来获得, 而对于一个指向类对象的指针来说,则用箭头操作符->调用该指针所指向对象的成员。 当类定义->重载操作符后,则既可以用箭头操作符,也可以用点操作符。 重载->操作符 重载箭头操作符必须定义为类成员函数。没有显式形参(而且是类成员,唯一隐式形参是this)。->的右操作数不...

IsBadStringPtr、IsBadWritePtr

判断调用进程是否拥有对指定字符串指针的读取权限,函数原型如下: BOOL IsBadStringPtr(     LPCTSTR lpsz,     UINT_PTR ucchMax); 参数: lpsz: 输入参数,指向字符串。 ucchMax:输入参数,读取字符串的最大长度。 返回值: 返回BOOL值,表示当前进程是否拥有字符串指针指向的字符串的度操作...

fs.readdirSync

fs.readdirSync 该方法将返回一个包含“指定目录下所有文件名称”的数组对象。 fs.readdirSync(path) arr.forEach 遍历数组 forEach只能遍历数组,原数组是不会变的,要创建新数组,就得用arr.map,通过下面的例子可知map方法好像是万能的 var arr=['apple','pear','banan',...