C语言基础-指针(一篇文章看懂各种类型的指针)

C语言基础-指针(一篇文章看懂各种类型的指针)

指针

指针内存地址基地址取址符为什么引入指针

变量指针与指针变量指针变量的定义指针变量的使用两个有关运算符的使用

指针变量做函数参数

指针变量指向数组数组元素的指针指针的运算通过指针引用数组元素数组名作函数参数案例

数组指针与指针数组数组指针一维数组指针:二维数组指针:案例1案例2

指针数组

字符数组和字符指针字符串实现案例:

字符数组和字符指针的联系案例

字符串作为形式参数案例案例

函数指针与指针函数函数指针案例

回调函数概念为什么用回调函数回调函数的作用工作原理示例异步回调的示例**应用场景**优点与注意事项总结

指针函数定义语法案例

二级指针说明定义定义格式举例举例

main函数原型指针案例常量指针与指针常量常量:分为字面量和只读常量

常量指针指针常量常量指针常量

动态内存分配常用函数malloccallocreallocfree

野指针、空指针、空悬指针野指针空指针空悬指针

void与void *的区别voidvoid *

内存操作常用内存操作函数内存填充内存拷贝内存比较内存查找

指针

内存地址

字节:字节是内存的容量单位,英文名byte,一个字节有8位,即1byte = 8bits地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存空间,简称地址

基地址

单字节数据:对于单字节数据而言,其地址就是其字节编号多字节数据:对于多字节数据而言,其地址是其所有字节字节中编号最小的那个,称为基地址(首地址)

取址符

每个变量都是一块内存,都可以通过取址符&获取其地址

int a = 100;

printf("整型变量a的地址是:%p\n",&a);

char c = 'x';

printf("整型变量c的地址是:%p\n",&c);

注意:

虽然不同的变量的尺寸是不同的,但是它们的地址的尺寸是一致的不同的地址虽然形式上看起来是一样的,但由于它们代表的内存尺寸和类型都不同,因此它们在逻辑上是严格区分的

为什么引入指针

为函数修改实参提供支持

为动态内存管理提供支持

为动态数据结构(链表,队列等)提供支持

为内存访问提供另一种途径

变量指针与指针变量

内存地址:系统为了内存管理方便,将内存划分为一个个内存单元(通常是1个字节),并为每一个内存单元进行编号。内存单元的编号称之为该内单元的地址。一般情况下,每一个变量都是由多个内存单元构成的,多于每个变量的内存单元,其实就是这个变量对应的第一个内存单元的地址,也叫基地址/首地址

**变量指针:**变量地址称之为该变量的指针(本质是地址)。变量地址往往是变量在内存中第一个内存单元的编号(首地址)

int a;

&a --- 变量a的地址,也称为变量a的指针

int arr[2];

&arr --- 数组arr的地址,也称为数组arr的指针

指针就是地址,地址就是指针

指针变量:专门存放指针的变量(本质上变量),简单来说,用来存放地址的变量就是指针变量

指向:指针变量存放谁的地址,就说明该指针变量指向了谁

指针的尺寸

指针的尺寸指的是指针所占内存的字节数

指针所占内存,取决于地址的长度,而地址的长度则取决于系统寻址范围,即字长

结论:指针尺寸只跟系统的字长有关系,跟具体的指针类型无关

Linux 系统中打印地址时,最多显示12个十六进制数,为什么?

Linux64位操作系统中,一个内存地址占8个字节,一个字节8bit位,所以一个地址8*8=64bit位,每4个bit可以表示1个十六进制数;64个bit位用十六进制表示最多有16个数值位;

系统为了寻址方便,默认当前计算机系统没必要寻址64bit位,只寻址了48个bit位,所以用12个十六进制数表示一个地址

二进制: 01001010 十六进制: 0x4A --> 4*16+10=74

在Linux64位操作系统中,指针类型的变量占8个字节的内存空间在Linux32位操作系统中,指针类型的变量占4个字节的内存空间

在C语言中对内存数据(如变量、数组元素等)的存取有两种方式:

直接存取:

通过基本类型(整型、浮点型、字符型)的变量,访问这个变量代表的内存空间的数据

通过数组元素的引用,访问这个引用代表的内存空间的数据

int a = 10; // 存

printf("%d",a); // 取

int arr[] = {11,22,33}; //存

arr[0] = 66; //取

printf("%d",arr[0]); //存

间接存取:

通过指针变量,间接的访问内存中的数据。 #include

int main()

{

//定义一个普通变量

int i = 3;

//定义一个指针变量,并赋值

int *i_point = &i;

//访问普通变量(直接访问)

printf("直接访问-%d\n",i);

//访问指针(地址访问)

printf("地址访问-%p,%p\n",i,i_point);

//访问指针变量(间接访问)

printf("间接访问-%d\n",*i_point);

}

指针变量的定义

语法:

数据类型 *变量列表;

举例:

int a; //普通变量,拥有真实的数据存储空间

int *a_,*b_; //指针变量,无法存储数据,只能存储其他变量的地址

注意:

虽然定义指针变量*a,是在变量名前加上*,但是实际变量名依然为a,而不是*a

使用指针变量间接访问内存数据时,指针变量必须要有明确的指向

如果想要借助指针变量间接访问指针变量保存的内存地址上的数据,可以使用指针变量前加上*来间接访问。指针变量前加上*,称之为对指针变量解引用

int i = 5,*p;

p = &i; //将i的地址赋值给指针变量p

printf("%x,%p\n",p,p); //两个都是打印地址,%x打印的地址不带0x,%p打印的地址带0x

printf("%d,%d\n",i,*p);

*p = 10;

printf("%d,%d\n",i,*p);

指针变量只能指向同类型的变量,借助指针变量访问内存,一次访问的内存大小是取决于指针变量的类型

指针变量在定义时可以初始化,与普通变量一样

int i = 5;

int *p = &i; //将i的地址赋给指针变量p

printf("%d\n",*p);

指针变量的使用

指针变量的赋值

//方式1

int a, *p;

p = &a;

//方式2

int a, *p, *q = &a;

p = q;

操作指针变量的值

int a,*p,*q = &a;

p = q;

printf("%p",p);

操作指针变量指向的值

int a = 6, *q = &a, b = 25;

*q = 10;

printf("%d,%d\n",*q,a);

q = &b;

*q = 15;

printf("%d,%d\n",*q,b);

两个有关运算符的使用

&取地址运算符,&a是变量a的地址*指针运算符(或称之为"间接访问"运算符,“解引用符”),*p是指针变量p指向的对象的值

例子:

通过指针变量访问整型变量

#include

void main()

{

int a = 3,b = 4,pointer_1 = &a,pointer_2 = &b;

printf("a=%d,b=%d\n",*pointer_1,*pointer_2);

}

指针变量做函数参数

指针变量做函数参数往往传递的是变量的地址(首地址),借助于指针变量间接访问是可以修改实参变量数据的

案例1

需求:要求函数处理,用指针变量做函数的参数

方式1:交换指向(指向的普通变量的值不变)

#include

//自定义一个函数,实现两个数的比较

void swap(int *p_a,int *p_b)

{

int *p_t;

//这种写法只会改变指向,不会改变对应空间的数据

p_t = p_a;

p_a = p_b;

p_b = p_t;

printf("%d > %d\n",*p_a,*p_b);

}

void main()

{

int a = 3, b = 5;

if(a < b)

swap(&a,&b);

else

printf("%d > %d\n",a,b);

}

方式2:交换数据(指向的普通变量的值改变)

#include

//自定义一个函数,实现两个数的比较

void swap(int *p_a,int *p_b)

{

int t;

//这种写法会改变对应空间的数据

t = *p_a;

*p_a = *p_b;

*p_b = t;

printf("%d > %d\n",*p_a,*p_b);

}

void main()

{

int a = 3, b = 5;

if(a < b)

swap(&a,&b);

else

printf("%d > %d\n",a,b);

}

指针变量指向数组

数组元素的指针

数组的指针就是数组中第一个元素的地址,也就是数组的首地址

数组元素的指针是指数组的首地址。因此同样可以用指针变量来指向数组或数组元素

在C语言中,由于数组名代表数组的首地址,因此,数组名实际上也是指针

//定义了一个普通数组

int a[] = {11,22,33};

//使用指针变量存储数组的第一个元素的首地址,也就是数组的首地址

int *p1 = &a[0];

//在C语言中,由于数组名代表数组的首地址,因此,数组名实际上也就是指针

int *p2 = a;

printf("%p,%p,%p\n",p1,p2,a); //三个值一样

return 0;

注:虽然定义了一个指针变量接受了数组地址,但不能理解为指针变量指向了数组,而应该理解为指向了数组的元素

指针的运算

指针运算:指针变量必须要指向数组的某个元素

序号指针运算说明1自增:p++、++p、p=p+1|p+=1让指针变量指向下一个元素2自减:p–、–p、p=p-1|p-=1让指针变量指向上一个元素3加一个数:p+1下一个元素(首)地址4减一个数:p-1上一个元素(首)地址5指针相减:p1-p2p1、p2之间相差几个元素6指针比较:p1

说明:

①如果指针变量p已指向数组中的一个元素,则p+1指向同一数组中的下一个元素,p-1指向同一数组中的上一个元素。即p+1或p-1也表示地址。但要注意的是,虽然指针变量p中存放的是地址,但p+1并不代表该地址加1,而表示在原地址的基础上加了该数据类型所占的字节数d

②如果p原来指向的a[0],执行++p后p的值改变了,在p的原值基础上加d,这样p就指向数组的下一个元素a[1],d是数组元素占的字节数

③如果p的初值为&a[0],则p+i和a+i就是数组元素a[i]的地址,或者是,它们指向a数组的第i个元素

④*(p+i)或*(a+i)是p+i或a+i所指向的数组元素,即a[i]

⑤如果指针变量p1和p2都指向同一数组,如执行p2-p1,结果是两个地址之差除以数组元素的长度d

通过指针引用数组元素

引用一个数组元素,可以用:

① 下标法:如a[i]形式

② 指针法:*(a+i)或者 *(p+i),其中,a是数组名,p是指向数组元素的指针变量,其初值:p=a;

案例:

需求:有一个整型数组a,有10个元素,输出数组中的全部元素

分析:要输出各元素的值,三种方法:

下标法:通过改变下标输出所有元素

#include

void main()

{

int arr[10];

int i;

//给数组赋值

for(i = 0; i < 10; i++)

{

scanf("%d",&arr[i]);

}

//遍历数组

for(i = 0; i < 10; i++)

{

printf("%-4d",arr[i]);

}

printf("\n");

}

指针法(地址法):通过数组名计算出数组元素的地址,找出数组元素值

#include

void main()

{

int arr[10];

int i;

//给数组赋值

for(i = 0; i < 10; i++)

{

scanf("%d",&arr[i]);

}

//遍历数组

for(i = 0; i < 10; i++)

{

printf("%-4d",*(arr+i));

}

printf("\n");

}

指针法(指针变量):用指针变量指向数组元素

#include

void main()

{

int arr[10];

int i,*p;

//给数组赋值

for(i = 0; i < 10; i++)

{

scanf("%d",&arr[i]);

}

//遍历数组

for(p = arr; p < (arr + 10); p++)

{

printf("%-4d", *p);

}

printf("\n");

}

以上三种写法比较:

第①种写法和第②种写法执行效率相同,系统是将arr[i]转换为*(arr+i)处理的,即先计算出地址,因此比较费时第③种方法比第①种、第②种方法快,用指针变量直接指向数组元素,不必每次都重新计算地址。(p++)能大大提高执行效率用第①种写法比较直观,而用地址法或者指针变量的方法难以很快判断出当前处理的元素

使用指针变量指向数组元素时(第③种),注意以下三点:

①*(p--)相当于arr[i--],先*p,再p--

②*(++p)相当于arr[++i],先++p,再*

③*(--p)相当于arr[--i],先--p,再*

数组名作函数参数

表现形式:

形参和实参都是数组名

void fun(int arr[],int len)

{}

void main()

{

int arr[] = {11,22,33};

fun(arr,sizeof(arr)/sizeof(arr[0]));

}

实参用数组名,形参用指针变量

void fun(int *p,int len)

{}

void main()

{

int arr[] = {11,22,33};

fun(arr,sizeof(arr)/sizeof(arr[0]));

}

实参形参都用指针变量

void fun(int *p,int len)

{}

void main()

{

int arr[] = {11,22,33};

int *p = arr;

fun(p,sizeof(arr)/sizeof(arr[0]));

}

实参为指针变量,形参为数组名

void fun(int arr[],int len)

{}

void main()

{

int arr[] = {11,22,33};

int *p = arr;

fun(p,sizeof(arr)/sizeof(arr[0]));

}

案例

数组翻转

#include

void inv(int arr[],int len)

{

//反转思路:将第0个和n-1个进行对调

int i;

for(i = 0; i < len / 2; i++)

{

int temp = arr[i];

arr[i] = arr[len - i - 1];

arr[len - i - 1] = temp;

}

}

void print_arr(int arr[],int len)

{

for(int i = 0; i < len; i++)

{

printf("%-4d",arr[i]);

}

printf("\n");

}

int main(int argc,char *argv[])

{

int arr[] = {11,22,33,44,55,66};

int len = sizeof(arr)/sizeof(arr[0]);

print_arr(arr,len);

inv(arr,len);

print_arr(arr,len);

return 0;

}

数组指针与指针数组

数组指针

概念:数组指针是指向数组的指针,本质上还是指针

特点:

现有数组,后有指针它指向的是一个完整的数组

一维数组指针:

语法:

数据类型 (*指针变量名)[容量];

案例:

#include

int main()

{

//一维数组指针

//先有数组,再有指针

int arr[] = {11,22,33};

//获取数组的元素个数

int len = sizeof(arr) / sizeof(arr[0]);

//定义一个数组,指向arr这个数组

int (*p)[3] = &arr; //此时p不是指向arr数组的第一个元素,而是指向arr数组本身

printf("%p\n",p);

//p++; 此时p++会跳出整个数组,访问到一块未知内存,程序种尽量避免这种写法

//printf("%p\n",p);

//如何访问数组指针

printf("%d\n",(*p)[2]); //33

//遍历

for(int i = 0; i < len; i++)

{

printf("%d\n",(*p)[i]);

}

}

之前的是指向数组元素的指针,本质上还是指针变量;现在是指向数组的指针,叫作数组指针。

二维数组指针:

语法:

数据类型 (*指针变量名)[容量]

案例:

#include

int main(int argc,char *argv[])

{

int arr[][3] = {10,20,30,100,200,300,1000,2000,3000};

int (*p)[3] = arr;

printf("2000-%d,%d,%d\n",*(*(p+2)+1),*(p[2]+1),p[2][1]);

return 0;

}

指针和数组种符合优先级:() > [] > *

通过指针引用多维数组

案例1

需求:用指向元素的指针变量输出二维数组元素的值

void arr_fun1()

{

//定义一个普通的二维数组

int a[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23};

int (*p)[4] = a[0];

for(;p < a[0] + 12;p++)

{

if((p - a[0]) % 4 == 0 && p != a[0])

printf("\n");

printf("%-4d",*p);

}

printf("\n");

}

案例2

需求:数组指针-输出二维数组任一行任一列元素的值

/**

*需求:数组指针-输出二维数组任一行任一列的元素

*/

void arr_fun2()

{

//定义一个二维数组

int arr[3][4] = {1,3,5,7,9,11,13,15,17,19,21,23};

//创建一个一维的数组指针指向一个二维的数组

int (*p)[4] = arr;//等价与&arr[0]

int row,col;

printf("请输入行号和列号:\n");

scanf("%d%d",&row,&col);

row--;

col--;

printf("arr[%d][%d] = %d\n",row,col,*(*(p+row)+col));

}

指针数组

特点:

先有指针,后有数组指针数组的本质是一个数组,只是数组中的元素类型为指针

语法:

数据类型 *数组名[容量]

案例:

#include

int main(int argc,char *argv[])

{

//定义三个变量

int a = 10,b = 20,c = 30;

//定义指针数组,用来存放指针

int *arr[3] = {&a,&b,&c};

printf("%d\n",*arr[1]);

return 0;

}

字符数组和字符指针

字符串实现

在C语言中,表示一个字符串有以下两种形式:

1. 用字符数组存放一个字符串 1. 用字符指针指向一个字符串

案例:

#include

/**

* 方式1:使用字符数组实现字符串

*/

void str_test1()

{

char str[] = "I love you";

printf("%s\n",str);

}

/**

* 方式2:使用字符指针实现字符串

*/

void str_test2()

{

char *str = "I love you";

printf("%s\n",str);

}

int main()

{

str_test1();

str_test2();

}

注: 字符数组和字符指针变量都能实现字符串的存储与运算(字符指针->字符类型的指针变量)

字符数组和字符指针的联系

字符数组由元素组成,每个元素中存放一个字符,而字符指针变量中存放的是地址,也能作为函数参数。只能对字符数组中的各个元素赋值,而不能用赋值语句对整个字符数组赋值

char arr[3];

arr[2] = 'A'; //正确,对字符数组中的元素赋值

arr = {'A','B','C'}; //错误(理解为数组名就是一个常量,一旦创建,不能改变)

字符数组名虽然代表地址,但数组名的值不能改变,因为数组名是常量对于字符串中字符的存取,可以用下标法,也可以用指针

案例

#include

int main()

{

char str1[] = "你好!";

char str2[] = "你好!";

str1 = "Hello!"; //错误,不能对字符数组整体赋值,使用strcpy()!

str2 = "Hello!";

printf("%s,%s\n",str1,str2);

char a[] = "I love you!";

char *b = "I love you!";

printf("%c,%c,%c,%c\n",a[2],*(a+2),b[2],*(b+2));

return 0;

}

字符串作为形式参数

实参与形参都可以是字符数组

void fun(char str[],int len){..}

void main()

{

char str[] = "hello";

fun(str,sizeof(str) / sizeof(str[0]));

}

实参用字符数组,形参用字符指针

void fun(char *str,int len){

str[2] = 'A';

}

void main()

{

char str[] = "hello";

fun(str,sizeof(str) / sizeof(str[0]));

}

实参和形参都是指针变量(在函数内部不能对字符串中的字符做修改)

void fun(char *str,int len)

{

str[2] = 'A';

}

void main()

{

char *str = "hello";

fun(str,sizeof(str) / sizeof(str[0]));

}

实参是指针类型,形参是字符数组(在函数内部不能对字符串中的字符做修改)

void fun(char str[],int len)

{

str[2] = 'A';

}

void main()

{

char *str = "hello";

fun(str,sizeof(str) / sizeof(str[0]));

}

注:

字符数组在创建的时候,会在内存中开辟内存空间,内存空间可以存放字符数据;字符指针在创建的时候,需要依赖于字符数组,字符指针在内存开辟的内存空间中,存放的是数组元素的地址。字符指针的创建依赖于字符数组,字符数组可以独立存在,而字符指针不能独立存在字符数组可以初始化,但不能赋值;字符指针可以初始化,也可以赋值

案例

#include

/**

* 定义函数,实现字符串的拷贝,返回字符串的长度

*/

int my_strcpy(char *source,char *target)

{

int i = 0;

while(source[i] != '\0')

{

*(target+i) = *(source+i);

i++;

}

target[i] = '\0';

return i;

}

int main(int argc,char *argv[])

{

char source[20],target[20];

printf("输入一个字符串:\n");

scanf("%s",source);

int len = my_strcpy(source,target);

printf("%s,%s,%d\n",source,target,len);

return 0;

}

案例

#include

/**

* 设计一个函数,截取字符串中从start到end之间的字符串,含头不含尾

*/

int str_split(char *source,int start,int end,char *target)

{

int i = 0, k = 0;

while(source[i] != '\0')

{

if(i >= start && i < end)

{

target[k] = source[i];

k++;

}

i++;

}

target[k] = '\0';

return k;

}

int main(int argc,char *argv[])

{

char *str = "abcdefgh";

char target[100];

int len = str_split(str,2,5,target);

printf("%s,%s,%d\n",str,target,len);

return 0;

}

函数指针与指针函数

函数指针

定义:函数指针本质上时指针,它时函数的指针(定义了一个指针变量,变量中存储了函数的地址)。函数都有一个入口地址,所谓指向函数的指针,就是指向函数的入口地址。函数名就代表入口地址。

函数指针存在的意义:

让函数多了一种调用方式函数指针作为形参,可以形式调用(回调函数)

定义格式:

返回值类型 (*变量名) (形式参数列表);

举例:

int (*p)(int a,int b);

函数指针的初始化:

定义的同时赋值

//函数指针需要依赖于函数,先有函数,后有指针

int add(int a,int b)

{

return a+b;

}

//定义一个函数指针,并给他赋值

int (*p)(int a,int b) = add;

//通过上述代码,可以发现:函数指针的返回类型和依赖函数的返回类型一致,函数指针的参数个数类型和以来函数一致

先定义后赋值

int add(int a,int b)

{

return a+b;

}

int (*p)(int,int);

p = add;

注:

函数指针指向的函数要和函数指针定义的返回值类型,形参列表对应,否则编译报错函数指针是指针,但不能指针运算,如p++等,没有实际意义函数指针作为形参,可以形成回调函数指针作为形参,函数调用时的实参只能是与之对应的函数名,不能带小括号()函数指针的形参列表的变量名可以省略

案例

#include

int max(int a,int b)

{

return a>b?a:b;

}

int main(int argc,char *argv[])

{

//定义测试数据

int a = 3,b = 2,c;

//函数调用

c = max(a,b);

printf("两数最大是%d\n",c);

//定义一个函数指针

int (*p)(int,int) = max;

//间接函数调用

c = p(a,b);

printf("两数最大是%d\n",c);

c = (*p)(a,b);

printf("两数最大是%d\n",c);

return 0;

}

注:

在调用函数指针时,有两种方式

p(a,b)

直接使用指针变量p调用函数编译器知道p时一个指向函数的指针,因此将其视为一个函数调用简洁常用这种方式 (*p)(a,b)

使用解引用运算符*明确表示对指针p的解引用,以获取函数地址,然后调用函数由于*p是函数本身,(*p)(a,b)被解析为函数调用

在C标准中

函数指针在调用时会自动解引用,p(a,b)会隐式地处理为(*p)(a,b)函数名本身也可以看作一个指向函数的指针,类似于数组名与数组指针的关系

因此,p(a,b)与(*p)(a,b)两者等价

回调函数

概念

回调函数就是一个通过函数指针调用的函数。如果把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行相应

为什么用回调函数

因为可以把调用者与被调用者分开,所以调用者不关心谁是被调用者。只需知道存在一个具有特定原型和限制条件的被调用函数

回调函数就是允许用户把需要调用的方法的指针作为参数传递给一个函数,以便该函数在处理相似实践的时候可以灵活使用不同的方法。

回调函数的作用

解耦代码逻辑

回调函数将逻辑与具体实现分离,使代码更加灵活和可扩展。提供了一个统一的接口,允许用户自定义处理方式。 提高代码的通用性

主函数实现通用逻辑,而回调函数负责特定的行为。不需要在主函数中写死具体的逻辑。 实现动态行为

主函数在运行时动态调用回调函数,允许程序根据运行时状态执行不同的操作。 支持异步操作

在异步编程中,回调函数可以在特定事件完成时被调用(如文件读取、网络请求)。 简化事件驱动编程

事件驱动系统中,回调函数被用来响应特定事件,如按钮点击或数据到达。

工作原理

定义一个函数指针,指向特定的函数。将该函数指针作为参数传递给另一个函数。在主函数或框架中,通过调用该函数指针来执行回调逻辑。

示例

以下是一个简单的回调函数实现:

#include

// 定义回调函数类型

typedef int (*Callback)(int, int);

// 一个通用的计算函数,接收一个回调函数

int compute(int a, int b, Callback cb) {

return cb(a, b);

}

// 两个具体的回调函数

int add(int a, int b) {

return a + b;

}

int multiply(int a, int b) {

return a * b;

}

int main() {

int x = 5, y = 10;

// 使用加法作为回调

printf("Sum: %d\n", compute(x, y, add));

// 使用乘法作为回调

printf("Product: %d\n", compute(x, y, multiply));

return 0;

}

输出结果:

Sum: 15

Product: 50

异步回调的示例

回调函数在异步操作中非常重要,例如模拟一个异步事件完成后的回调:

#include

#include

#include

// 回调函数

void onComplete(int result) {

printf("Callback received with result: %d\n", result);

}

// 模拟异步操作的函数

void* asyncOperation(void* arg) {

void (*callback)(int) = arg;

printf("Async operation started...\n");

sleep(2); // 模拟耗时操作

printf("Async operation completed!\n");

callback(42); // 调用回调函数

return NULL;

}

int main() {

pthread_t thread;

// 创建线程,执行异步操作

pthread_create(&thread, NULL, asyncOperation, onComplete);

sleep(1);

printf("Doing other work while waiting...\n");

// 等待线程完成(可选,根据需求)

pthread_join(thread, NULL);

return 0;

}

输出结果:

Async operation started...

Doing other work while waiting...

Async operation completed!

Callback received with result: 42

应用场景

事件处理

如 GUI 程序中的按钮点击、鼠标移动等事件的响应。 异步编程

文件读写、网络通信等耗时操作完成后,调用回调函数处理结果。 算法的扩展

如排序算法中,自定义比较函数通过回调实现灵活性。 框架与库设计

提供统一的接口,用户可以通过回调函数实现自己的逻辑。 信号与中断处理

嵌入式系统中,硬件信号或中断触发时调用预设的回调函数。

优点与注意事项

优点:

提高代码的复用性和灵活性。

实现解耦,让代码更具模块化。

支持异步编程和事件驱动机制。

注意事项

函数指针的正确性

确保回调函数指针有效,否则可能引发未定义行为。 类型匹配

回调函数的签名(参数和返回值)必须与函数指针声明一致。 线程安全

在多线程环境下,回调函数的使用需要注意同步问题。

总结

回调函数是 C 语言中一种强大的机制,用于动态调用用户定义的函数。它可以提高程序的灵活性,特别是在框架设计、事件驱动编程和异步操作中,是不可或缺的工具。

指针函数

定义

本质上是函数,这个函数的返回值类型是指针,称之为指针函数

语法

指针类型 函数名(形参列表)

{

函数体;

retrun 指针变量;

}

举例:

int *get(int a)

{

int *b = &a;

return b;

}

int main()

{

int *a = get(5);

printf("%d\n",a);

}

注:

在函数中不能直接返回一个局部变量的地址,因为函数调用完毕后,局部变量会被回收,使得返回的地址不明确,此时的指针就是野指针

解决方案:

给局部变量添加static,延长它的生命周期,从而避免野指针(少用,存在内存泄漏)

案例

#include

/**

* 定义一个函数,传入学生的序号,返回这个学生的所有课程成绩

* @param p 二维数组

* @param n 学生索引

* @return 学生成绩(行号对应的列数组)

*/

float *search(float (*p)[4],int n)

{

//定义一个指针,用来接收查询到的某个学生的所有课程

float *pt;

pt = *(p+n);

return pt;

}

int main(int argc,char *argv[])

{

float score[][4] = {60,70,80,89,55,66,77,88,90,89,90,91};

int m;

float *p;

printf("请输入学生序号(0~2):\n");

scanf("%d",&m);

printf("第%d个学生的成绩:\n",m);

//用来接收某个学生的所有成绩

p = search(score,m);

//遍历成绩

for(int i = 0; i < 4; i++)

printf("%5.2f\t",*(p+i));

printf("\n");

return 0;

}

二级指针

说明

指针除了一级指针,还有多级指针,但是开发中很少用到三级及以上的指针,三级指针本质上用法与二级指针差不多

定义

二级指针,又被称为多重指针,引用一级指针的地址,此时这个指针变量就得定义成二级指针。

int a = 10;

int *p = &a;

int **q = &p;

int ***w = &q;

定义格式

数据类型 **变量名 = 指针数组的数组名或者一级指针的地址

举例

//指针数组

int arr = {11,22,33};

int *arr_ = {&arr[0],&arr[1],&arr[2]};

//一级指针

int a = 10;

int *p = &a;

//二级指针和指针数组

//字符型指针数组,本质上是一个二维的char数组

char *str[3] = {"abc","aaabbc","123a21"};

//如果要用一个指针变量来接收,就需要一个二级指针

char **p_ = str; // str指向的是首元素地址

//二级指针和二维数组

int arr[2][3] = {1,2,3,11,22,33};

int **k = arr;

int a = 90; //变量

int *p = &a; //p存放a的地址

int **p = &p; //k存放p的地址

printf("%p,%p,%p\n",k,*k,**k);

结论:

二级指针和指针数组是等效,和二维数组不等效二维数组和数组指针是等效,和二级指针不等效

二级指针的用法

如果是字符的二级指针,可以像遍历字符串数组一样遍历它如果是其他的二级指针,需要解引用两次访问它指向的数据所指向的数据

举例

#include

/**

* 二级指针:使用指向指针数组的指针变量

*/

void fun1()

{

//字符指针

char *name[] = {"Follow me","BASIC","Great Wall","For","Computer"};

//定义二级指针

char **p;

//定义循环变量

int i = 0;

do{

p = name + i;

printf("%s\n",*p);

i++;

}while(i < 5);

printf("\n");

}

void fun2()

{

int arr1[5] = {11,22,33,44,55};

//创建一个指针数组

int *arr[] = {&arr1[0],&arr1[1],&arr1[2],&arr1[3],&arr1[4]};

int **p = arr;

int i = 0;

for(;i < 5; i++)

{

printf("%-5d",*(*p+i));

//也可以使用以下方法读取数据

//printf("%-5d",**p);

//p++;

}

printf("\n");

}

int main(int argc,char *argv[])

{

fun1();

fun2();

return 0;

}

main函数原型

定义

main函数由多种定义格式,main函数也是函数,函数相关的结论对main函数也有效(也可以定义main函数的函数指针)

main函数的完整写法:

int main(int argc,char *argv[]){}

int main(int argc,char **argv){}

拓展写法:

main(){} ==> int main(){}

int main(void){}

void main(){}

void main(void){}

int main(int a){}

int main(int a,int a,int c){}

...

说明:

argc,argc是形参,可以修改

main函数的拓展写法有些编译器不支持,会报警告

argc和argv的常规写法

argc:存储了参数的个数

argv:存储了所有参数的字符串形式

#include

int main(int argc,char *argv[])

{

printf("argc = %d\n",argc);

int i = 1;

for(;i < argc; i++)

{

printf("%s,%s\n",argv[i],*(argv+i));

}

return 0;

}

指针案例

如果一个函数需要返回数组,建议将这个函数定义成指针函数(返回值为指针的函数)

如果一个被调函数需要接收主调函数传递一个非char类型的数组,建议被调函数的参数用数组指针

#include

/**

* 每个学生有四门成绩

* 定义一个函数,取出某一个学生的成绩

*/

float *get_score(float (*arr)[4],int n)

{

return arr[n];

}

int main(int argc,char *argv[])

{

float scores[3][4] = {66,67,78,88,99,89,78,86,56,78,67,57};

//取出第01个学生的成绩

float *p = get_score(scores,1);

//取出01个学生的第02个成绩

printf("%5.2f\n",*(p+2));

return 0;

}

如果一个被调函数的参数是一个字符数组{“aaa”,“bbb”…},建议将参数类型定义为字符指针数组char *arr[]或者字符二级指针char **arr

#include

//需求:用一个指针数组,存储一组字符串,要求写一个函数,取出数组中的字符串

char *get_str(char **p,int n)

{

return *(p+n);

}

int main(int argc,char *argv[])

{

char *arr[3] = {"hello","Hi","nihao"};

char *str = get_str(arr,2);

printf("%s\n",str);

return 0;

}

如果需要将一个函数作为另一个函数的形参,建议将该函数的形参用函数指针表示

#include

int add(int a,int b){return a+b;}

int sub(int a,int b){return a-b;}

int jisuan(int a,int b,int (*calculate)(int,int))

{

printf("开始计算:\n");

return calculate(a,b);

}

int main()

{

int a = 5,b = 3;

int ret1 = jisuan(a,b,add);

printf("%d\n",ret1);

int ret2 = jisuan(a,b,sub);

printf("%d\n",ret2);

}

常量指针与指针常量

常量:分为字面量和只读常量

字面量:直接操作的常量

只读常量:使用const修饰,凡是被这个关键字修饰的变量,一旦复制,就不能改变

语法:

//字面量

printf("%d\n",12);

//只读常量

const int a = 10;

a = 21; //错误!!这个变量是只读变量,不可更改其值

常量指针

定义:常量的指针,本质上是一个指针,指针指向的数据不能改变

格式:

const 数据类型 *变量名

结论:

常量指针指向的数据不能被改变(不能解引用间接修改数据)常量指针的地址可以被改变(指向是可以改变)

应用场景:作为形式参数时,实际参数需要给一个常量

#include

int main()

{

int a = 10;

const int *p = &a;

*p = 100; //错误,常量的值不能被改变

printf("%d\n",*p);

int b = 20;

p = &b; //正确,指向的地址能改变

printf("%d\n",*p);

}

指针常量

定义:指针的常量,指针的指向不能改变

格式:

数据类型* const p

结论:

指针常量的指向不能改变(不能给指针变量重新赋地址值)指针常量的指向的数据可以改变

注:指针变量在定义时就要赋值;不能先定义后赋值,否则编译报错

#include

int main()

{

int a = 10;

int* const p = &a;

*p = 100; //正确,指向的值可以改变

printf("%d\n",*p);

int b = 20;

p = &b; //错误,指向的地址不能改变

printf("%d\n",*p);

}

常量指针常量

语法:

const 数据类型* const 变量名

举例:

const int* const p;

作用:p的指向不能被改变(地址不可更改),p指向的数据不能改变(地址对应的数据不可更改)

动态内存分配

要想实现动态内存分配,就需要学习标准C提供的函数库(API):

函数所属的库文件函数的原型-函数的声明

函数名形参返回值类型 函数功能

注:内存分配函数在申请内存时,建议用多少申请多少,可以有少量的预留量;但不能越界访问(即使不报错,也会产生野指针,导致不安全)

常用函数

malloc

头文件:#include 函数功能:C库函数void *malloc(size_t size);分配所需的内存空间,并返回一个指向它的指针函数原型:

函数名:malloc形式参数:size_t size:需要申请的内存块的大小,以字节为单位。本质上是一个unsigned long int返回值类型:void *(万能指针):该函数返回一个指针,指向已分配大小的内存,如果请求失败,返回NULL(0x000000000000:对应一块不可访问的区域) 举例

int *p = (int *)malloc(sizeof(int));

//清零,不是释放内存,而是将内存中的随机值清理掉

bzero(p,sizeof(int));

//释放内存

free(p);

说明

malloc函数分配的内存没有默认值,内存中的数据是随机值(大概率是0),使用前需要借助bzero()清零malloc函数申请的内存空间连续

calloc

头文件:#include

函数功能:C库函数void *calloc(size_t nitems,size_t size);分配所需的内存空间,并返回一个指向它的指针

malloc和calloc之间的区别:malloc不会设置内存为0,需要使用bzero()清零,而calloc会设置内存为0

函数原型:

函数名:calloc形式参数:

size_t nitems:申请多少个size_t size:一个占几个内存单元(一个内存单元==一个字节) 返回值类型:void *(万能指针):该函数返回一个指针,指向已分配大小的内存,如果请求失败,返回NULL(0x000000000000:对应一块不可访问的区域) 举例

int *p = (int *)calloc(1,sizeof(int));

//释放内存

free(p);

说明

calloc函数分配的内存有默认值,每个内存单元都是0calloc函数申请的内存空间连续calloc大多数时候为数组中的元素申请内存 案例:

#include

#include

/**

* 需求:转存栈中数组中的数据

*/

int main(int argc,char *argv[])

{

//在栈区创建一个数组

int arr[3] = {11,22,33};

//在堆区申请内存

int *p = (int *)calloc(3,sizeof(int));

//转存

for(int i = 0; i < 3; i++)

p[i] = arr[i];

for(int i = 0; i < 3; i++)

printf("%-4d",p[i]);

printf("\n");

free(p);

p = NULL;

printf("p = %p\n",p);

return 0;

}

realloc

头文件:#include

函数功能:尝试重新调整之前调用malloc或calloc所分配的ptr所指向的内存块的大小

函数原型:void *realloc(void *ptr,size_t size);

函数名:realloc形式参数:

void *ptr:是malloc或者calloc的返回值size_t size:重新分配后的内存大小 返回值类型:void *(万能指针):该函数返回一个指针,指向已分配大小的内存,如果请求失败,返回NULL(0x000000000000:对应一块不可访问的区域) 举例

int *p = (int *)malloc(4);

int *w = (int *)realloc(p,20);

//int *q = (int *)realloc(p,0); ==>等效于free(0);

说明

realloc以原来malloc返回的内存地址开始,分配总共20个字节的内存空间如果原来的内存空间后有20个连续空间,就扩容16个内存空间,返回原来旧的内存首地址如果原来的内存空间后不够20个连续内存空间,就重新找一个内存地址开始,申请20个内存单元。并将原来的数据拷贝到新的内存中,回收旧的内存单元,并返回新的内存首地址

free

头文件:#include 函数功能:释放之前调用malloc、calloc、realloc所分配的内存空间,访问完使用NULL置空函数原型:void free(void *ptr);

函数名:free形式参数:

void *ptr:是malloc或者calloc的返回值 返回值类型:void:没有返回值 注意:

堆内存中的指针才需要回收,栈中系统会自动回收堆内存不能重复回收,运行会报错

说明:

堆的内存空间相比较栈要大很多内存分配函数返回的指针变量可以参与运算(只读),但不能被修改(p++或者p+=i是错误的)

野指针、空指针、空悬指针

野指针

定义:指向一块未知区域(已经销毁或者访问首先的内存区域外的已存在或不存在的内存区域)的指针,被称为野指针,野指针是危险的。

危害:

引用野指针,相当于访问了非法的内存,常常会导致段错误(segmentation fault),也有可能编译运行不报错引用野指针,可能会破坏系统的关键数据,导致系统奔溃等严重后果

野指针产生的场景:

变量未初始化,通过指针访问该变量

int a;

int *p = &a; //p是野指针

printf("%d\n",*p); //访问野指针,数据不安全

指针变量未初始化

int *p; //p是野指针

printf("%d\n",*p); //访问野指针,数据不安全

int a = get();

p = &a;

指针指向的内存空间被(free)回收了

int *p = malloc(4);

*p = 12; //p不是野指针

free(p);

printf("%d\n",*p); //p是野指针,访问野指针,数据不安全

指针函数中直接返回了局部变量的地址

int *get_num()

{

int a = 15;

int *p = &a;

return p;

}

main()

{

int *p = get_num(); //此时p是野指针

}

如何避免野指针?

指针变量要及时初始化,如果暂时没有对应的值,建议赋初值NULL数组操作(遍历和指针运算)时,注意数组的长度,避免越界指针指向的内存空间被回收,建议给这个指针变量赋初值为NULL

int *p = (int *)malloc(10);

free(p);

p = NULL;

指针变量使用之前要检查它的有效性(非空检验)

int *p = NULL;

if(!p)

{

return -1;

}

空指针

很多情况下,不可避免的会遇到野指针,比如刚定义的指针无法立即为其分配一块恰当的内存,又或者指针指向的内存已经被释放了。一般的做法时将这些危险的野指针指向一块确定的内存,比如零地址内存(NULL)

定义:空指针即保存了零地址的指针(赋值为NULL的指针),也就是指向零地址的指针。(NULL是空常量,它的值为0,这个NULL一般存放在内存0x000000000000的位置,这个地址只能存NULL,不能被其他程序修改)

示例:

//1.刚定义的指针,让其指向零地址以确保安全

char *p1 = NULL;

int *p2 = NULL;

//2.被释放了内存的指针,让其指向的零地址以确保安全

char *p3 = malloc(100);

free(p3);

p3 = NULL;

空悬指针

在C语言中,悬空指针值的是指向已删除(或释放)的内存位置的指针。如果一个指针指向的内存已经被释放,但指针本身并未重新指向其他有效的内存地址,那么这个指针就变成了悬空指针。悬空指针会引发不可预知的错误,并且如果一旦发生,就很难定位,因此在编程中尽量避免使用悬空指针。

char *p3 = malloc(100);

free(p3);

printf("%p,%c\n",p3,*p3); //地址依然可以访问,但地址对应的原本数据不可访问

void与void *的区别

定义:

void:是空类型,数据类型的一种void *:是指针类型,是指针类型的一种,可以匹配任意类型的指针,类似于通配符,又被叫做万能指针。

void

说明:void作为返回值类型使用,表示没有返回值;作为形参,表示形参列表为空,在调用的时候不能给实参

举例

//函数定义

void fun(void){...}

//函数调用

fun();

void *

说明:void *是一个指针类型,但该指针的数据类型不明确,无法通过解引用获取内存中的数据,因为void *不知道访问几个内存单元

void *是一种数据类型,可以作为函数返回值类型,也可以作为形参类型

void *类型的变量在使用之前必须强制类型转换,明确它能够访问几个自己的内存空间

int *p = (int*)malloc(4);

举例:

#include

#include

//函数定义

void *fun(void *p)

{

int *p;

return p;

}

//函数调用

{

int m = 10;

int *p = &m;

void *a = fun(p);//这种接收方式,实际上没有意义,推荐使用:int *a = (int *)fun(p);

printf("%p\n",a);//可以正常打印,打印出一个地址

*a = 10;//编译报错,void *变量不能解引用访问数据

int *w =(int *)a;

*w = 10;//编译和运行正常,void *变量a在使用前已经强制类型转换了,数据类型明确了,访问的内存单元明确了

}

说明:

void *作为返回值类型,这个函数可以返回任意类型的指针void *作为形参类型,这个函数在调用时,可以给任意类型的指针 总结

void *类似于通配符,不能对void *类型的变量解引用(因为不明确数据类型,所以无法确定内存单元的大小)void *在间接访问(解引用)前要强制类型转换,但不能太随意,否则存和取的数据类型不一致

内存操作

对于内存的操作需要依赖于string库(对应的头文件string.h)完成内存的操作

常用内存操作函数

内存填充

头文件:#include 函数原型:void *memset(void *s,int c,size_t n)函数功能:填充s开始的堆内存前n个字节,使得每个字节值为c函数参数:

void *s:代操作内存首地址int c:填充的字节数据size_t n:填充的字节数 返回值:返回s注意:c常常设置为0,用于动态内存初始化

#include

#include

#include

int main(int argc,char *argv[])

{

//在堆内存申请空间

int *p = (int *)malloc(4 * sizeof(int));

if(!p)

{

perror("内存申请失败!");

return -1;

}

//给内存初始化操作

memset(p,0,4 * sizeof(int));

printf("%d\n",*(p+1));

free(p);

p = NULL;

return 0;

}

内存拷贝

头文件:#include

函数原型:void *memcpy(void *dest,const void *,size_t n);适合目标地址与源地址内存无重叠情况。

​ void *memmove(void *dest,const void *src,size_t n);

函数功能:拷贝src开始的堆内存空间前n个字节,到dest对应的内存中

函数参数:

void *dest:目标内存首地址void *src:原内存首地址size_t n:拷贝的字节数 返回值:返回dest

注意:

内存申请了几个内存空间,就访问了几个内存空间,否则数据不安全

memcpy与memmove一般情况下时一样的,更建议使用memmove进行内存拷贝。

因为memmove函数是自适应(从后往前或者从前往后)拷贝,当被拷贝的内存和目的地的内存有重叠时,数据不会出现拷贝错误。而memcpy函数是从前往后拷贝,当被拷贝的内存和目的地内存有重叠时,数据会出现拷贝错误。

案例

#include

#include

int main(int argc,char *argv[])

{

//创建源空间和目标空间

int src[4] = {11,22,33,44};

int dest[6] = {11,222,333,444,555,666};

//将src中的数据拷贝到dest中

memmove(dest+1,src+1,2 * sizeof(int));

printf("源数组-src:\n");

for(int i = 0; i < 4; i++)

printf("%-5d",src[i]);

printf("\n目标数组-dest:\n");

for(int i = 0; i < 6; i++)

printf("%-5d",dest[i]);

printf("\n");

return 0;

}

内存比较

头文件:#include

函数原型:void *memcmp(void *dest,const void *src,size_t n);

函数功能:比较src和dest所代表的内存前n个字节的数据

函数参数:

void *dest:目标内存首地址void *src:源内存首地址size_t n:比较的字节数 返回值:

0:数据相同>0:dest中的数据大于src<0:dest中的数据小于src 注意:n一般和src、dest的总容量一致,如果不一致,内存比较的结果就不确定了

案例

#include

#include

#include

int main(int argc,char *argv[])

{

int *src = (int *)malloc(3*sizeof(int));

int *dest = (int *)calloc(4,sizeof(int));

if(!src || !dest)

{

perror("内存申请失败!");

return -1;

}

//对使用malloc申请的空间清零

bzero(src,3*sizeof(int));

*src = 65;

*(src + 1) = 66;

*dest = 70;

*(dest + 1) = 5;

int result = memcmp(dest,src,2*sizeof(int));

char *a = (char *)src;

char *b = (char *)dest;

int result2 = memcmp(b,a,sizeof(char));

printf("%d,%d\n",result,result2);

free(dest);

free(src);

src = NULL;

dest = NULL;

return 0;

}

内存查找

头文件:#include

函数原型:void *memchr | *memrchr(const void *s,int c,size_t n);

函数功能:在s开始的堆内存空间前n个字节中查找字节数据c

函数参数:

const void *s:待操作内存的首地址int c:待查找的字节数据size_t n:查找的字节数 返回值:返回查找到的字节数据地址

注意:如果内存中没有重复数据,memchr和memrchr结果是一样的;如果内存中有重复数据,则不一样

注意:

void *memrchr(); //在使用的时候编译报错,需要使用外部声明

//外部声明

extern void *memrchr();

案例1

#include

#include

#include

//申请外部定义的函数

extern void *memrchr(const void *s,int c,size_t n);

int main(int argc,char *argv[])

{

//在堆内存申请空间

int *s = (int *)calloc(4,sizeof(int));

if (!s)

{

perror("内存申请失败!");

return -1;

}

//给变量赋值

for (int i = 0; i < 4; i++)

{

s[i] = i*2;

}

s[3] = 2;

//输出

for (size_t i = 0; i < 4; i++)

{

printf("%d\n",s[i]);

}

printf("\n");

//内存查找memchr,匹配首个查到的位置

int *x = (int *)memchr(s,2,4*sizeof(int));

printf("%p,%p,%d\n",x,s,*x);

//内存查找memrchr,匹配末个查到的位置

int *y = (int *)memrchr(s,2,4*sizeof(int));

printf("%p,%p,%d\n",y,s,*y);

free(s);

s = NULL;

return 0;

}

案例2

#include

#include

#include

extern void *memrchr(const void *s, int c, size_t n);

int main(int argc, char *argv[])

{

char *s = (char *)calloc(4, sizeof(char));

if (!s)

{

perror("内存申请失败!");

return -1;

}

// 给变量赋值

for (int i = 0; i < 4; i++)

{

s[i] = i + 65;

}

s[3] = 66;

// 输出

for (int i = 0; i < 4; i++)

{

printf("%c\n", s[i]);

}

printf("\n");

//内存查找memchr,匹配首个查到的位置

int *x = (int *)memchr(s,'B',4*sizeof(char));

printf("%p,%p,%c\n",x,s,*x);

//内存查找memrchr,匹配末个查到的位置

int *y = (int *)memrchr(s,'B',4*sizeof(char));

printf("%p,%p,%c\n",y,s,*y);

free(s);

s = NULL;

return 0;

}

相关推荐