文章目录
- 一、回调函数:通过函数指针实现灵活调用
 - 
- [1.1 什么是回调函数?](#1.1 什么是回调函数?)
 - [1.2 回调函数的实际应用:简化计算器代码](#1.2 回调函数的实际应用:简化计算器代码)
 
 - 二、qsort函数
 - 
- [2.1 qsort函数的参数说明](#2.1 qsort函数的参数说明)
 - [2.2 使用qsort排序整型数据](#2.2 使用qsort排序整型数据)
 - [2.3 使用qsort排序结构体数据](#2.3 使用qsort排序结构体数据)
 - [2.4 qsort函数的模拟实现:用冒泡排序理解通用排序](#2.4 qsort函数的模拟实现:用冒泡排序理解通用排序)
 - 
- [2.4.1 模拟实现思路](#2.4.1 模拟实现思路)
 - 
- [1. `main()`主程序:](#1. 
main()主程序:) - [2. `bubble_sort()`](#2. 
bubble_sort()) - 3.`swap():`
 
 - [1. `main()`主程序:](#1. 
 - [2.4.2 模拟实现代码](#2.4.2 模拟实现代码)
 
 
 - 三、`sizeof`与`strlen`
 - 
- [3.1 sizeof:计算内存大小](#3.1 sizeof:计算内存大小)
 - [3.2 strlen:丈量字符串长度](#3.2 strlen:丈量字符串长度)
 - [3.3 核心区别对比](#3.3 核心区别对比)
 
 - 四、数组与指针笔试题解析
 - 
- [4.1 一维数组:数组名](#4.1 一维数组:数组名)
 - [4.2 字符数组:`\0`](#4.2 字符数组:
\0) - [4.3 二维数组:行地址与元素地址的嵌套](#4.3 二维数组:行地址与元素地址的嵌套)
 - [4.4 指针运算](#4.4 指针运算)
 
 
一、回调函数:通过函数指针实现灵活调用
1.1 什么是回调函数?
回调函数是一种特殊的函数调用方式:当一个函数的指针(地址)作为参数传递给另一个函数,并且这个指针在被调用时指向的函数被执行,那么这个被执行的函数就称为回调函数。
简单来说,回调函数不是由函数本身直接调用,而是在特定条件下由其他函数通过指针触发,用于响应特定事件或完成特定逻辑。
1.2 回调函数的实际应用:简化计算器代码
以计算器程序为例,传统实现中,switch语句需要重复处理输入操作数、调用计算函数、输出结果等逻辑,代码冗余度高。
改造前(传统写法):
            
            
              c
              
              
            
          
          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("******************");
		printf(" ******0:退出 *****");
		printf(" ******1:add *****");
		printf(" ******2:sub *****");
		printf(" ******3:mul *****");
		printf(" ******4:div *****");
		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;
		}
	} while (input != 0);
	return 0;
}
        我们发现每个case下都有大量的重复语句,可以把这些语句封装成函数calc,提高代码的可利用性。但是每个case下调用的函数不一样,不能直接用一个函数笼统表示。这就需要利用函数指针,把需要调用的函数作为参数传给calc:
改造后(回调函数版):
            
            
              c
              
              
            
          
          // 定义通用计算函数,接收函数指针作为参数
void calc(int(*pf)(int, int)) {
    int x, y, ret;
    printf("输入操作数:");
    scanf("%d %d", &x, &y);
    ret = pf(x, y);  // 通过函数指针调用回调函数
    printf("ret = %d\n", ret);
}
// 主函数中直接通过函数名调用
case 1: calc(add); break;   
case 2: calc(sub); break;  
case 2: calc(mul); break;  
case 2: calc(div); break;  
        优势 :将重复的输入输出逻辑封装到calc函数中,通过传递不同的计算函数指针(add/sub等)实现灵活调用,减少代码冗余,提高可维护性。
二、qsort函数
qsort是C语言标准库中的快速排序 函数,支持对任意类型的数据进行排序(整型、结构体等),其核心是通过回调函数定义排序规则。
2.1 qsort函数的参数说明
            
            
              c
              
              
            
          
          void qsort(
    void* base,        // 待排序数组的起始地址
    size_t nmemb,      // 数组元素个数
    size_t size,       // 每个元素的大小(单位:字节)
    int (*compar)(const void*, const void*)  // 比较函数(回调函数)
);
        - 比较函数 :需用户自定义,返回值规则:
- 若
p1 > p2,返回正数; - 若
p1 == p2,返回0; - 若
p1 < p2,返回负数。 
 - 若
 
注意:
- 使用
qsort()要先包含头文件#include<stdlib.h> void*是无具体类型的指针,不可以直接解引用,也不可以进行±整数的运算,必须先强制类型转换。
2.2 使用qsort排序整型数据
            
            
              c
              
              
            
          
          #include <stdio.h>
#include <stdlib.h>  // qsort所在头文件
int int_cmp(const void* p1, const void* p2) {
    // 将void*转换为int*,再解引用获取值
    return *(int*)p1 - *(int*)p2;
}
int main() {
    int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    qsort(arr, n, sizeof(int), int_cmp);
    
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);  // 输出:0 1 2 3 4 5 6 7 8 9
    }
    return 0;
}
        2.3 使用qsort排序结构体数据
结构体包含多种类型成员(如姓名、年龄),可通过不同的比较函数实现按不同字段排序。
示例:学生信息排序
            
            
              c
              
              
            
          
          #include <stdio.h>
#include <stdlib.h>
#include <string.h>  // strcmp所在头文件
struct stu
{
	char name[20];
	int age;
};
int cmp_stu_by_name(const void* p1, const void* p2)
{
	return strcmp(((struct stu*)p1)->name, ((struct stu*)p2)->name);
}
int cmp_stu_by_age(const void* p1, const void* p2)
{
	return ((struct stu*)p1)->age - ((struct stu*)p2)->age;
}
int main()
{
	struct stu arr[3] = { {"rare",20},{"daisy",18}, {"sivan",22} };
	int sz = sizeof(arr) / sizeof(arr[0]);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_name);
	qsort(arr, sz, sizeof(arr[0]), cmp_stu_by_age);
return 0;
}
        
注意:
- 比较字符串大小可以用
strcmp()函数,其所在的头文件是string.h strcmp()是按照对应字符的ASCII码值比较的,不是比较长度。例如:"abq">"abcd"- 结构体中指针的访问要用
-> 
2.4 qsort函数的模拟实现:用冒泡排序理解通用排序
qsort的核心是通用性 (支持任意类型),其底层可基于冒泡、快速排序等算法实现。以下用冒泡排序模拟qsort的逻辑。
2.4.1 模拟实现思路
1. main()主程序:
- 创建一个无序数组
 - 计算数组元素个数
sz - 调用
bubble_sort()函数 
2. bubble_sort()
qsort()四个参数:- 首个地址
void* base - 元素个数
size_t sz - 每个元素大小
size_t width - 自定义比较函数
int(*cmp)(const void* p1 , const void* p2) 
- 首个地址
 - 调用
cmp比较大小:- 将首个元素地址转为
char*类型,因为char大小为1字节,方便不同类型传入,进行比较。 - 这里相当于"指针 ± 整数",指针类型是
char*,所以 ± 整数都是跳过整数个字节,j*width就是跳过一个元素的长度。因此这里传入的是第j和j+1个元素进行比较。 
 - 将首个元素地址转为
 - 调用
swap: 升序排序,cmp()>0则交换。这里原理同上,传入第j和j+1个元素的地址。 
3.swap():
- 逐字节进行交换,循环次数就是
width,每个元素的大小。 
2.4.2 模拟实现代码
            
            
              c
              
              
            
          
          #include <stdio.h>
int cmp_int(const void* p1, const void* p2)
{
	return *((int*)p1) - *((int*)p2);
}
void swap(char* buf1, char* buf2, size_t width)
{
	for (int i = 0; i < width; i++)
	{
		char temp = *buf1;
		*buf1 = *buf2;
		*buf2 = temp;
		buf1++;
		buf2++;
		//逐字节交换
	}
}
void bubble_sort(void* base,size_t sz,size_t width,int(*cmp)(const void* p1 ,const void* p2))
{
	for (int i = 0; i < sz - 1; i++)
	{
		for (int j = 0;j < sz - 1 - i;j++)
		{
			if ( cmp( (char*)base + j * width,(char*)base + ( j + 1 )*width   )> 0)
			{
				swap((char*)base + j * width, (char*)base + (j + 1) * width,width);
			}
		}
	}
}
int main()
{
	int arr[10] = { 1,2,7,6,9,8,10,6,4,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	bubble_sort(arr,sz,sizeof(arr[0]),cmp_int);
	for (size_t i = 0; i < 10; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}
        三、sizeof与strlen
在C语言中,指针与数组的结合往往是初学者的"绊脚石",而sizeof与strlen的混淆更是常见的"雷区"。本讲将通过对比分析sizeof与strlen的核心区别,并结合经典笔试题,带你彻底厘清数组与指针的底层逻辑。
3.1 sizeof:计算内存大小
sizeof是C语言的操作符,其核心功能是计算变量或类型所占用的内存空间大小(单位:字节)。它的特点是:
- 只关注内存占用,不关心内存中存储的数据;
 - 操作数可以是变量,也可以是类型(如
sizeof(int)); - 对于数组名,
sizeof(数组名)计算的是整个数组的大小(其他情况数组名通常表示首元素地址)。 
示例代码:
            
            
              c
              
              
            
          
          #include <stdio.h>
int main() {
    int a = 10;
    printf("%d\n", sizeof(a));    // 结果:4(int类型占4字节)
    printf("%d\n", sizeof a);     // 结果:4(变量名可省略括号)
    printf("%d\n", sizeof(int));  // 结果:4(int类型的大小)
    return 0;
}
        3.2 strlen:丈量字符串长度
strlen是C语言标准库函数(声明于<string.h>),功能是计算字符串的长度,其核心逻辑是:
- 从传入的地址开始,逐个字符计数,直到遇到
\0停止 (\0不计入长度); - 若字符串中没有
\0,会越界查找,导致结果未定义(UB)。 
示例代码:
            
            
              c
              
              
            
          
          #include <stdio.h>
#include <string.h>
int main() {
    char arr1[3] = {'a', 'b', 'c'};  // 无\0
    char arr2[] = "abc";             // 隐含\0
    printf("%d\n", strlen(arr1));    // 结果:随机值(越界查找)
    printf("%d\n", strlen(arr2));    // 结果:3(遇到\0停止)
    return 0;
}
        3.3 核心区别对比
| 特性 | sizeof | 
strlen | 
|---|---|---|
| 本质 | 操作符(编译期计算) | 库函数(运行期计算) | 
| 功能 | 计算内存空间大小(字节) | 计算字符串长度(\0前字符数) | 
| 关注内容 | 仅关心内存占用,与数据无关 | 依赖\0结束符,无\0则越界 | 
| 参数类型 | 变量、类型、表达式等 | 仅接受char*(字符串首地址) | 
| 返回值 | size_t(无符号整数) | 
size_t(无符号整数) | 
四、数组与指针笔试题解析
4.1 一维数组:数组名
代码示例:
            
            
              c
              
              
            
          
          int a[] = {1,2,3,4};
printf("%d\n", sizeof(a));        // 结果:16(整个数组大小:4元素×4字节)
printf("%d\n", sizeof(a+0));      // 结果:4/8(首元素地址,指针大小)
printf("%d\n", sizeof(*a));       // 结果:4(首元素是int,占4字节)
printf("%d\n", sizeof(a+1));      // 结果:4/8(第二个元素地址,指针大小)
printf("%d\n", sizeof(a[1]));     // 结果:4(第二个元素是int)
printf("%d\n", sizeof(&a));       // 结果:4/8(数组地址,指针大小)
printf("%d\n", sizeof(*&a));      // 结果:16(*&a等价于a,整个数组大小)
printf("%d\n", sizeof(&a+1));     // 结果:4/8(跳过整个数组的地址,指针大小)
printf("%d\n", sizeof(&a[0]));    // 结果:4/8(首元素地址,指针大小)
printf("%d\n", sizeof(&a[0]+1));  // 结果:4/8(第二个元素地址,指针大小)
        关键结论:
- 数组名
a在sizeof(a)和&a中表示整个数组 ,其他情况均表示首元素地址。 - 指针运算(如
a+1)的结果仍是指针,大小为4/8字节(取决于32/64位系统)。 
4.2 字符数组:\0
字符数组的核心陷阱在于是否包含\0,这直接影响strlen的结果。
代码1:无\0的字符数组(sizeof解析)
        
            
            
              c
              
              
            
          
          char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));       // 结果:6(6个char,每个1字节)
printf("%d\n", sizeof(arr+0));     // 结果:4/8(首元素地址,指针大小)
printf("%d\n", sizeof(*arr));      // 结果:1(首元素是char)
printf("%d\n", sizeof(&arr));      // 结果:4/8(数组地址,指针大小)
printf("%d\n", sizeof(&arr+1));    // 结果:4/8(跳过整个数组的地址)
        代码2:无\0的字符数组(strlen解析)
        
            
            
              c
              
              
            
          
          char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", strlen(arr));       // 结果:随机值(无\0,越界查找)
printf("%d\n", strlen(arr+0));     // 结果:随机值(同arr,首元素地址)
printf("%d\n", strlen(*arr));      // 错误(*arr是'a',ASCII值97,视为地址越界)
        代码3:含\0的字符数组(字符串)
        
            
            
              c
              
              
            
          
          char arr[] = "abcdef";  // 隐含'\0',共7个元素
printf("%d\n", sizeof(arr));       // 结果:7(包含'\0')
printf("%d\n", strlen(arr));       // 结果:6('\0'前共6个字符)
        代码4:字符指针指向常量字符串
            
            
              c
              
              
            
          
          char *p = "abcdef";  // p存储字符串首地址
printf("%d\n", sizeof(p));        // 结果:4/8(指针大小)
printf("%d\n", strlen(p));        // 结果:6(字符串长度)
printf("%d\n", strlen(p+1));      // 结果:5(从'b'开始计数)
        4.3 二维数组:行地址与元素地址的嵌套
代码示例:
            
            
              c
              
              
            
          
          int a[3][4] = {0};  // 3行4列的二维数组
printf("%d\n", sizeof(a));        // 结果:48(3×4×4字节,整个数组大小)
printf("%d\n", sizeof(a[0]));     // 结果:16(第0行数组大小:4×4字节)
printf("%d\n", sizeof(a[0]+1));   // 结果:4/8(第0行第1列元素地址)
printf("%d\n", sizeof(a+1));      // 结果:4/8(第1行的地址,行指针)
printf("%d\n", sizeof(*(a+1)));   // 结果:16(第1行数组大小)
printf("%d\n", sizeof(*a));       // 结果:16(第0行数组大小,*a等价于a[0])
        核心逻辑:
- 二维数组
a可视为"数组的数组",a[i]是第i行的一维数组名。 a表示首行地址(行指针,类型为int(*)[4]),a+1指向第1行。
4.4 指针运算
题目1:数组地址的跨越
            
            
              c
              
              
            
          
          int a[5] = {1,2,3,4,5};
int *ptr = (int*)(&a + 1);  // &a是数组地址,+1跳过整个数组
printf("%d,%d", *(a+1), *(ptr-1));  // 结果:2,5
        a+1指向第1个元素(值2),ptr-1指向数组最后一个元素(值5)。
题目2:结构体指针的算术运算
            
            
              c
              
              
            
          
          struct Test { int Num; char *pcName; short sDate; char cha[2]; short sBa[4]; };
struct Test *p = (struct Test*)0x100000;  // 结构体大小20字节
printf("%p\n", p + 0x1);       // 结果:0x100014(+20字节,指针按结构体大小偏移)
printf("%p\n", (unsigned long)p + 0x1);  // 结果:0x100001(整数+1)
printf("%p\n", (unsigned int*)p + 0x1);  // 结果:0x100004(+4字节,按int*偏移)
        题目3:逗号表达式与数组初始化
            
            
              c
              
              
            
          
          int a[3][2] = { (0,1), (2,3), (4,5) };  // 逗号表达式取右值,等价于{1,3,5}
int *p = a[0];  // p指向第0行第0列
printf("%d", p[0]);  // 结果:1
        题目4:二维数组与指针偏移差异
            
            
              c
              
              
            
          
          int a[5][5];
int(*p)[4] = a;  // p是指向4个int的数组指针
printf("%d", &p[4][2] - &a[4][2]);  // 结果:-4
        p每次偏移4个int(16字节),a每次偏移5个int(20字节),累计偏移差为-4。
题目5:数组地址与元素地址的转换
            
            
              c
              
              
            
          
          int aa[2][5] = {1,2,3,4,5,6,7,8,9,10};
int *ptr1 = (int*)(&aa + 1);  // 指向数组后地址
int *ptr2 = (int*)(*(aa + 1));  // 指向第1行首元素
printf("%d,%d", *(ptr1-1), *(ptr2-1));  // 结果:10,5
        题目6:指针数组的偏移
            
            
              c
              
              
            
          
          char *a[] = {"work","at","alibaba"};
char **pa = a;  // pa指向指针数组首元素
pa++;  // 指向第二个元素("at")
printf("%s\n", *pa);  // 结果:at
        题目7:三级指针的嵌套访问
            
            
              c
              
              
            
          
          char *c[] = {"ENTER","NEW","POINT","FIRST"};
char **cp[] = {c+3,c+2,c+1,c};
char ***cpp = cp;
printf("%s\n", **++cpp);        // 结果:POINT(cpp指向cp[1],解引用得c+2)
printf("%s\n", *--*++cpp+3);   // 结果:ER(复杂偏移后指向"ENTER"+3)
printf("%s\n", *cpp[-2]+3);    // 结果:ST(cpp[-2]是cp[0],指向"FIRST"+3)
printf("%s\n", cpp[-1][-1]+1); // 结果:EW(cpp[-1]是cp[2],指向"NEW"+1)