第03章 语义"陷阱"

作者: rfish | 来源:发表于2015-10-09 14:05 被阅读450次

《C陷阱与缺陷》 Andrew Koenig) 阅读笔记 部分资料来自网上(内附链接)


3.1 指针与数组

注意:数组和指针并不是一样的,只是在数组的操作上可以和指针一样操作。

编译的时候,程序中的所有的变量都会分配一个固定的地址。

  • 数组a[ ]a分配了地址,那个地址处存的是a[0]的值。
  • 指针*p,p分配了地址,那个地址处存的是另外一个地址。

也就是指针的操作比数组会多一个步骤。

关于数组的操作,实际上都是通过指针进行的。(任何一个数组下标运算都等同于一个对应的指针运算),
*(a+1)是数组a中下标为1的元素的引用
*(a+i)是数组a中下标为i的元素的引用,由于常用,被简记a[i]

重点来了:
由于a+i和i+a的含义是一样的,所以,a[i],和i[a]也是一样的。(但是不推荐这样写)

#include <stdio.h>
int main(){
    int a[10]={0,1,2,3,4,};
    printf("%d\n",a[3]);
    printf("%d\n",3[a]);
    printf("%d\n",a[4]);//顺便看一下数组默认的初始化值
    printf("%d\n",a[5]);//所以多加了两行
}

运行结果如下图:

df215898-5f30-46cd-91a1-c6966179ce3d.png

指针指向的类型,实际上只是改变了在指针+1时的步长。
比如char *p; p+1实际上加了一个字节的偏移量
int *p; p+1加了4个字节的偏移量,因为int是4个字节。

3.2 非数组的指针

处理字符串时,注意字符串最后有一个'\0'
也就是我们在使用malloc( )的时候,如果是给一个字符串分配空间,需要考虑最后一个'\0'
特别是在malloc( )配合strlen( )使用的时候,因为strlen( )出来的长度是没有算上'\0'

3.3 作为参数的数组声明

在函数传参时,作为参数的数组会自动转换为相应的指针声明。
即:

int strlen(char s[]){

}

会转换为:

int strlen(char *s){

}

3.4 避免“举隅法”

指针保存的是地址,不能直接赋其他值。

3.5 空指针不是空字符串

  • 当常数0被转换为指针使用时,这个指针绝对不能被解除引用(操作其地址的值)。
    因为转换后的指针指向的是地址0,而其存储内容是未知的。

3.6 边界计算与不对称边界

  • 数组下边是0开始的
  • 栏杆错误(差一错误)
    • 比如计算数组下标2-5的长度,应该是5-2+1

避免栏杆错误的两个原则:

  • 首先考虑最简单的特例,然后将结果外推
  • 仔细计算边界

编程上的技巧:

  1. 用一个入界点和一个出界点表示一个数值范围。(不对称边界表示)
    例如:
    for( ;x>=6&&x<=37;x++)
    改写为:(左闭右开)
    for( ;x>=6&&x<38;x++)
  • 下界是入界点
  • 上界是出界点
  1. buff数组中的技巧
    有一个buff缓冲数组buffer[ ],我们再定义一个指针*bufptr指向buffer数据的末尾。
    也就有两个选择:
    指向数据的最后一个
    指向缓冲区未占用的第一个字符。
    考虑到计算缓冲区长度方便,可以如下图采用第二种方式。好处是显而易见,比如我在写入缓冲区用*bufptr++=c每次添加后,bufptr都是很指向第一个未占用的字符,那么我计算缓冲区已存放字符的长度的时候就可以直接bufptr-buffer就可以了
buffer 1 2 3 4 5 *bufptr 7 8
数据1 数据2 数据3 数据4 数据5 数据6 NULL NULL NULL

书中P52最后,有关于特殊print的思路

3.7 求值顺序

求值顺序不同于运算优先级,求值顺序是一种规则

c语言中只有四个运算符(&&||? :)存在规定的求值顺序。

  • &&||首先对左侧操作数求值,只有在需要时才对右侧求值
    • a<b && c<d 只有当a<b 成立时才对右侧求值
  • ? :选择求值
    • a?b:c求值a,然后根据a的值再求b或者c
  • ,对左侧求值,然后该值“丢弃”,再对右侧求值
    • 分割函数参数的逗号不是逗号运算符。
    • f(x,y)中求值顺序是未定的。

c语言对其他运算符的求值顺序是未定的。

示例1

if(y!=0 &&x/y > tolerance){
    complain();
}

上面的代码保证了运行时不出现“用0作为除数”的错误。因为当y==0时后面的一句话是不会执行的。

示例2

从数组x中复制前n个元素都数组y中。

i=0;
while(i<n){
    y[i]=x[i++];
}

上面的代码不能保证正确性,因为如果是先运算的左边没有问题,如果是先求值的右边,就会取完x[i],然后i++,然后再y[i],此时y[i]就不是以前的i了。

3.8 运算符&&、||和!

注意两点:

  • &,|和&&,||的区别
  • 他们运算的顺序

举例:

i=0;
while (i<tabsize && tab[i]!=x){
    i++;
}

如果错写成了while(i<tasize& tab[i]!=x)
虽然可以正确运行,因为:

  • &两边的结果都只有真或假(0 or 1),&&与&的结果是一样的
  • 改变后,虽然数组超界,但是只是读值,而没有对该值操作,不然会出现段错误。(gcc 4.4.8 读值越界无报错,对此网上给的答案是)
QQ截图20151007162445.png
(图片来自www.shiyanlou.com问答)

但是:

  • 运算顺序改变了,前一个只要不满足第一个条件就不会运算tab[i]!=x,而改变后,就会出现数组超界。
  • 运算改变了,逻辑与变成了按位与

3.9 整数溢出

参考文章
C语言中存在两类整数算数运算

  1. 有符号运算
  2. 无符号运算

只有在有符号有符号运算时候才会有“溢出”发生

  • 无符号与无符号运算时
    所有无符号的运算都是以2的n次方为模运算的(n指结果的位数)
    有的文章也有说是:“溢出后的数会以2^(8*sizeof(type))作模运算”,也就是说,如果一个unsigned char(1字符,8bits)溢出了,会把溢出的值与256求模。
    其实效果都一样 ,可以在理解上认为是进位“舍弃”了。例如8位的0xff+0x1,相加后的进位“舍弃”掉后,就是0了。
  • 有符号与无符号运算时
    有符号会转换为无符号整数计算

当两个都是有符号整数时,“溢出”就有可能会发生,而且“溢出“的结果是未定义的。
也就是编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。

signed char x =0x7f; //注:0xff就是-1了,因为最高位是1也就是负数了

printf("%dn", ++x);

上面的代码会输出:-128,因为0x7f + 0×01得到0×80,也就是二进制的1000 0000,符号位为1,负数,后面为全0,就是负的最小数,即-128。

检测有符号溢出方法:

if((unsigned)a + (unsigned)b > INT_MAX){
    //......
}

if(a > INT_MAX -b){
    //......
}

INT_MAX 表示可能的最大整数值(有符号),在<limits.h>有定义。

3. 10 为函数main提供返回值

main的返回值告知操作系统该函数的执行时成功(返回0)还是失败(返回非0)

而:

main(){

}

默认的返回类型为整形,而一个返回值为整形的函数如果失败,实际上是隐含地返回了某个”垃圾“整数(即返回的非0数)。
在不用到该数的时候无关紧要,然而当需要使用到该数的时候,结果可能让人惊讶。

如果一个程序的main函数不返回任何值,那么有可能看上去执行失败。

相关文章

网友评论

    本文标题:第03章 语义"陷阱"

    本文链接:https://www.haomeiwen.com/subject/tmpncttx.html