指针的主题,我们在初级阶段的《指针》章节已经接触过了,我们知道了指针的概念:
1. 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
2. 指针的大小是固定的4/8个字节(32位平台/64位平台)。
3. 指针是有类型,指针的类型决定了指针的+-整数的步长,指针解引用操作的时候的权限。
4. 指针的运算。
一、字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char* ;
一般使用:(*p存单个字符)
#include<stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;//字符指针存放一个字符变量的地址
*pc = 'b';
printf("%c\n", ch);
return 0;
}
还有一种使用方式如下:(*p存字符串)
#include<stdio.h>
int main()
{
char* p = "abcdef";
printf("%s\n", p);
return 0;
}//问题1:这里是把一个字符串放到p指针变量里了吗?
//其实这里特别容易让同学以为是把字符串 abcdef 放到字符指针 p 里了,但是本质是把字符串 abcdef 首字符的地址放到了指针p中。
//问题2:在某些代码编辑器中,char* p = "abcdef"这串代码可能会报错,可能会显示将这个字符的首地址赋给p是不安全的,那该如何解决呢?
假设代码这样写:
#include<stdio.h>
int main()
{
char* p = "abcdef";*p = ‘w’;//将*p的值进行修改
printf("%s\n", p);
return 0;
}//上面的代码虽然不会报错但是会挂起,原因是*p在改变abcdef的值的时候发生了写入访问冲突,也就是改变*p的值时发生了错误
//实际上我们这样写的时候,abcdef是常量字符串,常量字符串不可修改,但是*p是可以修改的,这两种的关系不匹配导致会报错
//如果想让指针p不能修改且要编译器报错,可以写成const char* p = "abcdef";
下面看一道面试题:
#include <stdio.h>
int main()
{
const char* p1 = "abcdef";
const char* p2 = "abcdef";char arr1[] = "abcdef";
char arr2[] = "abcdef";if (p1 == p2)
printf("p1==p2\n");
else
printf("p1!=p2\n");if (arr1 == arr2)
printf("arr1 == arr2\n");
else
printf("arr1 != arr2\n");
return 0;
}
输出结果:

原因分析:
- p1 和 p2 都指向相同的字符串字面量 "abcdef",它们的地址可能相等(因为编译器会合并相同的字符串字面量)。
- 像 "abcdef" 这样的字符串字面量,编译器通常会把它们放在程序的只读数据段。如果多个指针变量都被赋值为同一个字符串字面量,那它们的值(也就是内存地址)可能会指向同一个内存位置,或者编译器优化后也会把它们合并到同一个地址
- arr1 和 arr2 是两个独立的数组,它们在内存中开辟的空间肯定不同,所以它们的地址肯定不相等。
二、指针数组
在《指针》章节我们也学了指针数组,指针数组是一个存放指针的数组。
这里我们再复习一下数组是什么?定义一个整型数组,每个元素存的是整型的数字:
int arr[10];
如果每个元素存的是字符,叫做字符数组:
char ch[5];
那么指针数组就是每个元素是指针类型的,如下:
int* arr2[6]—数组的每个元素都是整型指针
char* arr3[5]—数组每个元素都是字符指针
下面看这样一段代码:
#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 };//存放指针的数组return 0;
}
它们在内存中存放的方式如下:

我们将上述代码进行修改:
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 };
//parr数组每个元素的下标 0 1 2
//parr[0]拿到arr1的首地址,即parr[0] = &arr1[0]
//parr[1]拿到arr2的首地址
//parr[2]拿到arr3的首地址
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;//每个arr数组的下标, j = 0,1,2,3,4
for (j = 0; j < 5; j++)
{
printf("%d ", *(parr[i] + j));//每行的起始地址加j
}
printf("\n");//打印完一行换行
}
return 0;
}
编译结果:

注:实际上这个东西并不是二维数组,二维数组指的是包含多个一维数组的数组,二维数组的元素是int型,我们只是用parr指针数组模拟出一个二维数组,parr数组实际上还是一维数组,而是里面存放类型为int*的元素。
三、数组指针
1 数组指针的定义
数组指针是指针?还是数组? 答案是:指针。
我们已经熟悉:
整形指针: int * pint; 能够指向整形数据的指针。
浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。
下面代码哪个是数组指针?
int *p1[10]; //指针数组—p1是数组名,10个元素,类型为int*
int (*p2)[10];//数组指针,p2可以指向一个数组,该数组有10个元素,每个元素是int类型
//p1, p2分别是什么?
int (*p) [10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
2 &数组名vs数组名
对于下面的数组:
int arr[10];
arr 和 &arr 分别是啥?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
我们看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%p\n", &arr);
//数组名是首元素地址?
//数组名通常都表示的都是数组首元素地址
//但是有两个除外
//1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小
//2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址
return 0;
}

可见数组名和&数组名打印的地址是一样的。
难道两个是一样的吗?
我们再看一段代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%p\n", arr);
printf("%p\n", arr+1);printf("%p\n", &arr[0]);
printf("%p\n", &arr[0]+1);printf("%p\n", &arr);
printf("%p\n", &arr+1);
return 0;
}

根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。
实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型
数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40(0x28).
3 数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。
看代码:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int (*p2)[10] = &arr;//整型指针是用来存放整型的地址
//字符数组是用来存放字符的地址
//数组指针是用来存放数组的地址
return 0;
}
一个数组指针的使用:
#include <stdio.h>
void print(int arr[3][5], int r, int c)//常规数组传参
{
int i = 0;
for (i = 0; i < r;i++)
{
int j = 0;
for (j = 0; j < c;j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}void print1(int (*p)[5], int r, int c)//数组指针的使用
{
//int (*p)[5]这种写法要知道如下:
//1.p的类型是:int (*)[5]
//2.p是指向一个整型数组的,数组5个元素 int [5]
//3.p+1跳过一个5个int元素的数组
int i = 0;
for (i = 0;i < r;i++)
{
int j = 0;
for (j = 0;j < c;j++)
{
printf("%d ", *(*(p + i) + 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} };
print(arr, 3, 5);
print1(arr, 3, 5);
//arr表示第一行的地址,第一行的地址,是一个一维数组的地址,
//假设第一行数组叫做date[5],&date传过去用一个指针p接收
//p = &arr = &arr[0]
//p+i --> p+0指向第一行,p+1指向第二行,p+2指向第三行
//*(p+i) --> arr[i]
//
return 0;
}
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];--》整型数组
int *parr1[10];--》整型指针数组,每个元素的类型是int*
int (*parr2)[10];--》整型数组指针,每个元素的类型是int
int (*parr3[10])[5];--》存放数组指针的数组,

四、数组参数、指针参数
1 一维数组传参
#include<stdio.h>
void test(int arr[])//ok,传形参数组,数组元素个数可省略
{
}void test(int arr[10])//ok,传形参数组
{
}void test(int* arr)//ok,数组名是首元素地址,首元素地址用指针存储
{
}void test2(int* arr[20])//ok,传形参指针数组
{
}void test2(int** arr)//ok,数组名首元素是指针类型的,传参可用二级指针接收数组首元素
{
}int main()
{
int arr[10] = { 0 };
int* arr2[20] = { 0 };
test(arr);
test2(arr2);
}
2 二维数组传参
void test(int arr[3][5])//ok?,正确,二维数组的形参表示
{
}void test(int arr[][])//ok?,错误,二维数组的形参列数不可省略
{
}void test(int arr[][5])//ok?,正确,二维数组形参的行可以省略
{
}void test(int* arr)//ok?,错误,二维数组的指针类型不匹配
{
}void test(int* arr[5])//ok?,错误, int* arr[5]的类型是数组不是指针
{
}void test(int (*arr)[5])//ok?,正确,二维数组的指针类型表示
{
}void test(int** arr)//ok?,一维数组的地址不能放在二级指针里面
{
}int main()
{
int arr[3][5] = { 0 };
test(arr);//
}
3 一级指针传参
void print(int* p, int sz)
{
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d\n", *(p + i));
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
问:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
void print(int* p)//一级指针可接收地址,一级指针本身,一维数组的首地址等等
{}
int a = 10;
int ptr = &a;
int arr[10];print(&a);
print(ptr);
print(arr);
4 二级指针传参
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int* p = &n;
int** pp = &p;
test(pp);
test(&p);
return 0;
}
问:当函数的参数为二级指针的时候,可以接收什么参数?
test(int** p)//二级指针可接收一级指针的地址,二级指针本身等等
{}
int* p1;
int** p2;
int* arr[10];test(&p1);
test(p2);
test(arr);
五、函数指针
指向函数的指针叫做函数指针
先看一段代码:

经过以上操作,我们果然取出了函数的地址
数组指针里面有数组的地址和数组首元素的地址,那么函数有这个区别吗,再看一段代码:

对于函数来说,&函数名和函数名都是函数的地址
函数地址的储存
int (*pf)(int, int) = &Add;
那这个有什么用呢,相当于可以通过pf解引用,找到Add函数,然后运行Add函数的功能,看如下代码:
int Add(int x, int y)
{
return x + y;
}int main()
{
int (*pf)(int, int) = &Add;
int ret = (*pf)(2, 3);
printf("%d\n", ret);return 0;
}
函数指针的用途:
我们先写一个简易的计算器
#include <stdio.h>
void menu()
{
printf("**************************\n");
printf("**** 1.add 2.sub ****\n");
printf("**** 3.mul 4.div ****\n");
printf("**** 0.exit ****\n");
printf("**************************\n");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Add(x, y);
printf("%d\n", ret);
break;
case 2:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Sub(x, y);
printf("%d\n", ret);
break;
case 3:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Mul(x, y);
printf("%d\n", ret);
break;
case 4:
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = Div(x, y);
printf("%d\n", ret);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误\n");
break;
}
} while (input);
return 0;
}
我们会发现一个问题,就是在选择计算的方式的时候,代码的重复度很高,我们可以通过封装一个函数来减少重复度,这时候就可以通过函数指针的方式来解决。
以下是简化版本:
#include <stdio.h>
void menu()
{
printf("**************************\n");
printf("**** 1.add 2.sub ****\n");
printf("**** 3.mul 4.div ****\n");
printf("**** 0.exit ****\n");
printf("**************************\n");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
void calc(int (*pf)(int, int))
{
int x = 0;
int y = 0;
int ret = 0;
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = (*pf)(x, y);
printf("%d\n", ret);
}
int main()
{
int input = 0;
do
{
menu();
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;
}
六、函数指针数组
先回顾一下指针数组:存放指针的数组
例如:
int* arr1[5];
char* arr2[6];
所以函数指针数组就是存放函数指针的数组
函数指针数组的怎么写呢?
我们参照指针数组的写法可以发现:
int (*arr[4])(int, int) = { Add, Sub, Mul, Div };
例如下面的代码:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int (*pf)(int, int) = Add;//pf是函数指针
int (*arr[4])(int, int) = { Add, Sub, Mul, Div };//arr就是函数指针的数组
int i = 0;
for (i = 0;i < 4;i++)
{
int ret = arr[i](8, 4);
printf("%d\n", ret);
}
return 0;
}
使用函数指针数组实现计算器:
#include<stdio.h>
void menu()
{
printf("**************************\n");
printf("**** 1.add 2.sub ****\n");
printf("**** 3.mul 4.div ****\n");
printf("**** 0.exit ****\n");
printf("**************************\n");
}
int Add(int x, int y)
{
return x + y;
}
int Sub(int x, int y)
{
return x - y;
}
int Mul(int x, int y)
{
return x * y;
}
int Div(int x, int y)
{
return x / y;
}
int main()
{
int input = 0;
int x = 0;
int y = 0;
int ret = 0;
int (*pfArr[5])(int, int) = { 0, Add, Sub, Mul, Div };
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
if (input == 0)
{
printf("退出计算器\n");
}
else if (input>=1 && input<=4)
{
printf("请输入两个操作数:>");
scanf("%d %d", &x, &y);
ret = pfArr[input](x, y);
printf("%d\n", ret);
}
else
{
printf("选择错误\n");
}
} while (input);
return 0;
}
注:使用函数指针数组的好处就在如果要加入其他的功能,可以极大的省略代码的修改
七、回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个 函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数 的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进 行响应。
首先演示一个非常经典的函数qsort:(展示快速排序)

解释一下这里面的参数
void* base 待排序数据的起始位置
size_t num 待排序数据元素的个数
size_t width 待排序数据元素的大小 (单位是字节)
int (*cmp)(const void* e1, const void* e2 ) 函数指针—比较函数
qsort的返回值如下:

首先演示一下qsort函数的使用:
例如我排序一个数组,之前使用了冒泡排序,这里我们用qsort函数
void bubble_sort(int arr[], int sz)//冒泡排序
{
int i = 0;//i是排序需要的次数
for (i = 0; i < sz - 1; i++)
{
int flag = 1;//假设数组是有序的
//一趟冒泡排序
int j = 0;//j是数组下标
for (j = 0; j < sz-1-i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 0;//产生交换操作时将标志位改变,说明数组不是有序数组
}
}
if (flag == 1)//如果本身就是有序数列,则跳出循环
{
break;
}
}
}
//void*指针不能解引用,所以比较大小的时候需要强制类转换
int cmp_int(const void* e1, const void* e2)
{
return (*(int*)e1 - *(int*)e2);
}
int main()
{
int arr[] = { 9,8,7,6,5,4,3,2,1,0 };
//把数组排成升序
int sz = sizeof(arr) / sizeof(arr[0]);
//bubble_sort(arr, sz);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0;i < sz;i++)
{
printf("%d ", arr[i]);
}
return 0;
}
与之相类似的,qsort函数也可以排序其他的类型,只需要修改比较函数的函数指针即可,不同的函数指针排序的效果截然不同。
比如说我可以比较结构体不同类型的大小,代码如下:
struct Stu//创建一个结构体
{
char name[20];
int age;
};
int cmp_stu_by_name(const void* e1, const void* e2)//通过名字比较
{
//strcmp 返回值--> >0,==0,<0
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int cmp_stu_by_age(const void* e1, const void* e2)//通过年龄比较
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int main()
{
struct Stu s[] = { {"zhangsan", 15},{"lisi", 30},{"wangwu", 25}};
int sz = sizeof(s) / sizeof(s[0]);
//qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
return 0;
}
以上说明了qsort函数的便捷之处,我们现在将这个函数模拟实现(使用冒泡排序的方式)
#include <stdio.h>
int cmp_int(const void* e1, const void * e2)
//比较两个数的大小需要知道这两个数的地址
{
return (*(int*)e1 - *(int*) e2);
}
void Swap(void* buf1, void* buf2, int width)
//Swap函数需要设置需交换字符的每一位字节地址,还需要设置字节长度,以便可以知道
//需要交换几次
{
int i = 0;
for (i = 0; i < width; i++)//交换每一节的字节,直到所有字节交换完成,则两元素互换
{
char tmp = *buf1;
*buf1 = *buf2;
*buf2 = tmp;
buf1++;
buf2++;
}
}
void bubble_sort(void* base, int sz, int width, int(*cmp)(const void* e1, const void* e2))
//bubble函数的格式需跟qsort函数的格式保持一致,即可模拟相似的功能
{
int i = 0;//i是排序需要的次数
for (i = 0; i < sz - 1; i++)
{
int flag = 1;
int j = 0;//j是数组下标
for (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);
//base需强制转换成char*类型才能进行地址的计算
flag = 0;
}
}
if(flag == 1)
{
break:
}
}
}
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
总结:本部分介绍回调函数概念,重点讲解 qsort 函数参数、使用示例,包括整数和结构体数组排序,最后用冒泡排序模拟 qsort 实现,突出其灵活性与便捷性。

2130

被折叠的 条评论
为什么被折叠?



