指针和数组
读文章前思考三个问题:
1.什么是指针?
2.什么是数组?
3.指针和数组之间有什么关系?
一.指针
1.1 指针的内存布局
int *p;
大家都知道这里定义了一个指针 p。但是 p 到底是什么东西呢?
我们把 p 称为指针变量,p 里存储的内存地址处的内存称为 p 所指向的内存。指针变量 p 里存储的任何数据都将被当作地址来处理。
我们可以简单的这么理解:一个基本的数据类型(包括结构体等自定义类型)加上* 号就构成了一个指针类型的模子。这个模子的大小是一定的,与* 号前面的数据类型无关。* 号前面的数据类型只是说明指针所指向的内存里存储的数据类型。所以,在 32 位系统下,不管什么样的指针类型,其大小都为 4byte 。可以测试一下 sizeof(void *) 。
1.2 int *p = NULL 和*p = NULL 有什么区别?
很多初学者都无法分清这两者之间的区别。我们先看下面的代码:
int *p = NULL;
这时候我们可以通过编译器查看p 的值为 0x00000000 。这句代码的意思是:定义一个指针变量 p ,其指向的内存里面保存的是int 类型的数据;在定义变量p 的同时把 p 的值设置为0x00000000 ,而不是把*p 的值设置为 0x00000000 。这个过程叫做初始化,是在编译的时候进行的。明白了什么是初始化之后,再看下面的代码:
int *p;
*p = NULL;
同样,我们可以在编译器上调试这两行代码。第一行代码,定义了一个指针变量 p ,其指向的内存里面保存的是 int 类型的数据;但是这时候变量p 本身的值是多少不得而知,也就是说现在变量p 保存的有可能是一个非法的地址。第二行代码,给*p 赋值为 NULL ,即给p 指向的内存赋值为NULL ;但是由于p 指向的内存可能是非法的,所以调试的时候编译器可能会报告一个内存访问错误。这样的话,我们可以把上面的代码改写改写,使p 指向一块合法的内存:
int i = 10;
int *p = &i;
*p = NULL;
在编译器上调试一下,我们发现 p 指向的内存由原来的 10 变为 0 了;而 p 本身的值, 即内存地址并没有改变。
1.3 如何将数值存储到指定的内存地址
假设现在需要往内存0x12ff7c 地址上存入一个整型数0x100 。我们怎么才能做到呢?我们知道可以通过一个指针向其指向的内存地址写入数据,那么这里的内存地址 0x12ff7c 其本质不就是一个指针嘛。所以我们可以用下面的方法:
int *p = (int *)0x12ff7c;
*p = 0x100;
需要注意的是将地址 0x12ff7c 赋值给指针变量p 的时候必须强制转换。
二.数组
2.1数组的内存布局
先看下面的例子:
int a[5];
所有人都明白这里定义了一个数组,其包含了 5 个int 型的数据。我们可以用 a[0],a[1] 等来访问数组里面的每一个元素,那么这些元素的名字就是 a[0],a[1]… 吗?看下面的示意图:
如上图所示,当我们定义一个数组a 时,编译器根据指定的元素个数和元素的类型分配确定大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为 a 。名字 a 一旦与这块内存匹配就不能被改变。a[0],a[1] 等为 a 的元素,但并非元素的名字。数组的每一个元素都是没有名字的。
sizeof(a)的值为 sizeof(int)*5,32 位系统下为 20。
sizeof(a[0])的值为 sizeof(int),32 位系统下为 4。
sizeof(a[5])的值在 32 位系统下为 4。并没有出错,为什么呢?
我们讲过 sizeof 是关键字不是函数。函数求值是在运行的时候,而关键字 sizeof 求值是在编译的时候。虽然并不存在a[5] 这个元素,但是这里也并没有去真正访问 a[5] ,而是仅仅根据数组元素的类型来确定其值。所以这里使用 a[5] 并不会出错。sizeof(&a[0]) 的值在32 位系下为4 ,这很好理解。取元素 a[0] 的首地址。
2.2 &a[0]和&a 的区别
&a[0] 和&a 到底有什么区别呢?a[0] 是一个元素,a 是整个数组,虽然&a[0] 和&a 的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。举个例子:湖南的省政府在长沙,而长沙的市政府也在长沙。两个政府都在长沙,但其代表的意义完全不同。这里也是同一个意思。
2.3 数组名 a 作为左值和右值的区别
简单而言,出现在赋值符“=” 右边的就是右值,出现在赋值符“=” 左边的就是左值。比如,x=y 。
左值:在这个上下文环境中,编译器认为 x 的含义是 x 所代表的地址。这个地址只有编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。
右值:在这个上下文环境中,编译器认为 y 的含义是 y 所代表的地址里面的内容。这个内容是什么,只有到运行时才知道。
C 语言引入一个术语-----“可修改的左值”。意思就是,出现在赋值符左边的符号所代表的地址上的内容一定是可以被修改的。换句话说,就是我们只能给非只读变量赋值。既然已经明白左值和右值的区别,下面就讨论一下数组作为左值和右值的情况:
a 作为右值的时候代表的是什么意思呢?很多书认为是数组的首地址,其实这是非常错误的。a 作为右值时其意义与&a[0] 是一样,代表的是数组首元素的首地址,而不是数组的首地址。这是两码事。但是注意,这仅仅是代表,并没有一个地方(这只是简单的这么认为,其具体实现细节不作过多讨论)来存储这个地址,也就是说编译器并没有为数组 a 分配一块内存来存其地址,这一点就与指针有很大的差别。a 作为右值,我们清楚了其含义,那作为左值呢?
a 不能作为左值!这个错误几乎每一个初学者都犯过。编译器会认为数组名作为左值代表的意思是 a 的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数组的某个元素而无法把数组当一个总体进行访问。所以我们可以把 a[i] 当左值,而无法把 a 当左值。其实我们完全可以把a 当一个普通的变量来看,只不过这个变量内部分为很多小块,我们只能通过分别访问这些小块来达到访问整个变量a 的目的。
三.指针与数组
很多初学者弄不清指针和数组到底有什么样的关系。我现在就告诉你:他们之间没有任何关系!只是他们经常穿着相似的衣服来逗你玩罢了。
指针就是指针,指针变量在32 位系统下,永远占4 个 byte ,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。
数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存任何类型的数据,但不能存函数。
既然它们之间没有任何关系,那为何很多人把数组和指针混淆呢?甚至很多人认为指针和数组是一样的。
3.1 以指针的形式访问和以下标的形式访问
(A) char *p = “abcdef”;
(B) char a[] = “123456”;
1.以指针的形式访问和以下标的形式访问指针
例子 (A)定义了一个指针变量 p ,p 本身在栈上占4 个 byte ,p 里存储的是一块内存的首地址。这块内存在静态区,其空间大小为 7 个 byte ,这块内存也没有名字。对这块内存的访问完全是匿名的访问。比如现在需要读取字符‘e’ ,我们有两种方式:
1)以指针的形式:*(p+4) 。先取出p 里存储的地址值,假设为0x0000FF00 ,然后加上 4 个字符的偏移量,得到新的地址 0x0000FF04 。然后取出 0x0000FF04 地址上的值。
2)以下标的形式:p[4] 。编译器总是把以下标的形式的操作解析为以指针的形式的操作。p[4] 这个操作会被解析成:先取出 p 里存储的地址值,然后加上中括号中 4 个元素的偏移量,计算出新的地址,然后从新的地址中取出值。也就是说以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了。
2.以指针的形式访问和以下标的形式访问数组
例子 B)定义了一个数组 a ,a 拥有7 个 char 类型的元素,其空间大小为7 。数组 a 本身在栈上面。对a 的元素的访问必须先根据数组的名字 a 找到数组首元素的首地址,然后根据偏移量找到相应的值。这是一种典型的“具名+匿名”访问。比如现在需要读取字符‘5’ ,
我们有两种方式:
1)以指针的形式:*(a+4) 。a 这时候代表的是数组首元素的首地址,假设为 0x0000FF00 ,然后加上 4 个字符的偏移量,得到新的地址0x0000FF04 。然后取出 0x0000FF04 地址上的值。
2)以下标的形式:a[4] 。编译器总是把以下标的形式的操作解析为以指针的形式的操作。a[4] 这个操作会被解析成:a 作为数组首元素的首地址,然后加上中括号中 4 个元素的偏移量,计算出新的地址,然后从新的地址中取出值。
由上面的分析,我们可以看到,指针和数组根本就是两个完全不一样的东西。只是它们都可以“以指针形式”或“以下标形式”进行访问。一个是完全的匿名访问,一个是典型的具名+匿名访问。一定要注意的是这个“以 XXX 的形式的访问”这种表达方式。
另外一个需要强调的是:上面所说的偏移量4 代表的是4 个元素,而不是4 个byte 。只不过这里刚好是char 类型数据 1 个字符的大小就为 1 个 byte 。记住这个偏移量的单位是元素的个数而不是 byte 数,在计算新地址时千万别弄错了。
3.2 a 和&a 的区别
main()
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
}
打印出来的值为多少呢? 这里主要是考查关于指针加减操作的理解。
对指针进行加 1 操作,得到的是下一个元素的地址,而不是原有地址值直接加 1 。所以,一个类型为 T 的指针的移动,以 sizeof(T) 为移动单位。 因此,对上题来说,a 是一个一维数组,数组中有 5 个元素; ptr 是一个 int 型的指针。
&a + 1 : 取数组 a 的首地址,该地址的值加上 sizeof(a) 的值,即 &a + 5*sizeof(int) ,也就是下一个数组的首地址,显然当前指针已经越过了数组的界限。
(int *)(&a+1) : 则是把上一步计算出来的地址,强制转换为 int * 类型,赋值给 ptr 。
*(a+1) : a,&a 的值是一样的,但意思不一样,a 是数组首元素的首地址,也就是 a[0] 的首地址,&a 是数组的首地址,a+1 是数组下一元素的首地址,即 a[1]的 首地址,&a+1 是下一个数组的首地址。所以输出 2*(ptr-1) : 因为 ptr 是指向 a[5] ,并且 ptr 是 int * 类型,所以 *(ptr-1) 是指向 a[4] ,输出 5 。
3.3 指针和数组的对比
更多内容,欢迎关注
|