指针的本质是内存地址。它是一个变量,其存储的值是另一个变量(或数据)在计算机内存中的位置(地址)。
在了解指针之前,首先需要知道变量与内存地址之间的联系。
在C语言中,创建变量实际上都是向内存申请一块空间
比如下面图中的代码就是创建了整型变量a,向内存中申请4个字节(一个整形占用4个字节),用于存放整数10,其中每个字节都有地址,图中4个字节的地址分别是:
1. 0x000000D20775F5E4
2. 0x000000D20775F5E5
3. 0x000000D20775F5E6
4. 0x000000D20775F5E7
那么如果想得到a的地址怎么办呢? 这时就需要一个操作符‘&’,‘&’的作用就是取出4个字节中较小的那一个地址。
进行&a之后,如果将其打印出来,就可以得到 0x000000D20775F5E4.
那如果我们想把&a的结果放到一个变量中,又该怎么办呢?这时候就需要引入一个知识--指针变量
一、指针变量
顾名思义,指针变量就是用于存放 地址 的变量,它的格式为 (类型)*(变量名),
继续沿用前面的例子,假如这个时候我们需要把a的地址存放到一个名为p的变量中,如何进行操作呢?
很简单,创建一个int类型的指针变量来存储a的地址即可,如下图所示
将&a的结果与变量p中的内容输出,可以确定,a的地址成功存入到了变量p中。
让我们来看a和p的具体内容。如图所示,每个方块代表申请的内存空间:变量a存储的数值是10,而变量p保存的是a的地址。既然p专门申请了空间来存储a的地址,那么p本身也必然拥有对应的内存地址。
既然我们现在掌握了变量a的地址,那么能否通过这个地址来访问a呢?答案是肯定的,只需对指针p进行解引用操作即可访问到a。解引用操作符--> ( * )
使用解引用操作符,我们可以通过地址来修改指针所指向变量的值,还是沿用上述例子,如果想要将a的值改为20,只需要 *p =20;便可以间接地修改掉a的值;
在VS2022中,运行 *p = 20;可以发现a的值确实被更改成为了20。
如何理解指针变量呢?
例如 int* p = &a int*代表的是p的类型,它指向&a,也就是指向a的地址。
如何区分int 与 int* ?
很好理解,int就是变量中为int类型,而int*代表变量中存放的是地址,地址指向的内容是int类型
指针变量的大小
指针变量的大小由操作系统决定,与类型无关,在x64(64位操作系统)中占用8个字节,在x86(32位操作系统)中占用4个字节,这里可以通过sizeof来验证:


二、指针运算
1.指针+-整数
指针+-整数本质上就是将指针向前或者向后移动x位。
如图所示,将arr+1后可以看见地址向后移动了4个字节,因为一个int占用4个字节这里如果将int更改为char类型+1,那么地址也将是只会向后移动一个字节。
注:指针的类型决定了指针向前或者向后走一步有多大(距离)。
注:这里是+-的操作是地址,与前面的指针变量所占用空间大小不是一个东西。
2.指针-指针
指针-指针可以得到两个地址之间的元素个数。
比如如下代码:
可以看到,当把arr数组最后一个元素的地址减去首元素的地址,最终的计算结果为10.
需要注意的是:指针-指针仅在同一块内存空间中才有意义,不同空间相减毫无意义。
另外需要知道是,指针+指针是毫无意义的,就如同日期+日期,没有意义。
3.指针的关系运算
指针与指针之间可以进行大小的比较。高地址>低地址 。
仅当两指针 指向同一连续内存块内的元素时(如数组),比较结果才有实际意义。
三、void*指针
void*指针是无类型的指针,其特点是无法修改void*指向的内容,如果想要使用,则需要将void*指针转换成相应类型的指针才可进行操作
void*指针出现的意义:可指向任意类型的内存地址,在动态内存分配中占据重要地位。在函数接受参数时候可以接受不同类型数据的地址。
四、野指针
野指针是指指针指向的地址是不可知的。
野指针成因:
指针未定义
#include <stdio.h> int main() { int* p;//指针未正确初始化 return 0; }
指针越界访问
#include <stdio.h> int main() { int arr[5]; int* p = &arr + 1;//越界访问 return 0; }
指针指向的空间被释放
int main() { int* p = malloc(4 * sizeof(int)); free(p);//free后p为野指针 return 0; }
那么如何避免野指针的出现?
1.正确初始化
2.避免指针越界访问
3.指针指向的空间释放后将指针置空4.在使用指针前检查指针其有效性
5.不要返回临时变量的地址
int main() { int* p = malloc(4 * sizeof(int)); if(p == NULL) { perror("malloc:"); return 1; } free(p); p = NULL; return 0; }
五、const修饰指针
我们知道,变量的值是可以被修改的,如果我们不想让其被更改的话,就可以使用到const来修饰变量,以达到锁定其值的作用。
1.const修饰变量
const修饰的变量表示其数值不可变,因此在这种情况下,可以使用const修饰出生年份变量,确保其数值不能被更改。
const int birth_year = 1990;
这样定义后,birth_year
的值将永远保持为1990,无法被改变。
当然,可以通过其他方式来更改birth_year的值,比如创建指针变量
const int birth_year = 1990;
int* p = &birth_year;
*p = 2025;//可以通过该方法更改,但是非法
通过这种方式绕过birth_year,使⽤birth_yaer的地址修改birth_year就能做到,但是这样做是在打破语法规则。
2.const修饰指针
const修饰指针变量一般分为两种情况 *号在const的左侧还是右侧(如下:)
#include <stdio.h>
int main()
{
int a = 10;
const int* p1 = &a;
int const* p2 = &a;
int* const p3 = &a;
return 0;
}
如果const在*左边,可以看出const修饰的是int类型,也就是指针指向的内容不可被修改
如果const在*右边,可以看出const修饰的是p3,p3是指针,也就是指针的内容无法被修改
但如果想要指针和变量都不被改变该怎么办呢,这时候只需要双重锁定即可,如下:
int a = 10; const int* const p = &a;
六、传值调用与传址调用
传值调用与传址调用的区别在哪里呢?通过下面的例子来了解:
void Swap(int x, int y)
{
int tmp = x;
x = y;
y = x;
}
int main()
{
int x = 10;
int y = 20;
Swap(x, y);
printf("x = %d y = %d",x,y);
}
上述代码中想要交换x与y的值,调用了Swap函数,进行交换x与y的值,那么运行结果如何呢?
下图就是运行结果,可以看见,实际上并没有交换x与y的值,那么是为什么呢?
问题出现在调用Swap函数时候传的是x与y的值,只是把x与y的值拷贝了一份赋给了Swap中的x与y,其在内容空间中存储方式如图中所示。
那么该怎么办呢?通过前面的介绍,我们知道可以通过指针间接修改变量的值,由此我们是否可以通过传x与y的地址过去达到交换x与y的值呢?答案是肯定的。

图中传给函数x与y的地址,就叫做传址调用
而先前的传给的是x与y的值,就叫做传值调用
七、指针与数组
1.数组名
我们知道,访问数组可以通过 数组名[下标] 的方式来访问数组中的元素,那么数组名与其中的关系是怎样的呢?
int main()
{
int arr[10];
printf("arr = %p \n", arr);
printf("&arr[0] = %p \n", &arr[0]);
return 0;
}
运行上述代码后,结果如下,可以看见,数组名与数组首元素的地址一致,由此可以推断出数组名代表的是 首元素的地址。
那么如果我们将数组名+整数结果会是怎样的呢?
从图中可见,将arr+1后,跳过了一个整形的大小,可以得到数组名+整数就是跳过数组内对应整数个元素。
从示意图中可以观察到,当我们&arr
并对其进行 +1
操作时,指针会跳过 整个数组,最终指向数组最后一个元素 之后 的那个内存地址。
而当我们 &arr[0]
并对其进行 +1
操作时,指针仅仅向前移动了一个元素的大小,最终指向数组中的第二个元素的地址。
下面我们通过示意图,具体观察数组上这两种 +1
操作的区别。
通过以上内容,我们可以发现,数组的访问本质上是通过指针算术实现的,那么我们是不是就可以通过指针来访问数组呢?
2.. 使用指针访问数组
通过指针访问数组元素 一般是通过指针指向已有数组的首地址,通过移动指针访问元素,如下图所示
八、一维数组传参
一维数组传参实际上传的是数组首元素的地址,本质上是指针,所以在函数中通过sizeof计算数组个数是不可取的。
计算结果为2,因为在x64环境下指针大小为8,8除以第一个元素所占字节数(4),得到的结果为2。

九、二级指针
二级指针,存放的是指针变量(一级指针)的地址
图解二级指针如下:
二级指针的使用:
需要解引用两次
参考图中例子
第一次解引用 *pp 后得到指向变量p,此时进行修改,修改的是p中的内容
第二次解引用 **pp 后指向变量a,此时进行修改,修改的a中的内容
下列代码运行后可以看到,当两次解引用变量pp之后,成功修改了a的值,说明两次解引用后指针的确指向了变量a
十、字符指针变量
在C语言中,字符指针变量指向一个字符类型的数据。
比如下面的例子
int main()
{
char str = 'w';
char* p = &str;
printf("%c", *p);
return 0;
}
该代码表示创建了一个char类型的名为str的变量,存放的是'w',此时创建一个字符指针变量p来存放str,如果将 其打印出来会看到输出了字符w。
这是字符指针的一种用法。
其实字符指针还能存放字符串,在这里有一个误区,许多初学者会认为str2与str3存放的是abcd与'\0',其实这是错误的,实际上只存放了首元素‘a’的地址
十一、数组指针变量
顾名思义,数组指针变量是存放数组的指针,可以通过指针指向数组
int (*p)[10];
p与*先结合,说明p是一个指针变量,该指针变量指向了一个类型为int,大小为10个整形的数组 。
数组指针变量的初始化:
int arr[10];
int (*p)[10] = &arr;
数组指针变量的使用如下:
十二、函数指针变量
顾名思义就是存放函数的指针变量,可以通过该变量间接调用函数。
函数指针变量的初始化:
返回值(*变量名)(参数1,参数2····) = 函数名;
例如下面的Swap函数:
void Swap(int* x,int* y){
int tmp = *x;
*x = *y;
*y = tmp;
}
int main(){
int x = 10;
int y = 20;
void(*p)(int*, int*) = Swap;
p(&x, &y);
printf("%d %d", x, y);
}
那么为什么可以通过这种方式间接调用函数呢?
我们不妨试试下面的代码:
void Swap(int* x, int* y){
int tmp = *x;
*x = *y;
*y = tmp;
}
int main(){
printf("&Swap = %p\n", &Swap);
printf(" Swap = %p\n", Swap);
return 0;
}
执行结果如下:
可以看到,函数也是有自己对应的地址的,而且函数名就是地址,所以可以通过指针来访问该函数。
十三、函数指针数组
函数指针数组,顾名思义 ,就是存放函数指针的数组,参考上述的函数指针,只需将其转换成数组的形式即可。
整形变量 int a;
整形数组 int a[x];
字符变量 char c;
字符数组 char c[x];
参考上述例子,那么函数指针数组便可以参照着写出来。
函数指针变量 (函数返回值)(*变量名)(函数参数);
函数指针数组 (函数返回值)(*变量名[x])(函数参数);
知道了函数指针数组如何初始化后,那么函数指针如何使用呢?
还是沿用之前的的知识。
整形数组的使用: 数组名[下标];
字符数组的使用: 数组名[下标];
参考上述例子,那么函数指针数组使用方式也一致:
函数指针数组的使用: 数组名[下标];
**需要注意的一点:如果所指向的函数返回值不是void,最好设置一个对应类型变量,用于接收返回的值。
十四、回调函数
C语言中,回调函数(Callback Function)是一种通过函数指针调用的函数。它的核心思想是将一个函数(回调函数)作为参数传递给另一个函数(主调函数),以便在适当的时候被调用执行。。
回调函数可以看做成一个黑箱,用户将一系列数据及操(用户预定义的逻辑)作传入黑箱中,黑箱再给出输出,下面我举一个计算器的例子。
在下面的代码中,void calc(double x, double y,double(*Calculation)(double,double))便是回调函数,用户只需要给出函数名字,以及对应函数所需要的参数内容,然后回调函数便会将该值传入到用户指定的函数中,之后在执行一系列其他操作(这里我只进行了值的输出,再往后可以添加其他内容)。
当然,在这个例子中它的缺点很明显,参数的类型已经锁死。
void calc(double x, double y,double(*Calculation)(double,double))
{
double result = Calculation(x, y);
printf("计算结果为(保留两位小数):%.2lf\n", result);
}
void menu()
{
printf("****************************************\n");
printf("******* 计算器(保留两位小数) *******\n");
printf("******* 1.Add 2.Sub *******\n");
printf("******* 3.Mul 4.Div *******\n");
printf("**************** 0.exit ************\n");
printf("****************************************\n");
}
double Add(double x, double y)
{
return x + y;
}
double Sub(double x, double y)
{
return x - y;
}
double Mul(double x, double y)
{
return x * y;
}
double Div(double x, double y)
{
return x / y;
}
int main()
{
int input = 0;
double x, y;
do
{
menu();
printf("请选择-->");
scanf("%d", &input);
if (input)
{
printf("请输入操作数x y:");
scanf("%5lf %5lf", &x, &y);
}
switch (input)
{
case 1:
calc(x, y, Add);
break;
case 2:
calc(x, y, Sub);
break;
case 3:
calc(x, y, Mul);
break;
case 4:
calc(x, y, Div);
break;
case 0:
printf("Exit\n");
break;
default:
printf("输入错误,重新输入\n");
break;
}
} while (input);
return 0;
}
十五、qsort函数
关于qsort
函数的使用方法已在个人博客中详细介绍,如需了解可参考以下链接:C语言qsort函数使用详解 - 优快云博客
C语言qsort函数使用详解-优快云博客https://blog.youkuaiyun.com/MuL__/article/details/148809704?spm=1001.2014.3001.5501
完.