目录
一. 指针 - 进阶
1.1 指针和数组
指针就是指针,指针变量在64 位操作系统下,永远占8个byte,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方都能通过这个指针变量访问到。
数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存任何类型的数据,但不能存函数。
以指针的形式访问 和 以下标的形式访问
(A) char *p = "abcdef";
(B) char a[] = "123456";
① 以指针的形式访问和以下标的形式访问指针
(A) 中定义了一个指针变量 p,p本身在栈上占8个 byte,p 里存储的是一块内存的首地址。这块内存在静态区,这块内存也没有名字。对这块内存的访问完全是匿名的访问。比如现在需要读取字符‘e’
我们有两种方式:
以指针的形式:*(p+4)。先取出p 里存储的地址值,假设为0x0000AA00,然后加上 4 个字符的偏移量,得到新的地址 0x0000AA04。然后取出 0x0000AA04地址上的值。以下标的形式:p[4]。p[4]这个操作会被解析成:先取出 p 里存储的地址值,然后加上中括号中 4个元素的偏移量,计算出新的地址,然后从新的地址中取出值。
也就是说以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了。
② 以指针的形式访问和以下标的形式访问数组
(B)定义了一个数组 a,a拥有7 个 char类型的元素,其空间大小为7。数组 a 本身在栈上面。对a的元素的访问必须先根据数组的名字 a找到数组首元素的首地址,然后根据偏移量找到相应的值。这是一种典型的“具名+匿名”访问。比如现在需要读取字符‘5’
我们有两种方式:
以指针的形式:*(a+4)。a 这时候代表的是数组首元素的首地址,假设为 0x0000FF00,然后加上 4个字符的偏移量,得到新的地址0x0000FF04。然后取出 0x0000FF04地址上的值。
以下标的形式:a[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操作。a[4]这个操作会被解析成:a作为数组首元素的首地址,然后加上中括号中 4个元素的偏移量,计算出新的地址,然后从新的地址中取出值。
总结:
指针和数组根本就是两个完全不一样的东西。只是它们都可以“以指针形式”或“以下标形式”进行访问。一个是完全的匿名访问,一个是典型的具名+匿名访问。
1.1.1 指针和数组的区别
1. 名字上的区别(数组名,变量名)
char str[32] = "helloworld"; //在栈空间中开辟一个32字节的空间(可读可写数据区),可以被修改 char *p = "helloworld"; //在栈空间中开辟一个8字节的空间并指向一个字符串(可读数据区) str++; //数组名为常指针(常量指针,不能被修改),即会报错 //若++,则是下一个32字节的空间(不存在) p++; //指向下一个元素(e)
2.在修改上的区别
str[0] = 'x'; p[0] = 'x'; //p指向的是一个字符串(常量),不能被修改
3.所占字节的区别
printf("%lu\r\n",sizeof(str)); //数组所占的字节(32),不是数组元素的个数 printf("%lu\r\n",sizeof(p)); //指针所占的字节(8)
4.数组作为参数,变为指针
void function(char a[]) { //会出警告,因为数组作为参数传进来,变为指针 //所以 (a所占的字节(指针))8 / (元素所占的字节)1 printf("%lu\r\n",sizeof(a) / sizeof(a[0])); } function(str);
1.2 数组指针 和 指针数组
“数组指针” 和 “指针数组”,只要在名词中间加上 “的” 字,就变成:
数组的指针:是一个指针,什么样的指针呢?指向数组的指针。
指针的数组:是一个数组,什么样的数组呢?装着指针的数组。
需要明确优先级顺序:() > [] > *,所以:
(*p)[n]:
根据优先级,先看括号内,则p是一个指针,这个指针指向一个一维数组,数组长度为n,这是“数组的指针”,即为数组指针;
*p[n]:
根据优先级,先看[],则p是一个数组,再结合 * ,这个数组的元素是指针类型,共n个元素,这是“指针的数组”,即为指针数组。
int *p1[5];
int (*p2)[5];
对于语句 int *p1[5] ,因为 [] 的优先级要比 * 要高,所以 p1 先与 [] 结合,构成一个数组的定义,数组名为 p1,而 int * 修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针,因此,它是一个指针数组。
对于语句 int(*p2)[5] ,() 的优先级比 [] 高, * 号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。
1.2.1 数组指针 (*p)[n]
数组指针:是指针——指向数组的指针。
数组指针是一个指针变量,占内存中一个指针的存储空间;
#include "stdio.h"
int main()
{
//定义一个一维数组
int a[5] = { 1, 2, 3, 4, 5 };
//定义一个步长为5的数组指针,即数组里有5个元素
int (*p)[5];
//把数组a的地址赋给p,则p为数组a的地址,则*p表示数组a本身
p = &a;
//在C中,几乎所有使用数组的表达式中,数组名的值是个指针常量,
//也就是数组第一个元素的地址,它的类型取决于数组元素的类型。
printf("%p\n", a); //数组名,一般用数组的首元素地址来标识一个数组,则输出数组首元素地址
printf("%p\n", p); //p为数组a的地址,所以输出数组a的地址
printf("%p\n", *p); //*p表示数组a本身,一般用数组的首元素地址来标识一个数组
printf("%p\n", &a[0]); //a[0]的地址
printf("%p\n", p[0]); //数组首元素的地址
printf("%d\n", **p); //*p为数组a本身,即为数组a首元素地址,则*(*p)为值,即为1
printf("%d\n", *p[0]); //p[0] 表示首元素地址,则*p[0]表示首元素本身,即首元素的值1
printf("%d\n", *p[1]); //为一个绝对值很大的负数,不表示a[1]...表示什么我还不知道
//定义一个二维数组
int b[3][4];
//定义一个数组指针,指向含4个元素的一维数组
int(*q)[4];
q = b; //将该二维数组的首地址赋给pp,也就是b[0]或&b[0],二维数组中q = b和 q = &b[0]是等价的
q++; //q = q+ 1,该语句执行过后q的指向从行b[0][]变为了行b[1][],q=&b[1]
return 0;
}
1.2.2 指针数组 *p[n]
指针数组:是数组——装着指针的数组。
指针数组是多个指针变量,以数组的形式存储在内存中,占有多个指针的存储空间。
#include "stdio.h"
int main()
{
int a = 6;
int b = 8;
//定义一个指针数组
int *p[2];
//将a的地址,存放在p[0]中
p[0] = &a;
//将b的地址,存放在p[1]中
p[1] = &b;
printf("%p\n", &a); //a的地址
printf("%p\n", p[0]); //a的地址
printf("%p\n", &b); //b的地址
printf("%p\n", p[1]); //b的地址
printf("%d\n", *p[0]); //p[0]表示a的地址,则*p[0]表示a的值
printf("%d\n", *p[1]); //p[1]表示b的地址,则*p[1]表示b的值
int *q[3]; //一个一维数组内存放着三个指针变量,分别是q[0]、q[1]、q[2],所以要分别赋值
int arr[3][4]; //定义一个二维数组
for (int i = 0; i < 3; i++)
q[i] = arr[i];
return 0;
}
1.3 指针函数 和 函数指针
“指针函数” 和 “函数指针”,只要在名词中间加上 “的” 字,就变成:
指针的函数: 是一个函数,什么样的函数呢? 返回指针的函数。
函数的指针: 是一个指针,什么样的指针呢? 指向函数的指针。
需要明确优先级的顺序: () > *, 所以:
int *fun(int x,int y):
在函数名前面多了一个*号,这个函数就是一个指针函数。其返回值是一个 int 类型的指针。
int (*fun)(int x,int y):
在函数名前面多了一个*号,并加上(),这个函数就是一个函数指针。
1.3.1 指针函数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*认识指针函数
*就是函数返回指针类型的函数
*切忌局部变量不能返回(栈内存)
* */
char *string()
{
//char str[32]; 由于该变量为栈内存,函数执行完该变量就会被释放
char *str = (char*)malloc(128); //堆内存,函数执行完,不会被释放
return str;
}
int main()
{
char *s = string();
strcpy(s,"hello");
free(s); //释放堆内存,防止内存泄露
s = NULL;
return 0;
}
一般为了方便使用,我们会选择:
typedef 函数返回值类型 (* 指针变量名) (函数参数列表);
注意:
在调用指针函数时,需要一个同类型的指针来接收其函数的返回值。
1.3.2 函数指针
#include <stdio.h>
typedef int (*T)(int,int); //声明一个函数指针类型T,T表示函数指针类型
/*认识函数指针*/
int add(int x,int y)
{
return x+y;
}
int main()
{
int (*q)(int,int); //定义一个函数指针
q = add; //函数和类型需与定义的函数指针相互兼容
printf("%d\r\n",q(3,2)); //利用函数指针调用函数
T q1 = add;
printf("%d\r\n",q1(1,6));
return 0;
}
1.4 回调函数
函数指针的调用,顾名思义是一个通过函数指针调用的函数;
把函数的指针(地址)作为参数,传递给另一个函数,当这个指针被用来调用,其所指向的函数时,就说这是回调函数。
即:
把一段可执行的代码像参数传递那样传给其他代码,而这段代码会在某个时刻被调用执行,就叫做回调。
如果代码立即被执行就称为同步回调;如果在之后晚点的某个时间再执行,则称为异步回调。
举例:
想象一下,家里有一个电饭煲,你想要煮熟一锅米饭。你打开电饭煲,设置好煮饭的时间和火候,然后你可以继续做其他的事情。
这时候,电饭煲就开始工作了。它会按照你设置的时间和火候煮饭,并且在饭煮熟后发出一个提示,比如“嘀嘀”的声音。
这里的回调函数就好像是“嘀嘀”的声音的功能。当电饭煲煮饭完成后,它会调用这个回调函数,以通知你饭已经煮好了。
换句话说,回调函数就是一个预先定义好的函数,你把它作为参数传递给另一个函数(比如电饭煲),当某个特定事件发生时,被调用的函数会执行你定义的代码。这个过程就好比是电饭煲在煮好饭后调用你定义的回调函数来提醒你。
1.4.1 为什么要使用回调函数?
回调函数作用:“解耦”,普通函数代替不了回调函数的这个特点。这是回调函数最大的特点。
“解耦”:指将程序中的模块或组件之间的依赖关系减少或消除,使得它们能够独立地进行修改、扩展或替换,而不会对其他部分产生过多的影响。
1.4.2 回调函数 与 普通函数调用区别
① 在主入口程序中,把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,且不需要修改库函数的实现,变的很灵活,这就是解耦。
② 主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况。
注:
使用回调函数会有间接调用,因此,会有一些额外的传参与访存开销,对于MCU代码中对时间要求较高的代码要慎用。
利用冒泡排序,实现升序排序和降序排序的代码示例:
#include <stdio.h>
/*回调函数就是一个被作为参数传递的函数
*在C语言中,回调函数只能使用函数指针实现*/
int less(int x,int y)
{
return (x > y) ?1:0;
}
int greater(int x,int y)
{
return (x < y) ?1:0;
}
void sort(int *a,int length,int (*p)(int,int))
{
int i,j;
for(i=0;i<length-1;i++)
{
for(j=0;j<length-i-1;j++)
{
if(p(a[j],a[j+1])) //利用回调函数,在冒泡排序中,实现从大到小或从小到大排序的功能
{
int t = a[j];
a[j] = a[j+1];
a[j+1] = t;
}
}
}
}
int main()
{
int a[10] = {0};
int i;
for(i = 0; i < 10;i++)
{
scanf("%d",&a[i]);
}
sort(a,10,greater); //将数组名,数组长度,函数名作为实参
for(i = 0; i < 10;i++)
{
printf("%d ",a[i]);
}
printf("\r\n");
}