目录
前言
指针是 C 语言的特色,也是精华所在,但同时也是 C 语言的难点。每一个合格的 C 语言程序员都应该掌握指针,软件开发行业有这样一句话,「没有掌握指针,就等于没有学过 C 语言」。这句话虽然有点夸张,但足以说明指针在 C 语言中的重要性。
指针的本质是内存空间的地址编号。由此可以看出指针与内存空间密切相关,因此,搞清指针与内存之间的关系是学习指针的第一步,也是最重要的一步。下面将从内存空间开始介绍,由浅入深地学习指针。
内存空间与内存地址
想要掌握指针,就必须清楚计算机中如何存储数据、访问数据。前面几章的程序中,都是通过变量名来操作数据,如:
int num=100;
上述程序的本质是:在内存中开辟 4 字节空间,然后给该内存空间命名为 num,最后将整数 100 保存在该内存空间中。
num=200;
上述程序的本质是:将整数 200 保存在名称为 num 的内存空间中,这样就会覆盖之前保存的 100,也即所谓的修改变量。
printf("%d\n",num);
上述程序的本质是:读取名称为 num 的内存空间中的数据,然后打印输出。
通过分析,读者有没有发现:计算机中的数据都是存储在内存中,因此读写数据的本质其实是读写内存。目前读写内存的唯一方式就是通过变量名,这种方式被称为「直接访问」。
既然有「直接访问」,相信读者就会问,那有没有「间接访问」呢?
答案是肯定的。其本质是通过内存地址访问内存空间。那么什么是内存地址呢?下面通过一个例子来介绍内存空间与内存地址。
在计算机中,内存空间的最小单位为字节,操作系统会为每个字节的内存空间进行编号,并且这个编号在当前程序中是唯一的。
这种方式类似于现实生活中的小区,小区的最小单位是住户,每一个住户的房子都有唯一的编号(门牌号),如 1-1 表示 1 栋 1 号、2-5 表示 2 栋 5 号等。如果想要找住在该小区的居民,可以根据门牌号找到他家。因此这里可以作形象类比:计算机内存空间相当于小区,每一个字节内存空间相当于每一个住户的房子,每一个字节内存空间编号相当于门牌号。
最后需要说明一点,所谓的内存空间编号在计算机应用中被称为「内存空间地址」,简称「内存地址」。因此在后续程序中,出现内存地址,默认表示某一个字节内存空间的编号。
注意:这里读者务必清楚,内存地址与内存空间虽然是一一对应的,但是真正存储数据的是内存地址所对应的内存空间,而不是内存地址本身,因为内存地址只是一个编号。
下面通过例子来深入理解内存空间与内存地址。
假设图 12-1 是宾馆中的一排房间,每个房间中都住着一个足球运动员,例如,101 号房间住着 7 号球员;105 号房间住着 2 号球员。
如果想要在这排房间中找到 2 号球员,只需知道他住的房间号是 105 即可。同理,要找其他球员也是如此,虽然宾馆的房间外表都一样,但是房号不同。这里可以作一个形象的类比:房号 101 相当于内存地址,101 房间相当于内存空间,7 号球员相当于内存空间中的数据。
前面讲过,要想找到 7 号球员,必须通过房间号来查找。同理,在计算机中,要想读写内存空间中的数据,也需要通过内存地址找到内存空间,然后进行读写操作。读写内存有如下两种方式。
第一种,通过变量名读写内存。
变量本质上是一块有名字的内存空间,可以通过变量名读写内存,如图 12-2 所示。
第二种,通过内存地址读写内存。
在计算机内存中,每一个字节的内存空间都有一个编号,这个编号被称为内存地址。通过该地址可以读写对应的内存空间,如图
变量的本质是一块有名字的内存空间。其实这块内存空间不仅有名字,还有编号,在 32 位操作系统下,这个编号是一个 4 字节的整数。通过「&变量名」的方式可以得到这个整数,例如:
int num=10;
printf(「%p\n」,&num); //以十六进制格式输出内存地址
输出结果如图
这个编号与一部分内存空间是对应的,通过这个编号可以找到对应的内存空间。
变量地址与内存地址
现阶段学习,在程序中得到内存地址的唯一方式是:&变量名。由于这种方式得到的内存地址是变量所对应的内存空间地址,又是通过变量名得到的,因此可以称为「变量地址」。这里读者务必清楚,变量地址本质上就是内存地址。
指针变量
用于保存内存地址的变量,称为指针变量。在 C 语言程序中不仅变量有类型,数据也有类型,例如,1—整数型,3.14—浮点数型,『c』—字符型。在存储数据时,需要使用与之匹配的类型变量进行保存。同理,内存地址也是一种数据,这种数据都是指针类型,因此需要指针类型变量来保存这种数据。
指针变量的定义与初始化
定义指针变量的一式
定义指针变量一般采用如下形式:
类型名* 变量名;
类型名表示该指针变量只能保存该类型变量的地址,*表示该变量是指针变量,只能保存地址数据,变量名即该变量的名称。例如:
int* p;
int 表示该指针变量只能保存 int 类型变量的地址,*表示变量 p 是指针变量,只能保存地址数据,p 是指针变量的名称。
2.指针变量初始化
指针变量和普通变量初始化方式相同,可以在变量定义时初始化,也可以先定义后初始化。例如:
int a=10;
int* p_a=&a; //定义 int 指针变量 p_a,并将变量 a 的内存地址赋值给 p_a
或者
int a=10;
int*p_a; //先定义 int 指针变量 p_a
p_a=&a; //然后将变量 a 地址赋值给变量 p_a
3.指针变量指向的变量
在 C 语言程序中,将某个变量的地址赋值给指针变量,就认为该指针变量指向了某个变量,例如:
int a=10;
int*p_a=&a;
上述程序,将整数变量 a 的地址赋值给指针变量 p_a,就认为 p_a 指向了变量 a
可以看到,变量 a 中存储的是整数 10,而变量 p_a 中存储的是变量 a 的地址。想要访问数据 10,必须先找到指针变量 p_a,通过变量 p_a 中的数据&a,找到变量 a,最后访问数据 10。
指针变量的引用
分以下两种情况。
第一种,引用指针变量。
对指针变量进行读写操作。
01 #include<stdio.h>
02 int main(void)
03 {
04 int a=10;
05 int b=20;
06 int* p1,*p2; //定义指针变量 p1、p2
07 p1=&a; //p1 指向变量 a
08 p2=p1; //p2 指向变量 a
09 printf("&a=%p p1=%p p2=%p\n",&a,p1,p2);
10 p1=&b; //p1 指向变量 b
11 p2=p1; //p2 指向变量 b
12 printf("&b=%p p1=%p p2=%p\n",&b,p1,p2);
13 getchar();
14 return 0;
15 }
运行结果如图 所示。
【程序分析】
1.第 4~5 行分别定义两个 int 变量 a、b。
2.第 6 行,定义两个 int 类型指针变量 p1、p2。
3.第 7 行,将变量 a 的地址赋值给指针变量 p1,因此 p1 指向变量 a。
4.第 8 行,将指针变量 p1 的值赋值给指针变量 p2,p2 也指向变量 a,如图 12-15 所示。
5.第 9 行,输出变量 a 的地址与指针变量 p1、p2 的值,可以看到指针变量 p1、p2 的值就是变量 a 地址,如图 12~14 第 1 行所示。
6.第 10 行,将变量 b 的地址赋值给指针变量 p1,因此 p1 指向变量 b。
7.第 11 行,将指针变量 p1 的值赋值给指针变量 p2,p2 也指向变量 b,如图 所示。
8.第 12 行,输出变量 b 的地址与指针变量 p1、p2 的值,可以看到指针变量 p1、p2 的值就是变量 b 地址,如图 12-14 第 2 行所示。
9.通过上述程序,读者可以发现,操作指针变量与操作普通变量是一样的。
第二种,引用指针变量指向的变量。
对指针变量指向的变量进行读写操作。
01 #include<stdio.h>
02 int main(void)
03 {
04 int a=10;
05 int* p=&a; //指针变量 p 指向变量 a 的内存地址
06 printf("a=%d *p=%d\n",a,*p);
07 *p=20; //修改 p 指向变量 a 中的数据
08 printf("a=%d *p=%d\n",a,*p);
09 getchar();
10 return 0;
11 }
运行结果如图 所示。
【程序分析】
1.第 5 行,定义 int 指针变量 p 并使用变量 a 的内存地址对其初始化,也即指针 p 指向变量 a。
2.第 6 行,因为 p 的值为&a,所以*p 等价于*&a,在 C 语言中*与&可以相互抵消,因此*p 等价于 a,最终*p 的输出结果与变量 a 的值一致,均为 10。
3.第 7 行,*p=20 等价于*&a=20,*与&相互抵消,因此*p=20 等价于 a=20,所以变量 a 的值被修改为 20。
4.第 8 行,由于第 7 行已经将变量 a 修改为 20,所以输出 a 与*p 的结果都为 20。
学习指针时,读者必须熟练掌握以下两个运算符。
(1)& :取地址运算符。&a 代表取变量 a 的地址。
(2)* :间接访问运算符。该符号的使用分以下两种情况。
第一种,*与变量类型一起出现时,表示定义指针变量。
例如:
int*p; //定义指针变量 p
第二种,*与指针变量一起出现,表示操作指针变量指向的变量。
例如:
int a=10;
int*p=&a;
*p=20; //将指针变量 p 指向的变量 a 修改为 20
指针和数组
数组本质上是一片连续的内存空间,每个数组元素都对应一块独立的内存空间,它们都有相应的地址。因此,指针变量既然可以指向变量,也就可以指向数组元素。本节将介绍指向数组元素的指针。
数组元素地址
在 C 语言中数组可以看作是相同类型变量的集合。通俗点讲,数组中每个元素类型都是相同的。例如:
char ch[10] //数组 ch 可以看作是由 10 个 char 变量组成
int a[10] //数组 a 可以看作是由 10 个 int 变量组成
float f[10] //数组 f 可以看作是由 10 个 float 变量组成
前面讲过,每字节内存空间都有唯一的内存地址编号。而数组本质上是一片连续的内存空间,数组元素又可以看作是单独的内存空间,如果将数组比作一排房间,数组元素就是单独的一个房间。因此,每个数组元素都有自己的内存空间地址,简称数组元素地址,它和变量地址本质上一致的。
由于数组元素本质上可以当作单独的变量,只不过没有名称,因此,可以使用指针变量来保存数组元素的地址,例如:
int a[5]={1,2,3,4,5}; //定义长度为 5 的 int 数组
int* p_a; //定义指向 int 变量的指针变量 p_a
p_a=&a[0] //把 a 数组第 0 个元素地址赋给指针变量 p_a
p_a 中保存了数组 a 中第 1 个元素 a[0]的地址,可以认为指针变量 p_a 指向数组 a 的第 1 个元素
:&a[0]等价于&(a[0]),由于运算符「[ ]」比取地址符「&」优先级高,因此&(a[0])中的括号可以省略,简写为:&a[0]。
2.定义指向数组元素的指针变量
数组元素本质上可以当作普通变量对待,数组元素类型在定义数组时确定,例如:
char c[10] //10 个数组元素类型都是 char
int i[10]; //10 个数组元素类型都是 int
float f[10]; //10 个数组元素类型都是 float
因此,可以认为数组元素 c[0]是 char 类型,数组元素 i[0]是 int 类型,数组元素 f[0]是 float 类型。分析到这里,读者就会发现数组元素和普通的变量除了名称之外,其他完全相同。所以,可以定义指向同一类型变量的指针变量来指向数组元素。定义与赋值语法如下。
数组元素类型* 变量名 = &数组名[数组下标];
或
数组元素类型* 变量名;
变量名 = &数组名[数组下标];
例如:
char c[10];
char* p_c; //定义 char 指针变量 p_c
p_c=&c[0];
指针变量 p_c 中保存数组元素 c[0]的地址,因此 p_c 指向 c[0]。
3.引用指向数组元素的指针变量
因为数组元素本质上可以看作是单独的变量,所以引用指向数组元素的指针变量与引用指向变量的指针变量方式相同,格式为「*指针变量名」。例如:
int i[5]={1,2,3,4,5};
int*p_i;
p_i=&i[0];
printf("%d\n",*p_i);
上述程序中,将数组元素 i[0]的地址赋值给指针变量 p_i,因此 p_i 等价于&i[0],然后通过*p_i 引用,等价于*&i[0]。前面讲过「*」与「&」同时出现可以相互抵消,所以,*p_i==*&i[0]==i[0]等价于*p_i==i[0]。
在计算机中内存的最小单位是字节,每个字节都对应一个地址。如果一个变量占用多个字节,就会占用多个内存地址。例如:char 类型变量占 1 字节就对应 1 个地址,short 类型变量占 2 字节对应 2 个地址,int 类型变量占 4 字节对应 4 个地址……其他类型依次类推。同理,数组元素类型不同占用的内存地址也不同。
在 C 语言中,数组名与数组首元素地址等价。也就是说,在程序中,输出数组名与输出数组首元素地址是相同的。
指针加减
指针本质上就是内存地址,在 32 位操作系统下,内存地址是 4 字节的整数。既然是整数,就可以进行加、减、乘、除等算术运算。不过需要注意的是,在 C 语言中一般只讨论指针加、减运算,乘、除等其他算术运算都没有意义。
在实际开发中,指针加、减运算多用于数组(或连续内存空间)。当指针变量 p 指向数组元素时,p+1 表示指向下一个数组元素,p-1 表示指向上一个数组元素。
注意:p+1、p-1 并不是数学中的加 1、减 1,而是表示移动一个「单元」,对于 int 类型来讲一个单元占 4 个字节,因此,p 和 p+1 的值相差 4 个字节。同理,p 和 p-1 也是如此,二者相差 4 个字节。
在 C 语言中,数组名等价于数组首元素地址。例如,int a[5],a 与&a[0]完全等价。12.2.4 小节介绍过,数组元素地址加 1 等于下一个数组元素的地址,例如:
&a[0]+1 等价于&a[1]、&a[0]+2 等价于&a[2]。而 a 又与&a[0]等价,因此,就会有这样的转换关系:
a+0 等价于 &a[0],a+0 指向 a[0];
a+1 等价于 &a[1],a+1 指向 a[1];
a+2 等价于 &a[2],a+2 指向 a[2]。
可以认为 a+i 等价于&a[i],a+i 指向 a[i],那么*(a+i)就是 a+i 所指向的数组元素 a[i]。因此,*(a+i)与 a[i]等价。