首页 > 其他分享 >C语言-指针进阶详解(万字解析)

C语言-指针进阶详解(万字解析)

时间:2023-06-20 23:01:33浏览次数:29  
标签:arr 进阶 int void C语言 详解 数组 printf 指针


前言

本篇内容主要针对指针的进阶详解,如果不懂指针的含义要自行去看书看视频了解一下。


指针

指针是个特殊的变量,其功能就是来存放地址,地址唯一标识一块内存空间。指针的大小有两种一种是32位操作系统下的4个字节,一种是64位操作系统的8个字节。同时指针是有类型的,不同的类型决定了指针增量的步长,以及指针解引用操作所用操控的权限大小。


字符指针

给一个例子

void point_Ex1(){
char ch='a';
char *pc=&ch;
    printf("%c",*pc);// 输出结果为a
}

这里*pc就是一个字符类型的指针,字符类型的指针它所能访问到的内存空间就是一个字节


void point_Ex2() {
    int a = 0x11223361;
    char *pa = (char *) &a;
    printf("%c", *pa);//输出结果为 a 
    //只数除了最后一个字节 61那个字节 
}

本质上是个指针

假设我们在定义得时候这样定义

char *p ="abcde";

那么这样是指p里存了"abcde"吗?并不是 其实p里只存储了这个常量字符串的第一个字符 a

void point_Ex3() {

 char *p ="abcde";//"abcde"是一个常量字符串
    //*p指向的其实是这个字符串的第一个字符也就是a
    printf("%c\n",*p);//a
    printf("%c\n",*p+1);//b

}

之所以说是常量字符串是因为这个值是不可改的,如果我们此时对这个*p进行赋值 ‘C’看会出现什么结果。

void point_Ex4(){
    char *p="abc";
    *p='C';
    printf("%s\n",p);
}

答案是会报错的

C语言-指针进阶详解(万字解析)_回调函数

由此在引入一个问题:

void point_Ex5(){
    char arr1[]="abcdef";
    char arr2[]="abcdef";
    if(arr1 ==arr2 )
        printf("是");
    else
        printf("否");

    char *p1="abcdef";
    char *p2="abcdef";

    if(p1 == p2 )
        printf("是");
    else
        printf("否");
    /*
     * "abcdef"常量字符串
     * 不能修改
     * 这两个又是一摸一样的
     * 所以在内存中只存了一份
     * 
     */
}

这里两个答案会是什么呢?

输出的结果为 :否是

也就是说

char arr1[]="abcdef";

char arr2[]="abcdef";

是两个不同的地址空间,那么两个a的地址肯定是不同的 所以第一个输出为否

但是

char *p1="abcdef";

char *p2="abcdef";

我们上面说了 这里 p1 和p2存的是字符串

而且是常量字符串。那么既然是常量其就不可改,而且二者 有完全一致,所以内存为了节省空间其实就只存了一份,所以这两个地址是一样的指向同一个空间,所以 第二个判断if(p1 == p2 )的值就为真所以输出结果为 是 


指针数组

本质上是个数组,只不过这个数组中存储的都是指针。

简单的例子

void point_arr_Ex1(){
    int a=1,b=2,c=3,d=4,e=5;
    int  arr[10]={0};//int类型数组
    char ch[3]={0};//字符类型数组
    int * p_int[5]={&a,&b,&c,&d,&e};//存放整型指针的数组
    char * p_char[5]={ };//存放字符指针的数组
}

C语言-指针进阶详解(万字解析)_数组指针_02

可以看到p_int中存放了五个int类型的地址。

这个例子就是帮助大家理解指针数组,实际中不是这样用的。实际中怎么用呢

我们看下面这个例子。

void point_arr_Ex2(){
    int arr1[]={1,2,3};
    int arr2[]={4,5,6};
    int arr3[]={7,8,9};
    int * p_int[]={arr1,arr2,arr3};
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            printf("%d",*(p_int[i]+j));
            
        }
        printf("\n");
    }

}

 输出结果为:

C语言-指针进阶详解(万字解析)_回调函数_03

也就是说在实际中我们可以通过一个指针数组同时维护多个不同的数组

最后还有一点

int* p_int[3];//一级整型指针数组
int** pp_int[3];//二级指针数组

指针数组 也同时有一级指针和二级指针两种。 


数组指针

数组指针本质上是指针

根据整型指针可以存放整型的地址,字符指针是可以存放字符的地址,类推可以得出:数组指针其实是指向数组的指针也就是可以存放数组的地址

int arr[5]={0};

这里面:

arr -是首元素的地址

&arr[0] 也是首元素的地址

但是!!

&arr不是首元素的地址了而是数组的地址

我们给出一个例子来证明上述所说的

int main(){
    int  arr[10]={0};
    printf("arr=%p\n",arr);
    printf("&arr=%p\n",&arr);
    printf("arr+1=%p\n",arr+1);
    printf("&arr+1=%p\n",&arr+1);

}

输出结果:

C语言-指针进阶详解(万字解析)_函数指针_04

数组名+1差的是一个元素的大小4个字节

但是取地址数组+1 差的是一个数组的大小 也就是10*4=40,大家可以自行计算。


那假设我现在想要一个指向数组的指针来指向这个数组arr[5],应该怎么办?

写法是这样

void  arr_point_Ex1(){
     int arr[5]={1,2,3,4,5 };
     int (*p)[5]=&arr;
  //输出
  for (int i = 0; i < 5; ++i) {
        printf("%d",(*p)[i]);
    }
}

其中,*p是个int类型指针 后面跟个[5]代表了它是个指向了含有5个元素的数组的指针。当然 [5]中的5也可以省略不写,可以写成,最好写上。

int (*p)[]=&arr;

是同样的意思。

输出的时候如上述代码。

那么我现在给出一个问题:

void  arr_point_Ex2(){
    char *arr[5];
   pa=&arr;
}

请你给pa前面补充完成,让pa是个指向*arr数组的指针。应该怎么写呢?

char (*pa)[5]=&arr;//A
char* (*pa)[5]=&arr;//B
char (**pa)[5]=&arr;//C

我给出了三个写法 ABC,答案应该是哪一个呢?

答案是 B

不知道各位想对没有。

其实这里可以这样分析:首先&arr一定是个地址,既然是个地址我们一定要一个指针变量来存储所以首先是(*pa) 但是arr是个数组的地址 所以我们这个指针也应该是个数组指针 那么就应该是(*pa)[] 但是还没结束,我们观察到char *arr[5] 是个存放字符指针的数组,那么我们这个数组指针指向的应该也是个存放字符指针的数组所以最终答案应该是char *(*pa)[]=&arr

从答案的角度看:pa是个变量的标识也就是变量的名字。 *pa 说明pa是个指针。(*pa)[]说明pa指向的是个数组。最后前面的char * 指代的是(*pa)[]指向的这个数组中存放的事多个元素类型为 char *的元素。


在使用的时候,我们从arr_point_Ex1()发现,数组指针使用的时候不是很好理解也很别扭,你看这块输出的时候,

void  arr_point_Ex1(){
     int arr[5]={1,2,3,4,5 };
     int (*p)[5]=&arr;
  //输出
  for (int i = 0; i < 5; ++i) {
        printf("%d",(*p)[i]);
    }
}

使用的是(*p)[i] ,我们不如直接这样,不实用数组指针。

void  arr_point_Ex1(){
     int arr[5]={1,2,3,4,5 };
     int (*p)[5]=&arr;
  //输出
  //for (int i = 0; i < 5; ++i) {
    //    printf("%d",(*p)[i]);
    //}
  int *pa=arr;
  for(int i=0,i<10,i++){
  printf("%d ",*(p+i));
  }
}

这样似乎就可以了,但实际上数组指针使用的时候并不是这样用的,还要在深层次点。

一般情况下,数组指针用到二维数组才能够发现其作用。才能方便点。

给出例子

void Cu_print1(int arr[3][5],int x,int y){
    for (int i = 0; i < x; ++i) {
        for (int j = 0; j < y; ++j) {
            printf("%d ", arr[i][j]);
        }
        printf("\n");

    }

}

void  arr_point_Ex4() {
    int d_arr[3][5]={{1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15}};
    //我们要将其打印,写个函数 Cu_print1() 假设我们没学过数组指针
    Cu_print1(d_arr,3,5);

}

正常情况下我们要打印输出一个初始化好二维数组d_arr[3][5],我创建了个打印函数Cu_print1();

在这个打印函数中的逻辑将这个数组传递过去,开辟一个新的空间存储,然后两次for循环来分别输出每一个值。

但是我们现在知道了数组指针的用法应该怎么写呢?

void Cu_print2(int (*arr)[5],int x,int y){
    for (int i = 0; i < x; ++i) {
        for (int j = 0; j < y; ++j) {
            printf("%d ",*(*(arr+i)+j));
        }
        printf("\n");

    }
}

我们知道d_arr[3][5]是个二维数组,但是二维数组又是个特殊的一维数组

C语言-指针进阶详解(万字解析)_回调函数_05

d_arr 中其实是有三个元素,每个元素都是一个地址,每个地址又对应一个含有5个元素的一维数组,我们知道数组名代表了第一个元素的地址,所以 这个二维数组名 d_arr代表的并不是 元素1的地址 而是 元素「1 2 3 4 5」这个一维数组的地址 所以我们可以利用数组指针这样改写上面的Cu_print1();

void Cu_print2(int (*arr)[5],int x,int y){
    for (int i = 0; i < x; ++i) {
        for (int j = 0; j < y; ++j) {
            printf("%d ",*(*(arr+i)+j));
        }
        printf("\n");

    }
}

既然现在知道了 数组名d_arr代表的是 「1 2 3 4 5」的地址,那么这样就好办了。我们可以将这个地址传参到Cu_print2中,用一个含有五个int类型元素的数组指针int (arr)[5]来接收。那么这样我们可以用*(arr+i)打印i行,再加个j,即*(*(arr+i)+j)来打印j列,那这样就可以打印出这个二维数组 ,最后这两个函数输出结果都是

C语言-指针进阶详解(万字解析)_函数指针_06

但是上面这样做的输出结果也还是很麻烦呀,有没有更简单的表达方式,我们说有,怎么做呢?

我们先看下面这个简单的例子

void  arr_point_Ex3(){
    int arr[10]={1,2,3,4,5,6,7,8,9,10};
        int *p=arr;
    for (int i = 0; i < 10; ++i) {
        printf("%d ",arr[i]);//A
        printf("%d ",*(p+i));//B
        printf("%d ",*(arr+i));//C
        printf("%d ",p[i]);//D
        
    }
}

在这例子中,ABCD四种打印都是相同的内容,也就是说他们是等价的。

首先arr[i]我们最熟知的方式,我们令*p=arr;  p指向的就是arr这个地址指向的。那么arr+i可以代表第 i个数字,所以有了(p+i)就指向arr[i]。同时(arr+i)也指向第i个数字,那么这三个都等价了我们也就能够退出来,p[i]也能代表第i个数字。也就是说 这个p其实跟arr的所代表含义的是相同的

那么我们就可以改写 Cu_print2()这个函数

void Cu_print2(int (*arr)[5],int x,int y){
    for (int i = 0; i < x; ++i) {
        for (int j = 0; j < y; ++j) {
//            printf("%d ",*(*(arr+i)+j));
         //*(arr+i) 就可以写成 arr[i]
         //那 *(arr[i]+j) 是不是就等于arr[i][j]
         //所以 上面的式子可以直接写成
            printf("%d ", arr[i][j]);
        }
        printf("\n");

    }
}

这样是不是看着也很简洁明了。

之所以这样处理是因为,不用在内存中多开辟一段空间存储这个二维数组

 学懂了指针数组和数组指针给大家来一道题看一看到底搞懂没有

int *parr1[10] 
int (*parr2)[10] 
 int (*parr3[10])[5]


答案是

//int *parr1[10] 是个数组 存放了十个指针的数组
//int (*parr2)[10] 是个指针 指向的是拥有10个int类型元素的数组
// int (*parr3[10])[5] 是个数组 ,存放了十个指针的数组,这十个指针分别指向十个数组  每个数组含有五个int类型元素的 。

int *parr1[10] 是个数组 存放了十个指针的数组

int (*parr2)[10] 是个指针 指向的是拥有10个int类型元素的数组

int (*parr3[10])[5] 是个数组 ,存放了十个指针的数组,这十个指针分别指向十个数组  每个数组含有五个int类型元素的 。


这里判断的时候可以参考变量名字 也就是parr是有没有跟[]直接挨着,直接挨着就是个数组,没有直接挨着有个括号隔开就是个指针。 

附上一张图更好地理解 int (*parr3[10])[5]

C语言-指针进阶详解(万字解析)_数组_07



数组传参和指针传参

数组在使用时也需要频繁的函数调用以及传递参数,那么我们接下来看看数组是如何传递参数的。

一维数组传参数

void test1(int arr[]){

}
void test2(int arr[10]){

}
void test3(int *arr){

}
void test4(int *arr[20]){

}
void test5(int **arr){

}

int  main(){
     int arr[10]={0};
     int *arr2[20]={0};//数组指针
    test1(arr);
    test2(arr);
    test3(arr);
    test4(arr2);
    test5(arr2);
}

如上述代码我给出了几种传递方式,大家觉得上面有没有错误?

其实上面的传递参数方式都是正确的,test1和test2 是传递数组

test3 传递的数组指针。 arr2是个数组指针,指向数组的指针。那么test4 是用指针的方式接受数组指针,数组指针本质上还是个指针,所以这个传参方式没有任何问题。test5形式参数部分是个二级指针,arr2是一级指针,一级指针传过去正好放在二级指针中,这样写也是没有问题的。

二维数组

void test_d_1(int *arr){

}
void test_d_2(int *arr[5]){

}
void test_d_3(int (*arr)[5]){

}
void test_d_4(int **arr){

}
void test_d_5(int arr[3][5]){

}
void test_d_6(int arr[][5]){

}

int  main(){
    int arr[3][5]={0};
    test_d_1(arr);
    test_d_2(arr);
    test_d_3(arr);
    test_d_4(arr);
    test_d_5(arr);
    test_d_6(arr);
}

同样,这几个有哪些有哪些是正确的,哪些是不正确的呢?

首先看test_d_5 和test_d_6 这两个比较常规,肯定没有问题。直接传递的是二维数组,开辟了新的空间。 这里二维数组中前面的[行]是可以省略不写的。跟我们定义的时候要求一致。但是[列]不能省略

test_d_1(int *arr)  ,我们分析一下,首先在这个函数中 int * arr其实是个整型指针,但是我们传过去的其实是二维数组中的第一行的地址,准确的说是个一维数组的地址,那一维数组的地址肯定不能存放在整型指针中。所以这个函数传参是错的。

void test_d_1(int *arr){

}//不正确

同时,test_d_2(int *arr[5])这个传递方式也是有问题的。这个函数里的参数部分是一个含有五个指针元素的指针数组,它肯定没有办法来存储我们传递过来的含有五个int元素的一维数组地址。所以这个传递方式也是不对的。

void test_d_2(int *arr[5]){

}//不正确

test_d_4(int **arr),接下来分析这个函数传参方式。首先我们回想二级指针,是用来存放一级指针变量的地址的。但是arr代表的是一个一维数组的地址,那这里二级指针肯定也没办法存储arr的。所以这个函数传参也是错误的。

void test_d_4(int **arr){

}//不正确

test_d_3(int (*arr)[5]),我们看这个函数。这个函数int (*arr)[5] 首先毫无疑问这个参数是个数组指针,指向了含有五个int类型元素的数组。那正好我们二维数组

arr[3][5] 传递arr过去 是一个含有五个int类型变量的数组地址。也就可以存储。所以这个传递方式是对的。


总结:如果不需要指针来传递数组,那么这种情况就比较简单,直接在形式参数部分定义成跟实际参数一样的形式即可。如果使用指针来传递二维数组,我们需要在形式参数部分声明一个数组指针变量。需要的是一个存储指向数组的指针变量。

指针传参

思考:当一个函数的参数部分是一级指针,函数能接收什么参数?

   

void test_p_1(int *p){

}
int  main(){
    int a=10;
    int *p1=&a;
    test_p_1(&a);//A
    test_p_1(p1);//B
}

当我们的函数参数部分是个指针,首先我们可以考虑将一个地址传入 如A

其次既然是个指针变量,我们可以直接将一个指针传递进去。也是ok的 如B。

当然类型要统一,int 就传递int。char就传递char。

同样 二级指针也是如此。

void test_p_2(int **p){

}
int main(){
    int n=10;
    int *p=&n;
    int **pp=&p;
    test_p_2(pp);//A
    test_p_2(&p);//B
}

二级指针的传递参数方式跟一级指针是类似的。 首先我们可以直接传入一个二级指针变量pp例如A其次我们也可以穿入一个一级指针的地址,&p如B这两种都是正确的。

思考:除此之外,面对二级指针我们还可以怎么传递参数?

举个例子:

void test_p_3(int **poin){

}
int main(){
    int  * arr[10];
    test_p_3(arr);

}

除了传递二级指针变量和一级指针地址以外,还可以传递进去一个指针数组。因为一个二级指针。你无非想要的就是一个一级指针变量的地址或者二级指针变量,那传递一个指针数组arr本身就是个第一个指针变量的地址,这里也是可以传递进去的。



函数指针

函数指针又是个什么东西呢?我们先回顾数组指针,我们说数组指针是一个指向数组的指针。那回过头来看函数指针,其实就是指向函数的一个指针。

先看下面一个简单加法调用的例子,用最普通的办法写


int Add(int a,int b){
    int z=0;
    z=a+b;
    return  z;
}
int main(){
    int x =1;
    int y=2;
    printf("%d\n", Add(x,y));
    return 0;
}

那我们看一下这个地方Add有没有地址。我们打印一下看看

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

输出结果是一个16进制的地址,可见函数是有地址的。

C语言-指针进阶详解(万字解析)_数组指针_08

那我们再想一下,数组名是一首元素的地址,&数组名是整个数组的地址,我们看看 Add的地址是什么。

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

C语言-指针进阶详解(万字解析)_传参_09

可以看到两个地址是相同的。但是这里Add地址可不是首元素的地址,因为函数没有什么首元素,所以 在函数里这两个都是这个函数的地址,是一个东西。

函数指针的写法:

    int Add(int a,int b){
    int z=0;
    z=a+b;
    return  z;
}
int main(){
    int x =1;
    int y=2;
    int (*pa)(int,int) =Add;//函数指针的写法
    printf("%d\n",(*pa)(x,y));
    return 0;

}

注意这里pa和*必须要用括号括起来。否则就表示的是一个int型指针的返回值的意思了。跟函数指针就大相径庭了。同时 后面参数类型要写上至于具体的变量名 要不要写都可以。

如果理解了函数指针,我们来看两端比较有意思的代码。出自《C陷阱和缺陷》

   (*(void (*)())0)();//代码A
    void (*signal(int ,void(*)(int )))(int );//代码B

首先分析代码A

我们从0入手,我们看向0的左侧。

    //0的左侧是个被括号括起来的 
	void (*)() 
	//这个我们就能看得懂是一个返回值类型为空的 、无参数的函数指针。

接着我们想到在一个数字前有一个括号括起来的类型。例如 

 char n=(int )9;

可以明确这里是一个强制类型转换将0转换成了某个函数的地址。

前面有个* 解引用。 就是调用 以0为地址的函数。因为这个函数是无参数的,那么我们*解引用调用这个函数后面那个()也是空的也是无参数的。

总体来说代码A是一次函数调用。

代码B

 我们从signal下手来看。首先只看中间这一部分

signal(int ,void(*)(int ) )

我们可以看出来这是个函数,函数名为signal,函数有两个参数,第一个参数是int类型,第二个参数是一个函数指针类型。   但是我们发现少了一个返回值类型。

那么此时我们将我们分析玩的这一部分给删掉会是个什么呢?

    void (*signal(int ,void(*)(int ) ))(int );
		void (*                           )(int );

我们发现剩下了一个函数指针, 是一个返回值为void 参数为int类型的函数指针,所以这个函数指针就是signal函数的返回值类型。

那其实根据我们平时使用函数的写法按照上述的分析应该这样写。

void (*)(int ) signal(int , void(*)(int ) )

前面是函数的返回值类型void ()(int ),后面是函数名signal 紧跟着的括号里的是int , void()(int ) 但是我们不能这样写。这里的*依然是要靠近函数名。

所以应该这样写

    void (*signal(int ,void(*)(int ) ))(int );

不知道是否能够理解。这里的这种写法确实不太好理解。

那我们其实可以对上述代码B做一个优化,这里我们使用到typedef关键字。

//简化。

typedef void(* pfun)(int );
 //这样一来代码就可以改成
 pfun signal(int ,pfun);
//这样就很好理解了。

总的来说:

signal是一个函数声明,

signal的函数的参数有两个,一个是int类型一个是函数指针类型,该函数指针指向的函数的参数是int,返回类型是void

signal函数返回类型也是一个函数指针,该函数指针指向的函数的参数是int,返回类型是void。

最后还需要注意一点

int Add(int a,int b){
    int z=0;
    z=a+b;
    return  z;
}
int main(){
    int x =1;
    int y=2;
    int (*pa)(int,int) =Add;
    printf("%d\n",(*pa)(x,y));
    printf("%d\n",(**pa)(x,y));
    printf("%d\n",(****pa)(x,y));
 	return 0;   
}

这段代码运行结果是:

C语言-指针进阶详解(万字解析)_回调函数_10

由此我们发现,pa前面再加*,是不起任何作用的

那我们能不能不把*去掉 试一下。

 printf("%d\n",(pa)(x,y));

这个结果还是3,那说明 如果pa是个函数指针,我们调用得时候可以写*也可以不写*。


函数指针数组

看到这里我们也还是先回向指针数组,指针数组本质上是个数组,里面存放的都是指针。那么我们函数指针数组,本质上也还是个数组,只不过里面存放的是函数指针。

我们看下面一个例子:

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(){
    //定义一个函数指针来存放Add函数
    int (*pa)(int ,int )=Add;
     
    return 0;
}

我上面定义了几个函数,加减乘除,他们的参数类型返回值类型都相同。

在mian函数中我定义了一个函数指针来存放Add函数,但是我现在发现加减乘除这四个函数从形式上来看都是一样的。那我想找一个东西来存放他们四个函数,也就是说我可以通过这样一个东西能够同时找到这四个函数。应该怎么做呢,我们能不能用数组呢?这里就用到了函数指针数组。

怎么定义呢?

  int (*pa[4])(int ,int )={Add,Sub,Mul,Div};//函数指针数组

怎么用呢? 

我们用循环依次调用这几个函数,来计算当x=9 y=3时的结果。

 
    for (int i = 0; i < 4; ++i) {
        printf("%d\n",pa[i](9,3)) ;

    }

输出结果是:

C语言-指针进阶详解(万字解析)_函数指针_11

跟我们实际计算结果是相同的。

那我们来一道简单的题来看看是否真的能用

char* my_strcpy(char * dest,const char *src);
//写一个函数指针 pfun,能够指向my_strcpy
//写一个函数指针数组 pfunArr,能够存放四个mystrcpy函数的地址。

首先写一个函数指针。

答案是:

int main(){
    //函数指针。
    char * (*pfun)(char*,const char *)=my_strcpy;
    //函数指针数组。
    char* (*pfunArr[4])(char*,const char *);
}

函数指针数组的使用案例

转移表

假设我们想写一个简单的计算器。

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(){
    printf("************************\n");
    printf("*****1.add   2.sub******\n");
    printf("*****3.mul   4.div******\n");
    printf("*********0.exit*********\n");
    printf("************************\n");

}

//计算器
int main(){
    int input=0;
    int x=0;
    int y=0;
    do {
        menu();
        printf("请选:");
        scanf("%d",&input);
        switch (input) {
            case 1:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Add(x,y));
                break;
            case 2:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Sub(x,y));
                break;
            case 3:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Mul(x,y));
                break;
            case 4:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Div(x,y));
                break;
            case 0:
                printf("退出");
                break;
            default:
                printf("选择错误");
                break;
        }

    }while (input);
}

但是我们如果想要一个计算器不仅仅只有加减乘除功能,还想要别的功能,这里的case会越来越多,怎么办?假设要增加一个异或 我们使用函数指针数组来写。

int  Xor(int x,int y){
    return  x^y;
}
int main(){
    int input=0;
    int x=0;
    int y=0;
    int (*pfunArr[])(int ,int )={0,Add,Sub, Mul,Div,Xor};
    do {
        menu();
        printf("请选择:");
        scanf("%d",&input);
        if(input>=1&&input<=5){
            printf("请输出两个操作数:");
            scanf("%d%d",&x,&y);
            int ret = pfunArr[input](x,y);
            printf("%d\n",ret);
        }else if (input ==0)){
            printf("退出\n");
        }else{
            printf("选择错误\n");

        }


    } while (input);
}

这样我们后续想要扩展计算的类型,我们可以在函数指针数组中加上这个函数地址即可。我们一般把这个pfunArr函数指针数组叫做转移表。



指向函数指针数组的指针

指向函数指针数组的指针本质上是一个指针,这个指针指向了一个数组,数组元素都是函数指针

int main(){
    
    int arr[10]={0};
    int (*p)[10]=&arr;
    return 0;
}

我们这里先写了一个数组指针p指向了arr这个数组。回顾一下数组指针。

int add(int x,int y){
    return x+y;
}
int main(){

    int arr[10]={0};
    int (*p)[10]=&arr;

   int  (*pfunArr[4])(int ,int );//这是一个数组-函数指针数组
   int  (*(*funpp)[4])(int ,int )=&pfunArr;
           //int  (*(*funpp)[4])(int ,int ) 这就是一个指向函数指针数组的指针。


    return 0;
}

我们分析一下int  (*(funpp)[4])(int ,int ) ,首先还是从内到外看。由于funpp和在一个括号里先确定它是个指针。是个什么指针呢?我们将(*funpp)看作是一个变量名f那么上述式子就变成了 int (*f[4])(int,int ) 就变成了一个函数指针数组。也就是说这个指针指向了一个函数指针数组,那么我们就称之为函数指针数组。


回调函数

回调函数定义:

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

我手写了个简单的例子

void test(char  * ch){
    printf("hello,%s",ch);
}
void callback(void (*p)(char*)){
    printf("打印test\n");
    p("world");
}
int main(){
   callback(test);
}

我写了个callback函数 参数给的是一个函数的指针p,这个指针p指向的是我写的另一个函数test ,那我在住函数中调用的是callback,假设我给这个callback中加入了一些判断或者其他逻辑,满足的时候使用函数指针调用了test函数,那么我们就可以把这个callback函数认为是回调函数。

我们回顾上面有一个计算器的例子,我们拿下来。

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(){
    printf("************************\n");
    printf("*****1.add   2.sub******\n");
    printf("*****3.mul   4.div******\n");
    printf("*********0.exit*********\n");
    printf("************************\n");

}

//计算器
int main(){
    int input=0;
    int x=0;
    int y=0;
    do {
        menu();
        printf("请选:");
        scanf("%d",&input);
        switch (input) {
            case 1:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Add(x,y));
                break;
            case 2:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Sub(x,y));
                break;
            case 3:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Mul(x,y));
                break;
            case 4:
                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);
                printf("%d\n",Div(x,y));
                break;
            case 0:
                printf("退出");
                break;
            default:
                printf("选择错误");
                break;
        }

    }while (input);
}

从这个代码里我们看到

                printf("输入两个操作数:");
                scanf("%d%d",&x,&y);

这两句代码被多次重复执行,产生了冗余。我们可以怎么解决呢?

我们就使用回调函数。

写一个calc函数

void calc(int (*punf)(int ,int )){
    int x=0;
    int y=0;
    printf("输入两个操作数:");
    scanf("%d%d",&x,&y);
    printf("%d\n",punf(x,y));
}

把main()函数稍微修改一下

//计算器 通过回调函数优化冗余部分。
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("退出");
                break;
            default:
                printf("选择错误");
                break;
        }

    }while (input);
}

这样就通过回调函数解决这个问题。

当然这个案例比较简单。我们看一个稍微难一点的案例。

首先我们先看一下冒泡排序,我自己手写了一份冒泡排序,针对这个冒泡排序 我就不再多说了。

//冒泡排序
void bubble_sort(int arr[],int size){

    int flag =1 ;//对排序的优化

    for (int i = 0; i < size-1; ++i) {
        for (int j = 0; j < (size-1-i); ++j) {
          if(arr[j]>arr[j+1]){
              int e=arr[j];
              arr[j]=arr[j+1];
              arr[j+1]=e;
              flag=0;
          }
        }
        if(flag==1)
            break;

    }
    for (int k = 0; k < size; ++k) {
        printf("%d ",arr[k]);
    }

}

这样排序整数当然没问题,但是如果要排序浮点型呢?如果要排序结构体类呢?假设我现在定义了一个结构体 学生Stu 

struct  Stu{
    char name[20];
    int age;
};int main(){
    struct Stu s[3]={{"小王",20},{"小李",10},{"小魏",30}};
    return 0;
}

这里应该写一个函数对Stu 中的年龄怎么去排序。我们先看一下C语言内置库函数 qsort函数,C语言文档中对qsort这个库函数是这样描述的。

在cplusplus这个网站中我们找到C library 其中有个stdlib.h ,我们想要看的qsort就在这个头文件中。

C语言-指针进阶详解(万字解析)_数组_12

C语言-指针进阶详解(万字解析)_数组指针_13

void qsort (void* base, 
            size_t num,
            size_t size, 
            int (*compar)(const void*,const void*)
            );


简单说一下

void *base指的是要排序的数组序列的起始地址

num指的是这个数组序列元素的数量。

size指的是每个元素的在内存中所占的大小 单位 byte

compar是个函数指针,指向了一个参数为静态void类型的变量a和b,返回值是int类型的函数。这个函数用来对a,b两个参数做比较。 之所以用void类型是因为void *可以接收各种类型的地址。并且void *类型的指针不能解引用,并且也不能进行+-操作。


我写个简单的代码让大家理解怎么用qsort这个库函数。

//比较函数
int  compare(const void *a,const void *b){
    return (*(int *)a-*(int *)b);//因为我们比较的是int型
    //但是void * 并不能解引用,所以我把它们进行强制类型转换
    //转换为int * 在对其解引用,就可以找到对应的值。
}

int main(){
	//要比较的数组序列
    int arr[12]={2,4,6,3,6,7,8,1,8,1,9,5};
	//调用qsort,因为我比较的是整型数组,12个元素,把需要的参数传递进去
    //qsort并没有什么返回值,它直接对所需排序的数组进行排序。
    qsort(arr,12,sizeof (int) ,compare2);
    //我直接循环打印这个排序后的数组。
    for (int i = 0; i <12 ; ++i) {
        printf("%d ",arr[i]);
}

输出结果

C语言-指针进阶详解(万字解析)_数组_14

那么这个是对int类型排序。 如果是对一个结构体数组Stu中的元素按照年龄排序会是怎么样呢?

  

int  compare_stu(const void *a,const void *b){

    return  ((struct Stu*)a)->age  -  ((struct Stu*)b)->age;
		//还是强制类型转换
    	//将void *转换成 struct Stu *类型
    	//年龄是int类型,直接相减即可
}

int main(){
    struct Stu s[3]={{"小王",20},{"小李",10},{"小魏",30}};
    int sz =sizeof s /sizeof s[0];
   	//调用qsort
    qsort(s,sz,sizeof s[0],compare_stu);
    //输出
    for (int i = 0; i < sz; ++i) {
        printf("姓名:%s",s[i].name);
        printf("年龄 %d", s[i].age);
        printf("\n");
    }
    return 0;
}

输出结果

C语言-指针进阶详解(万字解析)_函数指针_15

但是如果我们自己想实现一个这样的函数,应该怎么去写呢?我们应该嘴我上面给出的bubble函数怎么修改来实现这样的想法。

int  compare(const void *a,const void *b){
    return (*(int *)a-*(int *)b);
}

void swap(char* a,char* b,int len){
    for (int i = 0; i < len; ++i) {
            char tmp=*a;
            *a=*b;
            *b=tmp;
            a++;
            b++;
    }
}

void bubble_All(void *arr,
                int size,
                int len,
                int (*compa)(const void *a,const void *b )){

    for (int i = 0; i < size-1; ++i) {

        for (int j = 0; j < (size-1-i); ++j) {
            //两个元素比较
            if(compa((char *)arr+j*len,(char *)arr+(j+1)*len)>0 ){
                //交换
                swap((char *)arr+j*len,(char *)arr+(j+1)*len,len);
            }
        }
    }
}

int main(){
    int arr[12]={2,4,6,3,6,7,8,1,8,1,9,5};
    int sz=sizeof arr /sizeof arr[0];
    bubble_All(arr,sz,sizeof arr[0],compare);
    for (int i = 0; i <12 ; ++i) {
        printf("%d ",arr[i]);
    }

}

我这个冒泡排序就是仿照着qsort来写的。其中需要注意的是我们要排序的不仅仅是int 而是用户想排什么就能排什么。核心代码在bubble_All中

两层循环这个不能动,重点是在比较之处。我们不知道如何比较两个不确定类型的变量怎么办?我们现在只有一个数组元素的首地址,还有第j个元素,还有每个元素所占字节大小,那根据这三个条件可不可以进行比较?

答案是可以,我们可以用“分子方法” 

我们可以将传入进来的数组需要比较的第j个元素强制转换成(char *)类型 (最基本的单位1个字节)

这样我们可以确定每次所加的字节数是1,然后根据j*len确定第j个元素地址,然后同样的方法,用(j+1)*长度len 算出下一个元素的地址,这样将这两个元素的地址传入用户自定义compa函数中进行比较即可。

这个比较函数compa跟qsort中使用的compare是一样的。不再过多赘述。直接调用即可。

然后还有一个关键点是如何交换两个不知道什么类型的元素。

void swap(char* a,char* b,int len){
    for (int i = 0; i < len; ++i) {
            char tmp=*a;
            *a=*b;
            *b=tmp;
            a++;
            b++;
    }
}

我们还是分子方法 将需要比较的元素地址按照char类型地址传入,在传入元素的宽度。

可以用循环将宽度内的一个字节一个字节进行交换。如上代码所述。即可实现两个位置类型元素的交换。

同样对 结构体Stu排序也是可以的

int  compare_stu(const void *a,const void *b){

    return  ((struct Stu*)a)->age  -  ((struct Stu*)b)->age;
}
struct  Stu{
    char name[20];
    int age;
};
int main(){
    struct Stu s[3]={{"小王",20},{"小李",10},{"小魏",30}};
    int sz =sizeof s /sizeof s[0];
    bubble_All(s,sz,sizeof s[0],compare_stu);
    for (int i = 0; i < sz; ++i) {
        printf("姓名:%s",s[i].name);
        printf("年龄 %d", s[i].age);
        printf("\n");
    }

}

输出结果:

C语言-指针进阶详解(万字解析)_回调函数_16

这样就实现了灵活的冒泡排序。

其中那个比较函数compare 就是回调函数,在函数内部通过指针去调用函数的机制成为回调函数机制。

 

以上是对指针这部分进行了详细的分析,主要包括指针、字符指针、指针数组、数组指针、数组传递参数和指针传递参数(一维指针、二维指针)、函数指针、函数指针数组、指向函数指针数组的指针、回调函数,这些部分。希望大家能够有所收获。


标签:arr,进阶,int,void,C语言,详解,数组,printf,指针
From: https://blog.51cto.com/u_16160587/6525793

相关文章

  • Python魔术方法详解
    前言魔术方法(MagicMethod)是Python内置方法,格式为"方法名",不需要主动调用,存在目的是为了给Python的解释器进行调用,几乎每个魔术方法都有一个对应的内置函数,或者运算符,当我们对这个对象使用这些函数或者运算符时就会调用类中的对应的魔术方法,可以理解为重写这些python的内置函数。......
  • 音视频开发进阶|第七讲:分辨率与帧率·下篇
     在视频系列的上一篇推文中,我们简单总结了色彩、像素、图像和视频等基础概念之间的关系。并且主要关注了两个组合:像素和图像,图像和视频之间的构成逻辑。我们先来简单回顾一下:从像素到图像:一定数量、记录了不同色彩信息的像素组合,得到一帧完整的图像;从图像到视频:一帧帧图像按一定频......
  • 音视频开发进阶|第七讲:分辨率与帧率·下篇
    ​在视频系列的上一篇推文中,我们简单总结了色彩、像素、图像和视频等基础概念之间的关系。并且主要关注了两个组合:像素和图像,图像和视频之间的构成逻辑。我们先来简单回顾一下:从像素到图像:一定数量、记录了不同色彩信息的像素组合,得到一帧完整的图像;从图像到视频:一帧帧图像按一......
  • 插入排序及C语言实现
    一、插入排序原理插入排序是一种简单的排序算法,其基本思想是将未排序序列中的每个元素依次插入到已排序的序列中合适的位置。具体来说,假设待排序的序列为a1,a2,⋯,an,则从a2开始遍历整个序列,将ai插入到前面的已排序序列a1,⋯,ai−1中,直到所有的元素都被插入到已排序的序列中......
  • 树状数组详解!(C++_单点/区间查询_单点/区间修改)
    先把这张著名的树状数组结构图摆在最前面,接下来我们就以这张图讲起!       首先图中的A数组就是所谓的原数组,也就是普通的数组形态,C则是我们今天要说的树状数组(可以看出一个树的形状,但其实和树没多大关系)从图中可以明显看到以下几个式子:有点像前缀和不是?但这样还看不出什......
  • iOS开发系列课程(03) --- UIView详解
    深入UIViewMVC架构模式  MVC(Model-View-Controller)是实现数据和显示数据的视图分离的架构模式(有一定规模的应用都应该实现数据和显示的分离)。其中,M代表模型,就是程序中使用的数据和状态,它不理会用户界面或表现方式,只负责数据和状态的存储;V代表视图,是呈现给用户看的东西,当然用户也......
  • iOS开发系列课程(08) --- 事件处理详解和手势操作
    iOS中的事件分发事件的分类TouchEvents(多点触摸事件)touchesBegan:withEvent:方法:一个或多个手指置于视图或窗口上touchesMoved:withEvent:方法:一个或多个手指在移动touchesEnded:withEvent:方法:一个或多个手指离开视图或窗口touchesCancelled:withEvent:方法:如果其他系统事件(如内......
  • 精通C语言中的函数:创建模块化代码
    在C语言中,函数是一种非常重要的概念,它允许我们将代码划分为模块化的部分,提高代码的可读性和可维护性。函数还可以被多次调用,避免代码的冗余。本文将探索C语言中的函数,并提供相关的代码示例,帮助你更好地理解和应用函数的概念。函数的定义和调用在C语言中,函数由函数头和函数体组成。......
  • 探索C语言的控制流:循环和条件语句
    在C语言中,控制流是编程中的核心概念之一。它允许我们根据特定的条件或循环来决定程序的执行路径。掌握C语言的控制流对于编写高效和灵活的程序非常重要。本文将深入探索C语言中的控制流,重点介绍循环和条件语句,并提供相应的代码示例。条件语句在C语言中,最常用的条件语句是if-else语......
  • C语言现代方法
    1、书2、习题答案书中有w图标的题目答案:http://knking.com/books/c2/answers/index.html书中所有题目的答案:https://gitcode.net/mirrors/williamgherman/c-solutions?utm_source=csdn_github_accelerator......