指针 - pointer
这节课,我们来学习指针。指针是 C 语言中最难以理解的内容,就算是已经学习过 C 语言的同学也会有很多误区,同时,指针也是 C 语言最强大的功能。难理解的原因在于,它和其他数据类型不同,存储的值并不是实际的数据,而是内存地址。强大的原因是指针可以直接对内存数据进行精细化的操作。因此,想绕过内存单独解释指针是不可能的。
在学习指针之前,我们还需要补充一些 内存地址
的知识。
内存地址
我们可以先拿看电影举例子,为什么你可以在电影院准确的找到你的座位呢?因为你的电影票上有你的座位号。由于电影院中的座位很多,因此,需要对所有座位进行编号,这样才能方便观众迅速的找到自己的座位。

在计算机中也是一样的,由于计算机的内存也很大,因此,我们把计算机内存以字节为单位划分为许多独立的单元,并且给每个单元进行编号,这些独立的编号就是 内存地址
,为了方便,我们就把内存地址简称为 地址
。

接下来以变量为例,看看变量的内存地址是如何表示的。
想要在代码中获取某个变量的地址很简单,只需要在变量前加上取地址符 &
即可。想要在 printf()
中打印地址就需要用到指针地址的占位符 %p
,%p
是 C 语言中用于格式化输出指针地址的格式说明符,通常以十六进制形式显示指针变量的内存地址,示例如下:
#include <stdio.h>
int main() {
// 定义 3 个相同类型的变量
int a, b, c;
a = 100;
b = 50000;
printf("变量 a 的地址为:%p\n", &a);
printf("变量 b 的地址为:%p\n", &b);
printf("变量 c 的地址为:%p\n", &c);
return 0;
}
运行结果:
变量 a 的地址为:0000002e3c7ffbbc
变量 b 的地址为:0000002e3c7ffbb8
变量 c 的地址为:0000002e3c7ffbb4
通过运行以上代码,我们会得到 3 个 16 进制数,这些就是变量的 内存地址
。
获取到地址之后,那变量是如何存储的呢?在学习 sizeof 运算符的时候,我们已经简单的了解过 字节 Byte
与 比特 bit
,以及计算机是以二进制的方式存储数据的。
计算机中的 内存
是程序运行时存储数据的物理空间,是由一个个的字节组成的,每个字节可以保存 8bit(8 个 0 或 1)。我们在 C 语言中定义变量,其实就是在将某个地址开始的连续内存分配给变量。
下图展示了上述代码 3 个整型变量在内存中的存储方式,int 类型占 4 个字节,并且我们知道他所占的地址是连续的,就拿变量 a 举例子,从 0000002e3c7ffbbc
到 0000002e3c7ffbbf
都是变量 a 的地址。但是,一个数据不可能拥有四个地址,所以我们就以第一个地址为准,将这个地址称为 首地址
,之后称变量的地址也就是变量的首地址。

上述代码中,我们可以看到变量 a、b、c 的地址是相邻的,但是实际上这些地址是由计算机分配的,我们不能控制,而且在不同的计算机上运行程序或在同一计算机的不同时刻运行程序,变量被分配到的位置也不同。
指针变量
地址本身也要被保存起来,就像把一个房间的门牌号写在纸上,把各章节的页码写在书的目录页中。在程序中地址也需要变量来保存,然而地址不能被保存在普通变量中,示例如下:
C 语言提供了用来保存地址的变量,这种变量称为 指针变量
,也可简称为 指针
。
例如定义一个整型变量 num = 100,并打印其地址,代码如下:
#include <stdio.h>
int main() {
int num = 100;
printf("num 的地址为:%p\n", &num);
return 0;
}
运行结果如下:
num 的地址为:00000083ebdff95c
根据运行结果,我们知道变量 num 的首地址为 00000083ebdff95c
,占用了 00000083ebdff95c ~ 00000083ebdff95f
的 4 个字节。为了将 num 的地址保存起来,需要定义专用于保存地址的 指针变量
。
定义指针变量预定义普通变量类似,仅仅只需要在变量名前加上一个 *
号即可,其结构如下:
数据类型 * 变量名;
示例如下:
// 定义指针变量,以下三种形式均可
int *ptr;
int* ptr;
int * ptr;
星号 *
是一个标志,有了星号 *
才表示所定义的是指针变量,才能保存地址,如果没有星号 *
,则 ptr 与 num 相同,都是 int 型的普通变量,将只能保存普通的整型数据不能保存地址。
注意
变量名是 ptr,不是 *ptr,,星号 *
永远不可能作为变量名的一部分。因为变量名只能由字母、数字、下划线组成,变量名是永远不能含星号 *
的。
ptr 也不是 int 型,num 才是 int 型。ptr 专用于保存地址,ptr 的类型是 int * 型。
可以通过如下语句将 num 的地址保存到 ptr 中:
// 将 num 的地址赋值给指针变量 ptr
ptr = #
把变量 num 的地址赋值给了指针变量 ptr 之后,我们就可以说 指针变量 ptr 指向了变量 num
,也可以说是 ptr 是变量 num 的指针
。
在使用指针时,要避免以下情况的出现:
// 定义一个变量
int num = 1000;
// 定义另一个普通变量保存 num 的地址
int a = #
上述代码中,a 不是指针变量,不能保存地址。反之,如果我们用指针变量保存整数、小数等数据也是错误的,代码如下:
// 定义一个指针变量
int * p;
// 给 p 赋值为 100;
p = 1;
在一条定义语句中可以同时定义多个指针变量,也可以同时定义普通变量与指针变量,例如:
double *a, *b;
short x, *y, z;
上述代码中,我们可以看到指针并不是只有 int * 一种类型。int *
仅表示该指针所指向的数据是 int 型,也就是说指针变量要保存的地址必须是基类型这种类型数据的地址,指针只能指向同基基本数据类型的数据。同理 double *
指向的是 double 类型变量,short *
指向的是 short 类型变量,代码如下:
int a = 1, b = 2;
double c = 3, d = 4;
int *p;
double *q;
p = &a;
p = &b;
q = &c;
q = &d
以上这些都是正确的,那么什么是错误呢我们写来举几个反例:
p = &c; // 错误: p 不能保存 c 的地址,因 c 是 double 型不是 int 型
q = &a; // 错误: q 不能保存 a 的地址,因 a 是 int 型不是 double 型
我们已经定义好了指针,那该怎么去使用指针呢?那首先最基本的我们得会使用指针查询和修改变量的值,其格式如下:
* 指针名;
例如:
// 定义普通变量并赋值
int a = 1;
// 定义指针变量
int *p;
// 将 a 的内存地址存入指针变量 p 中,p 指向变量 a
p = &a;
// 查询数据
printf("a 的原始数据:%d\n", *p); // 原始值为 1
printf("a 的原始数据:%d\n", a); // 原始值为 1
// 存储或修改数据
*p = 2;
// 查询修改后的数据
printf("修改后 a 的数据:%d\n", *p); // 修改后的值为 2
printf("修改后 a 的数据:%d\n", a); // 修改后的值为 2
注意
定义指针时的 int *ptr
中的星号 *
与打印时 printf("%d\n", *ptr)
中的星号 *
是两个不同的概念
前者是定义指针时的标识符,表示当前定义的变量是指针,而后者为解引用运算符,通过后面的内存地址获取对应的数据。
数组与指针
指针可以保存一个变量的地址,当然也可以保存一个数组的地址,像这样指向数组的指针,被称为 数组指针
。通过 数组指针
我们可以方便地操作数组中的各种数据。
数组指针的定义方式与指针变量的定义方式是一致的,区别在于如何获取数组的首地址,获取数组首地址的方法有以下两种:
#include <stdio.h>
int main() {
// 定义一个数组
int array[5] = {11, 22, 33, 44, 55};
// 将指针变量赋值为数组名
int *ptr_1 = array;
// 将数组的第一个元素的地址传递给指针
int *ptr_2 = &array[0];
// 打印指针指向的地址
printf("array[0] 的地址为: %p\n", &array[0]);
printf("指针 ptr_1 存储的地址为:%p\n", ptr_1);
printf("指针 ptr_2 存储的地址为:%p\n", ptr_2);
// 打印指针的值
printf("指针 ptr_1 对应的值为:%d\n", *ptr_1);
printf("指针 ptr_2 对应的值为:%d\n", *ptr_2);
return 0;
}
运行上述代码,我们可以得到 *ptr_1 与 *ptr_2 都是 11,也就是数组 array 的第 0 个元素的值。现在,我们已经获取到了数组及数组中第一个元素的地址了,那么,我们该如何获取其他元素的地址呢?
假设数组 array 的首地址为 0x1000,则数组元素 array[0] 的地址也为 0x1000。int 数据类型占 4 个字节的大小,array[0] 应占据 0x1000 ~ 0x1003 的 4 个字节。由于数组的每个元素都可被当做普通变量,并且数组元素在计算机内存中占据相邻的地址空间
,因此 array[1] 应占据 0x1004 ~ 0x1007 的 4 个字节,地址为 0x1004;同理,array[2] 应占据 0x1008~0x100B,地址为 0x1008,如下图:

同理,下图展示了计算机内存是如何存储 short 型与 char 类型数组的:

需要注意的是,在存储 char 与 short 类型数组的地址时,我们要使用与之对应的指针类型 - char * 与 short *。
那我们该如何通过指针获取数组的其他元素呢?这个问题就涉及到了指针的运算。
指针加减整数并不是简单的地址加减。在计算机内存中,每个变量都有一个唯一的存储位置,这个位置由其地址表示。当你对指针执行加法或减法操作,实际上是改变了指针指向的位置,使其指向新的地址。这个新地址通常对应于原来地址基础上增加或减少指定的字节数,这取决于所使用的数据类型(比如 int、char 等)的大小。
指针加减 1 表示 指针所指向的地址 +(-) 指针所指向的数据类型的字节数
,其中指针移动 1 次,地址改变的字节数被称为 步长
。比如,int * 指针加 1,因为 int 占 4 个字节,所以步长为 4,地址会移动 4 个字节的长度;short * 指针加减 1,因为 short 占 2 个字节,所以步长为 2,地址会移动 2 个字节的长度;char * 指针加减 1,因为 char 占 1 个字节,所以步长为 1,地址会移动 1 个字节的长度,代码如下:
#include <stdio.h>
int main() {
// int 类型
int array_1[5] = {11, 22, 33, 44, 55};
int* ptr_1 = array_1;
printf("ptr_1 指向的地址为:%p\n", ptr_1);
printf("ptr_1+1 指向的地址为:%p\n", ptr_1+1);
printf("ptr_1-1 指向的地址为:%p\n", ptr_1-1);
// short 类型
short array_2[5] = {11, 22, 33, 44, 55};
short * ptr_2 = array_2;
printf("ptr_2 指向的地址为:%p\n", ptr_2);
printf("ptr_2+1 指向的地址为:%p\n", ptr_2+1);
printf("ptr_2-1 指向的地址为:%p\n", ptr_2-1);
// char 类型
char array_3[5] = {11, 22, 33, 44, 55};
char * ptr_3 = array_3;
printf("ptr_3 指向的地址为:%p\n", ptr_3);
printf("ptr_3+1 指向的地址为:%p\n", ptr_3+1);
printf("ptr_3-1 指向的地址为:%p\n", ptr_3-1);
return 0;
}
也就是说无论是何种类型的数组,以下等式均成立
ptr_1 + 1 = array[1] 的地址
ptr_1 + 2 = array[2] 的地址
因此,如果将一个数组 array 的首地址赋值给一个指针 ptr_1,*(ptr_1+n) 和 array[n] 是等价的。代码如下:
#include <stdio.h>
int main() {
// 声明 int 数组与指针
int array_1[5] = {10, 20, 30, 40, 50};
int *ptr_1 = array_1;
printf("ptr_1 对应的值为:%d\n", *ptr_1);
printf("ptr_1 + 1 对应的值为:%d\n", *(ptr_1+1));
// 声明 short 数组与指针
short array_2[5] = {10, 20, 30, 40, 50};
short *ptr_2 = array_2;
printf("ptr_2 对应的值为:%d\n", *ptr_2);
printf("ptr_2 + 2 对应的值为:%d\n", *(ptr_2+2));
// 声明 char 数组与指针
char array_3[5] = {10, 20, 30, 40, 50};
char *ptr_3 = array_3;
printf("ptr_3 对应的值为:%d\n", *ptr_3);
printf("ptr_3 + 3 对应的值为:%d\n", *(ptr_3+3));
return 0;
}
需要注意的是,在解引用时需要加括号 *(ptr_1 + 1),这样,我们才能执行先相加再取值的操作,
指针除了加减运算外,还可以使用自增自减运算符 ++、--、+=、-=。
由此,我们可以通过指针对数组进行遍历,代码如下:
#include <stdio.h>
int main() {
// 声明 int 数组与指针
int array_1[5] = {10, 20, 30, 40, 50};
int *ptr_1 = array_1;
int length = sizeof(array_1) / sizeof(int);
for (int i = 0; i < length; ++i) {
printf("数组的第 %d 个元素是:%d\n", i, *ptr_1++);
}
return 0;
}
由于使用了自增运算符,在进行完以上运算之后,ptr_1 最终指向了该数组的末尾,因此,再次调用时结果会出现差异。
函数与指针
函数有 4 个部分组成:
返回值类型
函数名
参数
函数体
指针本质上依然是变量,因此,指针在函数中应用分为以下 3 种:
- 指针作为函数参数
- 返回值类型为指针 - 指针函数
- 函数名为指针 - 函数指针
指针作为函数参数
目前我们对于指针的学习还停留在通过指针访问和修改数据,但是类似这样的操作,我们使用变量或数组也是完全可以处理的。接下来,我们来学习一些变量没有办法处理,但是通过指针可以轻松实现的操作。
指针变量也可以做函数的参数,向函数传递地址。这里我们来看一道练习题:
定义一个函数 swap(),要求交换两个变量中的值。
如果不使用函数,可以通过定义第三个变量的方式轻松实现交换值的功能,代码如下:
#include <stdio.h>
int main() {
int a = 1, b = 2; // 定义两个需要交换值的变量
int c = a; // 定义第三个变量作为中转,并将其中一个变量的值赋给 c
printf("交换前 a 的值为:%d, b 的值为 %d\n", a, b);
a = b;
b = c; // 完成变量值的交换
printf("交换后 a 的值为:%d, b 的值为 %d\n", a, b);
return 0;
}
但是,如果把上述代码封装成一个函数,代码如下:
#include <stdio.h>
void swap(int num_1, int num_2) {
int c = num_1;
num_1 = num_2;
num_2 = c;
}
int main() {
int a = 1, b = 2; // 定义两个需要交换值的变量
printf("交换前 a 的值为:%d, b 的值为 %d\n", a, b);
swap(a, b); // 调用交换值函数
printf("交换后 a 的值为:%d, b 的值为 %d\n", a, b);
return 0;
}
运行以上代码,可以发现 a、b 的值并没有发生变化。这是因为,我们在调用 swap 函数时,仅把 a、b 的值传递给了该函数中的两个参数 num_1、num_2。swap 函数体中执行的是操作是将 num_1 与 num_2 的值进行了交换操作,而函数的参数是局部变量,在执行完函数后销毁。
这个时候,我们就需要使用指针来操作了,代码如下:
#include <stdio.h>
void swap(int* ptr_1, int* ptr_2) {
int c = *ptr_1;
*ptr_1 = *ptr_2;
*ptr_2 = c;
}
int main() {
int a = 1, b = 2; // 定义两个需要交换值的变量
printf("交换前 a 的值为:%d, b 的值为 %d\n", a, b);
swap(&a, &b); // 调用交换值函数
printf("交换后 a 的值为:%d, b 的值为 %d\n", a, b);
return 0;
}
以上两端代码唯一的区别就是将自定义函数 swap 的参数从普通变量改成了指针变量,这样 ptr_1 与 ptr_2 只能保存地址,不能保存数据。swap 函数可以通过参数 ptr_1、ptr_2 来获取 a、b 的地址,在 swap 函数中通过这两个地址就可以任意存取 a、b 这两个变量了,而不论这两个变量位于哪个函数。
接着,我们再来看第 2 个练习:
定义一个函数 calc_rect,根据矩形的长和宽计算面积与周长。
通过 calc_rect 函数既要计算矩形的面积,又要计算矩形的周长。然而,一个函数只能由一个返回值,函数计算完成后,如何能同时返回面积与周长两个结果呢?
我们可以把这两个结果通过指针的方式传回主函数,那么 calc_rect 函数的参数除了长(length)和宽(width)两个普通变量外,还需要两个指针类型的参数分别来接受面积与周长的地址,代码如下:
#include <stdio.h>
//定义一个函数 calc_rect(),根据矩形的长和宽计算面积与周长。
void calc_rect(int length, int width, int* area, int* peri) {
*area = length * width;
*peri = 2*(length + width);
}
int main() {
int length = 50, width = 30;
int area, peri;
calc_rect(length, width, &area, &peri);
printf("矩形的面积为:%d, 周长为:%d", area, peri);
return 0;
}
返回值类型为指针 - 指针函数
函数的返回值也可以是一个指针类型(地址)的结果,我们将这样的函数称为 指针函数
。如果函数的返回值是地址,在定义函数时,需要在函数名前加 * 号标志。函数调用方式和执行过程与返回普通数据的情况均一致,语法如下:
返回值类型 * 函数名(形参列表)
首先它是一个函数,只不过这个函数的返回值是一个地址值,函数返回值必须用同类型的指针变量来接受。也就是说,指针函数一定有函数返回值。而且,在主函数中,函数返回值必须赋给同类型的指针变量。
下面是一道练习题:
通过返回地址值的函数 find_max,求两个整数的较大值。
#include <stdio.h>
int* find_max(int* num_1, int* num_2) {
if (*num_2 > *num_1) {
return num_2;
}
return num_1;
}
int main(){
int a = 1, b = 2;
int* max_ptr = find_max(&a, &b);
printf("a 的地址为:%p\nb 的地址为:%p\n", &a, &b);
printf("较大值为:%d, 地址为:%p", *max_ptr, max_ptr);
return 0;
}
用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C 语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。请看下面的例子:
#include <stdio.h>
int *func(){
int n = 100;
return &n;
}
int main(){
int *ptr = func();
int num = *ptr;
printf("num: %d\n", num);
return 0;
}
野指针与悬空指针
野指针:指针指向的空间未分配。
悬空指针:指针指向的空间已经分配,但是被释放了。
实际上这两种指针,我们在前面两节课中都已经见过了。
我们先来看一个最简单的野指针的案例:
#include <stdio.h>
int main() {
int a = 10;
int* ptr_1 = &a;
printf("ptr_1 对应的值为:%d\n", *ptr_1);
// ptr_2 的值为 ptr_1 + 5 个步长
int* ptr_2 = ptr_1 + 5;
printf("ptr_2 对应的值为:%d\n", *ptr_2);
return 0;
}
我们在使用指针的自增自减运算对数组进行遍历,由于指针 ptr_1 的值一直在增加,当循环结束时,再打印一次 ptr_1 对应的值就会出现意义不明的数字这是因为 ptr_1 指向的地址已经超出了数组所占用的字节,来到了未被分配的空间,这时候,ptr_1 就成了野指针,代码如下:
#include <stdio.h>
int main() {
// 声明 int 数组与指针
int array_1[5] = {10, 20, 30, 40, 50};
int *ptr_1 = array_1;
int length = sizeof(array_1) / sizeof(int);
for (int i = 0; i < length; ++i) {
printf("数组的第 %d 个元素是:%d\n", i, *ptr_1++);
}
return 0;
}
接着是悬空指针的案例:
#include <stdio.h>
int* func() {
int n = 10;
return &n;
}
int main(){
int* ptr = func();
printf("%p %d", ptr, *ptr);
return 0;
}
这个程序存在一个关键问题:返回局部变量的地址
。func()
中的 n 是局部变量,当 func()
执行结束时,其栈帧被销毁,n 的内存空间被释放(不再有效)。return &n
返回了一个已被释放的内存地址。在 main 函数通过 *ptr 访问这个地址属于非法操作,也就成了悬空指针。