易混淆概念(一)指针数组与数组指针

xiaoxiao2021-02-28  64

參考:http://blog.csdn.net/touch_2011/article/details/6966980

                   http://c.biancheng.net/cpp/html/476.html 指针数组与数组指针        ()优先级高于[ ](数组下标)优先级高于*(解除引用)

1、数组指针(指向数组的指针)

定义 int (*P)[n]; ()优先级高,首先说明P是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步 长。也就是说执行p+1时,p要跨过n个整型数据的长度。

如要将二维数组赋给一指针,应这样赋值: int a[2][2]; int (*p)[2]; //该语句是定义一个数组指针,指向含2个元素的一维数组。  p=a;        //将该二维数组的首地址赋给p,也就是a[0]或&a[0][0]  p++;       //该语句执行过后,也就是p=p+1;p跨过行a[0][]指向了行a[1][]即++之后指向下一行

所以数组指针也称指向一维数组的指针,亦称行指针

(1)数组在内存中的表示

创建一个数组就是在内存里面开辟一块连续的空间,比如int a[4];就是在内存里面开辟了一个大小为4*sizeof(int)字节的内存空间。二维数组是特殊的一维数组。

先来看一段代码:

[cpp]  view plain  copy void main()   {       int a[2][2]={1,2,3,4};//这是一个2*2的二维数组       int (*p)[2];//数组指针       p=a;//令p指向数组a   }

注意到代码中这句话:int (*p)[2];这里的p是一个数组指针变量。

a中各个元素在内存中是这样存放的:

 

(2)理解数组名和数组指针变量

OK,现在我们思考a,a[0],a[1],p,a+1,a[0]+1,p+1到底是什么,思考3秒钟:

分析:

a是一个数组名,类型指向一维数组的指针不是变量,a的值是指针常量,即不能有a++或者a=p这些操作。a指向这块连续空间的首地址,是&a[0][0]。

a[0]是一维数组名,类型是指向整型的指针,是&a[0][0],这个值是一个常量。

a[1]是一维数组名,类型是指向整型的指针,是&a[1][0],这个值是一个常量。

p是一个数组指针变量指向一维数组的指针变量是&a[0][0]。可以执行p++;p=a等操作。

a+1表示指向下一行元素,也可以理解为指向下一个一维数组

*(a+1)是取出第一行的首地址。

a[0]+1是指向第0行第1个元素,也可以理解为指向一维数组a[0]的第一个元素。

p+1同a+1

*(p+1)同*(a+1)

虽然a跟a[0]值是一样,但类型不一样,表示的意义不一样。通过分析就不难理解为什么*(*(a+i)+j)和a[i][j]等效了。

 

(3)指针是数组的迭代器

[cpp]  view plain  copy #include<stdio.h>   #define M 2   #define N 3      int main()   {       int a[M][N]={1,2,3,4,5,6};       int *start=&a[0][0];       int * const end=start+M*N;       for(;start!=end;start++)           printf("%-5d",*start);       putchar('\n');       return 0;   }

理解这段代码,用指针遍历一个二维数组,是不是很像C++标准库里面vector的迭代器。注意这里只用了一个for循环,这也可以说明二维数组其实就是特殊的一维数组

 

(4)数组名与数组指针变量的区别

 

从(2)中的分析中得出数组名是指针类型指向元素类型的指针,但指针常量(地址),声明数组时编译器会为声明所指定的元素数量保留内存空间。数组指针指向数组的指针,声明指针变量时编译器只为指针本身保留内存空间。

 

看看这个代码:

[cpp]  view plain  copy #include<stdio.h>   void main()   {       int a[2][2]={1,2,3,4};//这是一个2*2的二维数组       int (*p)[2];//数组指针       p=a;//令p指向数组a       printf("%d\n%d\n",sizeof a,sizeof p);  

猜一猜输出是什么?

困惑了吗?为什么结果会是这样的呢,让我们先初步了解一下sizeof关键字吧,下面是MSDN上sizeof的说明:

 

注意到说明中的红色字体,当sizeof用于变量时返回这个变量占用的实际空间的大小。当sizeof用于数组名时,返回整个数组的大小(这里的大小指占用的字节数)。p是一个指针变量,这个变量占用四个字节。而a是数组名,所以sizeof a返回数组a中的全部元素占用的字节数。

 了解了sizeof,猜猜下面这段代码输出什么

[cpp]  view plain  copy #include<stdio.h>      void main()   {       int a[2][2]={1,2,3,4};//这是一个2*2的二维数组       int (*p)[2];//数组指针       p=a;//令p指向数组a       printf("%d\n%d\n",sizeof(a+1),sizeof(p+1));       printf("%d\n%d\n",sizeof(a+0),sizeof(p+0));  

运行结果:

从结果中看出,a在做+运算时是转化成了指针变量,此时a+i的类型是一个指针变量而不是一个数组名。但a[i]是一个一维数组的数组名,sizeof(a[0])的值是8

 

现在再来看一段代码:

[cpp]  view plain  copy ">#include<stdio.h>      void f(int a[][2])   {       printf("%d\n",sizeof a);   }   void main()   {       int a[2][2]={1,2,3,4};//这是一个2*2的二维数组       printf("%d\n",sizeof a);       f(a);   }

再猜一下输出是什么?

是不是又有点困惑呢?

解释:这是因为传参的时候数组名转化成指针变量,注意到函数f中f(int a[][2])这里并不需要指定二维数组的长度,此处可以改为int (*a)[2]。所以传过来的就是一个数组指针变量。这样明白了吧!

 

总结:数组名的类型是指向元素类型的指针,值是指针常量。(a+1)的类型是一个指针变量。把数组名作为参数传递的时候实际上传递的是一个指针变量。sizeof对变量和数组名操作时返回的结果会不一样。数组指针是指向数组的指针,其值可以是变量。

 2、指针数组(存放指针的数组)(常用于字符串)

定义 int * p[n]; []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]...p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。 如要将二维数组赋给一指针数组: int *p[3]; int a[3][4]; p++; //该语句表示p数组指向下一个数组元素。注:此数组每一个元素都是一个指针 for(i=0;i<3;i++) p[i]=a[i] 这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2] 所以要分别赋值。

(1)认识指针数组

一个存放int类型的数组称为整型数组,那么存放指针的数组就叫指针数组。

[cpp]  view plain  copy #include<stdio.h>      void main()   {       int i=1,j=2;       //p先跟[]结合,然后再跟*结合       int *p[2];//指针数组,存放指针的数组       p[0]=&i;       p[1]=&j;       printf("%d",sizeof(p));   }

 断点调试分析:

 

此例数组p就两个元素,p[0]是指向i的指针,p[1]是指向j的指针。这两个指针都是int型指针,所以p是存放int型指针的数组。sizeof(p)返回数组占用的总空间,所以程序输出是8 (指针变量占4字节,共有两个指针)

 

(2)指针数组用法举例

来自《the c programming language》的一个例子,对字符串进行排序,看了下面这个例子,相信你就体会到了指针数组的好处了。

 

3、指针数组和数组指针的内存布局

初学者总是分不出指针数组与数组指针的区别。其实很好理解: 指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它是“储存指针的数组”的简称。 数组指针:首先它是一个指针,它指向一个数组。在32 位系统下永远是占4 个字节,至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。

A)

int *p1[10];

B) int (*p2)[10]; 每次上课问这个问题,总有弄不清楚的。这里需要明白一个符号之间的优先级问题。 “[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1, int *修饰的是数组的内容,即数组的每个元素 。那现在我们清楚,这是一个数组,其包含10 个指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针。我们可以借助下面的图加深理解:

4、论a 和&a 之间的区别

既然这样,那问题就来了。前面我们讲过a 和&a 之间的区别,现在再来看看下面的代码: int main() {    char a[5]={'A','B','C','D'};    char (*p3)[5] = &a;    char (*p4)[5] = a;    return 0; } 上面对p3 和p4 的使用,哪个正确呢?p3+1 的值会是什么?p4+1 的值又会是什么?毫无疑问,p3 和p4 都是数组指针,指向的是整个数组。 &a 是整个数组的首地址,a是数组首元素的首地址,其值相同但意义不同。 在C 语言里,赋值符号“=”号两边的数据类型必须是相同的,如果不同需要显示或隐式的类型转换。p3 这个定义的“=”号两边的数据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。在Visual C++6.0 上给出如下警告:    warning C4047: 'initializing' : 'char (*)[5]' differs in levels of indirection from 'char *'。 还好,这里虽然给出了警告,但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,所以运行并没有什么问题。不过我仍然警告你别这么用。 既然现在清楚了p3 和p4 都是指向整个数组的,那p3+1 和p4+1 的值就很好理解了。 但是如果修改一下代码,把数组大小改小点,会有什么问题?p3+1 和p4+1 的值又是多少呢? int main() {    char a[5]={'A','B','C','D'};    char (*p3)[3] = &a;    char (*p4)[3] = a;    return 0; } 甚至还可以把代码再修改,把数组大小改大点: int main() {    char a[5]={'A','B','C','D'};    char (*p3)[10] = &a;    char (*p4)[10] = a;    return 0; } 这个时候又会有什么样的问题?p3+1 和p4+1 的值又是多少? 上述几个问题,希望读者能仔细考虑考虑,并且上机测试看看结果。 测试结果: (1).char (*p2)[5]=a;必须使用强制转换,如:char (*p2)[5]=(char (*)[5])a; (2).把数组大小改变,都会编译不通过,提示: error C2440: 'initializing' : cannot convert from 'char (*)[5]' to 'char (*)[3]' error C2440: 'initializing' : cannot convert from 'char (*)[5]' to 'char (*)[10]' (3).把以上程序测试代码如下: int main() {    char a[5]={'a','b','c','d'};    char (*p1)[5]= &a;    char (*p2)[5]=(char (*)[5])a;    printf("a=%d\n",a);    printf("a=%c\n",a[0]);    printf("p1=%c\n",**p1);    printf("p2=%c\n",**p2);    printf("p1+1=%c\n",**(p1+1));    printf("p2+1=%c\n",**(p2+1));    return 0; } 输出: a=1638208 a=a p1=a p2=a p1+1=? p2+1=? Press any key to continue 结论: 根据指针类型及所指对象,表示指针大小,每次加1,表示增加指针类型大小的字节.----后面还会有解释说明.

5、地址的强制转换

先看下面这个例子: struct Test {    int Num;    char *pcName;    short sDate;    char cha[2];    short sBa[4]; }*p; 假设p 的值为0x100000。如下表表达式的值分别为多少?    p + 0x1 = 0x___ ?    (unsigned long)p + 0x1 = 0x___?    (unsigned int*)p + 0x1 = 0x___? 我相信会有很多人一开始没看明白这个问题是什么意思。其实我们再仔细看看,这个知识点似曾相识。一个指针变量与一个整数相加减,到底该怎么解析呢? 还记得前面我们的表达式“a+1”与“&a+1”之间的区别吗?其实这里也一样。指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是byte 而是元素的个数。所以:p + 0x1 的值为0x100000+sizof(Test)*0x1。至于此结构体的大小为20byte,前面的章节已经详细讲解过。所以p +0x1 的值为:0x100014。 (unsigned long)p + 0x1 的值呢?这里涉及到强制转换,将指针变量p 保存的值强制转换成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就是一个无符号的长整型数加上另一个整数。所以其值为:0x100001。 (unsigned int*)p + 0x1 的值呢?这里的p 被强制转换成一个指向无符号整型的指针。所以其值为:0x100000+sizof(unsigned int)*0x1,等于0x100004。 上面这个问题似乎还没啥技术含量,下面就来个有技术含量的:在x86 系统下,其值为多少? intmain() {    int a[4]={1,2,3,4};    int *ptr1=(int *)(&a+1);// 指向a数组后面的内存单元,&a+1表示向后移16个存储单元    int *ptr2=(int *)((int)a+1);// 表示a的存储单元的地址增加一个字节    printf("%x,%x",ptr1[-1],*ptr2);// ptr1[-1]其实指向的是a数组的最后一个单元 ,*ptr1则表示a数组的地址后移一个字节之后的4个连续存储单元所存储的值    return 0; } 这是我讲课时一个学生问我的题,他在网上看到的,据说难倒了n 个人。我看题之后告诉他,这些人肯定不懂汇编,一个懂汇编的人,这种题实在是小case。下面就来分析分析这个问题: 根据上面的讲解,&a+1 与a+1 的区别已经清楚。 ptr1:将&a+1 的值强制转换成int*类型,赋值给int* 类型的变量ptr,ptr1 肯定指到数组a 的下一个int 类型数据了。ptr1[-1]被解析成*(ptr1-1),即ptr1 往后退4 个byte。所以其值为0x4。 ptr2:按照上面的讲解,(int)a+1 的值是元素a[0]的第二个字节的地址。然后把这个地址强制转换成int*类型的值赋给ptr2,也就是说*ptr2 的值应该为元素a[0]的第二个字节开始的连续4 个byte 的内容。 其内存布局如下图: 好,问题就来了,这连续4 个byte 里到底存了什么东西呢?也就是说元素a[0],a[1]里面的值到底怎么存储的。这就涉及到系统的大小端模式了,如果懂汇编的话,这根本就不是问题。既然不知道当前系统是什么模式,那就得想办法测试。大小端模式与测试的方法在第一章讲解union 关键字时已经详细讨论过了,请翻到彼处参看,这里就不再详述。我们可以用下面这个函数来测试当前系统的模式。 int checkSystem() {   union check   {       int i;       char ch;   } c;   c.i = 1;   return (c.ch ==1);//如果当前系统为大端模式这个函数返回0;如果为小端模式,函数返回1。 } 如果当前系统为大端模式这个函数返回0;如果为小端模式,函数返回1。也就是说如果此函数的返回值为1 的话,*ptr2 的值为0x2000000。如果此函数的返回值为0 的话,*ptr2 的值为0x100。

转载请注明原文地址: https://www.6miu.com/read-59661.html

最新回复(0)