目录
指针是什么?
指针类型的意义
野指针
二级指针
指针数组
数组指针
函数指针
函数指针数组
函数指针数组指针
回调函数
qsort函数的使用
指针是什么?
简单来说,指针就是地址,而指针变量是用来存放变量的地址。
int main(void) {
int a = 10;
printf("a的地址:%p\n", &a);
int* pa = &a;
printf("a的地址:%p\n", pa);
*pa = 20;
printf("a = %d\n", a);
return 0;
}
运行结果:
pa 用来存放地址,在C语言中叫指针变量
* 说明pa是指针变量
int 说明pa指向的对象是int类型
*pa = 20;
* : 解引用操作,返回指针pa所指的对象
*pa就是通过pa里面的地址找到a,并对其赋值为20
补充: 指针的大小是相同的,因为指针是用来存放地址的,指针需要多大的存储空间,取决于地址存储需要多大的空间。(32位计算机 4B / 64位计算机 8B)
指向的位置是不可知的(随机的、不正确的、没有明确限制的)
导致野指针的原因:
int* p;
*p = 20;
int arr[10];
int i = 0;
int* p = arr;
for (i = 0; i < 11; i++) {
*p = i;
p++;
}
int* test() {
int a = 10;
return &a;
}
int main(void) {
int* p = test();
*p = 20;
return 0;
}
如何避免野指针:
- 指针初始化,不知道初始化什么,初始化为NULL
- 小心指针越界
- 指针指向空间释放及时至NULL
- 指针使用之前检查有效性
int main(void) {
int a = 10;
int* pa = &a;
//pa也是个变量,&pa取出pa在内存中的起始地址
int** ppa = &pa;
//pa是int*类型变量,所以要取pa的地址,就是int* *ppa
return 0;
}
如果是三级指针的话,int*** pppa = &ppa;
int arr[10]; //整型数组 存放整形数据
char ch[5]; //字符数组 存放字符类型数据
int* parr[10]; //整形指针数组 存放整形数据地址
int main(void) {
int a = 10;
int b = 20;
int c = 30;
int* arr[3] = { &a,&b,&c };
int i = 0;
for (i = 0; i < 3; i++) {
// arr[i] 为第i个元素的地址
// *arr[i]解引用为每个元素的值
printf("%d ", *arr[i]);
}
return 0;
}
指针数组里面也可以存放数组的地址
int main(void) {
int a[5] = { 1,2,3,4,5 };
int b[5] = { 2,3,4,5,6 };
int c[5] = { 3,4,5,6,7 };
int* arr[3] = { &a,&b,&c };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++) {
for (j = 0; j < 5; j++) {
// arr[i] 第i个元素的地址
// arr数组里面的元素为各数组的地址
// 所以arr[i]+j为arr数组里面的元素(数组)的第j个元素的地址
// *(arr[i] + j) 解引用为个元素值
printf("%d ", *(arr[i] + j));
//printf("%d ", arr[i][j]);
}
printf("\n");
}
return 0;
}
整形指针,指向整形的指针
字符指针,指向字符的指针
数组指针,指向数组的指针
int a = 10;
int* pa = &a; // pa是整形指针
char ch = 'w';
char* pc = &ch; // ch是字符指针
//那么数组指针怎么写呢?
int arr[5] = { 1,2,3,4,5 };
int (*parr)[5] = &arr;
// &arr为数组的地址
// 那么就要把&arr放到一个指针变量parr里面去,parr是啥类型呢
// *parr 一定是个指针,那么(*parr)[5]就是指向一个数组的指针
// int (*parr)[5]说明指向的是一个整型数组 数组指针
数组名是首元素地址,但有2个例外:
用指针类型打印一个一维数组:
int main(void) {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*pa)[10] = &arr;
// *pa相当于数组名,数组首元素地址
// (*pa) + i :第i个元素地址
// *((*pa) + i) 解引用
int i = 0;
for (i = 0; i < 10; i++) {
printf("%d \n", *((*pa) + i));
}
return 0;
}
用指针类型打印一个二维数组:
void print_arr(int(*p)[5], int r, int c) {
// int(*p)[5] 是指向一维数组的指针
int i = 0;
int j = 0;
for (i = 0; i < r; i++) {
for (j = 0; j < c; j++) {
printf("%d ", *(*(p + i) + j));
// p相当于首元素地址,而arr是一个二维数组
// 所以p就是二维数组的第一行的地址,相当于是一维数组的地址
// p+i就是第i行的地址
// *(p+i)解引用就是第i行第一个元素的地址
// *(p+i)+j就是第i行第j列的元素地址
// *(*(p + i)+j)再解引用,就得到了第i行第j列的元素
}
printf("\n");
}
}
int main(void) {
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
print_arr(arr, 3, 5);
return 0;
}
思考一下:
int (*parr3[10])[5];
int (*parr2)[10];
分别是什么?
int (*parr3[10])[5];是一个存储数组指针的数组,该数组能够存放10个数组指针,每个数组指针能够指向一个数组,数组元素有5个,每个元素的类型是int型
怎么理解:挖掉括号里面的东西,剩下int (*)[5],不就是个数组指针类型嘛,*parr3[10]是一个指针数组,那么int (*parr3[10])[5]就是用来存放数组指针的数组,可以存放10个数组指针,而每个数组指针指向一个数组,一个数组里面有5个元素,都为int型。
int (*parr2)[10]; 同理,为数组指针,能够指向一个数组,数组有10个元素,每个元素类型为int整形。
一维数组传参分析:
void test1(int arr[]){} // 参数也为整型数组,可以
void test1(int arr[10]) {} // 参数也为整型数组,可以
void test1(int *arr) {}
//int *arr是一个整形指针,而传过来的是首元素地址,那肯定是可以的
void test2(int *arr[10]) {} // 参数也为指针数组,和int* arr2[10]类型一样,可以
void test2(int **arr) {}
//int* *arr是一个类型为int*的指针,而传过来的是首元素地址,那肯定是可以的
int main(void) {
int arr1[10] = { 0 }; //整形数组
int* arr2[10] = { 0 }; //指针数组
test1(arr1); //本质上传过去的是首元素的地址
test2(arr2); //本质上传过去的是首元素的地址,元素类型为int*
return 0;
}
二维数组传参分析:
void test(int arr[3][5]) {} // √
void test(int arr[][]) {} // ×
void test(int arr[][5]) {} // √
// 二维数组传参,函数形参的设计只能省略第一个[]的数字
// 因为对于一个二维数组,可以不知道有多少行,但是必须知道一行有多少元素
void test(int *arr) {} // ×
void test(int* arr[5]) {} // ×
void test(int(*arr)[5]) {} // √
// *arr 是一个指针,指向一个5个int类型元素的数组
// 而传过来的是第一行(5个int类型元素的数组)的地址
void test(int** arr) {} // ×
// 传过来的就不是二级指针(地址),传过来的是一个一维数组的地址
// 不能用二级指针接收
int main(void) {
int arr[3][5] = { 0 };
test(arr); //首元素地址,第一行的地址,是一个一维数组的地址
return 0;
}
指向函数的指针,存放函数地址的指针
int main(void) {
// 函数指针
// &函数名,取到的就是函数的地址
printf("%p\n", add); // 010113BB
printf("%p\n", &add); // 010113BB
// 函数名 == &函数名
return 0;
}
int arr[10] = { 0 };
int(*parr)[10] = &arr; // 数组指针
那么函数指针怎么写呢?
pf = &add 那么pf的类型怎么写呢既然是指针,那么就应该是*pf,函数指针指向函数,可以写成*pf(),但是这样pf就会和()先结合,就变成一个函数了,如果希望pf先和*结合的话,就应该括起来,(*pf)(),然后也应该说明指向函数的参数是什么和指向函数的返回类型是什么,所以就可以写成 int(*pf)(in,int)
int(*pf)(in,int) = add; 此时pf就是一个函数指针变量了
既然得到了指针变量,那要怎么使用呢?
int add(int x, int y) {
return x + y;
}
int main(void) {
int ret = 0;
int (*pf)(int, int) = add; //add === pf
// 怎么调用呢?
// pf里面存的已经是这个函数的地址了
// 解引用就相当于找到了这个函数
// 找到这个函数后就可以调用了,然后再传参
ret = (*pf)(1, 2);
// add == &add
// int (*pf)(int, int) = add
// 可以说明 add === pf
//通过函数名调用
ret = add(1, 2);
// =====》
ret = pf(1, 2);
printf("%d \n", ret);
return 0;
}
分析以下代码:
(*(void (*)())0)();
void (*signal(int, void(*)(int)))(int);
(*(void (*)())0)();
//void (*)() 是函数指针类型 (void (*p)()),那么p不就是函数指针嘛
// 所以把p去掉,void (*)()就是函数指针类型,
// (void (*)())0 对0进行强制类型转换,被解释为一个函数的地址
// *(void (*)())0 对函数的地址(0)解引用,
// (*(void (*)())0)() 调用0地址处放的函数
//调用0地址处放的函数,该函数无参,返回类型为void
void (*signal(int, void(*)(int)))(int);
// void(*)(int)函数指针类型
// signal 函数名 int, void(*)(int) 函数参数
// 把signal(int, void(*)(int)) 去掉,得到void (*)(int) ,又是函数指针类型
// signal函数返回类型是一个函数指针
// 该函数指针指向一个参数为int,返回值为void
// signal是一个函数声明
// 简化:
// void(*)(int) signal(int, void(*)(int)); //编译器不允许
// typedef void (*)(int) pfun_t //但是这样写编译肯定是不通过的
// 要写成typedef void (*pfun_t)(int) //对void (*)(int)的函数指针重命名为pfun_t
存放函数指针的数组
整形指针 int *
整形指针数组 int* arr[5]
那函数指针数组呢?
int add(int x, int y) {
return x + y;
}
int sub(int x, int y) {
return x - y;
}
int main(void) {
int (*pf1)(int, int) = add; //函数指针
int (*pf2)(int, int) = sub; //函数指针
// pfarr为函数指针数组
int (*pfarr[2])(int, int) = { add,sub };
return 0;
}
那函数指针数组有什么用呢?如果让我们实现一个计算机程序,按照普通方法实现如下:
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 menu(void) {
printf("\n========================\n");
printf("1.+ 2.-\n");
printf("3.* 4./\n");
printf("0.exit\n");
printf("========================\n");
}
int main(void) {
//计算器 +-*/
int input = 0;
do {
menu();
int x = 0;
int y = 0;
int ret = 0;
printf("请输入》");
scanf("%d", &input);
switch (input) {
case 0: break;
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;
default:
printf("请重新输入\n"); break;
}
} while (input);
return 0;
}
这样显然代码量很多,重复的代码很多,这时可以用函数指针数组来实现,代码如下:
int main(void) {
//计算器 +-*/
int input = 0;
do {
menu();
int x = 0;
int y = 0;
int ret = 0;
printf("请输入》");
scanf("%d", &input);
int (*pfarr[5])(int, int) = { NULL,add,sub,mul,div };
if (input >= 1 && input <= 4) {
printf("请输入两个数》");
scanf("%d,%d", &x, &y);
ret = pfarr[input](x, y);
printf("%d \n", ret);
}
else if (input == 0) {
printf("退出程序");
break;
}
else {
printf("选择错误!");
}
} while (input);
return 0;
}
int(*p)(int, int); //函数指针
int(*p2[4])(int, int); //函数指针数组
int(*(*p3)[4])(int, int) = &p2; //取出的是函数指针数组的地址
//所以p3就是一个指向函数指针数组的指针
理解:把(*p3)[4]挖出来,int(* )(int, int) 就是一个函数指针类型了,(*p3)[4]是一个数组指针,所以int(*(*p3)[4])(int, int) p3就是一个指向函数指针数组的指针了。
回调函数就是一个通过函数指针调用的函数,如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,这就是回调函数。回调函数不是由该函数的实现方 直接调用,而是在特定的事件或条件发生时由另外一方调用的,用于对该事件或条件进行响应。
A函数
B函数(A函数的地址)
此时B函数的形参是A函数的地址
所以当进入B函数的时候,A函数的指针返回来去调用A函数,A函数不是直接调用的,通过指针返回来再去调用A函数
还是以计算机为例:
重复的代码是不是有很多,那能不能将绿色框起来的写成一个函数呢, 显然,中间还有一块是不一样的,但是,我们可以通过函数的参数传进来。
假设写一个函数Calc(参数为函数),如下,那这个函数该怎么实现呢?
int Calc(int (*pf)(int, int)) {
int x = 0;
int y = 0;
printf("请输入两个数》");
scanf("%d,%d", &x, &y);
return pf(x, y);
}
先来看看冒泡排序
void bubble_sort(int arr[], int sz) {
int i = 0;
int j = 0;
int temp = 0;
// 冒泡排序的趟数
for (i = 0; i < sz - 1; i++) {
// 一趟冒泡排序的次数
for (j = 0; j < sz - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
void print_arr(int arr[], int sz) {
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int main(void) {
int arr[10] = { 3,6,1,9,0,5,4,2,8,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
print_arr(arr, sz);
return 0;
}
其实在C语言库里面有一个函数帮我们实现类似效果,就是qsort函数 ,是八大排序算法中的快速排序,能够排序任意数据类型的数组其中包括整形,浮点型,字符串甚至还有自定义的结构体类型,它不挑类型,但是刚才的冒泡排序就挑类型了,因为传进去的只能是一种数据类型。
void qsort (void* base, // 首元素地址
size_t num, // 元素个数
size_t size, // 元素大小
int (*compar)(const void*,const void*) // 自定义比较函数
);
// int compar(const void *p1, const void *p2);
// 如果compar返回值小于0(< 0),那么p1所指向元素会被排在p2所指向元素的左面;
// 如果compar返回值等于0(= 0),那么p1所指向元素与p2所指向元素的顺序不确定;
// 如果compar返回值大于0(> 0),那么p1所指向元素会被排在p2所指向元素的右面。
来使用一下这个函数
对整形数据排序:
int cmp_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2;
}
int main(void) {
int arr[10] = { 3,6,1,9,0,5,4,2,8,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
return 0;
}
对结构体数据排序:
struct Stu {
char name[20];
int age;
};
int sort_by_name(const void* e1, const void* e2) {
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
int sort_by_age(const void* e1, const void* e2) {
//升序
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int main(void) {
int i = 0;
/*int arr[10] = { 3,6,1,9,0,5,4,2,8,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
qsort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);*/
struct Stu s[3] = { {"张三",21},{"李四",20},{"王五",24} };
int sz = sizeof(s) / sizeof(s[0]);
// 按名字比较
/*qsort(s, sz, sizeof(s[0]), sort_by_name);
for (i = 0; i < sz; i++) {
printf("%s \n", s[i].name);
}*/
// 按年龄比较
qsort(s, sz, sizeof(s[0]), sort_by_age);
for (i = 0; i < sz; i++) {
printf("%d \n", s[i].age);
}
return 0;
}
模仿qsort实现一个冒泡排序的通用算法:
void swap(char* buf1, char* buf2, int width) {
int i = 0;
for (i = 0; i < width; i++) {
char temp = *buf1;
*buf1 = *buf2;
*buf2 = temp;
buf1++;
buf2++;
}
}
void bubble_sort(int* base, int sz, int width, int (*compar)(const void*, const void*)) {
int i = 0;
int j = 0;
// 冒泡排序的趟数
for (i = 0; i < sz - 1; i++) {
// 一趟冒泡排序的次数
for (j = 0; j < sz - 1 - i; j++) {
// 比较
if (compar((char*)base + j * width, (char*)base + (j + 1) * width) > 0) {
// 交换
swap((char*)base + j * width, (char*)base + (j + 1) * width, width);
}
}
}
}
// 整形数据排序
void test1(void) {
int arr[10] = { 3,6,1,9,0,5,4,2,8,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz, sizeof(arr[0]), cmp_int);
print_arr(arr, sz);
}
void test2(void) {
int i = 0;
struct Stu s[3] = { {"张三",21},{"李四",20},{"王五",24} };
int sz = sizeof(s) / sizeof(s[0]);
bubble_sort(s, sz, sizeof(s[0]), sort_by_age);
for (i = 0; i < sz; i++) {
printf("%d\n", s[i].age);
}
}
void test3(void) {
int i = 0;
struct Stu s[3] = { {"张三",21},{"李四",20},{"王五",24} };
int sz = sizeof(s) / sizeof(s[0]);
bubble_sort(s, sz, sizeof(s[0]), sort_by_name);
for (i = 0; i < sz; i++) {
printf("%s\n", s[i].name);
}
}
int main(void) {
// 整形数据进行冒泡排序
// test1();
// 结构体数据根据年龄进行冒泡排序
// test2();
// 结构体数据根据姓名进行冒泡排序
test3();
return 0;
}
补充:
void:无类型
void*:无类型指针
void指针可以指向任意类型的数据,也就是说可以用任意类型的指针对void指针赋值,无需进行强制类型转换,例如:
int* a;
void* p;
p = a;
如果要将void指针p赋值给其它类型的指针,则需要进行强制类型转换 例:a = (int*)p;
在内存的分配中我们可以见到 void 指针使用:内存分配函数 malloc 函数返回的指针就是 void * 型,用户在使用这个指针的时候,要进行强制类型转换,也就是显式说明该指针指向的内存中是存放的什么类型的数据 (int *)malloc(1024) 表示强制规定 malloc 返回的 void* 指针指向的内存中存放的是一个个的 int 型数据
因为"无类型"可以包容"有类型",而"有类型"则不能包容"无类型"
void*的指针像个垃圾桶一样,什么类型的地址都可以放进去
void*不能解引用操作