指针可以说是 C 语言中比较重要的内容了,跟很多方面都存在联系。这里对 C 语言中的指针部分做个小总结。
内存和指针的关系
- 内存是线性的
- 内存是以字节为单位进行编址的,内存中的每个字节都对应一个地址
- 程序的执行过程中,数据都要保存到内存当中
- 数据是有类型的,计算机会根据数据类型分配连续的一段内存作为数据的存储空间
- 指针存储的是指向数据的地址
变量地址
- & 运算符后加变量名就变成了变量名的地址,& 可以称为取地址运算符
- 变量地址前加 * 运算符就能够访问到变量,* 可以成为取内容运算符
- 地址一般是用存储空间的第一个字节的地址表示变量地址,即低字节地址
- 地址也是有大小的
#include <stdio.h>
int main()
{
char a = 97;
int b = 97;
float c = 97.0;
double d = 97.0;
printf("&a = %p,sizeof(&a) = %d,a = %c\n",&a,sizeof(&a),*(&a));
printf("&b = %p,sizeof(&b) = %d,b = %d\n",&b,sizeof(&b),*(&b));
printf("&c = %p,sizeof(&c) = %d,c = %f\n",&c,sizeof(&c),*(&c));
printf("&d = %p,sizeof(&d) = %d,d = %f\n",&d,sizeof(&d),*(&d));
return 0;
}
运行上述的代码,结果为:
&a = 0060FEAF,sizeof(&a) = 4,a = a
&b = 0060FEA8,sizeof(&b) = 4,b = 97
&c = 0060FEA4,sizeof(&c) = 4,c = 97.000000
&d = 0060FE98,sizeof(&d) = 4,d = 97.000000
从上边的结果可以看出,计算机对每个变量都赋予了一个地址,每个地址的大小是固定的,可以通过 & 获得变量的地址,* 获得变量地址对应的内容。
指针与地址
每个变量地址对应的字节大小是相同的,这是由计算机和开发工具决定的。但是变量地址却是有类型的常量。我们由上边的代码已经知道 a,b,c,d 所对应的地址,试看下边的代码:
#include <stdio.h>
int main()
{
int a = 0x12345678;
printf("&a = %p,sizeof(&a) = %d\n",&a,sizeof(&a));
printf("char a = %x\n",*((char *)&a));
printf("short a = %x\n",*((short *)&a));
printf("int a = %x\n",*((int *)&a));
return 0;
}
运行上边的代码,可以得到下面的结果:
&a = 0060FEAC,sizeof(&a) = 4
char a = 78
short a = 5678
int a = 12345678
从上边的运行结果来看,&a 对应的就是 0060FEAC 这一个地址。根据这个结果再看下面的程序:
#include <stdio.h>
int main()
{
int a = 0x12345678;
printf("&a = %p,sizeof(&a) = %d,a = %x\n",&a,sizeof(&a),*(0x0060FEAC));
return 0;
}
这个程序是不能运行的,因为没有指定最后一项输出的类型。从这里来看,我们可以知道:
- &a 取出来的地址是有类型的
- *(0xXXXXXXXX) 这样的形式是不对的,至少应该写成 *((datatype *)0xXXXXXXXX) 的形式,但不推荐这样用
- 指针的类型决定的是指针的寻址能力,即将该起始位置后连续的几个字节当作一个数据
指针变量
定义
上边提到用 &a 的形式可以取到变量 a 的地址,但是这个地址是不可更改的。有时我们希望定义一个变量,用来存放指针的量,该变量叫做指针变量,定义为:
type * variable;
上面指针变量定义中的 * 就表示后方的变量为指针变量,type 则代表的是该指针变量的寻址能力。
初始化与赋值
根据之前提到的,地址是赋值给指针变量,又不推荐直接给指针变量赋值地址的形式,因此一般情况下都是把一个已经声明的变量地址赋值给指针变量,如:
#include <stdio.h>
int main()
{
int a;
int *pa = &a;
printf("&a = %p,pa = %p\n",&a,pa);
printf("a = %d,*pa = %d\n",a,*pa);
return 0;
}
与数值变量类似,指针变量也可以重新赋值,从而可以指向新的地址。
NULL
指针变量如果指向无效空间,该指针变量就变成了 invalid。通常有两种情况会出现 invalid:
- 指针未进行初始化
- 该指针变量指向的空间已经被释放
此时,通常用 NULL 给未进行初始化的指针向量初始化,或者对已经释放的内存空间进行指针赋值。
NULL 是一个宏,定义为:
define NULL ((void *) 0)
NULL 是一个很特别的指针,但读不出东西,也不能写入东西,所以当指针变量被赋值为 NULL 后进行读写操作,是不会有内存数据损坏的。
指针运算
指针的运算主要是改变指针指向的运算。
算术运算
算术运算主要是加减运算,对于指针来说并不是简单的加减数值的运算。之前已经提到过,指针是有类型的,所以说在指针的运算中也是包含类型的,因此指针的算术运算可以说是一种数值和类型的复合运算。可以看下边的程序:
#include <stdio.h>
int main()
{
short *a;
int *b;
float *c;
double *d;
printf("a = %p\tb = %p\tc = %p\td = %p\n",a,b,c,d);
printf("a+1 = %p\tb+1 = %p\tc+1 = %p\td+1 = %p\n",a+1,b+1,c+1,d+1);
return 0;
}
结果为:
a = 00000059 b = 00000059 c = 00000000 d = 00401790
a+1 = 0000005B b+1 = 0000005D c+1 = 00000004 d+1 = 00401798
由于没有定义指针的指向,所以忽略该段程序的实用性。只有当指针变量能够指向一串连续的存储单元时,指针变量的算术运算才有意义,因为这时指针变量才能够访问到有意义的数据。
但是从最后的结果可以看出,由于指针变量定义的类型不同,对指针变量进行算术运算之后的结果也是不同的,这就是之所以说指针的算术运算是数值和类型的复合运算的原因。
比较运算
除了可以对指针变量进行简单的算术运算之外,还能够对指针变量进行比较:
#include <stdio.h>
int main()
{
int *p1, *p2;
int value1 = 1;
int value2 = 2;
p1 = &value1;
p2 = &value2;
printf("p1 = %p\tp2 = %p\n",p1,p2);
if (p1 > p2)
printf("The pointers are same\n");
else
printf("The pointers are different\n");
return 0;
}
结果为:
p1 = 0060FEA4 p2 = 0060FEA0
The pointers are same
可以看出指针变量是能够比较大小的,但是这基本上没有意义,因为无法确定计算机分配内存的位置。但是可以利用 == 判断两个指针指向的是否是同一块内存。
指针和一维数组
之前已经提到过数组名其实就是指针,可以对该指针进行算术运算进而访问数组中的其它数据。
下标访问
下标访问即是通过 [] 运算符或者数组名加数值的形式实现的操作,可以看下边的程序:
#include <stdio.h>
int main()
{
int array[10] = {1,2,3,4,5,6,7,8,9,10};
for(int i=0; i<10; i++)
printf("array[%d] = %d\n",i,array[i]);
printf("\\***************************\\\n");
for(int i=0; i<10; i++)
printf("array[%d] = %d\n",i,*(array+i));
return 0;
}
结果为:
array[0] = 1
array[1] = 2
array[2] = 3
array[3] = 4
array[4] = 5
array[5] = 6
array[6] = 7
array[7] = 8
array[8] = 9
array[9] = 10
\***************************\
array[0] = 1
array[1] = 2
array[2] = 3
array[3] = 4
array[4] = 5
array[5] = 6
array[6] = 7
array[7] = 8
array[8] = 9
array[9] = 10
需要注意的是 array[i] = *(array+i),这就是 [] 运算符的含义了。
指针访问
指针访问是通过指针变量进行访问,可以看下边的程序:
#include <stdio.h>
int main()
{
int array[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = array;
for(int i=0; i<10; i++)
printf("array[%d] = %d\n",i,*p++);
return 0;
}
结果为:
array[0] = 1
array[1] = 2
array[2] = 3
array[3] = 4
array[4] = 5
array[5] = 6
array[6] = 7
array[7] = 8
array[8] = 9
array[9] = 10
可以看出两者的执行结果是一样的。但是也有一些区别:
- 数组名是一个常量,不允许重新赋值
- 指针变量是一个变量,可以重新赋值
- p + i 和 array + i 所表示的含义相同
- *(p + i ) 和 *(array + i) 所表示的含义相同
- p[i] 和 array[i] 所表示的含义相同
- 由于运算符的优先级和结合性,要明白 *p++ == *(p++)
指针和二维数组
二维数组虽然也是由一维数组扩展过来的,但是两者也是有些许区别的。
下标访问
二维数组可以通过 array[i][j] 来访问数组 array 第 i 行第 j 列的对应元素。
指针偏移访问
一维数组可以通过 *(array + i) 来访问数组 array 的第 i 个元素,但是二维数组却有些不同:
#include <stdio.h>
int main()
{
int array1[10] = {1,2,3,4,5,6,7,8,9,10};
int array2[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
for (int i = 0; i < 10; i++)
{
printf("array1 + %d = %p\tarray1[%d] = %d\n",i,array1 + i,i,array1[i]);
}
printf("\\*******************************************************************\\\n");
for (int i = 0; i < 3; i++)
{
printf("array2 + %d = %p\t*(array2 + %d) = %08X\t**(array2 + %d) = %d\n",i,array2 + i,i,*(array2 + i),i,**(array2 + i));
}
printf("\\*******************************************************************\\\n");
for (int i = 0; i < 3; i++)
{
printf("&array2[%d] = %p\tarray2[%d] = %08X\t*array2[%d] = %d\n",i,&array2[i],i,array2[i],i,**(array2 + i));
}
printf("\\*******************************************************************\\\n");
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("*(array2 + %d) + %d = %p\t*(*(array2 + %d) + %d) = %d\n",i,j,*(array2 + i) + j,i,j,*(*(array2 + i) + j));;
}
}
printf("\\*******************************************************************\\\n");
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("&array2[%d][%d] = %p\tarray2[%d][%d] = %d\n",i,j,&array2[i][j],i,j,array2[i][j]);
}
}
return 0;
}
结果为:
array1 + 0 = 0060FE6C array1[0] = 1
array1 + 1 = 0060FE70 array1[1] = 2
array1 + 2 = 0060FE74 array1[2] = 3
array1 + 3 = 0060FE78 array1[3] = 4
array1 + 4 = 0060FE7C array1[4] = 5
array1 + 5 = 0060FE80 array1[5] = 6
array1 + 6 = 0060FE84 array1[6] = 7
array1 + 7 = 0060FE88 array1[7] = 8
array1 + 8 = 0060FE8C array1[8] = 9
array1 + 9 = 0060FE90 array1[9] = 10
\*******************************************************************\
array2 + 0 = 0060FE3C *(array2 + 0) = 0060FE3C **(array2 + 0) = 1
array2 + 1 = 0060FE4C *(array2 + 1) = 0060FE4C **(array2 + 1) = 5
array2 + 2 = 0060FE5C *(array2 + 2) = 0060FE5C **(array2 + 2) = 9
\*******************************************************************\
&array2[0] = 0060FE3C array2[0] = 0060FE3C *array2[0] = 1
&array2[1] = 0060FE4C array2[1] = 0060FE4C *array2[1] = 5
&array2[2] = 0060FE5C array2[2] = 0060FE5C *array2[2] = 9
\*******************************************************************\
*(array2 + 0) + 0 = 0060FE3C *(*(array2 + 0) + 0) = 1
*(array2 + 0) + 1 = 0060FE40 *(*(array2 + 0) + 1) = 2
*(array2 + 0) + 2 = 0060FE44 *(*(array2 + 0) + 2) = 3
*(array2 + 0) + 3 = 0060FE48 *(*(array2 + 0) + 3) = 4
*(array2 + 1) + 0 = 0060FE4C *(*(array2 + 1) + 0) = 5
*(array2 + 1) + 1 = 0060FE50 *(*(array2 + 1) + 1) = 6
*(array2 + 1) + 2 = 0060FE54 *(*(array2 + 1) + 2) = 7
*(array2 + 1) + 3 = 0060FE58 *(*(array2 + 1) + 3) = 8
*(array2 + 2) + 0 = 0060FE5C *(*(array2 + 2) + 0) = 9
*(array2 + 2) + 1 = 0060FE60 *(*(array2 + 2) + 1) = 10
*(array2 + 2) + 2 = 0060FE64 *(*(array2 + 2) + 2) = 11
*(array2 + 2) + 3 = 0060FE68 *(*(array2 + 2) + 3) = 12
\*******************************************************************\
&array2[0][0] = 0060FE3C array2[0][0] = 1
&array2[0][1] = 0060FE40 array2[0][1] = 2
&array2[0][2] = 0060FE44 array2[0][2] = 3
&array2[0][3] = 0060FE48 array2[0][3] = 4
&array2[1][0] = 0060FE4C array2[1][0] = 5
&array2[1][1] = 0060FE50 array2[1][1] = 6
&array2[1][2] = 0060FE54 array2[1][2] = 7
&array2[1][3] = 0060FE58 array2[1][3] = 8
&array2[2][0] = 0060FE5C array2[2][0] = 9
&array2[2][1] = 0060FE60 array2[2][1] = 10
&array2[2][2] = 0060FE64 array2[2][2] = 11
&array2[2][3] = 0060FE68 array2[2][3] = 12
一维数组我们已经很熟悉了,就不再赘述了。对于二维数组,从上边的结果可以看出:
- array2 是数组首元素的地址,所以 array2,*(array2),&array2[0],array2[0],*(array2 + 0) + 0,&array2[0][0] 所对应的地址都是相同的
- array2[0] 本身就是一个一维数组,所以 array2 + i,&array2[i] 中间的地址间隔为 16
- 可以将 array2 看作是一个二重指针
- array2[0] 对应的是一个一维数组的起始地址,即一个整数大小对象的地址
- 而 array2 对应的是一个二维数组的起始地址,即一个一维数组大小对象的地址
对上述的结果还可以总结为:
- 二维数组名解引用,降维为一维数组名
- 一维数组名,对其引用,升级为二维数组名
- & 和 * 可以认为是互为相反的关系
指针访问
与之前提到过的类似,对比推理即可。