收集飞花令碎片——C语言指针
暑假已尽,小编也开始进入肝文模式了
从今天起,我们开始进入C语言最难的环节————指针
- 作为每位码农学习
C语言
时候的“拦路虎”,C语言
指针的复杂性、多元化、思维深度大,让99%的代码萌新都顺利入坑
今天小编就用一片博文,带大家从基础、进阶到技巧、运用,层层进化,带大家打通C语言指针的任督二脉
C语言指针
- 内存的基本概念
- 地址的基本概念
- 指针的基本概念
- 指针:(一)指针的定义与声明
- 指针:(二)关键运算符
- (1)取地址运算符` & `
- (2)解引用运算符` * `
- 指针:(三)指针初始化
- 指针:(四)指针大小
- 指针:(五)指针变量
- 指针:(六)指针运算
- 指针+-整数
- 指针 - 指针
- 指针:(七)void* 指针
- void指针的用处
- 指针:(八)指针与const
- (1)指向常量的指针 (const int *ptr)
- (2)常量指针(int *const ptr)
- (3)指向常量的常量指针(const int *const ptr)
- 指针:(九)野指针
- 野指针成因(1):指针未初始化
- 野指针成因(2):指针越界访问
- 野指针成因(3):指针指向的空间释放
- 规避野指针(1):指针初始化
- 规避野指针(2):注意越界访问
- 规避野指针(3):及时将闲置指针设置成NULL
- 规避野指针(4):避免返回局部变量的地址
- 指针:(十)assert断言
- 定义assert的规则
- assert的工作原理
- assert的缺点
- 指针:(十一)指针的使用和传值调用
- (1)strlen的模拟实现
- (2)传值调用和传址调用
- 函数调用方法(传值调用)
- 函数调用方法(传址调用)
- 指针:(十二)数组与指针
- 数组名与数组首地址
- 数组地址的特例
- 使用数组传递指针
- 一维数组传参的本质
- 指针数组
- 指针数组模拟二维数组
- 数组指针变量
- 数组指针变量的定义方式
- 初始化数组指针
- 数组指针与指针数组的区别
- 二维数组传参本质
- 指针:(十三)二级指针
- 二级指针与一级指针的区别
- 二级指针的运算
- 指针:(十四)字符指针变量
- 字符指针的三种使用方式
- 字符指针注意事项
- 指针:(十五)函数指针变量
- 函数指针的声明与定义方式
- 函数指针类型解析
- 函数指针使用示例
- 函数指针的两段有趣的代码
- typedef关键字
- 指针:(十六)函数指针数组
- 基本语法和声明
- 函数指针数组的用途:转移表
- 指针:(十七)回调函数
- qsort函数
- 语法格式
- qsort函数模拟实现
- 利用冒泡排序实现qsort函数
- 文章总结
内存的基本概念
我们知道计算机CPU(中心处理器)在处理数据,需要的数据是在内存中读取的,处理后的数据也会放回内存中
我们把内存划分为一个个内存单元,每个内存单元存储一个字节
每个内存单元一个字节空间里面能放8个比特位
1Byte = 8bit
1KB = 1024Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
地址的基本概念
我们观察下面这一组代码
#include <stdio.h> int main(){int a = 10;return 0; }
我们打开监视
我们再打开内存窗口
指针的基本概念
每个内存单元也都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。
在计算机中,我们把给内存空间起的编号称为地址
内存空间的编号=地址=指针
简单来讲,指针是一种变量,但它存储的不是普通的数据值,而是内存地址。通过指针,可以直接访问或修改该地址上存储的数据。
指针:(一)指针的定义与声明
数据类型 *指针变量名;
int *p; // p 是一个指向 int 类型的指针
char *c; // c 是一个指向 char 类型的指针
float *f; // f 是一个指向 float 类型的指针
指针:(二)关键运算符
(1)取地址运算符&
- 用于获取变量的内存地址
int num = 10;
int *p = # // p 存储了 num 的地址
(2)解引用运算符*
- 用于访问指针指向的内存地址中的值
int value = *p; // value = 10(获取 p 指向地址的值)
*p = 20; // 修改 p 指向地址的值,num 现在等于 20
我们观察下面的两段代码
#include <stdio.h>//这段代码将n的四个字节全部改为0
int main() {int n = 0x11223344;int* p = &n;*p = 0;printf("%d\n", n);return 0;
}
#include <stdio.h>
int main() {int n = 0x11223344;printf("n的值为:%x\n");char *p = &n; //&n类型指向int指针//p的类型是char *(指向char类型的指针)*p = 0;printf("n的值为:%x\n");return 0;}
调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0
结论 :指针的类型决定了,对指针解引用的时候有多⼤的权限(一次能操作几个字节)。
比如: char* 的指针解引⽤就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。
指针:(三)指针初始化
-
指针在使用前必须初始化,否则可能成为野指针(指向未知内存)。
-
可以初始化为 NULL(空指针),表示不指向任何有效地址。
指针:(四)指针大小
- 指针的大小取决于系统架构:
-
- 32位系统:指针占 4字节
-
- 64位系统:指针占 8字节
printf("指针的大小:%zu字节\n", sizeof(int*));
指针:(五)指针变量
我们通过取地址符
&
,可以获得一个地址的值(如:0x006FFD70)
这个数值也是需要存储起来的,方便后期能使用
那么这个数值是存储在哪里呢?
这边我们就要引入一个新概念:指针类型
我们要如何理解指针类型呢?
我们先观察下面的代码
int a = 10; int * pa = &a;
pa
左边的int *
中,*
是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int)类型的对象。
在32位平台下,指针变量大小是4个字节
在64位平台下,指针变量大小是8个字节
#include <stdio.h>
int main()
{printf("%zd\n", sizeof(char *));printf("%zd\n", sizeof(short *));printf("%zd\n", sizeof(int *));printf("%zd\n", sizeof(double *));
return 0;
}
指针:(六)指针运算
指针运算有三种形式:
- 指针±整数
- 指针-指针
- 指针的关系运算
指针±整数
因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素。
#include <stdio.h>int main() {int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int sz = sizeof(arr) / sizeof(arr[0]);for (int i = 0; i < sz; i++) {printf("%d ", *(p + i));}return 0;
}
p+i 就是数组中下标为
i
元素的地址
*(p+i)就是下标为i
的这个元素
#include <stdio.h>int main() {char arr[] = "Hello World"; //这个数组真实的样貌是:"Hello World\0"//printf("%s \n", arr);char* p = &arr[0];while(*p != '\0'){ printf("%c", *p);p++;}return 0;
}
arr[]数组的真实样貌是
Hello World\0
指针 - 指针
指针-指针:得到的是两个指针之间的元素个数
前提: 两个指针指向了同一块空间,否则不能相减
#include <stdio.h>int main(){int arr[10] = { 0 };printf("%lld\n", &arr[ 9 ] - &arr[ 0 ]);printf("%lld\n", &arr[ 0 ] - &arr[ 9 ]);//数组随着下标的增长,地址由低到高变化的return 0;}
数组随着下标的增长,地址由低到高变化的
所以&arr[ 9 ] - &arr[ 0 ] = 9
指针:(七)void* 指针
void*
可以理解为无具体类型指针(或者称为泛型指针),这种类型的指针可以用来接受任意类型地址。
void*指针不能进行指针±操作和解引用操作
观察下面的代码
将一个int类型的变量赋值给char类型的指针变量
编译器会因为类型不兼容而给出一个警告
而void指针就不会有这样的问题
利用void指针接受地址
#include <stdio.h>int main(){int num = 0;void* p = #void* pc = #*p = 10;*pc = 10;return 0;
}
这里我们可以看到,void类型指针可以接收不同类型的地址,但是无法进行直接的计算
void指针的用处
void一般是使用在函数的参数部分,用来实现接收不同数据类型的地址,用来实现泛型编程的效果
指针:(八)指针与const
const int *ptr1; // 指向常量的指针,指针可变,值不可变
int *const ptr2; // 常量指针,指针不可变,值可变
const int *const ptr3;// 指向常量的常量指针,都不可变
(1)指向常量的指针 (const int *ptr)
特点: 指针可以指向别的变量,但不能通过指针修改变量值
int a = 10;
const int *ptr = &a; // ptr指向a,但不能通过ptr修改a的值// *ptr = 20; // 错误!不能通过ptr修改a
a = 20; // 正确,可以直接修改aint b = 30;
ptr = &b; // 正确,可以改变指针指向
(2)常量指针(int *const ptr)
特点: 指针永远指向同一个变量,但可以通过指针修改变量值
int x = 10;
int *const ptr = &x; // ptr将永远指向x*ptr = 20; // 正确,可以修改x的值int y = 30;
// ptr = &y; // 错误!ptr不能指向别的变量
(3)指向常量的常量指针(const int *const ptr)
特点: 指针不能改变指向,也不能通过指针修改变量值
int m = 10;
const int *const ptr = &m; // ptr永远指向m,且不能通过ptr修改m// *ptr = 20; // 错误!不能通过ptr修改m
// ptr = &n; // 错误!不能改变指针指向
简单记忆法
看const和*的位置关系:
- const在*左边:值不能改
- const在*右边:指针不能改
- 两边都有const:都不能改
指针:(九)野指针
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针成因(1):指针未初始化
#include <stdio.h>int main() {int *p; //局部变量指针未初始化,默认值为随机值*p = 20;return 0;
}
野指针成因(2):指针越界访问
#include <stdio.h>int main() {int arr[10] = { 0 };int* p = arr;for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {printf("%d\n", *p);*p = i;p++;}return 0;
}
野指针成因(3):指针指向的空间释放
#include <stdio.h>
int* test()
{int n = 100; //n是局部变量,函数结束则生命周期结束return &n;
}
int main()
{int* p = test(); //这时p指向的就是一个野指针printf、printf("%d\n", *p);return 0;
}
这时候我们可以使用静态变量static
#include <stdio.h>
int* test()
{static int n = 100; // 静态变量,生命周期贯穿整个程序return &n;
}
int main()
{int* p = test();printf("%d\n", *p);return 0;
}
规避野指针(1):指针初始化
如果明确指针指向哪里就直接赋值指针
如果不知道就直接给指针赋值NULL
(空指针)
NULL
指针是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
规避野指针(2):注意越界访问
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
规避野指针(3):及时将闲置指针设置成NULL
#include <stdio.h>int main() {int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = arr;int i = 0;for (i = 0; i < 5; i++) {*p = 5; //将指针p指向的当前地址的值改为5p++;}p = NULL;//现在又想用pp = arr;if (p == NULL) {printf("p是空指针\n");}return 0;
}
规避野指针(4):避免返回局部变量的地址
int* test()
{int n = 100; return &n;
}
指针:(十)assert断言
assert
头文件定义了宏:assert
用来确保程序在运行时,符合指定条件;如果不符合条件,则终止运行
定义assert的规则
assert(条件表达式);
assert的工作原理
#include <assert.h>
void assert(int expression);
- 如果expression的值为真(非0),assert什么都不做
- 如果expression的值为假(0),assert会
– 输出错误信息(包含文件名、行号、失败的表达式)
– 调用abort()函数终止程序
– 如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流stderr
中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
如果已经确认程序没有问题,不需要再做断言,就在
#include <assert.h>
语句的前⾯,定义一个宏NDEBUG
#define NDEBUG #include <stdio.h>
assert的缺点
assert在调用的时候会引入额外的检查,增加程序运行的时间
一般我们可以在
Debug
中使用,在Release
版本中选择禁⽤assert
就行,在 VS 这样的集成开发环境中,在Release
版本中,直接就是优化掉。这样在debug
版本写有利于程序员排查问题,在Release
版本不影响用户使用程序的效率。
指针:(十一)指针的使用和传值调用
(1)strlen的模拟实现
库函数strlen的原型是求字符串的长度
统计的字符串\0
之前的个数
strlen函数的使用
#include <stdio.h>int main() {char arr[] = "abcdefg";size_t len = strlen(arr);printf("%zu\n", len);return 0;
}
我们知道strlen只需要将字符串的起始地址传递给strlen就行
那我们能不能自己把strlen函数自己编写出来?
#include <stdio.h>
#include <assert.h> // 需要包含assert.h头文件// 计算字符串长度
// 参数:str - 指向以null结尾的字符串的指针
// 返回值:字符串的长度(不包括结尾的null字符)
// 使用size_t(无符号整型)作为返回类型是最合适的,因为长度不可能是负数
size_t my_strlen(const char* str) {size_t count = 0; // 计数器,用于统计字符数量// 使用断言确保传入的指针不为NULL,避免对空指针进行解引用assert(str != NULL);// 遍历字符串,直到遇到字符串结束符'\0'while (*str != '\0') {count++; // 计数器加1str++; // 指针移动到下一个字符}return count; // 返回字符串长度
}int main() {char str[] = "abcdefg"; // 定义一个测试字符串size_t len = my_strlen(str); // 调用自定义的字符串长度函数printf("%zu\n", len); // 使用%zu格式说明符打印size_t类型的值return 0;
}
(2)传值调用和传址调用
代码中什么问题是非指针解决不可的呢?
例如: 写一个函数,交换两个整型变量
在指针之前我们可能会写下面的代码
#include <stdio.h>
void Swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}
int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap1(a, b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}
当我们运行代码,结果如下:
我们发现a、b并没有发生交换,为什么?
下面讲解一下传值调用
就能明白
函数调用方法(传值调用)
通过调试我们发现:a和b的值并没有发生交换
不知道VS的调试技巧可以看这里
- 我们在main函数里面创建的两个变量:a和b,并给a与b分配了两个地址
- 在调用Swap函数时,将a与b作为实参传递过去。在Swap函数内部创建两个形参变量x和y负责接收a和b
- 但是,x、y、a、b的地址不一样,x和y确实接收了。但是x和y相当于是两个独立的空间,改变的也只是x和y的值,a和b自然就不会受影响了
- 当Swap函数调用结束后,回到main函数,a和b没办法发生交换
结论:实参传递给形参的时候,形参会单独创建一份单独空间来接收实参,对形参的修改不影响实参
函数调用方法(传址调用)
传值调用只是传递两个实参变量给Swap函数,但是函数交换的空间对应着形参的地址,与实参的地址不同
下面我们提供另外一种函数调用的方法:传址调用
观察下面的代码
#include <stdio.h>// 交换两个整数的函数
// 参数:pa - 指向第一个整数的指针,pb - 指向第二个整数的指针
void Swap1(int* pa, int* pb)
{int tmp = 0; // 定义临时变量用于交换tmp = *pa; // 将pa指向的值赋给临时变量*pa = *pb; // 将pb指向的值赋给pa指向的变量*pb = tmp; // 将临时变量的值(原pa的值)赋给pb指向的变量
}int main()
{int a = 0; // 定义整型变量a并初始化为0int b = 0; // 定义整型变量b并初始化为0// 从标准输入读取两个整数,分别存入a和bscanf_s("%d %d", &a, &b);// 打印交换前的a和b的值printf("交换前:a=%d b=%d\n", a, b);// 调用交换函数,传入a和b的地址Swap1(&a, &b);// 打印交换后的a和b的值printf("交换后:a=%d b=%d\n", a, b);return 0; // 程序正常结束
}
我们发现Swap顺利完成了任务
传址调用,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量
- 所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。
- 如果函数内部要修改主调函数中的变量的值,就需要传址调用。
指针:(十二)数组与指针
数组名与数组首地址
数组名的地址 ==
数组首元素的地址
#include <stdio.h>int main() {int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("&arr[0]=%p", &arr[0]);printf("arr = %p", arr);return 0;
}
数组地址的特例
- sizeof(数组名):代表的是整个数组的大小,代表的是整个数组的大小,单位是字节
- &数组名:代表的是整个数组的地址
整个数组大小和数组首地址大小还是有区别的
#include <stdio.h>
int main()
{// 声明并初始化一个包含10个整数的数组int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };// 打印第一个元素的地址// &arr[0] 获取数组第一个元素的地址printf("&arr[0] = %p\n", &arr[0]);// 打印第一个元素地址加1后的地址// 由于是int指针,+1会移动sizeof(int)个字节(通常是4字节)printf("&arr[0]+1 = %p\n", &arr[0]+1);// 打印数组名(数组名在大多数情况下会退化为指向第一个元素的指针)printf("arr = %p\n", arr);// 打印数组名加1后的地址(同样移动sizeof(int)个字节)printf("arr+1 = %p\n", arr+1);// 打印整个数组的地址(虽然值相同,但类型不同)// &arr 的类型是 int(*)[10](指向10个整数数组的指针)printf("&arr = %p\n", &arr);// 打印整个数组地址加1后的地址// 这里会移动整个数组的大小(10 * sizeof(int) = 40字节)printf("&arr+1 = %p\n", &arr+1);return 0;
}
&arr[0]
和arr
指向的是数组的首元素地址,+1移动4字节- &arr和sizeof(arr)是指向整个数组的大小,+1移动40个字节
使用数组传递指针
#include <stdio.h>int main()
{int arr[10] = { 0 }; // 声明并初始化一个包含10个整数的数组,所有元素初始化为0// 计算数组长度int sz = sizeof(arr) / sizeof(arr[0]);// 输入部分printf("请输入 %d 个整数:\n", sz); // 添加提示信息,提高用户体验int* p = arr; // 定义指针p指向数组首地址for (int i = 0; i < sz; i++) // 将i的声明移到循环内部,限制作用域{printf("请输入第 %d 个数:", i + 1); // 添加序号提示scanf_s("%d", p + i); // 使用指针算术访问数组元素// 等价写法:// scanf_s("%d", &arr[i]); // 使用数组下标// scanf_s("%d", arr + i); // 使用数组名指针算术}// 输出部分printf("\n您输入的数组是:\n");for (int i = 0; i < sz; i++){printf("%d ", p[i]); // 使用指针下标表示法输出// 等价写法:// printf("%d ", *(p + i)); // 使用指针解引用// printf("%d ", arr[i]); // 使用数组下标}printf("\n"); // 换行使输出更美观return 0;
}
我们可以使用arr[i]访问数组元素,也可以使用p[i]访问数组元素
arr[i]等价于*(arr+i)
p[i]等价于*(p+i)
一维数组传参的本质
首先先从一个问题引入:能不能将一个数组传递给函数,在这个函数内部算出数组的个数呢?
#include <stdio.h>void test(int arr[]) {int len2 = sizeof(arr) / sizeof(arr[0]);printf("len2 = %d\n", len2);}int main() {int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int len1 = sizeof(arr) / sizeof(arr[0]);printf("len1 = %d\n", len1);test(arr);return 0;
}
发现函数内部并没有正确得出数组元素个数
关键点:
- 数组作为函数参数时会退化为指针
- sizeof在编译时确定大小,无法在运行时获取通过指针传递的数组长度
在函数内部我们写sizeof(arr)实际上计算的是数组地址的大小,而不是数组的大小
总结:
- 一维数组传参 形参可以是指针形式也可以是数组形式
指针数组
我们类比一下
整型数组是存放整型变量的数组
字符数组是存饭字符变量的数组
所以指针数组便是存放指针数据的数组
指针数组的每一个元素都是地址,并指向一块区域
指针数组模拟二维数组
#include <stdio.h>int main() {// 完整初始化所有数组元素int arr1[3] = { 1, 2, 3 };int arr2[4] = { 11, 22, 33, 44 }; // 添加第4个元素int arr3[5] = { 111, 222, 333, 444, 555 }; // 添加第4、5个元素int* arr[3] = { arr1, arr2, arr3 };// 定义每个子数组的实际长度int lengths[3] = {sizeof(arr1) / sizeof(arr1[0]), // 3sizeof(arr2) / sizeof(arr2[0]), // 4 sizeof(arr3) / sizeof(arr3[0]) // 5};printf("指针数组模拟不规则二维数组:\n\n");for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {printf("第%d行 (长度=%d):\n", i+1, lengths[i]);for (int j = 0; j < lengths[i]; j++) {printf(" arr[%d][%d] = %d\n", i, j, arr[i][j]);}printf("\n");}return 0;
}
arr[i]是访问arr数组的元素并指向整型一维数组
arr[][]就是访问整型一维数组中的元素
数组指针变量
指针数组是数组
那数组指针变量便是指针变量
数组指针变量存放的是数组的地址,能够指向数组的指针变量
数组指针变量的定义方式
// 指向整型数组的指针
int (*ptr)[10]; // ptr是指向包含10个整数的数组的指针
解释:
ptr
先和*
结合,说明p
是⼀个指针变量,然后指针指向的是⼀个大小为10个整型的数组。所以ptr
是一个指针,指向一个数组,叫数组指针。
// 指向字符数组的指针
char (*cptr)[20]; // cptr是指向包含20个字符的数组的指针
初始化数组指针
#include <stdio.h>int main() {int arr[5] = {1, 2, 3, 4, 5}; // 定义一个包含5个整数的数组// 定义并初始化数组指针// int (*ptr)[5] 表示ptr是一个指针,指向包含5个整数的数组// &arr 获取的是整个数组的地址,而不是第一个元素的地址int (*ptr)[5] = &arr; // 注意:取整个数组的地址// 打印数组的首地址(整个数组的地址)printf("数组地址: %p\n", &arr);// 打印指针变量ptr存储的地址值(应该与&arr相同)printf("指针值: %p\n", ptr);// 解引用ptr得到数组本身,然后通过[0]访问第一个元素printf("第一个元素: %d\n", (*ptr)[0]);return 0;
}
我们通过调试也能发现&arr
与p
的类型是完全一致的
数组指针与指针数组的区别
-
数组指针
int (*p)[5] ————指向整个数组的指针 -
指针数组
int *p[5]————包含5个整型指针的数组
特性 | 数组指针 | 指针数组 |
---|---|---|
定义 | int (*ptr)[n] | int *ptr[n] |
本质 | 指向数组的指针 | 存储指针的数组 |
内存占用 | 1个指针的大小 | n个指针的大小 |
指针运算 | 以整个数组为单位 | 以指针大小为单位 |
主要用途 | 处理多维数组 | 存储多个地址/字符串 |
二维数组传参本质
数组退化为指针
二维数组作为函数参数传递时,会退化为指向数组首元素的指针
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{int i = 0;int j = 0;for (i = 0; i < r; i++){for (j = 0; j < c; j++){printf("%d ", *(*(p + i) + j));}printf("\n");}
}
int main()
{int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7} };test(arr, 3, 5);return 0;
}
总结:二维数组传参,形参部分可以是数组形式,也可以是指针形式
指针:(十三)二级指针
指针变量也是变量,也有自己的地址
二级指针变量是用来存放一级指针变量的地址的!
二级指针与一级指针的区别
一级指针
int a = 10; int *pa = &a; //pa是指针变量,pa是一级指针
pa指向的对象是int类型
二级指针
int **ppa = &pa; //ppa是二级指针变量
ppa指向的对象是int*类型的
二级指针的运算
#include <stdio.h>int main() {int a = 10;int *p = &a;int **pp = &p;printf("变量a的值: %d\n", a);printf("变量a的地址: %p\n", &a);printf("\n一级指针p:\n");printf("p的值(指向的地址): %p\n", p);printf("p的地址: %p\n", &p);printf("*p的值: %d\n", *p);printf("\n二级指针pp:\n");printf("pp的值(指向的地址): %p\n", pp);printf("pp的地址: %p\n", &pp);printf("*pp的值(即p的值): %p\n", *pp);printf("**pp的值(即a的值): %d\n", **pp);return 0;
}
指针:(十四)字符指针变量
在指针中,有一种指针类型叫做字符类型
定义方式:
char *str; // 声明一个字符指针变量
字符指针的三种使用方式
- 方式一:指向单个字符变量
char ch = 'w';
char *pc = &ch;
pc
指向字符变量ch
可以修改*pc
的值(即修改ch
的值)
pc
存储的是变量ch
的地址
- 方式二:指向字符数组
char str = "abcdef";
char *ps = str; //数组名就是数组首元素的地址
arr
是在栈上分配的字符数组,包含7个字符符:'a','b','c','d','e','f','\0'
pc
指向数组的首元素arr[0]
- 可以修改数组内容:
pc[0] = 'A' 或 arr[0] = 'A'
- 方式三:指向字符串字面量
char* pc = "abcdef";
"abcdef"
是字符串字面量,存储在只读内存区域pc
存储的是字符串首字符 ‘a’ 的地址- 不能修改内容:
pc[0] = 'A'
会导致运行时错误
字符指针注意事项
int main()
{const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?printf("%s\n", pstr); //%s读取的是地址
return 0;
}
代码 const char* pstr = "hello bit.";
特别容易让同学以为是把字符串hello bit
放到字符指针pstr
里了,但是本质是把字符串 hello bit
. 首字符的地址放到了pstr
中。
指针:(十五)函数指针变量
整数指针是用来存放整数数据类型的,数组指针是用来存放数组的,那函数指针呢?
#include <stdio.h>void test(){};int main(){printf("test: %p\n", test);printf("&test: %p\n", &test);return 0;}
所以我们看到函数其实是有地址的,函数名就是函数的地址
我们可以通过&函数名
来调用函数的地址
函数指针的声明与定义方式
声明格式:
返回值类型 (*指针变量名)(参数类型) = 函数名或者&函数名;
定义方式:
#include <stdio.h>// 定义一个函数
int add(int a, int b) {return a + b;
}int main() {// 定义函数指针并初始化int (*func_ptr)(int, int) = add;// 通过函数指针调用函数int result = func_ptr(3, 5);printf("3 + 5 = %d\n", result); // 输出: 8return 0;
}
函数指针类型解析
int (* pf3) (int x, int y)
| | ------------
| | |
| | pf3指向函数的参数类型和个数的交代
| 函数指针变量名
pf3指向函数的返回类型int (*) (int x, int y) //pf3函数指针变量的类型
函数指针使用示例
#include <stdio.h> // 需要包含头文件以使用printf// 声明一个无参数无返回值的函数
void test()
{printf("hehe\n");
}// 函数指针的声明和初始化:
// 方式1:使用取地址运算符&(&是可选的,因为函数名会被隐式转换为函数地址)
void (*pf1)() = &test; // 正确:使用&test显式获取函数地址
void (*pf2)() = test; // 正确:函数名test会隐式转换为函数地址// 声明一个带参数的函数
int Add(int x, int y)
{return x + y;
}// 函数指针的声明和初始化:
// 方式1:不使用参数名(只有类型)
int (*pf3)(int, int) = Add; // 正确:函数名隐式转换为函数地址// 方式2:使用参数名(参数名会被编译器忽略,只有类型信息有效)
int (*pf4)(int x, int y) = &Add; // 正确:使用&Add显式获取函数地址// 注意:不能重复定义同名的函数指针变量(原代码中pf3被定义了两次)
// 因此将第二个改为pf4int main()
{// 测试函数指针调用pf1(); // 输出: hehepf2(); // 输出: heheprintf("%d\n", pf3(2, 3)); // 输出: 5printf("%d\n", pf4(5, 7)); // 输出: 12return 0;
}
函数指针的两段有趣的代码
查看blog
typedef关键字
查看blog
指针:(十六)函数指针数组
函数指针数组是存储多个函数指针的数组,常用于实现回调机制、状态机、命令模式等
基本语法和声明
使用 typedef 定义函数指针类型
typedef 返回类型 (*函数指针类型名)(参数列表);
声明函数指针数组
函数指针类型名 数组名[大小];
函数指针数组的用途:转移表
我们先看最简单的计算器的实现
#include <stdio.h>int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a * b;
}int div(int a, int b)
{return a / b;
}int main()
{int x, y;int input = 1;int ret = 0;do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);switch (input){case 1:printf("输入操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("输入操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("输入操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("输入操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}} while (input);return 0;
}
利用函数指针数组(转移表)制作
#include <stdio.h>int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a * b;
}int div(int a, int b)
{return a / b;
}int main()
{int x, y;int input = 1;int ret = 0;int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);if ((input <= 4 && input >= 1)){printf("输入操作数:");scanf("%d %d", &x, &y);ret = (*p[input])(x, y);printf("ret = %d\n", ret);}else if(input == 0){printf("退出计算器\n");}else{printf("输入有误\n");}} while (input);return 0;
}
指针:(十七)回调函数
回调函数是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
简单来讲:就是通过函数指针调用的函数
回调函数不是由该函数的实现方直接调⽤,⽽是在特定的事件或条
件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。
多说无益,我们看一下下面这段代码
// 1. 定义回调函数类型
typedef void (*SimpleCallback)(int);// 2. 具体回调函数实现
void print_number(int num) {printf("数字: %d\n", num);
}void square_number(int num) {printf("%d的平方: %d\n", num, num * num);
}// 3. 接收回调的函数
void process_number(int num, SimpleCallback callback) {printf("处理数字 %d...\n", num);callback(num); // 调用回调
}int main() {process_number(5, print_number);process_number(5, square_number);return 0;
}
代码分析:
- 从
main
函数起手,调用process_number
函数并传入两个参数 - 再看
process_number
函数,参数5
用到了第一个输出语句,第二个形参传入了print_number
函数指针(退化成指针),给print_number
重命名,再调用callback
函数 - 后面我们要在函数里面调用其他函数,只需要利用回调函数就可以了
下面我们用回调函数编写简易计算器
#include <stdio.h>
#include <stdlib.h>// 定义函数指针类型,用于表示所有计算类型的函数
// 所有计算函数都要传入两个double类型的参数,返回一个double类型的结果
typedef double (*Callback_Function)(double, double);// 加法函数
double add(double a, double b) {return a + b;
}// 减法函数
double subtract(double a, double b) {return a - b;
}// 乘法函数
double multiply(double a, double b) {return a * b;
}// 除法函数
double divide(double a, double b) {// 检查除数是否为零,避免除零错误if (b != 0) {// 除数不为零,执行除法运算并返回结果return a / b;}else {// 除数为零,打印错误信息printf("Error: Division by zero!\n");// 终止程序执行,返回失败状态exit(EXIT_FAILURE);}
}/*** 计算函数 - 通过回调函数执行具体运算* @param a 第一个操作数* @param b 第二个操作数* @param callback 指向具体计算函数的指针* @return double 计算结果*/
double calculation(double a, double b, Callback_Function callback) {return callback(a, b);
}/*** 显示计算器菜单*/
void DisplayMenu() {printf("\n=== 简易计算器 ===\n");printf("1. 加法 (+)\n");printf("2. 减法 (-)\n");printf("3. 乘法 (*)\n");printf("4. 除法 (/)\n");printf("5. 退出\n");printf("请输入您的选择 (1-5): ");
}int main() {int choice;double num1, num2, result; // 修正:改为double类型以匹配函数参数// 定义函数指针数组,用来存储所有可能的计算操作// 数组顺序与菜单选项顺序对应(加法=0,减法=1,乘法=2,除法=3)Callback_Function callbacks[] = { add, subtract, multiply, divide };while (1) {DisplayMenu();scanf_s("%d", &choice);// 清除输入缓冲区,避免后续输入问题while (getchar() != '\n');// 检查用户是否选择退出系统if (choice == 5) {printf("感谢使用计算器,下次再见!\n");break;}// 验证输入是否有效if (choice < 1 || choice > 4) {printf("您输入的选项无效,请重新输入。\n");continue;}// 获取用户输入的操作数printf("请输入第一个数字: ");scanf_s("%lf", &num1); // 使用%lf读取double类型printf("请输入第二个数字: ");scanf_s("%lf", &num2); // 使用%lf读取double类型// 清除输入缓冲区while (getchar() != '\n');// 使用回调函数执行计算// callbacks[choice-1] 根据用户选择获取相应的函数指针// 例如:选择1 → callbacks[0] (add函数)// 选择2 → callbacks[1] (subtract函数)result = calculation(num1, num2, callbacks[choice - 1]);// 显示计算结果,保留两位小数printf("计算结果: %.2lf\n", result);}return 0;
}
qsort函数
qsort
是C标准库<stdlib.h>
中提供的一个通用排序函数,它使用快速排序(QuickSort)算法(注意:C标准并不强制要求具体实现方式,但通常确实是高效的快速排序)来对任意类型的数据进行排序。
语法格式
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));
参数列表:
参数1:void *base
- 类型:
void
指针(通用指针)- 作用: 指向要排序数组的起始地址
参数2:
size_t nitems
- 类型:
size_t
(无符号整型,通常是unsigned long)- 作用: 指定数组中元素的数量
参数3:
size_t size
- 类型:
size_t
- 作用: 指定数组中每个元素的大小(字节数)
如何获取:使用sizeof
运算符
如何获取:通常用sizeof(array) / sizeof(array[0])
计算
参数4:
int (*compar)(const void *, const void*)
- 类型:
函数指针
- 作用:
指向比较函数的指针
分解:
*compar
:函数指针变量名(const void *, const void*)
:比较函数的参数列表int
:比较函数的返回类型
qsort函数模拟实现
下面我们利用
qsort
函数对整型数据进行排列
#include <stdio.h>// 包含标准输入输出头文件,用于printf函数
#include <stdlib.h>// 包含标准库头文件,qsort函数实际上在这里定义(原代码缺失)// qsort函数的使⽤者得实现⼀个⽐较函数
// int_cmp: 整型比较函数
// 参数:p1, p2 - 指向要比较的两个元素的void指针
// 返回值:负数(p1<p2),0(p1==p2),正数(p1>p2)
int int_cmp(const void* p1, const void* p2) {int a = *(int*)p1;int b = *(int*)p2;if (a < b) return -1;if (a > b) return 1;return 0;// 1. 将void*转换为int*类型指针// 2. 解引用获取实际整数值// 3. 用p1指向的值减去p2指向的值// - 如果结果为负:p1 < p2,返回负数 → 升序排列// - 如果结果为0:p1 == p2,返回0// - 如果结果为正:p1 > p2,返回正数
}int main() {// 定义并初始化一个整型数组int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };int i = 0; // 循环计数器// 调用qsort函数对数组进行排序qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);// 参数1: arr - 要排序的数组的起始地址// 参数2: sizeof(arr) / sizeof(arr[0]) - 计算数组元素个数// sizeof(arr): 整个数组的字节大小 (10个int × 4字节 = 40字节)// sizeof(arr[0]): 第一个元素的字节大小 (4字节)// 40 / 4 = 10个元素// 参数3: sizeof(int) - 每个元素的大小(4字节)// 参数4: int_cmp - 比较函数的指针(函数名就是函数指针)// 打印排序后的数组 for(i = 0;i<sizeof(arr)/sizeof(arr[0]);i++){printf("%d ", arr[i]);// 依次输出每个元素}printf("\n");// 换行return 0;// 程序正常结束
}
利用冒泡排序实现qsort函数
#include <stdio.h>// 整型比较函数
// 参数:p1, p2 - 指向要比较的两个元素的void指针
// 返回值:负数(p1<p2),0(p1==p2),正数(p1>p2)
int int_cmp(const void *p1, const void *p2)
{// 将void指针转换为int指针,然后解引用获取整数值// 用p1指向的值减去p2指向的值实现升序排序return (*(int *)p1 - *(int *)p2);
}// 通用交换函数
// 参数:p1, p2 - 指向要交换的两个元素的指针
// size - 每个元素的大小(字节数)
void _swap(void *p1, void *p2, int size)
{int i = 0;// 逐字节交换两个元素的内容for (i = 0; i < size; i++){// 将指针转换为char*类型进行字节级操作// 临时保存p1的第i个字节char tmp = *((char *)p1 + i);// 将p2的第i个字节复制到p1*((char *)p1 + i) = *((char *)p2 + i);// 将临时保存的字节复制到p2*((char *)p2 + i) = tmp;}
}// 通用冒泡排序函数
// 参数:base - 指向数组起始位置的指针
// count - 数组中元素的数量
// size - 每个元素的大小(字节数)
// cmp - 比较函数的指针
void bubble(void *base, int count, int size, int (*cmp)(void *, void *))
{int i = 0;int j = 0;// 外层循环:控制排序的轮数// 每完成一轮,最大的元素就会"冒泡"到末尾for (i = 0; i < count - 1; i++){// 内层循环:进行相邻元素的比较// count-i-1:每轮后,末尾的i个元素已经有序,不需要再比较for (j = 0; j < count - i - 1; j++){// 计算当前元素和下一个元素的地址:// (char *)base + j*size → 第j个元素的地址// (char *)base + (j+1)*size → 第j+1个元素的地址// 使用比较函数比较相邻元素if (cmp((char *)base + j * size, (char *)base + (j + 1) * size) > 0){// 如果顺序错误,交换两个元素_swap((char *)base + j * size, (char *)base + (j + 1) * size, size);}}}
}int main()
{// 定义并初始化测试数组int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};int i = 0;// 计算数组元素个数int element_count = sizeof(arr) / sizeof(arr[0]);// 调用通用冒泡排序函数// 参数1: arr - 数组起始地址// 参数2: element_count - 元素个数 (10)// 参数3: sizeof(int) - 每个元素的大小 (4字节)// 参数4: int_cmp - 比较函数指针bubble(arr, element_count, sizeof(int), int_cmp);// 打印排序后的数组printf("排序后的数组: ");for (i = 0; i < element_count; i++){printf("%d ", arr[i]);}printf("\n");return 0;
}
文章总结
本文涵盖了C语言指针95%的内容,剩余的一些是项目实战问题。我也会出一篇文章来讲解数组与指针的笔试强训
如果你觉得这篇文章对你学习指针帮助,请给文章一个三连吧