[C语言]第十三节 指针一基础知识到高级技巧的全景探索
目录
13.1. 内存和地址
13.1.1. 内存和地址
13.2. 指针变量和地址
13.2.1 取地址操作符(&)
13.1.2 指针变量和解引⽤操作符(*)
13.1.3 指针变量的⼤⼩
13.3. 指针变量类型
13.3.1 指针的解引⽤
13.3.2 指针+-整数
13.3.3 void* 指针
13.4. const修饰指针
13.4.1 const修饰变量
13.4.2 const修饰指针变量
13.5. 指针运算
13.5.1 指针+- 整数
5.2 指针-指针
13.5.3 指针的关系运算
13.6. 野指针
13.6.1 野指针成因
12.6.2 如何规避野指针
13.7. assert断⾔
断言的工作原理:
使用 assert() 的好处:
如何禁用 assert():
assert() 的缺点:
13.8. 指针的使⽤和传址调⽤
13.8.1 strlen的模拟实现
13.8.2传值调用和传址调用。
13.9. 数组名的理解
13.9.1 数组名的理解
13.10. 使⽤指针访问数组
13.11. ⼀维数组传参的本质
13.12. 冒泡排序
13.13. ⼆级指针
13.14. 指针数组
13.15. 指针数组模拟⼆维数组
13.16. 字符指针变量
13.16.1. 字符指针变量
13.17. 数组指针变量
13.17.1 数组指针变量是什么?
13.18. ⼆维数组传参的本质
13.19. 函数指针变量
13.19.1 函数指针变量的创建
13.19.1 函数指针变量的使⽤
13.20. 函数指针数组
13.21. 转移表
13.22. 回调函数是什么?
13.23. qsort使⽤举例
13.23.1 使⽤qsort函数排序整型数据
13.24. qsort函数的模拟实现
3. bubble 函数
13.1. 内存和地址
13.1.1. 内存和地址
bit - ⽐特位byte - 字节KBMBGBTBPB
1b yte = 8b it1 KB = 1024b yte1 MB = 1024 KB1 GB = 1024 MB1 TB = 1024 GB1 PB = 1024 TB
13.2. 指针变量和地址
13.2.1 取地址操作符(&)
#include <stdio.h>
int main()
{
int a = 10;
return 0;
}
0x006FFD700x006FFD710x006FFD720x006FFD73
#include <stdio.h>
int main()
{
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
按照上述的图,会打印处理:006FFD70 &a取出的是a所占4个字节中地址较⼩的字节的地 址。 变量在内存中的存储 虽然整型变量占⽤4个字节,我们只要知道了第⼀个字节地址,顺藤摸⽠访问到4个字节的数据也是可 ⾏的。
13.1.2 指针变量和解引⽤操作符(*)
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存储到指针变量pa中
return 0;
}
指针变量也是⼀种变量,这种变量就是⽤来存放地址的,存放在指针变量中的值都会理解为地址。
int a = 10;
int * pa = &a;
char ch = 'w';
pc = &ch;
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
上⾯代码中第7⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间, *pa其实就是a变量了;所以*pa = 0,这个操作符是把a改成了0.
13.1.3 指针变量的⼤⼩
#include <stdio.h>
//指针变量的⼤⼩取决于地址的⼤⼩
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%zd\n", sizeof(char *));
printf("%zd\n", sizeof(short *));
printf("%zd\n", sizeof(int *));
printf("%zd\n", sizeof(double *));
return 0;
}
X86环境输出结果 X64环境输出结果
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节• 注意指针变量的⼤⼩和类型是⽆关的,只要指针类型的变量,在相同的平台下,⼤⼩都是相同的。
13.3. 指针变量类型
13.3.1 指针的解引⽤
//代码1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
//代码2
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
13.3.2 指针+-整数
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
13.3.3 void* 指针
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
⼀般 void* 类型的指针是使⽤在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得⼀个函数来处理多种类型的数据,在《深⼊理解指针(4)》中我们会讲解。
13.4. const修饰指针
13.4.1 const修饰变量
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
13.4.2 const修饰指针变量
#include <stdio.h>
// 代码1
void test1()
{
int n = 10;
int m = 20;
int* p = &n;
printf("Before: n = %d\n", n); // 打印 n 的初始值
*p = 20; // 修改 n 的值
printf("After *p = 20: n = %d\n", n); // 打印修改后的 n
p = &m; // p 指向 m,但不影响 n
printf("After p = &m: n = %d\n", n); // n 不受影响
}
// 代码2
void test2()
{
int n = 10;
int m = 20;
const int* p = &n;
printf("Before: n = %d\n", n); // 打印 n 的初始值
// *p = 20; // 错误,不能修改 *p 指向的内容
p = &m; // p 指向 m,但不影响 n
printf("After p = &m: n = %d\n", n); // n 不受影响
}
// 代码3
void test3()
{
int n = 10;
int m = 20;
int* const p = &n;
printf("Before: n = %d\n", n); // 打印 n 的初始值
*p = 20; // 修改 n 的值
printf("After *p = 20: n = %d\n", n); // 打印修改后的 n
// p = &m; // 错误,p 是常量指针,不能重新赋值
printf("After p = &m (not executed): n = %d\n", n); // n 不受影响
}
// 代码4
void test4()
{
int n = 10;
int m = 20;
int const* const p = &n;
printf("Before: n = %d\n", n); // 打印 n 的初始值
// *p = 20; // 错误,p 指向的内容是常量,不能修改
// p = &m; // 错误,p 是常量指针,不能重新赋值
printf("After (no operations on n): n = %d\n", n); // n 不受影响
}
int main()
{
// 测试无 const 修饰的情况
printf("Test 1:\n");
test1();
// 测试 const 放在 * 的左边情况
printf("\nTest 2:\n");
test2();
// 测试 const 放在 * 的右边情况
printf("\nTest 3:\n");
test3();
// 测试 * 的左右两边都有 const
printf("\nTest 4:\n");
test4();
return 0;
}
代码1
代码2
代码3
代码4
test2()
:由于p
是const int*
,不能修改*p
指向的值。p
可以重新指向m
,但n
的值不变。test3()
:由于p
是常量指针,不能重新指向m
。但是,*p = 20
会修改n
的值为20
。test4()
:由于p
是常量指针且指向常量数据,既不能修改p
指向的内容,也不能修改指针本身。n
保持不变。
13.5. 指针运算
• 指针+- 整数• 指针-指针• 指针的关系运算
13.5.1 指针+- 整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
数组元素和下标
#include <stdio.h>
//指针+- 整数
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));//p+i 这⾥就是指针+整数
}
return 0;
}
5.2 指针-指针
#include <stdio.h>
// 自定义字符串长度计算函数
int my_strlen(char *s)
{
char *p = s; // 声明一个指针 p,并初始化为指向字符串 s 的起始位置
while (*p != '\0') // 遍历字符串,直到遇到字符串结束符 '\0'
p++; // p 向后移动,指向下一个字符
return p - s; // 返回指针 p 和指针 s 之间的差值,即字符串的长度
}
int main()
{
printf("%d\n", my_strlen("abc")); // 输出 "abc" 的长度
return 0;
}
my_strlen("abc")
会返回 3
,因为 "abc"
是由 3 个字符组成的字符串。
13.5.3 指针的关系运算
#include <stdio.h>
int main()
{
// 定义一个包含 10 个整数的数组,并初始化
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 定义一个指针 p,指向数组 arr 的第一个元素
int *p = &arr[0];
// 计算数组元素的个数
int sz = sizeof(arr) / sizeof(arr[0]);
// 使用指针遍历数组,直到 p 超过数组的有效范围
while (p < arr + sz) // 比较指针 p 和数组的结束位置 arr + sz
{
// 输出当前指针 p 所指向的数组元素
printf("%d ", *p);
// 移动指针 p,指向下一个数组元素
p++;
}
// 输出一个换行符以便于美观
printf("\n");
return 0;
}
13.6. 野指针
13.6.1 野指针成因
#include <stdio.h>
int main()
{
int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
#include <stdio.h>
int main()
{
int arr[10] = {0};
int *p = &arr[0];
int i = 0;
for(i=0; i<=11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();
printf("%d\n", *p);
return 0;
}
12.6.2 如何规避野指针
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
下面做个比喻:指针就像一条狗,它可以指向一块内存区域,就像狗可以找到自己的栓住的地方。但如果我们不再需要这个指针去访问内存区域时,最好把它设置为 NULL(就像把狗栓到树上)。为什么呢?
想象一下,如果我们不把狗栓起来,它就变成了一只“野狗”,自由地乱跑。没有人控制它,这样就会很危险,因为它可能随时跑到不该去的地方,或者出事。同样,指针如果没有被正确地设置为 NULL,它也变成了一个“野指针”,它指向的地方可能已经不再有效,访问它会导致程序崩溃或者产生不可预测的错误。
所以, NULL指针 就像是我们把那只野狗栓住了——只要指针是 NULL,我们就不会去访问它。这样就能避免错误。但是,虽然我们把指针设置为 NULL 了,使用指针时还是需要小心。我们不能盲目地去“挑逗”指针(访问它),而是应该在使用之前,先检查一下指针是否为 NULL,确保它不会指向一个无效的地方。如果指针是 NULL,那就说明它已经被“栓起来了”,我们就不去用它;如果不是 NULL,那我们就可以安全地使用它。
13.7. assert断⾔
assert(p != NULL);
这行代码的意思是在程序运行到这儿时,检查指针 p
是否为 NULL
。如果 p
不为 NULL
,程序会继续执行。如果 p
为 NULL
,程序会报错并停止执行,同时打印错误信息,告诉你哪里出错了,在哪个文件和哪一行。
断言的工作原理:
assert()
宏接受一个表达式作为参数。如果表达式为真(即非零),assert()
不做任何事情,程序继续执行。- 如果表达式为假(即为零),
assert()
会报错。错误信息会输出到标准错误流stderr
,并且显示出错误的表达式、文件名和行号。
使用 assert()
的好处:
- 自动定位错误:它不仅会输出错误信息,还会显示出错的文件和行号,方便你快速定位问题。
- 灵活启用或禁用:你可以轻松地控制是否使用断言。例如,在调试时开启断言,而在正式发布版本时禁用它,避免影响程序性能。
如何禁用 assert()
:
如果你确定程序没有问题,不想让断言继续运行,可以通过定义一个宏 NDEBUG
来禁用断言:
#define NDEBUG
#include <assert.h>
这样,编译器就会忽略掉所有的 assert()
语句,程序继续正常运行。如果之后需要调试,发现问题,可以把 #define NDEBUG
注释掉,重新编译,断言功能就会重新启用。
assert()
的缺点:
- 增加运行时间:因为引入了额外的检查,
assert()
会使程序的运行时间稍微增加。 - 调试阶段使用:一般情况下,我们在调试阶段使用
assert()
来帮助找出潜在的问题,而在发布版本中禁用它,以避免影响性能。在集成开发环境(IDE)如 Visual Studio 中,发布版本(Release)默认会优化掉这些断言。
13.8. 指针的使⽤和传址调⽤
13.8.1 strlen的模拟实现
size_t strlen ( const char * str );
int my_strlen(const char * str)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
13.8.2传值调用和传址调用。
在编程中,我们经常会遇到需要在一个函数中修改另一个函数变量的情况。要想实现这种功能,我们需要理解两种函数调用方式:传值调用和传址调用。
1. 假设我们写一个函数来交换两个整数的值,使用传值调用时,代码如下:
#include <stdio.h>
void Swap1(int x, int y) {
int tmp = x;
x = y;
y = tmp;
}
int main() {
int a = 0, 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
的值并没有交换。为什么呢?
原因:在 Swap1
函数中,a
和 b
的值被传递给了 x
和 y
,但是 x
和 y
是局部变量,它们并没有改变 a
和 b
本身。因为在传值调用中,函数内部的修改不会影响外部变量。
2. 传值调用的总结:
在传值调用中,当传递变量时,函数会创建新的变量(形参)来接收值,这些变量是独立的。对形参的修改不会影响实参。
3. 传址调用的解决方案:
为了能够在 Swap
函数中直接修改 a
和 b
,我们可以使用传址调用。传址调用是通过传递变量的地址,让函数内部操作的就是原始的变量,而不是它们的副本。
下面是使用传址调用修改后的代码:
#include <stdio.h>
void Swap2(int* px, int* py) {
int tmp = 0;
tmp = *px; // 解引用px,获取a的值
*px = *py; // 将b的值赋给a
*py = tmp; // 将a的原值赋给b
}
int main() {
int a = 0, b = 0;
scanf("%d %d", &a, &b);
printf("交换前:a=%d b=%d\n", a, b);
Swap2(&a, &b); // 传递a和b的地址
printf("交换后:a=%d b=%d\n", a, b);
return 0;
}
结果分析:
这次我们成功地交换了 a
和 b
的值。为什么成功呢?
因为我们在 Swap2
函数中传递了 a
和 b
的地址(通过 &a
和 &b
),在函数内部通过解引用(*px
和 *py
)直接修改了原始变量的值。
4. 传址调用的总结:
传址调用通过传递变量的地址,可以让函数直接修改主调函数中的变量。这种方式适用于需要在函数内部修改外部变量的情况。
13.9. 数组名的理解
13.9.1 数组名的理解
数组名实际上就是数组第一个元素的地址,类似于一个指针。比如:
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = &arr[0];
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
在 arr
变量中,它存储的就是 arr[0]
的地址。你可能会好奇:为什么 sizeof(arr)
输出的是整个数组的大小,而不是指针的大小?这是因为在 sizeof(arr)
中,arr
代表的是整个数组,而不是单个元素的地址。如果你用 &arr
来打印,&arr
是整个数组的地址,而不是数组第一个元素的地址。这就是为什么 &arr
和 arr
不完全一样的原因。
数组名如果是数组⾸元素的地址,那下⾯的代码怎么理解呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
sizeof(数组名),sizeof中单独放数组名,这⾥的数组名表⽰整个数组,计算的是整个数组的⼤⼩, 单位是字节&数组名,这⾥的数组名表⽰整个数组,取出的是整个数组的地址(整个数组的地址和数组⾸元素 的地址是有区别的)除此之外,任何地⽅使⽤数组名,数组名都表⽰⾸元素的地址。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
三个打印结果⼀模⼀样,那arr和&arr有啥区别呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
13.10. 使⽤指针访问数组
#include <stdio.h>
int main()
{
int arr[10] = {0};
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
int* p = arr;
// 输入
for(i = 0; i < sz; i++)
{
scanf("%d", p + i);
}
// 输出
for(i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i) ,同理arr[i] 应该等价于 *(arr+i),数组元素的访问在编译器处理的时候,也是转换成⾸元素的地址+偏移 量求出元素的地址,然后解引⽤来访问的。
13.11. ⼀维数组传参的本质
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr)/sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int sz1 = sizeof(arr)/sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
void test(int arr[])//参数写成数组形式,本质上还是指针
{
printf("%d\n", sizeof(arr));
}
void test(int* arr)//参数写成指针形式
{
printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
test(arr);
return 0;
}
13.12. 冒泡排序
// 方法1
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
for(i = 0; i < sz - 1; i++)
{
int j = 0;
for(j = 0; j < sz - i - 1; j++)
{
if(arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for(i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
// 方法2 - 优化
#include <stdio.h>
void bubble_sort(int arr[], int sz)
{
int i = 0;
for(i = 0; i < sz - 1; i++)
{
int flag = 1; // 假设这一趟已经有序了
int j = 0;
for(j = 0; j < sz - i - 1; j++)
{
if(arr[j] > arr[j + 1])
{
flag = 0; // 发生交换就说明无序
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if(flag == 1) // 这一趟没交换就说明已经有序,后续无序排序了
break;
}
}
int main()
{
int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for(i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
13.13. ⼆级指针
**ppa 先通过 *ppa 找到 pa ,然后对 pa 进⾏解引⽤操作: *pa ,那找到的是 a .
13.14. 指针数组
13.15. 指针数组模拟⼆维数组
#include <stdio.h>
int main()
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
int* parr[3] = {arr1, arr2, arr3};
int i = 0, j = 0;
for(i = 0; i < 3; i++)
{
for(j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
13.16. 字符指针变量
13.16.1. 字符指针变量
int main() {
char ch = 'w';
char *pc = &ch; // pc 存储了 ch 的地址
*pc = 'w'; // 通过 pc 修改 ch 的值
return 0;
}
int main() {
const char* pstr = "hello bit."; // pstr 存储的是字符串的首字符地址
printf("%s\n", pstr); // 输出 "hello bit."
return 0;
}
13.17. 数组指针变量
13.17.1 数组指针变量是什么?
int *p1[ 10 ];int (*p2)[ 10 ];
数组指针变量怎么初始化 数组指针变量是⽤来存放数组地址的,那怎么获得数组的地址呢?就是之前学习的 &数组名 。
int arr[10] = {0};
&arr;//得到的就是数组的地址
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int(*p)[10] = &arr;
int (*p) [ 10 ] = &arr;| | || | || | p 指向数组的元素个数| p 是数组指针变量名p 指向的数组的元素类型
13.18. ⼆维数组传参的本质
#include <stdio.h>
void test(int a[3][5], int r, int c) {
int i, j;
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 [5] ,所以第⼀⾏的地址的类 型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第⼀ ⾏这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:
总结:⼆维数组传参,形参的部分可以写成数组,也可以写成指针形式。
13.19. 函数指针变量
13.19.1 函数指针变量的创建
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
int (*pf3) ( int x, int y)| | ------------| | || | pf3 指向函数的参数类型和个数的交代| 函数指针变量名pf3 指向函数的返回类型int (*) ( int x, int y) //pf3 函数指针变量的类型
13.19.1 函数指针变量的使⽤
#include <stdio.h>
int Add(int x, int y) {
return x + y;
}
int main() {
int (*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3)); // 使用函数指针调用函数
printf("%d\n", pf3(3, 5)); // 直接使用函数指针调用函数
return 0;
}
13.20. 函数指针数组
数组是⼀个存放相同类型数据的存储空间,⽐如:
int *arr[10];
//数组的每个元素是int*
要把函数的地址存到⼀个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是:parr1
13.21. 转移表
函数指针数组的⽤途:转移表
#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, input, ret;
int (*operations[4])(int, int) = {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 > 0 && input < 5) {
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = operations[input - 1](x, y); // 调用相应的函数
printf("ret = %d\n", ret);
} else if (input == 0) {
printf("退出程序\n");
} else {
printf("选择错误\n");
}
} while (input);
return 0;
}
13.22. 回调函数是什么?
#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 \n");
printf(" 2:sub \n");
printf(" 3:mul \n");
printf(" 4:div \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;
}
void calc(int (*pf)(int, int))
{
int ret = 0;
int x, y;
printf("输入操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
do {
printf("******************\n");
printf(" 1:add \n");
printf(" 2:sub \n");
printf(" 3:mul \n");
printf(" 4:div \n");
printf("******************\n");
printf("请选择:");
scanf("%d", &input);
switch (input) {
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
13.23. qsort使⽤举例
13.23.1 使⽤qsort函数排序整型数据
#include <stdio.h>
#include <stdlib.h>
// 比较函数,用于 qsort
int int_cmp(const void * p1, const void * p2)
{
return (*(int *)p1 - *(int *)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);
// 输出排序后的数组
for (i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
13.24. qsort函数的模拟实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义学生结构体
struct Stu
{
char name[20]; // 名字
int age; // 年龄
};
// 按照年龄比较的函数
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
// 按照名字比较的函数
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
void test2()
{
struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
int sz = sizeof(s) / sizeof(s[0]);
// 使用 qsort 按照年龄排序
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
// 输出排序后的结果
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
}
void test3()
{
struct Stu s[] = {{"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15}};
int sz = sizeof(s) / sizeof(s[0]);
// 使用 qsort 按照名字排序
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
// 输出排序后的结果
for (int i = 0; i < sz; i++)
{
printf("%s %d\n", s[i].name, s[i].age);
}
}
int main()
{
test2();
test3();
return 0;
}
1. int_cmp
函数
这个函数是用来比较两个整数的大小的。由于 qsort
(以及模拟实现)需要一个通用的比较函数,因此它接收的是 void*
类型的指针(可以指向任何类型的数据)。在 int_cmp
函数中,通过强制类型转换把 void*
转换为 int*
,然后解引用获取整数值并进行比较。返回值是:
- 正数:
p1
大于p2
- 负数:
p1
小于p2
- 0:
p1
等于p2
2. _swap
函数
这个函数实现了交换两个内存块的数据。由于我们使用 void*
指针来表示任意类型的数据,因此我们需要通过 char*
类型来进行字节级别的操作。size
参数表示要交换的数据的大小(以字节为单位)。交换过程是:
- 从
p1
和p2
指向的内存位置逐字节交换数据。 - 使用
char
类型来遍历内存块,这样可以处理任意类型的数据。
3. bubble
函数
这个函数模拟了 qsort
的排序逻辑,使用了冒泡排序算法。具体来说:
base
是指向待排序数组的指针。count
是数组中的元素个数。size
是每个元素的大小(以字节为单位)。cmp
是一个回调函数,用来比较数组中两个元素的大小。
冒泡排序的基本思想是,通过多次比较相邻元素并交换它们,直到数组排好序。具体步骤:
- 外层循环控制排序的轮数,从第一个元素到倒数第二个元素。
- 内层循环依次比较相邻的元素,如果前一个元素比后一个大,就交换它们的位置。
- 交换是通过调用
_swap
函数来实现的,传递的是两个元素的位置和元素大小。
在 main
函数中:
- 定义了一个整数数组
arr
,包含了未排序的数字。 - 调用
bubble
函数对数组进行排序:arr
是待排序的数组。sizeof(arr) / sizeof(arr[0])
计算出数组中元素的个数。sizeof(int)
传递给bubble
函数,表示每个数组元素的大小(以字节为单位)。int_cmp
是用于比较数组元素的函数。
- 排序完成后,通过
for
循环遍历并打印数组中的元素。
代码执行流程总结
- 首先定义一个未排序的数组
arr
。 - 使用冒泡排序算法(通过回调函数
int_cmp
比较元素)对数组进行排序。 - 最后,打印排序后的数组