《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]);//所以多加了两行
}
运行结果如下图:
指针指向的类型,实际上只是改变了在指针+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
避免栏杆错误的两个原则:
- 首先考虑最简单的特例,然后将结果外推
- 仔细计算边界
编程上的技巧:
- 用一个入界点和一个出界点表示一个数值范围。(不对称边界表示)
例如:
for( ;x>=6&&x<=37;x++)
改写为:(左闭右开)
for( ;x>=6&&x<38;x++)
- 下界是入界点
- 上界是出界点
- 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 读值越界无报错,对此网上给的答案是)
(图片来自www.shiyanlou.com问答)
但是:
- 运算顺序改变了,前一个只要不满足第一个条件就不会运算
tab[i]!=x
,而改变后,就会出现数组超界。 - 运算改变了,逻辑与变成了按位与
3.9 整数溢出
参考文章
C语言中存在两类整数算数运算
- 有符号运算
- 无符号运算
只有在有符号
与有符号
运算时候才会有“溢出”发生
- 无符号与无符号运算时
所有无符号的运算都是以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函数不返回任何值,那么有可能看上去执行失败。