语义陷阱
3.1 指针和数组###
任何一个数组下标运算都等同于一个对应的指针运算,因此完全可以依据指针行为定义数组下标的行为。
数组名被当作该数组下标为0的元素的指针。
sizeof(a)的结果是整个数组的大小,而不是指向数组a的元素的指针的大小。
a即数组a中下标为0的元素的引用,同理(a+1)是数组a中下标为1的元素的引用,以此类推,*(a+i)即数组a中下标为i的元素的引用,简记为a[i].
带方括号的下标形式很明显要比完全用指针来表达简便得多。
声明指向数组的指针的方法:
int calendar[12][31];
int (*monthp)[31];
monthp=calendar;
monthp将指向数组calendar的第一个元素,也就是数组calendar的12个有着31个元素的数组类型元素之一。
以指针monthp以步进的方式遍历数组calendar:
int (*monthp)[31];
for(monthp=calendar;monthp<&calendar[12];monthp++)
//处理一个月份的情况
处理指针monthp所指向的数组的元素:
int (*monthp)[31];
for(monthp=calendar;monthp<&calendar[12];monthp++)
{
int *dayp;
for(dayp=*monthp;dayp<&(*monthp)[31];dayp++)
*dayp=0;
}
3.2 非数组的指针
在C语言中,字符串常量代表了一块包括字符串中的所有字符以及一个空字符(' ')的内存区域的地址。
char *r, *malloc();
//加1是因为strlen返回参数中的字符串所包含的字符数目并不包括空字符,故加1
r=malloc(strlen(s)+strlen(t)+1);
//malloc函数分配失败检测
if(!r)
{
complain();
exit(1);
}
strcpy(r,s);
strcat(r,t);
//释放内存
free(r);
3.3 作为参数的数组声明
使用数组名作为参数,那么数组名会立刻被转换为指向该数组第一个元素的指针.
下面两个语句作用相同:
char hello[]="hello";
printf("%s
",hello);
printf("%s
",&hello[0]);
下面两个函数作用相同:
main(int argc,char * argv[]);
main(int argc,char ** argv);
3.4 避免举隅法
举隅法(synecdoche):以含义更宽泛的词语来替代含义相对较窄的词语,或者相反。
经常会混淆指针与指针所指向的数据
char *p, *q;
//p的值是一个指向有'x','y','z'和' '四个字符组成的数组的起始元素的指针。
p="xyz";
//此时,复制指针后q和p是两个指向内存同一地址的指针,但是没有复制内存中的字符。
q=p;
赋值指针并不同时复制指针所指向的数据。
3.5 空指针并非空字符串
当常数0倍转换为指针使用时,不能使用该指针所指向的内存中存储的内容。
if (strcmp(p,(char *) 0) == 0)....
上述语句会报错,因为其试图使用0转换为指针后的存储的内容。
3.6 边界计算与不对称边界
栏杆错误,也称“差一错误(off-by-one error),要避免栏杆错误,需要运用一下两个通用原则:
1.首先考虑最简单情况下的特例,然后将所得的结果外推
2.仔细检查边界,绝不掉以轻心
运用:
计算假如整数x满足边界条件x>=16且x<=37,那么此范围内x的可能取值个数有多少个?
根据原则一,最简单的情况是求x>=16且x<=16,此时x的取值个数为1个,因此,当上界与下界重合时,此范围满足条件的整数序列只有一个元素。
再将结果外推,假定下界为1,上界为h。如何满足上界与下界重合,即h-1=0,那个满足条件的整数序列就有h-1+1个元素,因此本例中答案为37-16+1=22。
可以通过另外一种方式来解决,上例中x>=16且x<=37,等同于x>=16且x<38。这里的上界是”出界点“,即不包括在取值范围之中;下界是”入界点“,即包括在取值范围之中。我们可以得出以下三个结论:
1.取值范围的大小就是上界与下界之差。
2.如果取值范围为空,那么上界等于下界。
3.即使取值范围为空,上界也永远不可能小于下界。
不对称边界适用于C语言,对数组赋值时可以写成:
int a[10],i;
for (i=0; i<10 ; i++)
a[i]=0;
对于处理缓冲区时,可以将上界视作某序列中第一个被占用的元素,把下界视作序列中第一个被释放的元素。比如:
#define N 1024
//缓冲区的大小
static char buffer[N];
//指针变量,指向缓冲区的当前位置
static char *bufptr;
//初始化指针变量
bufptr=buffer;
void bufwrite(char *p,int n)
{
//进行n次迭代
while(--n>=0)
{
//k为每次移动的字符数,rem为缓冲区剩下可容纳的字符数
int k,rem;
//这里运用了不对称边界
if(bufptr==&buffer[N])
//将缓冲区的内容写出,并重置指针
flushbuffer();
rem=N-(bufptr-buffer);
//控制边界大小
k=n>rem?rem:n;
//输出k的字符,从缓冲区的第一个字符开始复制
memcpy(bufptr,p,k);
//将指针bufptr的地址向前移动k个字符,使其仍然指向第一个未被占用的字符
bufptr+=k;
//输入字符串指针p前移k个字符
p+=k;
//将待转移的字符数减去k
n-=k;
*bufptr++=*p++;
}
}
//每次移动k的字符
void memcpy(char *dest,const char *source,int k)
{
while(--k>=0)
*dest++=*source++;
}
另外一个计数的例子:
此程序按照一定顺序生成一些整数,并将这些整数按列输出,程序的输出可能包括若干页的整数,每页包括NCOLS列,每列包括NROWS个元素,每个元素就是一个待输出的整数。
//最后一列不必缓存,直接输出
#define BUFSIZE (NROWS*(NCOLS-1))
static int buffer[BUFSIZE];
static int *bufptr=buffer;
//缓冲区满时打印数据
void print(inti n)
{
if(bufptr==&buffer[BUFSIZE])
{
static int row=0;
int *p;
for(p=buffer+row;p<bufptr;p+=NROWS)
printnum(*p);
//打印当前行的最后一个元素
printnum(n);
//另起新的一行
printnl();
if(++row==NROWS)e
{
printpage();
//重置当前行号
row=0;
//重置指针
bufptr=buffer;
}
}
else
*bufptr++=n;
}
//打印缓冲区所有剩余元素
void flush()
{
int row;
//计算缓冲区中剩余的数目
int k=bufptr-buffer;
if(k>NROWS)
k=NROWS;
if(k>0)
{
for(row=0;row<k;row++)
{
int *p;
for(p=buffer+row;p<bufptr;p+=NROWS)
printnum(*p);
printnl();
}
printpage();
}
}
3.7 求值顺序
C语言中的某些运算符总是以一种已知的,规定的顺序来对其操作数进行求值,但是不是所有都是如此。
运算符&&和运算符||对于保证检查操作按照正确的顺序执行至关重要。
if (y !=0 && x/y > tolerance)
complain();
保证仅当y非0时才进行求值操作。
i=0;
while (i<n)
y[i] = x[i++];
上述代码并不能保证y[i]的地址在i的自增操作之前被求值,因为赋值运算符并不保证任何求值顺序,应该改为:
i=0;
while(i<n)
y[i]=x[i];
i++;
或者改为:
for (i=0;i<n;i++)
y[i]=x[i];
3.9 整数溢出
整数溢出的情况出现在有符号运算中。
下面的代码是检查a+b是否溢出:
if (a + b <0)
complian();
其中a,b都是非负整型变量。a和b进行相加时,内部寄存器的状态可能为“溢出”而不是负,导致不能正常检查。
如何解决:
1.将a和b强制转换为无符号整数。
if ((unsigned)a + (unsigned) b >INT_MAX)
complain();
其中INT_MAX是一个已定义常量,代表可能的最大整数值。
2.通过类型自动转换来进行
if (a >INT_MAX-b)
complain();
3.10 为函数main提供返回值
函数main与其他函数一样,如果并未显式声明返回类型,那么函数返回类型默认为整型。但是main函数的返回值可以告知系统程序调用执行成功或者失败。
#include <stdio.h>
main()
{
printf("hello world
");
return 0;
}