第三章 语义“陷阱”

 

         花了几天时间读了第三章,这一章是我认为最好的一章,至少对于现在的我来说是这

 

样的,在这一章中作者所讲述的有关数组、指针、循环的一些东西都是以前所没有听说过

 

的,给了我很大的启发。

 

1.指针和数组。

   

    C语言中的数组值得注意的地方:

 

    1.C语言中只有一位数组,而且数组的大小必须在编译期就作为常数确定下来。数组

 

  素可以是任何类型的对象,包括数组类型。

 

    2.对于数组我们只需要做两件事:1.确定该数组的大小。2.获得指向该数组下标为0

 

  指针。

 

    数组的下标运算实际上都是通过指针进行的。换句话说,任何一个数组下标运算都等

 

同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。

 

    任何指针都是指向某一类型的变量。指针可以相减,得到一个整数,指针也可以加一

 

个整数,但指针之间不能相加。如果两个指针指向同一个数组,那么两个指针相减是有意

 

义的,如过指向不同数组,相减是没有意义的。当指针指向一个数组时,指针加1,就指

 

向下一个数组元素。

 

    int *p,a[3];

   

    这段代码声明了一个指向int类型变量的指针p和一个维度为3int类型的一维数

 

a p = a; 是将a的地址给pp就指向a0号下标,p+1就指向a1号下标,依

 

此类推。如果希望p指向a1号下标可以有如下几种方法:

 

    p = p + 1;

   

 

p++;

 

    C语言中 p = &a; 是非法的,因为&a是一指向数组的指针,而p是一个指向整型变量

 

的指针,它们类型不匹配。

 

    数组名a除了被用作sizeof的参数的情况,其它情况下a都代表指向数组a0号下

 

标元素的指针。sizeof(a)是表示数组a的大小,而不是指针a的大小。

 

    实际上a+ii+a含义是一样的,因此a[i]i[a]含义也相同。

 

    现在讨论一下二维数组。

 

    int calendar[12][31];

    int *p;

    int i;

   

    p = calendar[4];这句话的意思是将指针p指向了数组calendar[4]中下标为0的元

 

素。由于calendar[4]是一个数组,因此我们可以有以下语句:

   

    i = calendar[4][7];

 

也可以写成

 

    i = *(calendar[4]+7);

 

 

    i = *(*(calendar+4)+7);

 

    然而语句 p = calendar; 是非法的,因为calendar是一个二维数组,即数组的数

 

,因此,calendar就是一个指向数组的指针(即指向指针的指针),而p是指向整型

 

的指针,类型不匹配,就如同之前所说的 p = &a; 一样,这里的calendar&a类似。因

 

此我们需要声明指向数组的指针的方法:

 

    int *(ap)[31];

 

    这个语句的效果就是说*ap指向一个拥有31个整型元素的数组,因此,ap就是指向数

 

组的指针(就是一个指向指针的指针,有些书上也称作二级指针。声明一个整型二级

 

指针的方法是 int **p;)。

 

    因此,以下用作清空calendar数组的代码就可以做出以下的处理:

 

    原始代码:

   

    int calendar[12][31];

         int month;

    for ( month = 0; month < 12; ++month ) {

        int day;

        for ( day = 0; day < 31; ++day )

            calendar[month][day] = 0;

    }

 

    由于calendar[month][day] = 0;可以表示为*(*(calendar+month)+day) = 0;

 

    处理代码:

 

    int calendar[12][31];

    int *(monthp)[31];

    for ( monthp = calendar; monthp < &calendar[12]; ++monthp) {

        int *dayp;

        for (dayp = *monthp; day < &(*monthp)[31]; ++dayp)

            *dayp = 0;

    }

 

2.非数组的指针。

 

    C语言中要求字符串常量以空字符(’/0’)作为结束符,而字符窜所代表的正是所有

 

字符以及一个空字符的内存区域的地址。

 

当我们需要将两个字符窜st复制到第三个字符窜r中时,我们应该注意以下几个问题:

   

    1.r是否指向一个地址

 

    2.r是否有足够的内存空间来容乃字符窜

 

    为了但达到这两个目的,我们需要使用mallocstrlen这两个函数。这时,又带来了几

 

个问题:

 

    1.malloc函数是否分配到了足够的内存

 

    2.使用了malloc函数分配内存后是否用free函数来释放内存了

 

    3.strlen函数返回的字符窜的长度并不将空字符计算在内

 

    因此,为了达到目的,正确的程序如下:

 

char * r;

 

r = malloc ( strlen (s) +strlen (t) + 1 );

 

if (!r) {

 

    complain();

   

    exit(1);

 

}

 

strcpy (r,s);

 

strcat (r,t);

 

/* 使用过后 */

 

free (r);

 

3.作为参数的数组声明

 

    C语言中,我们没有办法将数组作为函数的参数进行传递,如果我们以数组名作为参数,

 

那么数组名会自动转换成指向第一个元素的指针,因此数组作为函数的参数毫无意义。所以,C

 

语言会自动将作为参数的函数名转换成指针。因此,下列写法是完全等效的:

 

int strlen ( char s[] )

 

{

 

/* 内容 */

 

}

 

int strlen ( char *s )

 

{

 

/* 内容 */

 

}

 

    C语言程序员会经常错误的假设在其他情况也会自动转换,例如声明一个数组hello和声

 

明一个指针hello。如果一个一个指针参数并不实际代表一个数组,即使从技术上而言是正确

 

的,采用数组形式的记法经常会起到误导作用。如果一个指针参数代表一个数组,那么正如前

 

面所说的一样两种写法是等价的,唯一的区别只是在于,写成前一种时强调的作重点是这是一

 

个数组其实元素的指针。

 

4.避免“举隅法”

 

    所谓的举隅法就是指用整体来代表部分或者用部分来代表整体。在正式C语言中常见的

 

“陷阱”。例如:

 

char *p,*q;

 

p = “xyz”;

 

这里的p在某些时候我们可以不妨认为是将xyz赋值给p,然而实际情况不是这样。实际上,p

 

的值是一个指向’x’,’y’,’z’,’/0’4个字符组成的数组的其实元素的指针。

 

    我们需要记住,复制指针,并不是复制指针所指向的数据,而是将一个指针的地址复制给

 

另一个指针。例如:q = p;这条语句是使pq同时都指向了‘x’。

 

5.空指针并非空字符窜

 

    编译器保证由0转换而来的指针不等于任何有效的指针,因此,处于代码文档化的考虑,

 

常数0通常用符号NULL来代替。除此之外,在C语言中,将一个整数转换成一个指针最后得到

 

的结果都取决于具体的C编译器实现。当常数0被转换成指针使用时,这个指针绝对不允许被

 

解除引用。也就是说,当我们将0赋值给一个指针变量时,绝对不能企图使用这个指针所指向

 

的内存中所存储的内容。因此,下面的例子中,除了第一种写法外全部非法。

 

if ( p == ( char * ) 0 ) …

 

if (strcpy (p,(char *) 0) ==0) …

 

如果下列的p是空指针

 

printf(p);

 

printf (“%s/n”,p);

 

6.边界计算与不对称边界

 

    C语言中,一个拥有n个元素的数组,不存在下标为n的元素,它的元素下标是从0

 

n-1为止。有其他语言转战C语言的程序员使用数组时要尤其注意。例子:

 

int a[10],i;

 

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

 

    a[i] = 0;

 

在这个例子中,由于不存在a[10]这个元素,因此,实际上会将i的值设为0,这就陷入了死循

 

环。

 

C语言中的这一特别的设计,正是它的优势所在。正是由于这个特别的设计,从而可以避

 

免“差一错误”,也就是导读那一章的有关栏杆问题中的可能犯的错误。而降低这类错误的编程

 

技巧就是运用“不对称边界”。具体的就是:用第一个入界点和第一个出界点来表示一个数值的

 

范围。这里的下界是“入界点”,包含在范围之中;而上界是“出界点”,不包含之中。以上面

 

的例子为例,循环并不是写成i<=9,而是写作i<10。虽然这样做,也许从数学上而言不是那么

 

美观,但是它对于程序设计的简化结果却足以令人吃惊。

 

1.你可以一目了然的就知道这个数组的元素个数,这个范围就是上下界之差

 

2.如果取值范围为空,那么上界等于下界,这是第一条的直接推论

 

3.即使取值范围为空,上界也永远不可能小于下界

 

    另一种考虑不对称边界的方式是把上界视作一个被占用的元素,而把下界视作序列中第一

 

个被释放的元素。如图:

 

 

当处理各种不同的缓冲区时,这种看待问题的方法特别有用。

 

当一个指向某一数组元素的指针减去数组的首元素地址时所得到的就是这两个元素之间的元素

 

个数。例如:

 

int arr[10],*ptr;

 

ptr = arr;

 

那么,ptr-arr所得到的就是这两个元素之间的元素个数,0个。

 

arr[10]这个元素并不存在,因此无法引用,但是运用其地址是合法的。因此我们可以用

 

ptr == &arr[10]这种写法来代替ptr > &arr[9]

 

(后面的两个例子不想写了,买书看比较好,如果以后心情好了也许会补上,我认为这一节主

 

要需要掌握的内容我基本都写了,后面的例子是有关算法的。而且很不幸的是,我又发现一个

 

错误,本书的54页,我想应该跟后面的程序一样是赋值号,而不是用于逻辑判断的等号)

 

7.求值顺序

 

    C语言中,只有四个运算符存在规定的求值顺序,分别是“&&”、“||”、“?:”和“,”。

 

    运算符&& 先对左侧的操作数求值,如果是真则对右侧操作数进行求值,否则不对右侧

 

操作数进行求值

 

    运算符|| 先对左侧的操作数求值,如果是假则对右侧操作数进行求值,否则不对右侧

 

操作数进行求值

 

    运算符?: a?b:c)先对a求值,然后在根据a的结果对b或者c求值

 

    运算符, :先对左侧的操作数求值,然后“丢弃”,在对右侧的操作数求值

 

(注:分隔函数参数的逗号并不是逗号运算符。例如:f(x,y),这两个参数的求值顺序是不确定

 

的,而g((x,y))的求值顺序是一定的,函数g的参数只有一个,先对x求值,然后“丢

 

弃”,在对y求值。

 

    C语言中其他运算符的操作数运算顺序是未定义的,尤其是赋值符。根据优先级的关系

 

可以确定操作数的运算顺序,然而,当你没有优先级关系时就会发现问题了。例如:

 

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];

 

8.运算符&&||!

 

    C语言中有两类逻辑运算符,有些时候是可以互换的:按位运算符:&|~,以及逻辑运

 

算符:&&||!

 

按位运算符:按位运算符将要处理的操作数视作二进制的为序列,分别对每个位进行操作。

 

逻辑运算符:对操作数的处理方式是将其视作真或假,通常将“0”视作假,非“0”视作真。

 

这些运算符的返回结果只能是0(假)或1(真)。

 

如果你将这两类运算符互换,你会吃惊的发现,程序尽然能够“正常”工作。但这仅仅只

 

是某种条件下的“侥幸”而已,超出这个条件后你会发现结果会发生不可预料的错误。

 

9.整数溢出

 

    C语言中存在有符号运算和无符号运算两种这个数算术运算。在无符号算术运算中没有

 

“溢出”这一说法,所有的无符号运算都是以2n次方为模,这里的n是结果中的位数。如

 

果一个有符号整数和一个无符号整数进行运算,有符号整数会转换成无符号整数,因此也不会

 

发生“溢出”,但是两个有符号整数进行运算就会发生“溢出”。“溢出”的结果是为定义的。当

 

结果发生“溢出”时,任何假设都是不安全的。

 

10.为函数main提供返回值

 

    通常情况下,不为main函数提供返回值没有什么影响。一个函数如果失败了,会返回一

 

个“垃圾”整数,只要这个整数不被用到,就无关紧要。然而,某些情况下却并非无关紧要。

 

大多数C语言实现都通过函数main的返回值来告知系统该函数执行成功或失败,典型的处理方

 

式是返回0为成功,非0整数则代表失败。如果没有返回值,看上去可能执行失败了。而如果

 

你用软件管理系统关注该程序执行成功或失败时会得到令人惊讶的结果。因此,为main函数添

 

加上返回值是一个好主意。

 

 

 

    这是第三章。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PDF文件大小为130M,PDF带目录索引,高清版 这是Power BI 官方中文教程的高清无删减版 连接到 Oracle数据库 在 Power b| Desktop中运行 Python脚本 在 Power查询编辑器中使用 Python 将外部 Python|DE与 Power bl一起使用 使用 Python创建 Power b|视觉村象 在 Power b| Desktop中运行R脚本 在查询编辑器中使用R 将外部R|DE与 Power b|一起使用 受支持的R包 值接在 Power b| Desktop中输入数据 在 Power Bl Desktop中连接到Exce Power b| Desktop中的 Analysis Services多维数据 通过 Power b| Desktop连接到CS∨文件 在 Power b| Desktop中连接到 Google BigQuery数据库 在 Power BI Desktop中连接到 Impala数据库 通过 Power BI Desktop连接到 OData数据源 在 Power b| Desktop中连接到 Amazon redshift数据库 通过 Power BI Desktop连接到网页 连接到 Power BI Desktop中的雪花型计算仓库 连接到 Microsoft Azure Consumption Insights 在 Power Bl Desktop中使用 SAP HANA Power BI Desktop中的 Analysis Services表格数据 在 Power b| Desktop中使用 DirectQuery Oracle和 Teradata数据库的 DirectQuery DirectQuery FA SAP Business Warehouse(BW) DirectQuery和 SAP HANA Power BI Desktop中的假设引用完整性设置 在 Power b| Desktop中使用 SAP BW连接器(预览) 在 Power b| Desktop中使用 One Drive for business链接 第三方服务:适用于 Power b| Desktop的 Facebook连接器 第三方服务:适用于 Power b| Desktop的 Google Analytics连接器 Project Online:通过 Power BI Desktop连接到数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值