数组与指针

经过几天的学习,记录下在过程中对数组以及指针的理解。

一、数组名,指针,地址

1、 首先要明白什么是数组,什么是指针,什么是地址?

我在网上找到了一篇博客,博主纠正了很多人对于这个基本概念的误解。博客地址为:
https://www.cnblogs.com/sophia0405/archive/2008/09/23/1296602.html#commentform
这篇博客通篇就说明了一个重要的问题:数组名永远都不会是指针!
地址这个东西,本来就是一种基本数据类型,类似于整数、浮点、字符等基本类型。指针不是类型,真正的类型是地址,指针只是存储地址这种数据类型的变量
打个比方,对于 int i=10;其中,10是整数,而i是存储整数的变量,指针就好比这个i,地址就好比那个10。指针能够进行加减法,原因并不是因为它是指针,加减法则不是属于指针这种变量的,而是地址这种数据类型的本能,正是因为地址具有加减的能力,所以才使指针作为存放地址的变量能够进行加减运算。这跟整数变量因为整数能够进行加减乘除因而它也能进行加减乘除一个道理。
那么数组名又应该如何理解呢?用来存放数组的区域是一块在栈中静态分配的内存(非static),而数组名是这块内存的代表,它被定义为这块内存的首地址。这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。数组名跟枚举常量类似,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名是一个符号常量,本身就代表了首地址这个地址值,它就是这个地址,这就是数组名属于符号常量的意义所在。
由于数组名是一种符号常量,因此它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不会是左值,那么,数组名永远都不会是指针!就像int i=10;i为整型变量,10为整型常量,两者可以相等,但是两者却是不同的概念。

总之要牢牢记住,数组名是一个地址,一个地址符号常量,不是一个变量,更不是一个作为变量的指针!

2、在明白定义的基础上,要找出指针和数组名的区别

参考了博客:
https://blog.csdn.net/thinkinwm/article/details/8112962
先给出三个现象,然后对其进行解释。

2.1数组名不是指针

给出例程一

 #include <iostream.h>
2. int main(int argc, char* argv[])
3. {
4.  char str[10];  //定义数组
5.  char *pStr = str; //定义指针并指向数组
6.  cout << sizeof(str) << endl; //输出数组大小
7.  cout << sizeof(pStr) << endl;//输出指针大小
8.  return 0;
9. }

我们先证明数组名不是指针,使用反证法。
 假设:如果数组名是指针,则pStr和str都是指针;
 因为:在WIN32平台下,指针长度为4;
 所以:第6行和第7行的输出都应该为4;
 实际情况是:第6行输出10,第7行输出4;
 结果:数组名不是指针。

2.2数组名在某些用法上类似于指针

例程二

1. #include <string.h>
2. #include <iostream.h>
3. int main(int argc, char* argv[])
4. {
5.  char str1[10] = "I Love U";
6.  char str2[10]; 
7.  strcpy(str2,str1);
8.  cout << "string array 1: " << str1 << endl;
9.  cout << "string array 2: " << str2 << endl;
10.  return 0;
11. }

标准C库函数strcpy的函数原形中能接纳的两个参数都为char型指针,也就是说srt1和str2应该是两个指针程序才不会报错,而我们在调用中传给它的却是两个数组名,函数同样可以被执行。
结果:数组又在某些情况下可以当作指针来用,让人摸不着头脑。

2.3:数组名“退化”

例程三

1. #include <iostream.h>
2. void arrayTest(char str[])
3. {
4.  cout << sizeof(str) << endl;
5. }
6. int main(int argc, char* argv[])
7. {
8.  char str1[10] = "I Love U";
9.  arrayTest(str1); 
10.  return 0;
11. }

输出:4
从例程一中我们知道sizeof(数组),输出结果应该是数组长度,但是此处却输出了指针长度。
结果:数组名在作为形参之后,在此函数内部,“退化”为指针。

结论:

(1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组;
通常对于数组的初始化使用如下语句

int a[10];

实质上,我们可以换一种定义方法(虽然在c++中不支持这种定义方法,但是这样理解一目了然)

int[10] a;

由此可见,数组的定义实质上就是定义了一个int[10]的结构,常量a为数组名,同时也是其地址。

(2)数组名的外延在于其可以“转换”为指向其指代实体的指针,而且是一个指针常量
“指针常量”解释:
譬如对于数组int a[10],a++无法使用,因为a是一个常量,因此无法对其赋值,或者说不能作为一个左值。
“指代实体的指针”解释:例程二中,数组名可以转换为指针形参来使用。

这里要注意三点:

a)转换为逻辑上的转换,而非真的转换了其类型。C语言之所以把作为形参的数组看作指针,并非因为数组名可以转换为指针,而是因为当初ANSI委员会制定标准的时候,从C程序的执行效率出发,不主张参数传递时复制整个数组,而是传递数组的首地址,由被调函数根据这个首地址处理数组中的内容。那么谁能承担这种“转换”呢?这个主体必须具有地址数据类型,同时应该是一个变量,满足这两个条件的,非指针莫属了。因此,实际过程中并没有发生任何数组实体被转换为了指针实体。

b)如果函数形参是一个指针,数组名可以作为实参传递给那个指针,难道不是说明了数组名是一个指针吗?

函数参数传递的过程,本质上是一种赋值过程。形参实际上所期望得到的东西,并不是实参本身,而是实参的值或者实参所代表的值!举个例来说,对于一个函数声明:

void fun(int i);

我们可以用一个整数变量int n作实参来调用fun,就是fun(n);当然,也正如大家所熟悉的那样,可以用一个整数常量例如10来做实参,就是fun(10);那么,由于形参是一个整数变量,而10可以作为实参传递给i,岂不就说明10是一个整数变量吗?这显然是谬误。实际上,对于形参i来说,用来声明i的类型说明符int,所起的作用是用来说明需要传递给i一个整数,并非要求实参也是一个整数变量,i真正所期望的,只是一个整数,仅此而已,至于实参是什么,跟i没有任何关系,它才不管呢,只要能正确给i传递一个整数就OK了。当形参是指针的时候,所发生的事情跟这个是相同的。指针形参并没有要求实参也是一个指针,它需要的是一个地址,谁能给予它一个地址?显然指针、地址常量和符号地址常量都能满足这个要求,而数组名作为符号地址常量正是指针形参所需要的地址,这个过程就跟把一个整数赋值给一个整数变量一样简单!

c)在使用sizeof()的时候,为什么数组名又跟指针出现差异了?

sizeof(数组名)=数组大小

sizeof(指针)=指针大小

这其实是一个误解,很多人以为sizeof是一个函数,而实际上,它是一个操作符,不过其使用方式看起来的确太像一个函数了。语句sizeof(int)就可以说明sizeof的确不是一个函数,因为函数接纳形参(一个变量),世界上没有一个C/C++函数接纳一个数据类型(如int)为"形参"。

(3)数组名作为函数形参时,在此函数内部,其失去了本身的内涵,仅仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。所以,数据名作为函数形参时,其全面沦落为一个普通指针!它的贵族身份被剥夺,成了一个地地道道的只拥有4个字节的平民。

产生这种情况的原因:看过结论2就会明白,形参传递的只是地址,而在函数内部,进行各种操作的却是那个形参(指针类型)。因此并非数组名真的退化,而是在调用过程中数组名把地址传递给了指针,函数内部的操作是对该指针的操作。

个人总结:

1、数组名为常量,它本身就代表首地址的地址值。数组名为右值。

2、指针为变量,变量值=地址。指针为左值。

举例说明
a[3]={1,2,3};
int *p = a;

p为指针,a为数组名。a的值就为数组首元素地址即a或&a[0],p的值为数组a的地址,而数组a的地址就是a,即p=a。

此处注意,p=a但是p不是a,二者数值相等但是意义不同,p为指针变量,a为数组a的地址值。同时二维数组中的各个参数都要注意,后文详述。

因此在使用指针与数组的过程中,一定要明白,该是地址时就用地址,该是指针时就用指针。

三、一维数组、二维数组、指针

上文是从理论上解释数组、指针的区别,我们也可以从数据类型上对其进行理解。

  1. 数组名:所指向的的是一种数据结构(数组)。可转换为指向其指代实体的指针,即指向数组的指针,(注意此处是转换而不是等同),这种指针为常量且不可修改。
  2. 指针:为一种变量类型,就跟int、char一样。如果指针指向某一个数组,它内部仅仅存放数组的地址,但是却不包含原始数据结构。
    举例说明:
一维数组

int a[3]

名称数据类型
aint *
&aint **
a[0]int
&a[0]int *
二维数组

int b[2][5];

对二维数组应该有所理解,在内存中二位数据的存储方式同一维数组,但是可以将其拆分为两个维度,如图所示。

第一维数组,存放两个元素b[0]和b[1],b[0]和b[1]是一维数组名同时也是一维数组地址,;

第二维数组,存放5个元素(int变量),即b[0]和b[1]中各有5个元素。
在这里插入图片描述

名称数据类型
bint *[ ]
&bint *[ ][ ]
b[0]int *
&b[0]int *[ ]
b[0] [0]int
&b[0] [0]int *

从表格中我们可以看出:

  1. b[0]在此处为第0行数组,可以直接看作一维数组b[0],因此b[0]既是数组名,是一维数组的地址,同时也是此行的行地址。可以将其类型转化为int *。
  2. b是二维数组数组名,同时也是二维数组地址。从另一个角度来讲,b可以看作一个一维数组,数组每一个元素都是一个数组,即
    b={b[0], b[1]}
    那么数组b的地址就是首元素b[0]的地址,得到
    b=&b[0]
    推广到其他行就是b+1=&b[1]···,所以b与&b[0]是一致的。
  3. b[0][0]就代表了二维数组第0行第0列的数值,&b[0][0]为其地址,但是要注意,b[0][0]先是一维数组b[0]的元素,而后才是二维数组b的元素,因此对一维数组b[0]中的首元素b[0][0]取址之后得到的一维数组的地址(再强调一下,b[0]既是数组名,又是一维数组的地址),所以有
    b[0]=&b[0][0]。
小结

1、一维数组,数组名a为地址常量,也是首元素地址&a[0];

2、二维数组,将其看作元素为一维数组的一维数组,或者它就是数组的数组。这句话比较绕但是更容易让我们理解二维数组的使用方法。

二维数组每一行都是一个一维数组(行向量)b[0],b[1],b[2]···,你可以把b[0]看成c,把b[1]看成d,这样就更好理解。b[0][0]为c[0],b[1][2]为d[2]。

四、指针数组与数组指针

对于像我这样的初学者来说,指针数组和数组指针从名称上来说非常容易混淆,因此我们可以看看这两个的英文名字:

指针数组:array of pointers,即用于存储指针的数组,也就是数组元素都是指针
数组指针:a pointer to an array,即指向数组的指针
这样能大体明白这两个概念的区别。

博文阐明了两者定义上的区别:
http://www.cnblogs.com/hongcha717/archive/2010/10/24/1859780.html
https://www.cnblogs.com/xiaojingang/p/4451089.html
http://c.biancheng.net/cpp/html/476.html
指针数组:
定义: int *p[n];

[ ]优先级高,先与p结合成为一个数组,再由int * 说明这是一个整型指针数组,它有n个指针类型的数组元素。

解释一下,指针数组先是一个数组,数组内容为int型指针,因此p为数组名,同时也是第一个指针的地址,p+1指向第二个指针。
在这里插入图片描述
赋值时

int a[3];
int b[2][5];
int *p[2],*q[2]; 
*p = a;//a为一维数组地址,*p1为数组指针第一个元素,相当于p1[0]=a
*q = b[0];//b[0]为二维数组中第0行的行地址,也可将b[0]看作一维数组

执行p+1时,则p指向下一个数组元素,即数组中的第二个指针。
要注意
p=&a;
q=&b[0];
这两种赋值方法是错误的,因为指针数组先是一个数组,p、q为数组名,数组名是无法进行赋值的,只有数组中的元素才能被赋值,如p[0]、p[1]

*p等价于p[0]
*(p+1)等价于p[1]

如要将二维数组赋给 指针数组:

int *p[3];
int b[3][4];
for(i=0;i<3;i++)
p[i]=b[i]; //对每一个指针赋值,b[i]为二维数组第i行的行地址(数组名即地址)

这里int *p[3] 表示一个一维数组内存放着三个指针变量,分别是p[0]、p[1]、p[2],所以要分别赋值。
数组指针:
定义: int (*q)[n];
()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。
在这里插入图片描述
事实上如果想要更好地理解数组指针,我们可以仿照数组定义的方式对其进行定义

int (*)[4] q;

int(*)[4]为指针类型,q 是指针变量。事实上,数组指针的原型或者说类型确实就是这样子的,只不过为了方便与好看把指针变量指针变量名q 前移了而已。这种方法编译器不认可,但是只要我们心中明白就行了。
如要将二维数组赋给 数组指针,应这样赋值:

int a[3][4];
int (*q)[4]; //该语句是定义一个数组指针,指向含4个元素的一维数组。
 q=a;        //将该二维数组的首地址赋给p,也就是&a[0]

q++; 该语句执行过后,也就是q=q+1;p跨过行a[0][]指向了行a[1][]。

这样两者的区别就豁然开朗了,指针数组是多个指针变量,以数组形式存在内存当中,占有多个指针的存储空间,对其进行赋值的时候注意要对每个指针都进行赋值。数组指针只是一个指针变量,似乎是C语言里专门用来指向二维数组的,它占有内存中一个指针的存储空间。

还需要说明的一点就是,同时用来指向二维数组时,其引用和用数组名引用都是一样的。
比如要表示数组中i行j列一个元素:
(p[i]+j)、((p+i)+j)、((p+i))[j]、p[i][j]
优先级:()>[]>

五、需要注意的问题

问题1、2摘自
http://c.biancheng.net/cpp/html/476.html

1、a与&a的区别

这个问题前面已经提到过了,但是为了强调,再次重申一遍。

int main()
{
   char a[5]={'A','B','C','D'};
   char (*p1)[5] = &a;
   char (*p2)[5] = a;
   return 0;
}

问题:上面对p1 和p2的使用,哪个正确呢?p1+1 的值会是什么?p2+1 的值又会是什么?

毫无疑问,p1和p2 都是数组指针,指向的是整个数组。对于一维数组a来讲,a是数组名也是首元素地址,&a 是整个数组的首地址,其值相同但意义不同。

a为首元素地址,所以a=&a[0],+1的步长为每个元素的长度,a+1指向第二个元素,a+1=&a[1],以此类推。&a为数组地址,+1的步长为整个数组的长度5*sizeof(char),&a+1就会指向下一个数组,跨度为整个数组。数组指针是指向整个数组的指针。

在C 语言里,赋值符号“=”号两边的数据类型必须是相同的,如果不同需要显示或隐式的类型转换。p1 这个定义的“=”号两边的数据类型完全一致,而p2 这个定义的“=”号两边的数据类型就不一致了,左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。

2、地址强制转换

先看下面这个例子:

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___?

我相信会有很多人一开始没看明白这个问题是什么意思。其实我们再仔细看看,这个知识点似曾相识。一个指针变量与一个整数相加减,到底该怎么解析呢?

通过问题1对表达式“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 被强制转换成一个指向无符号整型的指针,所以要加上指针的长度,指针长度为0x4。所以其值为:0x100000+sizof(unsigned int)*0x1,等于0x100004。

下面这一个问题没太看明白:
上面这个问题似乎还没啥技术含量,下面就来个有技术含量的:在x86 系统下,其值为多少?

int main()
{
   int a[4]={1,2,3,4};
   int *ptr1=(int *)(&a+1);
   int *ptr2=(int *)((int)a+1);
   printf("%x,%x",ptr1[-1],*ptr2);
   return 0;
}

下面就来分析分析这个问题:
根据上面的讲解,&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]里面的值到底怎么存储的。这就涉及到系统的大小端模式了,如果懂汇编的话,这根本就不是问题。这里涉及到了大小端模式问题:
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。譬如存放放0x123456,a[0]=12,a[1]=34,a[2]=56.
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
既然不知道当前系统是什么模式,那就得想办法测试。我们可以用下面这个函数来测试当前系统的模式。
int checkSystem( )
{
union check
{
int i;
char ch;
} c;
c.i = 1;
return (c.ch ==1);
}
如果当前系统为大端模式这个函数返回0;如果为小端模式,函数返回1。也就是说如果此函数的返回值为1 的话,*ptr2 的值为0x2000000。如果此函数的返回值为0 的话,*ptr2 的值为0x100。

3、二维数组与指向指针的指针

此问题摘自博客:
https://www.cnblogs.com/stoneJin/archive/2011/09/21/2184211.html
一道面试题引发的问题,首先要知道[]的优先级高于*,题目:
char **p,a[6][8]; 问p=a是否会导致程序在以后出现问题?为什么?
直接用程序说明:

#include<stdio.h>

void main()
{
    char **p,a[6][8];
    p = a;
    printf("\n";
}

编译,然后就会发现通不过,报错:错误 1 error C2440: “=”: 无法从“char [6][8]”转换为“char **” ,发生这种情况的原因是类型不匹配。

二维数组可以使用数组指针代替,
而指针数组才可以用指向指针的指针代替。

#include<iostream>
using namespace std;
void main()
{
char *a[]={"Hello","the","World"};
char **pa=a;
pa++;
cout<<*pa;
}

①类型确定的数组,元素的类型和元素的个数是确定的;
②数组退化为对应的指针,元素的类型保持一致;
③所谓的二维数组只是数组的数组,即元素为一维数组。
因此二维数组在退化时,为保证元素类型不变,所以不能退化为二级指针,而只能退化为指向一维数组的指针。
所以下面的程序时错误的

#include <iostream.h> 
void main() 
{ 
 int a[2][3];
 int**p=a; //错误,类型不匹配
}  

这里可以看一下博客:
http://col1.blog.163.com/blog/static/1909775192012514111830946/
详细剖析了指针与地址的问题。

4、数组和指针参数是如何被编译器修改的?

摘自博客:
https://www.cnblogs.com/stoneJin/archive/2011/09/21/2184211.html
“数组名被改写成一个指针参数”规则并不是递归定义的。数组的数组会被改写成“数组的指针”,而不是“指针的指针”:

实参实参类型形参类型所匹配的形参
数组的数组char c[8][10];char (*)[10];数组指针
指针数组char *c[10];char **c;指针的指针
数组指针(行指针)char (*c)[10];char (*c)[10];不改变
指针的指针char **c;char **c;不改变

有一段程序看一下应该能帮助理解上述几个问题:

#include "stdafx.h" 
#include <iostream> 
using namespace std; 
 
int _tmain(int argc, _TCHAR* argv[]) 
{ 
    int arr1[3]; 
    int arr2[3]; 
    int arr3[3]; 
    int * ptr; 
    // ptr1是一个指向 int [3] 的指针,即ptr的类型和&arr1的类型是一样的,注意:arr1指向的内存区域定长 
    int ptr1[3][3]={{1,2,3},{1,2,3},{1,2,3}}; 
    // ptr2是一个指向 int * 的指针,即ptr2的类型和&ptr是一样的,注意:ptr指向的内存区域不定长 
    int * ptr2[3]={arr1,arr2,arr3}; 
    // ptr3是一个指向 int [3] 的指针,即ptr3的类型和&arr1的类型是一样的,注意:arr1指向的内存区域定长 
    int(* ptr3)[3]=&arr1; 
    ptr3=ptr1; // 没错,他们的类型相同 
 // ptr3=ptr2;//error 无法从“int *[3]”转换为“int (*)[3] 
 // ptr4是一个指向 int * 的指针,即ptr4的类型和&ptr是一样的,注意:ptr指向的内存区域不定长 
    int ** ptr4; 
    //ptr4=&arr1; //error 无法从“int (*)[3]”转换为“int ** 
    ptr4=ptr2; // 没错,他们的类型相同 
 //ptr4=ptr3; // error 无法从“int (*)[3]”转换为“int ** 
    return 0; 
}
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值