3.1 指针和数组
C 语言中数组值得注意的两点:1、C 语言中只有一维数组,而且 数组的大小必须在编译期就作为一个常数确定下来。然而,C语言中的数组元素可以是任意类型的对象,故也可以是另外一个数组,这样就“仿真”出一个多维数组; 2、对一个数组来说,只能够做两件事,一个是确定该数组的大小,另一个是获得指向数组下标为 0 的元素的指针,该指针是个指针常数,不能更改其值,固定的指向首地址。
任何指针都是指向某种类型的变量。
如果一个指针指向的是一个数组中的一个元素,那么对指针加 1 就能够得到指向该数组下一个元素的指针,同样,减一,获得指向同一个数组上一个元素的指针,同样,加减整数值获得其类似的效果。故对指针加减整数,得到的效果是对同一个数组向前或向后移动对应个数的元素,所获得得指针指向。
如果指针 p 和 q 指向同一个数组中的元素,则 p-q 是有意义的,获得的效果是指针所指元素之间的相差的元素的个数。
若有 int* p; int a[3]; 则 p=a; 是把数组 a 中下标为 0 的元素的地址赋值给 p,p+1获得数组a中下标为 1 的元素的地址, 但是 p=&a,是将数组的首地址赋值给了 p(可能非法,因为p本身只是指向int型的指针),则p+1 所得的是数组最后一个元素的下一个位置的地址。
对于数组名 a ,除了用作运算符sizeof 的参数之外,其他的情形中数组名 a 都代表了指向数组 a 中下标为 0 的元素的指针,只有在sizeof中做参数,得到的结果是整个数组 a 的大小,而不是指向数组 a 的元素的指针的大小。
比如如下声明:
int calendar [12] [31];
int *p;
p=calendar[4];
int (monthp)[31];
month=(calendar+4);
int i;
i=calendar[4][7];
数组声明了一个有12个数组类型元素的数组,每个数组类型元素是一个有31个整型元素的数组,所以calendar[4]代表的含义为calendar数组的第 5 个元素,是其12个元素之一,其行为表现为有着31个整型元素的数组的行为,例如sizeof(calendar[4])的结果为31Xsizeof(int)。
p=calendar[4]; 指的是指针 p 指向数组calendar[4]中的下标为0的元素,相当于p=(calendar+4);
int (*monthp)[31];声明了一个指向一个数组的指针,所指向的数组的数组 元素的数量为31,元素的类型为int。而int *monthp[31];是声明一个指针数组,数组名是monthp,数组元素是int类型的指针。故month=(calendar+4);相当于将calendar数组的第四个元素的地址复制给monthp.
i=calendar[4][7];相当于将 一个整型元素赋值给i.
3.1非数组的指针
C语言中字符串常量代表了一块包括字符串中所有字符以及一个空字符(‘\0’)的内存区域的地址。
假定有两个字符串s和t,将这两个字符串连接成单个的字符串r,则:
尝试方法1:
char *r;
strcpy(r,s);
strcat(r,t);
该方法的错误之处在于不能确定 r 指向何处,且不知道 r 所指的内存是否有足够的容量容纳字符串。
尝试方法2:
char r[100];
strcpy(r,s);
strcat(r,t);
该方法的错误之处在于不确定 r 的大小是否满足要求。若字符串 s 和 t 长度比较小,r 的长度足够,但一定程度上个浪费了空间,若 s 和 t 的长度比较大,则 r 的空间不满足要求。但C 语言强制要求必须为声明的数组的大小赋以常量,故不能够确保数组 r 是刚好满足要求的。
尝试方法3,使用库函数malloc()和库函数strlen():
char *r, *malloc();
r=malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);
该方法的错误之处有三点:(1)malloc函数可能不能够提供请求的内存,这种情况下返回值是一个空指针,以此作为“内存分配失败”事件的信号;(2)给 r 分配的内存在使用完成之后应该及时的释放,此时已经不再是局部变量,不会在函数执行完成后自动回收内存,必须手动使用free显式进行释放;(3)在调用malloc函数时,实际上并没有分配足够的内存,因为字符串是以‘\0’作为结束,而函数strlen只统计了‘\0’字符之前的所有字符,故使用malloc分配字符串所需的空间时,应多加一位字符空间。
正确方法:
char *r, *malloc();
r=malloc(strlen(s)+strlen(t)+1);
if(!r)
{
complain();
exit(1);
}
strcpy(r,s);
strcat(r,t);
……
free(r);
3.3 作为参数的数组声明
在C语言中,不能将数组作为一个函数参数直接传递,如果使用数组名作为参数,那么数组名会被转换 成指向该数组的第一个元素的指针。因此将数组作为函数参数毫无意义。比如:
char hello[ ]=“hellp”;
printf("%s\n",hello);
printf("%s\n",&hello[0]);
两个输出函数的意义完全一样。
又比如:
int strlen(char s[ ]){ }
int strlen(char *s){ }
两函数声明也完全相同。
3.4避免“举隅法”(即避免弄混指针和指所指向的数据)
注意避免弄混指针和指所指向的数据,尤其对于字符串类型,比如:
char *p,*q;//声明两个字符类型的指针
p=“xyz”;//将字符‘x’,‘y’,‘z’,‘\0’,组成成的数组的起始元素的地址赋值给指针p
q=p;//将指针p的值赋值给指针 q ,此时相当于两个指针指向相同的地址,该赋值语句并没有同时复制内存中的字符(即复制指针并不同时复制指针所指向的数据)
*q[1]=‘Y’;//直接改变q+1的值所指向的内存中的值,则此时 p 和 q 所指向的内存中的字符串为“xYz”
3.5空指针并非空字符串
将常数 0 转换成指针,编译器保证该指针不等于任何有效的指针。
常数 0 转换成的指针时,该指针绝对不能被解除引用,即绝对不能使用该指针所指向的内存中存储的内容,比如:
if(p==(char*)0)……//是完全合法的
if(strcmp(p,(char*)0)==0)……//非法操作,原因在于strcmp的实现中包括查看他的指针参数指向额内存中的内容的操作
printf(p);和printf(“%s”,p);这两个函数也是非法的。
3.6边界计算与不对称边界
C语言中,拥有 n 个元素的数组,不存在下标为 n 的元素,数组的下标范围是从 0 到 n-1 为止。
int i,a[10];
for(i=1;i<=10;++i)
a[i]=0;
在上述的代码中,把不存在的a[10]的值赋值为0,也就是内存中在数组 a 之后的一个字被赋值为 0,若编译器按内存地址递减的方式给变量分配内存,那么相当于将循环计数的 i 的值为10时再次赋值为0,这就造成了死循环。
此种错误称为“栏杆错误”或者“差一错误”,避免这种错误的通用原则:(1)首先考虑最简单情况下的特例,然后将得到的结果外推;(2)仔细计算边界,不能掉以轻心。
降低此种错误的编程技巧:使用第一个入界点和第一个出界点来表示数值范围,比如:x≥16&&x≤37的表示方法为x≥16&&x<37,这里下界是“入界点”,即包括在取值范围内,上界是“出界点“,即不包括在取值范围之中。这种编程技巧的好处:(1)取值范围的大小为上界和下界之差;(2)如果取值范围为空,则上界等于下界;(3)即使取值范围为空,上界也不可能小于下界;
另一种考虑不对成边界的方式是,把上界视作某序列中第一个被占用的元素,而把下界视作序列中的第一个被释放的元素,比如处理各种缓冲区时,使用这种看待问题的方式。例如,考虑这样的一个函数,该函数的作用是将长度无规律的输入数据送到缓冲区(即一块能够容纳N个字符的内存)中,每当这块内存被“填满”时,就将缓冲区中的内容写出。
#define N 1024;
static char buffer[N]; //缓冲区声明
static char *bufptr; //声明一个指针变量,让它指向当前缓冲区
//考虑到“不对称边界”偏好,让指针指向缓冲区第一个未被占用的字符
//比如编写语句,*bufptr++=c; 将输入字符 c 放到缓冲区中,然后将指针bufptr递增 1,指向缓冲区中第一个为被占用的字符。
//则根据“不对称边界”,当指针bufptr与&buffer[0]相等时,缓冲区的内容为空,故初始化声明可以为:bufptr=&buffer[0]; 或者 bufptr=buffer;
//则缓冲区内的字符的个数为 bufptr-buffer,当bufptr-buffer等于N时,缓冲区已满,而N-(bufptr-buffer)表示缓冲区内未被占用的字符数。
void bufwrite(char *p,int n)//函数第一个参数表示指向将要写入缓冲区的第一个字符,第二个参数表示要写入的字符数
{
while(–n≥0)//循环计数,输入字符个数为n,故先将n自减运算
{
if(bufptr==&buffer[N])//判缓冲区是否已经满了,注意此处buffer[N]并不存在对应的元素,只是取了其地址
flushbuffer();//若缓冲区满了,将缓冲区内的内容输出,并重新将bufptr指针重置到缓冲区开始位置
*bufptr++=p++;//将当前要输入的字符输入到当前要接受字符的缓冲区的对应位置,然后将两个指针都进行自加运算,指向下一个
}
}
上述代码运行的主要开销来自于每次迭代的两个检查:一个检查用于判断循环计数器是否到达终值;另一个检查用于判断缓冲区是否已满。这样的结果就是一次只能转移一个字符到缓冲区,现在考虑使用memcpy()函数,实现一次转移k个字符。如下:
void bufwrite(charp, int n)
{
while(n>0)//循环终止条件,总共输入n 个字符
{
int k,rem;
if(bufptr==&buffer[N])//判断缓冲区是否已满,若满了,则刷新缓冲区,重置指针bufptr
flushbuffer();
rem=N-(bufptr-buffer);//求出缓冲区的未被占用的内存的容量
k=n>rem?rem:n;//求出准备一次写入的字符的个数,此时的k是这次能够写入的最大的字符的个数
memcpy(bufptr,p,k);//写入k个字符到缓冲区
bufptr+=k;更新指针bufptr的指向
p+=k;更新指针p的指向
n-=k;更新循环计数器,获得还有多少要写入的字符
}
}
3.7求值顺序
求值顺序与运算的优先级是不同的。
运算的优先级是关于表达式 a+bc 应该被解释为 a+(bc),而不是(a+b)*c 类似的规则。
求值顺序是另外的一些规则。比如: a<b&&c<d 是先计算 a<b,若其是真确的,再计算c<d,但若a<b是错的,则不用计算后面的表达式(短路原则)。但是对于a<b这个表达式,是先计算 a 还是 b的值,C语言并没有进行规定。
C语言中只有四个运算符(&&,||,?:,和,)存在规定的求值顺序。注意,赋值运算符并不保证任何的求值顺序。
运算符&&和||首先对左操作数求值,只在需要时才对右操作数求值。这两种运算符对于保证检查操作按照正确的顺序执行至关重要。
运算符?:有三个操作数,a?b:c ;操作数a 首先被求值,根据a 的值再求 b或者 c 的值。
逗号运算符首先对左侧操作数进行求值,然后该值被丢弃,再对右侧操作数求值。
3.8 运算符&&,|| 和 !
按位运算符&,|,~ 对操作数的处理方式是将其作为一个二进制的位序列,分别对其每个位进行操作。
逻辑运算符&&,||,! 对操作数的处理方式是将其视作要么是“真”,要么是“假”。运算符的结果为“真”时,返回1,为“假”时,返回0,且只能返回1或者0.
3.9整数溢出
C语言中存在两种算数运算,有符号运算和无符号运算。
无符号算数运算没有所谓的溢出(所有的无符号运算都是以2的n次方为模,n是结果中的位数)。
如果算数运算中,一个为无符号数,另一个为有符号数,则将有符号数转化为无符号数进行运算,也没有所谓的溢出。
两个操作数都是有符号数时,存在溢出现象。
比如:if(a+b<0)……,a+b可能溢出。
正确的方法是将两个数都先转化为无符号数:if((unsigned)a+(unsigned)b>INT_MAX)……
3.10为函数main提供返回值
最简单的C程序:
mian(){ }
这个程序并没有声明返回类型,那么函数的返回类型默认为整型,但是这个函数并没有给出任何的返回值。
通常main函数返回 0 来通知操作系统该函数执行成功,返回非零表示执行失败,所以如果函数没有返回值,看上去像是执行失败。
严格来说,为 mian(){ return 0; }或者mian(){ exit(0); }