C语言:函数指针、二级指针、常量指针常量、野指针
Day 17-C语言
指针
函数指针
定义
函数指针本质上是指针,是一个指向函数的指针。函数都有一个入口地址,所谓指向函数的指针,就是指向函数的入口地址(函数名代表入口地址)
函数指针存在的意义:
- 让函数多了一种调用方式
- 函数指针可以作为形参,可以形式调用(回调函数)
- 遵循:先有函数,再有指针
语法:
返回类型 (*指针变量名)(形参列表);
举例:
int (*fun)(int a,int b);
int (*fun)(int,int);// 类似函数声明
函数指针的初始化
①定义的同时赋
//函数指针需要依赖于函数,先有函数,后有指针//定义一个普通函数
int add(int a,int b) {return a + b;}//定义一个函数指针,并初始化
//观察:函数指针的返回类型和指向函数的返回类型一致,函数指针的形参个数、类型、位置和指向函数的参数一致。
int (*p)(int a,int b) = add // 函数指针p指向函数add,这里的add不能带(),add就代表函数的入口地址
②先定义,后赋值
//定义一个普通函数
int add(int a,int b) {return a + b;}int (*p)(int,int); // 形参列表的函数名可以省略不写p = add; // 此时是将add的入口地址赋值给指针p
注意:
- 函数指针指向的函数要和函数指针定义的返回值类型,形参列表对应,否则编译报错
- 函数指针是指针,但不能指针运算,如p++等,没有实际意义
- 函数指针作为形参,可以形成回调
- 函数指针作为形参,函数调用时的实参只能是与之对应的函数名,不能带小括号()
- 函数指针的形参列表中的变量名可以省略
案例
- 需求:
- 代码
#include <stdio.h>int get_max(int a,int b)
{return a > b ? a : b;
}int main(int argc,char *argv[])
{//定义测试数据int a = 3,b = 4,max;//直接调用函数max = get_max(a,b);printf("%d,%d中最大的值是:%d\n",a,b,max);//定义一个函数指针int (*p)(int,int) = get_max;//间接调用-方式1max = p(a,b);printf("%d,%d中最大的值是:%d\n",a,b,max);//间接调用-方式2max = (*p)(a,b);printf("%d,%d中最大的值是:%d\n",a,b,max);return 0;
}
回调函数
定义
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时。我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
简单来说,就是使用函数指针作为函数的形参,这种函数就被称为回调函数。
为什么要用回调函数
因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。它只需知道存在一个具有特定原型和限制条件的被调用函数。
简而言之,回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似事件的时候可以灵活的使用不同的方法。
案例
#include <stdio.h>/*** 回调函数1*/
int callback_1(int a)
{printf("hello, this is callback_1:a=%d\n", a);return a;
}/*** 回调函数2*/
int callback_2(int b)
{printf("hello, this is callback_2:b=%d\n", b);return b;
}/*** 回调函数3*/
int callback_3(int c)
{printf("hello, this is callback_3:c=%d\n", c);return c;
}/*** 实现回调函数(形参是函数指针的函数)*/
int handle(int x, int (*callback)(int))
{printf("日志:开始执行任务!\n");int res = callback(x);printf("日志:执行结果:%d\n",res);printf("日志:结束执行任务!\n");
}int main(int argc,char *argv[])
{handle(100,callback_1);handle(200,callback_2);handle(300,callback_3);return 0;
}
二级指针
定义
二级指针(多重指针)用于储存一级指针的地址,需要两次解引用才能访问原始数据。其他多重指针的用法类似,但实际开发中最常见的指针是二级指针。
int a = 10; // a是普通变量,也就是原始数据
int *p = &a; // 一级指针,p指向a,解引用1次就可以获取a的值
printf("%d\n", *p); // 10int **w = &p; // 二级指针,w指向p,解引用2次就可以获取a的值
printf("%d\n", **w);// 10int ***k = &w; // 三级指针,k指向w,解引用3次就可以获取a的值
printf("%d\n", ***k); // 10 int a1 = ***k; int *a2 = **k; int **a3 = *k; int ***a4 = k;
语法
数据类型 **指针变量名 = 指针数组的数组名 | 一级指针的地址
特点
① 与指针数组的等效性 二级指针与指针数组在某些时候存在等效性,但与二维数组不等效。二维数组名是数组指针类型,如int (*)[3]
,而非二级指针。
// 指针数组
int arr[] = {11,22,33};
int *arr_[] = {&arr[0],&arr[1],&arr[2]};// 二级指针接收指针数组
char *str[3] = {"abc","aaa034","12a12"};
char **p = str; // p:数组首地址,行地址,默认0行 *p:列地址,默认0行0列 **p:列元素#include <stdio.h>int main(int argc,char *argv[])
{char *str[3] = {"abc","aaa034","12a12"};char **p = str;// 打印字符串// for (int i = 0; i < 3; i++)// {// printf("%s\n", *p);// p++;// }// 打印字符int i = 0;while(**p != '\0'){printf("%-2c",**p);(*p)++;}printf("\n");return 0;
}
② 与二维数组的差异 二维数组名是数组指针类型,直接赋值给二级指针会导致类型不匹配
// 数组指针可以指向一个二维数组
int arr[2][3] = {{1,3,5},{11,33,55}};
int (*p)[3] = arr;// 二级指针不等效二维数组
int **k = arr; // 编译报错 arr类型 int(*)[3] 不兼容 k类型 int**
解引用
① 字符型二级指针 可直接遍历字符串数组,类似一维数组操作:
#include <stdio.h>void fun1()
{// 定义一个字符类型的指针数组(字符串数组)char *arr[] = {"orange","apple","grape","banana","kiwi"};int len = sizeof(arr) / sizeof(arr[0]); // int len = 5 * 8(指针) / 8(指针) = 5for (int i = 0; i < len; i++) printf("%s\n", arr[i]); printf("\n");}void fun2()
{char *arr[] = {"orange", "apple", "grape", "banana", "kiwi"};int len = sizeof(arr) / sizeof(arr[0]);// 此时二级指针完全等价于指针数组char **p = arr; // p 指向 arr的首元素,也就是orangefor (int i = 0; i < len; i++){// printf("%s\n", p[i]); // 下标法printf("%s\n", *(p + i));// 指针法}printf("\n");
}void fun3()
{char *arr[] = {"orange","apple","grape","banana","kiwi"};int len = sizeof(arr) / sizeof(arr[0]);char **p;int i = 0;// 遍历数组do{p = arr + i; // arr代表行,+i,此时是行偏移,返回的行地址 p指向字符串printf("%s\n", *p);// 对行地址解引用得到列地址 int a = 10; int *p = &a;i++;} while (i < len);printf("\n");
}int main(int argc,char *argv[])
{fun1();fun2();fun3();return 0;
}
注意:如果需要一个字符串类型的数组,我们可以选择使用二级指针或者指针数组,此时两者完全等价。
② 其他类型的二级指针 需要两次解引用访问数据,常用于操作指针数组
#include <stdio.h>int main()
{// 创建一个一维数组int arr1[] = {11,22,33,44,55,66}; // 11:0x11// 创建一个指针数组int *arr[] = {&arr1[0],&arr1[1],&arr1[2],&arr1[3],&arr1[4],&arr1[5]}; // [0]:0x22 --> 0x11// 用一个二级指针接收指针数组int **p = arr; // p 指向 arr,p存储的arr第一个元素的地址// 遍历数组for(int i=0;i<sizeof(arr)/sizeof(arr[0]);i++){printf("%-6d", *p[i]); // 下标法(1.指针偏移,2.对新指针解引用)printf("%-6d", **(p+i));// 指针法 p+i 元素地址偏移 元素地址,对元素地址解引用,返回元素值(11..对应的地址)}printf("\n");
}
总结
类型 | 本质 | 内存布局 | 等效性 |
---|---|---|---|
二级指针(int** ) | 指向指针的指针 | 指针的指针 | 与指针数组等效 |
指针数组(int*[] ) | 元素为指针的数组 | 分散的指针 | 退化为二级指针 |
二维数组(int[][3] ) | 数组的数组 | 连续的数据块 | 数组指针(int(*)[3] ) |
main函数原型
定义
main函数有多种定义格式,main函数也是函数,函数相关的结论对main函数也有效。
main函数的完整写法:
int main(int argc, char *argv[]){..}int main(int argc, char **argv){..}
扩展写法:
main(){} 等价 int main(){} // C11之后不再支持 缺省 返回类型int main(void){} 等价 int main(){}void main(void){} 等价 void main(){}int main(int a){}int main(int a, int b, int c){}...
说明
① argc,argv是形参,他们俩可以修改
② main函数的扩展写法有些编译器不支持,编译报警告
③ argc和argv的常规写法
- argc:存储了参数的个数,默认是1个,也就是运行程序的名字
- argv:存储了所有参数的字符串形式
④ main函数是系统通过函数指针的回调调用。
演示
代码:
#include <stdio.h>int main(int argc, char **argv) // {"abc","aaa"} 对行地址解引用,得到首列地址
{// 访问参数个数 argcprintf("argc=%d\n", argc);// 遍历参数(每一个参数都是一个字符串常量)for(int i=0;i< argc; i++){printf("%s,%s\n", argv[i], *(argv+i));}printf("\n");
}
常量指针与指针常量
常量类型
① 字面量:直接使用固定值(如:12,hello,orange, 杨家辉三角),符号常量和枚举在编译器转换为了字面量
② 只读常量:用const
修饰的变量,初始化之后不可修改。
const int a = 10; // 只读常量
a = 21; // 编译报错
常量指针
-
本质:指向常量数据的指针
-
语法:
const 数据类型 *变量名; const 数据类型* 变量名;
-
举例:
const int *p; // p是常量指针
-
特性:
- 指向对象的数据不可改变(
int a = 10; const int *p = &a; *p = 20;
,非法) - 指针本身的指向可以改变(
int a = 10, b = 20; const int *p = &a; p = &b;
,合法)
- 指向对象的数据不可改变(
-
案例:
#include <stdio.h>int main() {int a = 10; // 变量const int *p = &a; // 常量指针// *p = 100; // 错误,指针指向的数据不可改变printf("%d\n", *p);// 10int b = 20; // 变量p = &b; // 正确,指针指向可以改变printf("%d\n", *p);// 20 }
指针常量
-
本质:指针本身是常量,指向固定地址
-
语法:
数据类型* const 变量名; 数据类型 *const 变量名;
-
特性:
- 指向对象的数据可以改变(
int a = 10; int* const p = &a; *p = 20;
,合法) - 指针本身的指向不可改变(
int a = 10, b = 20; int* const p = &a; p = &b;
,非法)
- 指向对象的数据可以改变(
-
注意:
定义时必须初始化:
int a = 10; int* const p = &a; // 正确
-
案例:
#include <stdio.h>int main() {int a = 10; // 变量int* const p = &a; // 指针常量*p = 100; // 正确,指针指向的数据可以改变printf("%d\n", *p);// 100int b = 20; // 变量// p = &b; // 错误,指针指向不可改变printf("%d\n", *p);// 100 }
常量指针常量
-
本质:指针指向和指向对象的数据都不可改变
-
语法:
const 数据类型* const 变量名; const 数据类型 *const 变量名;
-
举例:
const int* const p; // p是常量指针常量
-
特性:
- 指向对象的数据不可改变(
int a = 10; int* const p = &a; *p = 20;
,非法) - 指针本身的指向不可改变(
int a = 10, b = 20; int* const p = &a; p = &b;
,非法)
- 指向对象的数据不可改变(
-
注意:
定义时需要初始化:
int a = 10; const int *const p = &a; // 正确
简单理解:不管是常量指针、指针常量还是常量指针常量,本质上都是一个赋值受到限制的指针变量。
总结对比
类型 | 语法 | 指向可变 | 数据可变 |
---|---|---|---|
常量指针 | const int *p | ✔️ | ❌ |
指针常量 | int *const p | ❌ | ✔️ |
常量指针常量 | const int *const p | ❌ | ❌ |
关键点
const
在*
左侧:修饰数据(常量指针)const
在*
右侧:修饰指针(指针常量)- 函数参数优先使用常量指针,提高代码安全性
- 指针常量必须初始化,且不可重新指向
野指针、空指针、空悬指针
野指针
定义:
指向无效内存区域(比如未初始化、已释放或者越界访问)的指针称之为野指针。野指针会导致未定义(UB)行为。
危害:
- 访问野指针可能引发段错误(Segmentation Fault)
- 可能破坏关键内存数据,导致程序崩溃。
产生场景:
-
指针变量未初始化
int *p; // p未初始化,是野指针 printf("%d\n", *p); // 危险操作:p就是野指针
-
指针指向已释放的内存
int *p = malloc(sizeof(int)); // 在堆区申请1个int大小的内存空间,将该空间地址赋值给指针变量p free(p); // 释放指针p指向的空间内存 printf("%d\n", *p); // 危险操作:p就是野指针
-
返回局部变量的地址
int* fun(int a, int b) {int sum = a + b; // sum就是一个局部变量return ∑ // 将局部变量的地址返回给主调函数 }int main() {int *p = fun(2,3);printf("%d\n", *p); // 危险操作:p就是野指针 }
如何避免野指针:
-
初始化指针为NULL
-
释放内存后立即置指针为NULL
-
避免返回局部变量的地址
-
使用前检查指针有效性(非空校验,边界检查)。
int fun(int *pt) {int *p = pt;// 校验指针if(p == NULL) // 结果为假 等价于 if(!p) 其实底层: if(p == 0){printf("错误!");return -1;}printf("%d\n", *p);return 0; }
空指针
**定义:**值为NULL
的指针,指向地址0x000000000000
(系统保留,不可访问)
**作用:**明确表示指针当前不指向有效内存,一般用作指针的初始化。
示例:
int *p = NULL; // 初始化为空指针free(p); // 释放后置空
p = NULL;
空悬指针
**定义:**指针指向的内存已经被释放,但未重新赋值。空悬指针是野指针的一种特例。
示例:
char *p = malloc(100); // 在堆区分配100个char的空间给p
free(p); // 释放指针p指向的内存空间
printf("%p,%d\n", p, *p); // p可以正常输出,*p此时属于危险操作
// p指向的内存空间被回收,但是p指向空间的地址依然保留,此时这个指针被称作空悬指针
void与void*的区别
定义
-
void: 表示“无类型/空类型”,用于函数返回类型或者参数。
void func(void); // 没有返回值也没有参数,一般简写:void func();
-
*void:**通用指针类型(万能指针),可指向任意类型数据,但需要强制类型转换后才能解引用。
void* ptr = malloc(4); // ptr指向4个字节大小的堆内存空间 // 存放int类型数据 int *p = (int*)ptr; *p = 10;// 存放float类型数据 float* p1 = (float*)ptr; *p = 12.5f;// 存放char类型数组 char* p2 = (char*)ptr;// 以下写法完全错误 float* ptr = malloc(4); int *p = (int*)ptr; // 此时编译报错,类型不兼容 float* int*
注意:只能是具体的类型(
int*,double*,float*,char*...
)和void*
之间转换
注意事项
void
不能直接解引用(*ptr 会报错
)- 函数返回
void*
需要外部接收的时候明确类型(不明确类型,就无法解引用)
示例
#include <stdio.h>/*** 定义一个返回类型为void*类型的指针函数*/
void* proces_data(void* p)
{return p;
}int main(int argc, char *argv[])
{// int类型int m = 10;int* p_int = &m;int* result_int = (int*)proces_data(p_int);printf("Integer value:%d\n", *result_int);// double类型double pi = 3.1415926;double* p_double = πdouble* result_double = (double*)proces_data(p_double);printf("Double value:%lf\n", *result_double);// void* p_void = proces_data(p_double);// printf("Void value:%lf\n", *p_void);// *p_void = 20;// 注意:void* 修饰的指针是可以进行赋值操作的,但是不能对其解引用return 0;
}