当前位置: 首页 > news >正文

收集飞花令碎片——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 = &num;  // p 存储了 num 的地址
*p
num的地址


(2)解引用运算符*

  • 用于访问指针指向的内存地址中的值
int value = *p;  // value = 10(获取 p 指向地址的值)
*p = 20;         // 修改 p 指向地址的值,num 现在等于 20
value
指针p指向地址的值
*P=20
修改指针p指向地址的值



我们观察下面的两段代码

#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)类型的对象。

pa
0x006FFD70
a = 10



在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 = &num;void* pc = &num;*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的调试技巧可以看这里
在这里插入图片描述

  1. 我们在main函数里面创建的两个变量:a和b,并给a与b分配了两个地址
  2. 在调用Swap函数时,将a与b作为实参传递过去。在Swap函数内部创建两个形参变量x和y负责接收a和b
  3. 但是,x、y、a、b的地址不一样,x和y确实接收了。但是x和y相当于是两个独立的空间,改变的也只是x和y的值,a和b自然就不会受影响了
  4. 当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;
}

我们通过调试也能发现&arrp的类型是完全一致的



数组指针与指针数组的区别

  • 数组指针
    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;
}

代码分析:

  1. main函数起手,调用process_number函数并传入两个参数
  2. 再看process_number函数,参数5用到了第一个输出语句,第二个形参传入了print_number函数指针(退化成指针),给print_number重命名,再调用callback函数
  3. 后面我们要在函数里面调用其他函数,只需要利用回调函数就可以了


下面我们用回调函数编写简易计算器

#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%的内容,剩余的一些是项目实战问题。我也会出一篇文章来讲解数组与指针的笔试强训
如果你觉得这篇文章对你学习指针帮助,请给文章一个三连吧
在这里插入图片描述

http://www.dtcms.com/a/389549.html

相关文章:

  • MySQL 初识:架构定位与整体组成
  • 【开发者导航】规范驱动且开源的 AI 时代开发流程工具:GitHub Spec-Kit
  • 区块链加速器:Redis优化以太坊交易池性能方案
  • 资源分布的均衡性(Poisson Disk Sampling)探索
  • STM32开发(中断模式)
  • Qt QPieSlice详解
  • C++多线程编程
  • LangChain 父文档检索器:解决 “文档块匹配准” 与 “信息全” 的矛盾
  • COI实验室技能:基于几何光学的物空间与像空间的映射关系
  • springboot-security安全插件使用故障解析
  • 企业移动化管理(EMM)实战:如何一站式解决设备、应用与安全管控难题?
  • 高频面试题——深入掌握栈和队列的数据结构技巧
  • 【C++ qml】qml页面加载配置文件信息的两种方式
  • 运维笔记:神卓 N600 解决企业远程访问 NAS 的 3 个核心痛点
  • GitHub 热榜项目 - 日榜(2025-09-18)
  • 使用开源免费的组件构建一套分布式微服务技术选型推荐
  • 需求质量检测Prompt之是否涉及异常场景
  • QT按钮和容器
  • Kafka4.0 可观测性最佳实践
  • 深入解析 Spring AI 系列:解析函数调用
  • ​​[硬件电路-245]:电气制图软件有哪些
  • 不会索赔500万的苹果,翻车如期到来,不过已没啥影响了
  • 第十一章:AI进阶之--模块的概念与使用(一)
  • 【IoTDB】01 - IoTDB的基本使用
  • 【C++】模版语法基础:认识模版(初识篇)
  • 继承测试用例回归策略
  • 卡普空《怪物猎人》系列策略转变:PC平台成重要增长点
  • UML 顺序图 | 概念 / 组成 / 作用 / 绘制
  • 安装SSL证书后如何测试和验证其是否正确配置?
  • A股大盘数据-20250918分析