|
楼主 |
发表于 2014-5-4 22:50:05
|
显示全部楼层
第三章----语义"陷阱"
3.1 指针和数组
问题1:什么是指针,指针的内存布局是怎么样的?
解析:我们可以简单的这样理解:一个基本的数据类型(包括结构体等自定义的类型),加上‘*’号就构成了一个指针类型的模子,这个模子的大小是一定的,与‘*’号前面的数据类型无关,‘*’号前面的数据类型只是说明指针所指向的内存里存储的数据的数据类型,在32位系统下,不管是什么类型的指针,它的大小都是4字节.如下面定义了一个指针:
int *p;(我们称p为指针变量.要注意:p也是变量,也有自己的地址&p)
一个“int *”类型的模子在内存中就“咔”出了4字节的空间,然后把这个4字节大小的空间命名为p,同时限定这4字节的空间里面只能存储某个内存地址,即使你存入的是别的任何数据,计算机都将它们当做地址来处理,而且这个内存地址开始的连续4字节的空间上只能存储int型的数据.
C预言中的数组值得注意的地方有两点:
1. C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来(注意,const修饰的只读变量不能作为数组的维数).并且,C语言中数组的元素可以是任何类型的对象(注意,不能是函数),当然也可以是另外一个数组,这样,要构造一个多维数组就不是一件难事了.
2. 对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组首元素首地址的指针(即数组名),其他的有关数组的操作,哪怕它们看上去是以数组的下标进行运算的,实际上都是通过指针进行的.
指针和数组的区别:
1. 存储的内容:
指针:保存数据的地址,任何存入指针变量p的数据都会被当作地址来处理。p本身的地址由编译器另外存储,存储在哪里,我们并不知道。
数组:保存数据,数组名代表的是数组的首元素的首地址,&a才是代表整个数组的首地址。a本身的地址由编译器另外存储,存储在哪里,我们并不知道。
2. 访问数据的方式:
指针:间接访问数据,首先取得指针变量p的内容,把它作为地址,然后对这个地址内存进行读写数据操作。指针可以以指针的形式访问*(p+i),也可以以下标的形式访问p[i],但其本质都是先取p的内容(-内存地址-)然后加上i*sizeof(类型)字节作为数据的真正的地址。
数组:直接访问数据,数组名a是整个数组的名字,数组内的每个元素并没有名字,只能通过"具名+匿名"的方式来访问某个数组元素,不能把数组当做一个整体进行读写操作。数组可以以指针的形式访问*(a+i),也可以以下标的形式访问a[i],但其本质都是a所代表的数组的首元素的首地址加上i*sizeof(类型)字节作为数据的真正的地址。
编译器总是把以下标的形式的操作解析为以指针的形式的操作.
指针就是指针,指针变量在32位系统下,永远占据4字节空间,其值为某一个内存的地址。
数组就是数组,其大小与元素的类型和个数有关;定义数组的时候必须指定数组其维数。数组和指针没有任何的关系!
有了上面的一些概念,明白了数组和指针到底是什么,我们来看下面的一个例子:
int a[12][31];
这个语句声明了a是一个数组,该数组拥有12个数组类型的元素,其中每个数组元素都是一个拥有31个整形元素的数组.因此:
sizeof(a)的值就是12*31*sizeof(int)
如果a不是用于sizeof的操作数,而是用于其它的场合,那么a总是被转换为一个指向a数组首元素首地址的指针(即a=&a[0]).因此a是一个二维数组,即“数组的数组”,在此处的上下文中数组名称a会被转换为一个指向数组的指针(即 int (*)[31] )
那么定义一个指向数组的指针就很好理解了: int (*ap)[31];
这个语句的实际效果就是,声明了*ap是一个拥有31个整形元素的数组,因此,ap就是一个指向这类数组的指针.
则当执行 ap=a; 这条语句时,指针ap就指向了数组a的第一个数组元素(a[0]),也就是数组a的12个有着31个元素的数组类型元素之一.
----------------------------------------------------------------------------------------------------------------
关于数组和指针就讨论到这里了,下面我们讨论:一个指针变量加上或者减去一个整数产生的效果.
int a[10]={0};
int *p=a;
如果数组名的地址为2000,也就是数组a在内存的中的首元素的首地址为2000.若执行了p=a;则p的值也为2000.那么p+2的值为多少呢?
--->我们需要明白的是:给指针加上一个整数,与给该指针的二进制表示加上一个整数,两者的含义截然不同.如果p指向一个整数,那么p+1 指向的是计算机内存中的下一个整数,在大多数的计算机中,它都不同于p所指向的地址的下一个内存位置.
好了,那么我们就知道了p+2 的值为2000+2*sizeof(int).同时我么也明白了指针加1,不是简单的将指针的值加1,而是指向下一个数,系统会根据指针所指向的数据的数据类型自动地计算地址.
注意1:当指针指向数组时(并不是真的指向了整个数组,而是指向了数组的首元素),可通过指针形式和下标形式来访问数组元素,因为指针是变量而数组名是常量,所以指针的值可以改变,这就意味着,我们需要特别的注意指针的当前的指向,它是指向数组的哪个元素?还是已经指向了 数组所占内存空间以外的地方?如果已经指向了数组所占的内存空间以外的地方,则一般会出错,这个是指针常出错和最危险的地方.例子:
int a[10],i;
int *p=NULL; //当指针不用时,最好把它"栓"在NULL处
p=a;
for(i=0;i<10;i++)
*p++=i;
// p=a;(有没有这条语句的差别太大了)
for(i=0;i<10;i++)
printf("%-4d",*p++);
p=NULL; //当用完指针时,也把它"栓"在NULL处
上面的例子就是一个最好的说明.
注意2:指针之间的相减是受到类型影响的,不同类型的指针之间的运算时无法进行的.
如果两个指针指向的是同一个数组,我们可以把这两个指针相减,这样做是有意义的.如p指向s[i],q指向s[j],则(p-q)==(i-j).
值得注意的是,如果p和q指向的不是同一个数组的元素,即使它们做指向的地址在内存中的位置正好间隔一个数组元素的整数倍,所得的结果仍然是无法保证其正确性的.
----------------------------------------------------------------------------------------------------------------
3.2 非数组的指针
在C语言中,字符串常量代表了一块包括字符串所有字符以及一个空字符('\0')的内存区域的地址.(因为C语言要求字符串必须以'\0'结尾)
现在我们假定有这样两个字符串s和t,我们希望将这两个字符串连成单个字符串r.当然我们可以通过头文件<string.h>的strcat函数实现,前提要求是存储字符串s的数组要有足够的空间存放字符串s和t连接而成的字符串,那么我们就可以通过 strcat(s,t);的调用方式实现.
如果现在我们定义字符串的方式是通过字符指针的方式的话(即char *s="asd",*t="fgh";),我们就不能通过上面的strcat函数的调用方式了.
那么要做到这一点,我们就只有通过strcpy函数和strcat函数结合的方式实现了,让我们看下实现的代码:
char *r=NULL;
strcpy(r,s);
strcat(r,t);
上面的代码初看觉得好像可行,实际上是不行的.原因在于,不能确定指针r的指向,以及不能确定r指向的地址处是否有足够的空间容纳连接后的字符串.(你可能会问,这里的指针r不是指向了NULL处了吗?那为什么还不能使用呢?这个问题我们接下来会讲)
那我们这次为r分配一定的内存空间:
char r[100];
strcpy(r,s);
strcat(r,t);
只要s和t指向的字符串不是很大,上面的做法应该就大功告成了.不幸的是,C语言要求我们为数组声明的大小必须要是一个常量,因此我们不能确保r足够大.也许你应该想到了动态分配内存的方法.C语言为我们提供了malloc函数来分配一块连续的内存空间,以及为我们提供了strlen函数来求得一个字符串所包含的字符个数.(记住strlen函数计算字符串中字符的个数,并没有包括作为结束符的空字符('\0').)
那么这次我们就采用动态分配内存的处理这个问题:
char *r=NULL;
r=(char *)malloc(strlen(s)+strlen(t));
strcpy(r,s);
strcat(r,t);
也许你觉的这次应该大功告成了,其实不然,这个程序还是错误的.原因归纳起来有3个:
1. malloc函数有可能无法提供请求的内存,这种情况下malloc函数会返回一个空指针NULL来作为"内存分配失败"事件的信号.
2. 给r分配的内存在使用完之后应该及时的释放,这一点务必要记住,否者会照成内存泄露.
3. 第三个原因,也是最重要的一个原因,那就是在调用mallloc函数的时候并没有分配足够的内存空间.
我们再回忆一下上面刚刚讲的关于strlen函数需要注意的地方:strlen函数计算字符串中字符的个数,并没有包括作为结束符的空字符('\0')
如果strlen(s)的值是n,那么实际的字符串就需要 n+1 个字符的空间.所以,我们需要为r多分配一个字符的空间.
做到这些,并且检查了函数malloc是否调用成功,我们就得到了正确的结果:
char *r=NULL;
r=(char *)malloc(strlen(s)+strlen(t)+1);
if(NULL==r)
{
exit(0);
}
strcpy(r,s);
strcat(r,t);
free(r);
r=NULL;
----------------------------------------------------------------------------------------------------------------
3.3 空指针(NULL)并非空字符串
除了一个特殊的情况,在C语言中将一个整数转换成一个指针,最后得到的结果都取决于具体的C编译器实现.这个特例就是常数0,编译器保证由0转换而来的指针不等于任何一个有效的指针(这也是指针在不使用或者使用完毕的情况,将其“栓”在NULL的原因).出于文档化的考虑,常数0这个值经常用一个符号来代替: #define NULL 0
当然,无论是使用0还是使用NULL,效果都是相同的,需要记住的重要一点是:当常数0被转换为指针后,这个指针绝对不能被解除引用(dereference).换句话说,当我们将0赋值给一个指针变量的时候,绝对不能企图使用该指针所指向的内存中存储的内容.
下面的写法是合法的:
if(p==(char *)0) ......
或者
char *p=NULL;
//program code
if(NULL==p) ......
但是如果写成这样: if(strcmp(p,(char *)0)==0) ......
就是非法的了.原因在于,库函数strcmp()的实现会包括查看它的指针参数所指向的内存中的内容的操作.
如果p是一个空指针,即使printf(p);(运行的时候会提示assertion failure(断言失败))printf("%s\n",p);的行为也是未定义的.
----------------------------------------------------------------------------------------------------------------
3.4 作为参数的数组声明
C语言中,当一维数组作为函数参数时,编译器总是把它解析成一个指向其首元素首地址的指针。
这么做是有原因的,在C语言中,所有的非数组形式的数据实参均以传值的形式调用(指针作为实参时也不例外)(对实参做一份备份并传递给被调用函数,被调用函数不能够修改作为实参的实际变量的值,而只能修改传递给它的那份备份)然而,如果要复制整个数组,无论在空间上还是时间上,其开销都是非常大的。更重要的是,在大多数的情况下,你其实并不需要整个数组的备份,你只想告诉函数在哪一时刻对哪个特定的数组感兴趣,这样的话,为了节省空间和时间,提高程序的运行效率,就有了上述的规则。
同样的,函数的返回值也不能是一个数组,而只能是指针,这里要明确一个概念,函数本身是没有类型的,只有函数的返回值才有类型.
而这也解释了下面的程序的结果:
void fun(char a[])//char *a 实际传递的数组大小与函数形参指定的数组大小没有关系,因而可以不指定形参的数组的大小.
{
int i=sizeof(a);
printf("i=%d\n",i);
}
int main()
{
char a[100]={'0'};
fun(a);
return 0;
}
输出的结果为 4
如果一个指针参数并不实际代表一个数组,即使从技术上而言是正确的,但是,采用数组形式的记法经常会起到误导的作用.如果一个指针参数实际代表的就是一个数组,情况又会是怎么样呢?一个常见的例子就是函数main的第二个参数.
int main(int argc,char *argv[])
{
//program code
}
这种写法与下面的写法完全等价
int main(int argc,char **argv)
{
//program code
}
需要注意的是,前一种写法强调的重点在于argv是一个指向某个数组的起始元素的指针,该数组的元素为字符指针类型.因为这两种写法等价的,所以可以选择最能清楚的反应自己意图的写法.
---------------------------------------------------------------------------------------------------------------- |
|