漫谈C语言指针和数组
指针一直是被很多人误解,包括很多书中对指针的定义也存在很大的歧义,导致看书的人越看越懵,所以指针到底是个什么东西?(注意:文中所有图片不表示实际内存大小,纯属为了画图需要)
指针是地址吗?
很显然指针不是地址,地址只是一个数值,这个数值不可能代表指针。如图
伪代码:
int *ap = (int *)100;
long *bp=(long *)100;
*bp=1101;
指针变量bp 和指针变量ap存的地址值都是100,但是同样的100地址值代表的是不同的空间 一个是long 类型 1101,一个是Int类型 11 。
所以结果已经很明了,脱离了类型的地址不能称为指针,指针是一种数据类型,它的数据类型表明了它是指向某一类型的对象,而它的值是地址。指针一种广泛的数据类型统称,就像整型包含了short、int、long类型。
int a=1;
int *p=&a;
如上 a代表了一个int类型的数据对象,它在内存中有一个地址,&a 是一个指针,该指针是int* 类型 值是a在内存中的地址,&a 是指向a的指针。我们把这个&a指针,赋值给int *类型的变量p。
所以p是一个指针变量,但是为了方便称呼,很多时候我们就说p指针指向了a。
这里有些人会认为&a是取a的地址,这不完全是对的,一个地址是无法给一个int* 类型变量赋值,这会报错或者警告,只有同类型的才能赋值给该类型的变量。指针赋值指针变量才是正确的。
如果写 int *p=0xff30 肯定会被编译器警告,一个值类型int不能赋值给int* 指针类型,但是也能运行,那是因为0xff30 会被隐适转换位int* 类型 ,值是0xff30 。
指针只能指向自己相对应类型的数据对象,譬如定义一个int* 的指针,那只能指向int*类型 ,不能指向其他类型的数据对象,否则编译器会警告,并且被隐适转换成int* 类型
int a=1;
short b=1;
int *ap = &a; //正确,&a是int *类型指针 赋值给int* 类型变量ap
short *bp =&b; //正确,&b是short *类型指针 赋值给short* 类型变量bp
int *op = &b; //错误,&b是short *类型指针 赋值给int* 类型变量op
当然上面运行也没问题,指针&b中的地址保持不变,类型被隐适转换成变成int* 类型,赋值给变量指针变量op
这里op虽然拿到的是b的地址,但是指向的东西不一样,*bp 指向的一个short类型,*op指向的是一个Int类型,不过b后面那个空间的数据是未知的,所以图中直接用空白表示
指针面对数组也是如此,但很多人面对数组就直接搞不清楚了,下面讲到数组会再次讨论这么问题
指针可以参与加减运算,结果是产生同类型却指向另一个内存空间对象的指针,如:
int a=1;
int *p =&a;
int *op = p+1;
这边p+1会产生了一个新的指针,这个新指针中的地址,跨越当前int对应的字节数,例如原来a的地址是10,int表示4个字节,那么+1后新的地址是14,+2后新的地址是18
如果p是short *类型的指针,那么+1,新的地址就是7,+2新的地址是9。说白了就是对指针+1 ,直接跨过当前指针指向的那个数据。如图
数组是一块连续分配的存储同一数据类型对象的空间,数组有一维数组和多维数组,多维可以看成一维数组,只不过每个元素又是一个数组而已
C语言中指针和数组是不同的东西,只是在表达式中数组大多数会隐适转换成首元素的指针。
指针是指向某一个类型空间的 数据类型对象(值是空间的地址)
int arr[3]={1,2,3} 这就定义了一个数组,并且初始化了数据,通过数组名加下标的方式就可以访问数组中的每一个元素。
arr[0] 表示第一个数组元素,arr[1]表示第二个数组元素,为什么说数组就是采用了指针的方式呢?往下看
前面我们说指针变量必须赋值一个对应类型的指针才不会报错,那么我们试试这样写
int *p=arr; 完全没有任何警告,所以发现了没 数组名arr是一个指向int类型的指针,它指向的是数组的第一个元素。我们把arr赋值给int类型变量试试看会发生什么?
编译器警告说 int和int*间接级别不同,arr是被解释为int *类型。
分别输出数组的第一个,和第二个元素(1,2),如果arr是指向数组的指针,那当我们计算arr+1 也直接是跨越整个数组了,而不可能还输出2
其实arr[0],arr[1] 实际也是转换成 *(a) 和 *(a+1) 来计算的,因为这里arr是转换为指向第一个元素的指针,所以可以通过+1,+2便利整个数组。 当然arr作为左值时是一个不可修改的左值,也就是不能改变的,所以a=a+1这样操作是不行的,很多人说数组名是常指针所以不能赋值这是错误的讲法。数组名就是数组的标识符而已,跟普通类型的变量没什么区别。
所以看到没其实指针还可以通过下标的方式来访问,这不是数组的访问方式哦,不要以为是因为定义了数组,所以才可以用arr[0]来访问。
准确的说,数组在大多数情况下是会隐适转变成指向首元素的指针来用,当参与sizeof 或者 &运算符等除外
譬如上面那个例子 int* p =arr ; 是正确的,因为arr数组被隐适转换成了首元素的指针
譬如sizeof(arr) 我们发现是输出数组的长度 12,
我们看下这个: sizeof(*(&arr)) 输出的也是数组的长度12, arr是数组,所以&arr是数组的指针,所以 *(&arr) 也是数组,所以结果是12
但是我们Int *p= *(&arr),完全没问题,因为上面说过了 数组大多数情况会被隐适转换成首元素指针来用,所以在这个表达式中*(&arr) 又被当成了首元素的地址了(数组表达式转换为首元素指针)
因为栈的存储方式是从高地址到低地址的,所以我们取指针的时候要从最后面开始,这里我用的是c-free工具mingw 编译调试,不是用VS,因为VS下定义的局部变量,内存空间的排序不是连续的,也就是abc之间会空出很多,导致无法定位。
那&a是什么? 答案很肯定:&a是指向数组的指针。看下就知道了
从警告中我们看出,&arr被解析成一个int(*)[3] 的指针类型,说明它是一个指向数组的指针,而之前看过过arr是被解析成 int *类型。
我们可以这样定义一个指向arr数组的指针变量arrp, int (* arrp)[3]=&arr;
这个时候我们对arrp执行加减操作,执行跨越的是整个数组,假如这里int是4个字节arrp+1 实际地址是+12,因为数组有3个Int类型元素
我们可以看到+1和不加的地址相差了12,而且+1后的指针,指向的区域是另外一个数组,里面的每个数组元素都是随机的int类型值。
下面我们看下二维数组 int arrs[2][3]={{1,2,3},{4,5,6}}
我们只要想像成一维就可以了,{1,2,3}是arrs数组的第一个元素,{4,5,6}是arrs数组的第二个元素,那么很简单了
数组arrs转换为指向{1,2,3}这个子数组的指针,arrs+1 就是指向第二个数组{4,5,6}的指针,&arrs是指向整个大数组的指针,两个都是数组指针
只不过如果通过arrs+1,那么实际地址+12,而通过&arrs+1 ,那么实际地址加的是24,(假设int都是4字节)
&arrs因为是整个数组的指针,所以即使+1后,也是一个二维数组的指针
我们看一个例子:a,b,c分别是多少?
int arrs[2][3]={{1,2,3},{4,5,6}};
int (*p)[3]=arrs;
int a = (int)p;
int b = (int)*p;
int c = (int)**p;
我们分析下:
第二句申明了一个数组指针变量p ,arrs是也一个数组指针变量, 它指向的是{1,2,3},而p也是指向一个有3个Int类型的数组指针,所以arrs可以直接赋值给p,不会产生任何的警告,所以其实p和arrs一样都是数组{1,2,3}的指针
第二句,p指针中的地址就是{1,2,3}的地址,也等于元素1的地址,指针p通过强制转换为Int类型,a保存的是元素1的地址(毕竟参与转换的话肯定是变量的值吧,也就是那个地址去转换成Int)
第三句, p是指向数组{1,2,3}的指针,那*p 就是数组,然后*p 数组被隐适转换成的是首元素指针用 (很好记,你只要知道除了sizeof 和 & 外其他都是隐适转换成首元素指针就好了)
首元素的指针,强制转为Int 也是把存储的地址转为为了int 。
第四句 p是指向数组{1,2,3}的指针 那么*p就是数组{1,2,3} ,而数组作为右值表达式时会转换为指向首元素的指针, 也就是指向1的指针,那么**p就是1,其实这边不用强制转换,题目是为了迷惑故意加上去,不然你肯定猜到**p也是一个int类型了
如果把p后面的[3]改成[2]呢,答案其实一样,只不过这个时候p指向的是数组{1,2}了,在arrs在转换成p的时候地址还是原来的地址。
很多人误解指针,就是过多考虑指针右边赋值是什么,指针只要记住:我定义的是什么类型的指针,我指向的就是什么类型的数据,至于你是什么,我不管,不知道,也不关心!