暑假已尽,小编也开始进入肝文模式了
从今天起,我们开始进入C语言最难的环节————指针
C语言时候的“拦路虎”,C语言指针的复杂性、多元化、思维深度大,让99%的代码萌新都顺利入坑今天小编就用一片博文,带大家从基础、进阶到技巧、运用,层层进化,带大家打通C语言指针的任督二脉
我们知道计算机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 类型的指针&int num = 10;
int *p = # // p 存储了 num 的地址
*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* 的指针的解引⽤就能访问四个字节。
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*指针不能进行指针±操作和解引用操作
观察下面的代码

将一个int类型的变量赋值给char类型的指针变量 编译器会因为类型不兼容而给出一个警告

而void指针就不会有这样的问题
利用void指针接受地址
#include <stdio.h>
int main(){
int num = 0;
void* p = #
void* pc = #
*p = 10;
*pc = 10;
return 0;
}
这里我们可以看到,void类型指针可以接收不同类型的地址,但是无法进行直接的计算
void一般是使用在函数的参数部分,用来实现接收不同数据类型的地址,用来实现泛型编程的效果
const int *ptr1; // 指向常量的指针,指针可变,值不可变
int *const ptr2; // 常量指针,指针不可变,值可变
const int *const ptr3;// 指向常量的常量指针,都不可变 特点: 指针可以指向别的变量,但不能通过指针修改变量值
int a = 10;
const int *ptr = &a; // ptr指向a,但不能通过ptr修改a的值
// *ptr = 20; // 错误!不能通过ptr修改a
a = 20; // 正确,可以直接修改a
int b = 30;
ptr = &b; // 正确,可以改变指针指向特点: 指针永远指向同一个变量,但可以通过指针修改变量值
int x = 10;
int *const ptr = &x; // ptr将永远指向x
*ptr = 20; // 正确,可以修改x的值
int y = 30;
// ptr = &y; // 错误!ptr不能指向别的变量特点: 指针不能改变指向,也不能通过指针修改变量值
int m = 10;
const int *const ptr = &m; // ptr永远指向m,且不能通过ptr修改m
// *ptr = 20; // 错误!不能通过ptr修改m
// ptr = &n; // 错误!不能改变指针指向简单记忆法 看const和*的位置关系:
概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
#include <stdio.h>
int main() {
int *p; //局部变量指针未初始化,默认值为随机值
*p = 20;
return 0;
}#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;
}#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;
}如果明确指针指向哪里就直接赋值指针 如果不知道就直接给指针赋值
NULL(空指针)
NULL指针是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。
⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
#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指向的当前地址的值改为5
p++;
}
p = NULL;
//现在又想用p
p = arr;
if (p == NULL) {
printf("p是空指针\n");
}
return 0;
}int* test()
{
int n = 100;
return &n;
}
assert头文件定义了宏:assert用来确保程序在运行时,符合指定条件;如果不符合条件,则终止运行
assert(条件表达式);#include <assert.h>
void assert(int expression);– 输出错误信息(包含文件名、行号、失败的表达式)
– 调用abort()函数终止程序
– 如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号
如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前⾯,定义一个宏 NDEBUG
#define NDEBUG
#include <stdio.h>assert在调用的时候会引入额外的检查,增加程序运行的时间
一般我们可以在
Debug中使用,在Release版本中选择禁⽤assert就行,在 VS 这样的集成开发环境中,在Release版本中,直接就是优化掉。这样在debug版本写有利于程序员排查问题,在Release版本不影响用户使用程序的效率。
库函数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++; // 计数器加1
str++; // 指针移动到下一个字符
}
return count; // 返回字符串长度
}
int main() {
char str[] = "abcdefg"; // 定义一个测试字符串
size_t len = my_strlen(str); // 调用自定义的字符串长度函数
printf("%zu\n", len); // 使用%zu格式说明符打印size_t类型的值
return 0;
}代码中什么问题是非指针解决不可的呢?
例如: 写一个函数,交换两个整型变量
在指针之前我们可能会写下面的代码
#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的调试技巧可以看这里

结论:实参传递给形参的时候,形参会单独创建一份单独空间来接收实参,对形参的修改不影响实参
传值调用只是传递两个实参变量给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并初始化为0
int b = 0; // 定义整型变量b并初始化为0
// 从标准输入读取两个整数,分别存入a和b
scanf_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;
}
#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字节#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;
}发现函数内部并没有正确得出数组元素个数

关键点:
总结:
我们类比一下 整型数组是存放整型变量的数组 字符数组是存饭字符变量的数组 所以指针数组便是存放指针数据的数组
指针数组的每一个元素都是地址,并指向一块区域

#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]), // 3
sizeof(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 (*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); // 输出: 8
return 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被定义了两次)
// 因此将第二个改为pf4
int main()
{
// 测试函数指针调用
pf1(); // 输出: hehe
pf2(); // 输出: hehe
printf("%d\n", pf3(2, 3)); // 输出: 5
printf("%d\n", pf4(5, 7)); // 输出: 12
return 0;
}函数指针数组是存储多个函数指针的数组,常用于实现回调机制、状态机、命令模式等
使用 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是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_tsizeof运算符
如何获取:通常用sizeof(array) / sizeof(array[0])计算
参数4:int (*compar)(const void *, const void*)
函数指针指向比较函数的指针分解:
*compar:函数指针变量名(const void *, const void*):比较函数的参数列表int:比较函数的返回类型下面我们利用
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;// 程序正常结束
}#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%的内容,剩余的一些是项目实战问题。我也会出一篇文章来讲解数组与指针的笔试强训 如果你觉得这篇文章对你学习指针帮助,请给文章一个三连吧