C语言学习笔记:C指针

C语言学习笔记:C指针

参考博客:https://blog.csdn.net/weixin_44966641/article/details/120456141

https://blog.csdn.net/liu100m/article/details/90731422

一、指针的基本介绍

1、数据在内存中的存储与访问

在内存中,每一字节(8位)有一个地址。假设图中最下面的内存地址位0,内存地址向上生长,图中标识出的(下面)第一个字节的地址位201,地址向上生长一直到图中最上面的地址208。

当我们在程序中声明一个变量时,如int a,系统会为这个变量分配一些内存空间,具体分配多少空间则取决于该变量的数据类型和具体的编译器。常见的有int类型4字节,char类型1字节,float类型4字节等。其他的内建数据类型或用户定义的结构体和类的大小,可通过sizeof来查看。

我们声明两个变量:

int a;
char c;

假如他们分别被分配到内存的204-207字节和209字节。则在程序中会有一张查找表:

表中记录的各个条目是变量名,变量类型和变量的首地址。

当我们为变量赋值时,如a = 5,程序就会先查到 a 的类型及其首地址,然后到这个地址把其中存放的值写为 5。

2、指针的概念

指针是一个变量,它存放的是另一个变量的地址。

指针与它指向的变量 假设我们现在有一个整型变量a=4存放在内存中的204地址处(实际上应该是204-207四个字节中,这里我们用首地址204表示)。在内存中另外的某个地址处,我们有另外一个变量 p,它的类型是“指向整型的指针”,它的值为204,即整型变量a的地址,这里的 p 就是指向整型变量 a 的指针。

指针所占的内存空间 指针作为一种变量也需要占据一定的内存空间。由于指针的值是一个内存地址,所以指针所占据的内存空间的大小与其指向的数据类型无关,而与当前机器类型所能寻址的位数有关。具体来说,在32位的机器上,一个指针(指向任意类型)的大小为4个字节,在64位的机器上则为8个字节。

指针的修改 我们可以通过修改指针p的值,来使它指向其他的内存地址。比如我们将 p 的值修改为 208,则可以使它指向存放在208地址处的另一个整型变量 b。

指向用户定义的数据类型 除了内建的数据类型之外,指针也可以指向用户定义的结构体或者类。

3、指针的声明和引用

指针的声明 在C语言中,我们通过 * 来声明一个指向某种数据类型的指针:int *p。这个声明的含义即:声明一个指针变量 p,它指向一个整型变量。换句话说,p 是一个可以存放整型变量的地址的变量。

取地址 如果我们想指定 p 指向某一个具体的整型变量 a,我们可以:p = &a。其中用到了取地址运算符 &,它得到的是一个变量的地址,我们把这个地址赋值给 p,即使得 p 指向该地址。

这时,如果我们打印p, &a, &p的值会得到什么呢?不难理解,应该分别是204,204,64。

解引用 如果我们想得到一个指针变量所指向的地址存放的值,该怎么办呢?还是用 * 放在指针变量 p 前面,即 *p 注意这里的 * 就不再是声明指针的意思了,而称为解引用,即把 p 所指向的对象的值读出来。 所以如果我们打印 *p,则会得到其所指向的整型变量 a 的值:5。

实际上,我们还可以通过解引用直接改变某个地址的值。比如 *p = 8,我们就将204地址处的整型变量的值赋为8。此时再打印*p或者a,则会得到8。

4、指向指针的指针

我们之所以能够把整型变量 x 的地址存入 p, 是因为 p 是一个指向整型变量的指针int*。那如果想要把指针的地址也存储到一个变量中,这个变量就是一个指向指针的指针,即int**。

#include <stdio.h>

int main(){
    int x;
    int* p = &x;
    *p = 6;
    int** q = &p;
    int*** r = &q;

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

    return 0;
}

p存放的是整形变量x的地址
*p,对指针变量解引用,把 p 所指向的对象的值读出来,即x的值:6
&p是指针本身的地址,int**指向指针的指针
*q,对指针变量q解引用,把 q 所指向的对象的值读出来,即p指针本身的地址
&q是指针q本身的地址,int***指向指针的指针的指针
**q,q先与*结合,*q指p指针本身的地址,**q把 p 所指向的对象的值读出来,即x的值:6
*r,r先与*结合,*r指q指针本身的地址,**r把 q 所指向的对象的值读出来,即p指针本身的地址
***r,r先与
结合,*r指q指针本身的地址,**r把 q 所指向的对象的值读出来,即p指针本身的地址,***r把 p 所指向的对象的值读出来,即x的值:6

5、复杂类型说明

分析原则:从变量名处起,根据运算符优先级结合,一步一步分析。

  • int p; //这是一个普通的整型变量
  • int *p; //首先从p处开始,先与*结合,所以说明P 是一个指针,然后再与int 结合,说明指针所指向的内容的类型为int 型.所以P是一个返回整型数据的指针
  • int p[3]; //首先从P 处开始,先与[]结合,说明P 是一个数组,然后与int 结合,说明数组里的元素是整型的,所以P 是一个由整型数据组成的数组
  • int *p[3]; //首先从P 处开始,先与[]结合,因为其优先级比*高,所以P 是一个数组,然后再与 * 结合,说明数组里的元素是指针类型,然后再与int 结合,说明指针所指向的内容的类型是整型的,所以P 是一个由返回整型数据的指针所组成的数组
  • int (p)[3]; //首先从P 处开始,先与结合,说明P 是一个指针然后再与[]结合(与"()"这步可以忽略,只是为了改变优先级),说明指针所指向的内容是一个数组,然后再与int 结合,说明数组里的元素是整型的.所以P 是一个指向由整型数据组成的数组的指针
  • int **p; //首先从P 开始,先与*结合,说是P 是一个指针,然后再与*结合,说明指针所指向的元素是指针,然后再与int 结合,说明该指针所指向的元素是整型数据.
  • Int (*p)(int); //从P 处开始,先与指针结合,说明P 是一个指针,然后与()结合,说明指针指向的是一个函数,然后再与()里的int 结合,说明函数有一个int 型的参数,再与最外层的int 结合,说明函数的返回类型是整型,所以P 是一个指向有一个整型参数且返回类型为整型的函数的指针
  • int *(*p(int))[3]; //可以先跳过,不看这个类型,过于复杂。从P 开始,先与()结合,说明P 是一个函数,然后进入()里面,与int 结合,说明函数有一个整型变量参数,然后再与外面的结合,说明函数返回的是一个指针,然后到最外面一层,先与[]结合,说明返回的指针指向的是一个数组,然后再与结合,说明数组里的元素是指针,然后再与int 结合,说明指针指向的内容是整型数据.所以P 是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数.

6、指针的类型和指针所指向的类型

(1)int*ptr; 
(2)char*ptr; 
(3)int**ptr; 
(4)int(*ptr)[3]; 
(5)int*(*ptr)[4];

1.指针的类型

从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看上面代码中各个指针的类型:
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]
怎么样?找出指针的类型的方法是不是很简单?

2.指针所指向的类型

当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:
(1)int*ptr; //指针所指向的类型是int
(2)char*ptr; //指针所指向的类型是char
(3)int**ptr; //指针所指向的类型是int*
(4)int(*ptr)[3]; //指针所指向的类型是int()[3]
(5)int*(*ptr)[4]; //指针所指向的类型是int*()[4]

7、指针的算数运算

实际上,指针的唯一算术运算就是以整数值大小增加或减少指针值。如p+1、p-2等

#include <stdio.h>

int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("%d\n", p);
    printf("%d\n", p+1);

    return 0;
}

输出

358010748
358010752

指针的加1
这是因为指针 p 是一个指向整型变量的指针,而一个整型变量在内存中占4个字节, 对 p 执行加1,应该得到的是下一个整型数据的地址,即在地址的数值上面应该加4。
相应地,如果是p+2的话,则打印出的地址的数值应该加8。
危险
可能会造成危险的是,C/C++并不会为我们访问的地址进行检查,也就是说,我们可能通过指针访问一块未分配的内存,但是没有任何报错。这可能会造成我们不知不觉地弄错了一些数值。
比如,接着上面的例子,我们试图打印 p 和 p+1 所指向的地址所存放的值:

#include <stdio.h>

int main(){
    int a = 10;
    int* p;
    p = &a;
    printf("Addresses:\n");
    printf("%d\n", p);
    printf("%d\n", p+1);

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

    return 0;
}

输出:

Addresses:
-428690420
-428690416
Values:
10
-428690420

可以看到,对指针进行加法,访问 p+1 所指向的地址的值是没有意义的,但是C/C++并不会禁止我们这么做,这可能会带来一些难以察觉的错误。

#include<stdio.h> 
    int main() 
    { 
        char a[20]=" You_are_a_boy"; 
        char *p=a; 
        char **ptr=&p; 
        //printf("p=%d\n",p); 
        //printf("ptr=%d\n",ptr); 
        //printf("*ptr=%d\n",*ptr); 
        printf("**ptr=%c\n",**ptr); 
        ptr++; 
        //printf("ptr=%d\n",ptr); 
        //printf("*ptr=%d\n",*ptr); 
        printf("**ptr=%c\n",**ptr); 
    } 

ptr 的类型是char **,指向的类型是一个char *类型,该指向的地址就是p的地址(&p),当执行ptr++;时,会使指针加一个sizeof(char*),即&p+4;&p是指p本身的地址,而不是p的值(即不是p所指向的字符型数组的首地址),所以最后的输出会是一个随机的值,或许是一个非法操作,而不是数组中第五个元素。
总结
一个指针ptrold 加(减)一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold 的类型相同,ptrnew 所指向的类型和ptrold所指向的类型也相同。ptrnew 的值将比ptrold 的值增加(减少)了n 乘sizeof(ptrold 所指向的类型)个字节。就是说,ptrnew 所指向的内存区将比ptrold 所指向的内存区向高(低)地址方向移动了n 乘sizeof(ptrold 所指向的类型)个字节。

8、指针常量和常量指针

指针常量

int * const p =&a;

特点:指针的指向不可以修改,指针指向的内存的值可以修改
const限制的是指针p的值,但是没有限定p指向的内存地址里的数据,所以内存的数据可以修改

int a=10;
int * const p =&a;  //定义指针常量,指向int a的地址

*p = 20; //正确,指向的内存地址中的数据可以修改   
 
p=&b;  //错误,指向的内存地址不可以修改

常量指针

const int *p=&a;

特点: 指针的指向可以修改,但是指针指向的值不可以修改。
常量指针就是指向的值要是一个常量,但是指向的内存地址不做限制

int a=10;
int b=10;
const int *p=&a;  //定义常量指针,指向int a的地址

*p = 20; //错误,指向的内存地址中的数据不可以修改   
 
p=&b;  //正确,指向的内存地址可以修改

二、指针与数组

数组的数组名其实可以看作一个指针。

1、数组的声明

当我们声明一个整型数组int A[5]时,就会有五个整型变量:A[0] - A[4],被连续地存储在内存空间中。

2、数组名和指针

数组与指针的另一个联系是:数组名就是指向数组首元素的指针。数组的首元素的地址,也被称为数组的基地址。
数组名称和指针变量的唯一区别是,不能改变数组名称指向的地址,即数组名称可视为一个指向数组首元素地址的指针常量。也就是说数组名指针是定死在数组首元素地址的,其指向不能被改变。比如数组名不允许自加A++,因为这会它是一个不可改变的指针常量,而一般指针允许自加p++;还有常量不能被赋值,即若有数组名 A,指针 p,则A = p是非法的。

3、C语言中字符串的存储

在C语言中,我们通常以字符数组的形式来存储字符串。对于一个有 n nn 个字符组成的字符串,我们需要一个长度至少为 n + 1 n+1n+1 的字符数组。例如要存储字符串JOHN,我们需要一个长度至少为 5 的字符数组。

之所以字符数组的长度要比字符串中字符的个数至少多一个,是因为我们需要符号来标志字符串的结束。在C语言中,我们通过在字符数组的最后添加一个 \0 来标志字符串的结束。如下图。
在这里插入图片描述
在这个图中,我们为了存储字符串JOHN,我们使用了字符数组中的5个元素,其中最后一个字符 \0,用来标识字符串的结束。倘若没有这个标识的话,程序就不知道这个字符串到哪里结束,就可能会访问到5,6中一些未知的内容。

4、指针与二维数组

我们可以声明一个二维数组:int B[2][3],实际上,这相当于声明了一个数组的数组。如此例中,B[0], B[1] 都是包含3个整型数据的一维数组。

在这里插入图片描述
数组名相当于是指向数组首元素地址的指针常量。在这里,首元素不在是一个整型变量,而是一个包含3个整型变量的一维数组。这时int* p = B就是非法的了,因为数组名B是一个指向一维数组的指针,而非一个指向整型变量的指针。正确的写法应该是:int (*p)[3] = B。

#include <stdio.h>

int main(){
    int B[2][3] = {2, 3, 6, 4, 5, 8};
  
    printf("-----------------------\n");        // 指向一维数组的指针 	400
    printf("%d\n", B);
    printf("%d\n", &B[0]);
    printf("-----------------------\n");        // 指向整型的指针 		  400
    printf("%d\n", *B);
    printf("%d\n", B[0]);
    printf("%d\n", &B[0][0]);
    printf("-----------------------\n");        // 指向一维数组的指针 	412
    printf("%d\n", B+1); 
    printf("%d\n", &B[1]);
    printf("-----------------------\n");        // 指向整型的指针     412
    printf("%d\n", *(B+1));
    printf("%d\n", B[1]);
    printf("%d\n", &B[1][0]);
    printf("-----------------------\n");        // 指向整型的指针    420 
    printf("%d\n", *(B+1)+2);
    printf("%d\n", B[1]+2); 
    printf("%d\n", &B[1][2]);             
    printf("-----------------------\n"); 				// 整型  3
    printf("%d\n", *(*B+1));                  
    printf("-----------------------\n"); 
    
  	return 0;
}
  1. 第一组 (B,&B[0]):数组名B是一个指针,其指向的元素是一个一维数组,即二维数组第一个元素(第一个一维数组)的首地址。而B[0]就是二维数组的第一个元素,即二维数组的第一个一维数组,对其进行取地址运算,故&B[0]就是第一个一维数组的地址,也即第一个指向第一个一维数组的指针。
    所以说第一组是指向一维数组的指针,其值为 400。

  2. 第二组:(*B,B[0],&B[0][0]):对数组名B进行解引用,得到的是其第一个元素(第一个一维数组)的值,也就是一个一维数组名B[0](相当于前面几章的一维数组名A),这个一维数组名就相当于是一个指向整型数据的指针常量。而B[0][0]是一个整型数据2,对其进行取地址运算,得到的是一个指向整型变量的指针。
    所以说第二组是指向整型变量的指针,其值也为400,但与第一组指向的元素不同,注意体会。

  3. 第三、四组与第一、二组类似,关键区别在于加入了指针运算。这里需要注意的是对什么类型的指针进行运算,是对指向一维数组的指针(+12),还是对指向整型的指针(+4)。在这两组中都是对指向一维数组的指针(如二维数组名B)进行运算,所以地址要偏移12个字节。

  4. 第五组中开始有了对不同的指针类型进行指针运算的情况。在这一组中的,+1都是对指向一维数组的指针进行运算,要+(1*12),而+2都是对指向整型变量的指针进行运算,要+(2*4),故最终结果是420。

  5. 最后一组只有一个值。但需要一步一步仔细分析。首先*B是对二位数组名进行解引用,得到的是一个一位数组名,也就是一个指向整型的指针常量。对其加1,需要偏移4个字节,即(*B+1)是一个指向地址404处的整型变量的指针,对其进行解引用,直接拿出404地址处的值,得到3。

5、指针与高维数组

多维数组的本质其实就是数组的数组
我们可以这样声明一个三维数组:int C[3][2][2]。三维数组中的每个元素都是二维数组, 具体来说,它是由三个二维数组组成的,每个二维数组是由两个一维数组组成的,每个一维数组含有两个整型变量。图示如下:
在这里插入图片描述
类似地,如果我们想将三维数组名C赋值给一个指针的话,应该这样声明:int (*p)[2][2] = C。

三、指针与动态内存

1、内存四区简介

内存被分为四个区,分别是代码区,静态/全局区,栈区和堆区。

代码区:用于存放程序代码,字符串常量也存放于此。
静态区 / 全局区:存放静态或全局变量,也就是不在函数中声明的变量,它们的生命周期贯穿整个应用程序。
栈区:用来存放函数调用的所有信息,和所有局部变量。由编译器自动分配释放,存放函数的形参、局部变量等。当函数执行完毕时自动释放。
堆区:大小不固定,可以由程序员自由地分配和释放(动态内存申请与释放)。若程序员不释放,程序结束时可能由操作系统回收。
在这里插入图片描述
在整个程序运行期间,代码区,静态/全局区,栈区的大小是不会增长的。

2、C中的动态内存分配

在C中,我们需要使用四个函数进行动态内存分配:malloc(),calloc(),realloc(),free()。
malloc 和 free

#include <stdio.h>
#include <stdlib.h>

int main(){
    int a;
    int* p;
    p = (int*)malloc(sizeof(int));
    *p = 10;
    free(p);

    return 0;
}

malloc函数从堆上找到一块给定大小的空闲的内存,并将指向起始地址的void *指针返回给程序,程序员应当根据需要做适当的指针数据类型的转换。

向堆上写值的唯一方法就是使用解引用,因为malloc返回的总是一个指针。如果malloc无法在堆区上找到足够大小的空闲内存,则会返回NULL。

程序员用malloc在堆上申请的内存空间不会被程序自动释放,因此程序员在堆上申请内存后,一定要记得自己手动free释放。

free接收一个指向堆区某地址的指针作为参数,并将对应的堆区的内存空间释放。

四、指针与函数

1、函数传值 call by value

#include <stdio.h>

void Incremnet(int a){
    a = a + 1;
}

int main(){
    int a;
    a = 10;
    Incremnet(a);
    printf("a = %d\n", a);

    return 0;
}

程序会为每个函数创造属于这个函数的栈帧,我们首先调用main()函数,其中的变量a一直存储在main()函数自己的栈帧中。在我们调用Increment()函数的时候,会单独为其创造一个属于它的栈帧,然后main()函数将实参a=10传给Increment()作为形参,a会在其中加1,但是并没有被返回。在Increment()函数调用结束后,它的栈帧被释放掉,main()函数并不知道它做了什么,main()自己的变量值一直是10,然后调用printf()函数,将该值打印出来。

可以看到,局部变量的值的生命周期随着被调用函数Increment()的结束而结束了,而由于main()中的a和Incremet()中的a并不是同一个变量(刚才已经看到,二者并不在同一地址),因此最终打印出的值还是10。

2、传引用 call by reference

通过指针可以指向某个特定的变量,并可以通过解引用的方式对该变量再进行赋值,而又由于在程序未执行结束时,main()函数里分配的空间均可以被其他自定义函数访问。因此我们可以将main()中的变量地址传给Increment(),在其中对该地址的值进行加一,这样最终打印的变量就会是加过1的了。

#include <stdio.h>

void Incremnet(int* p){
    *p = *p + 1;
}

int main(){
    int a;
    a = 10;
    Incremnet(&a);
    printf("a = %d\n", a);

    return 0;
}

3、数组作为函数参数

我们可以通过sizeof函数获取到数组的元素个数:sizeof(A) / sizeof(A[0]),即用整个数组的大小除以首元素的大小,由于我们的数组中存储的元素都是相同的数据类型,因此可以通过此法获得数组的元素个数。

例程1
我们现在定义一个SumOfElements()函数,用来计算传入的数组的元素求和,该函数还需要传入参数size作为数组的元素个数。在main()函数中新建一个数组,并通过sizeof来求得该数组的元素个数,调用该函数求和。

#include <stdio.h>

int SumOfElements(int A[], int size){
    int i, sum = 0;
    for (i=0; i<size; i++){
        sum += A[i];
    }
    return sum;
}


int main(){
    int A[] = {1, 2, 3, 4, 5};
    int size = sizeof(A) / sizeof(A[0]);
    int total = SumOfElements(A, size);
    printf("Sum of elements = %d\n", total);
  
    return 0;
}

SumOfElements(int A[]) 其实是相当于SumOfElements(int* A)。
例程2
有人可能回想,既然我们已经将数组传入函数了,能不能进行进一步的封装,将数组元素个数的计算也放到调用函数内来进行呢?于是有了如下实现:

#include <stdio.h>

int SumOfElements(int A[]){
    int i, sum = 0;
    int size = sizeof(A) / sizeof(A[0]);
    
    for (i=0; i<size; i++){
        sum += A[i];
    }
    return sum;
}


int main(){
    int A[] = {1, 2, 3, 4, 5};
    int total = SumOfElements(A);
    printf("Sum of elements = %d\n", total);
  
    return 0;
}

分析
我们还是要画出栈区来进行分析:
在这里插入图片描述
我们期望的是向左边那样,在main()函数将数组A作为参数传给SOE()之后,会在SOE()的栈帧上拷贝一份完全相同的20字节的数组。但是在实际上,编译器却并不是这么做的,而是只把数组A的首地址赋值给一个指针,作为SOE()的形参。也就是说,SOE()的函数签名SumOfElements(int A[]) 其实是相当于SumOfElements(int* A)。这也就解释了为什么我们在其内部计算A的大小时,得到的会是一个指针的大小。结合我们之前介绍过的值传递和地址传递的知识。可以这样讲:数组作为函数参数时是传引用(call by reference),而非我们预期的值传递。

需要指出的是,编译器的这种做法其实是合理的。因为通常来讲,数组会是一个很长,占内存空间很大的变量,如果每次传参都按照值传递完整地拷贝一份的话,效率极其低下。而如果采用传引用的方式,需要传递的只有一个指针的大小。

4、多维数组作为函数参数

一维数组作为参数需要注意是传引用,另外在函数体内不修改数据时,注意在函数签名中将数组名指针声明为常量指针。
二维数组做参数:

  1. void func(int (*A)[3]
  2. void func(int A[][3])
    注意事项
    注意:多维数组做函数参数时,数组的第一个维度可以省略,但是其他维度必须指定。所以说,对一个需要接收二维数组的参数,将函数签名声明为void func(int **A) 是不可行的,因为这样没有指定任何数组维度。

注意:在调用时要正确地传递参数数组的类型。比如下面这样就是不可行的:

void func1(int Arr[][3]){}
void func2(int Arr[][2][2]){}

int main(){
  int A[2][2];
  int B[2][3];
  int C[3][2][2];
  int D[3][2][3];
  
  func1(A);	// 错误
  func1(B); // 正确
  
  func2(C); // 正确
  func2(D); // 错误
}

5、函数返回指针

指针本质上也是一种数据类型(就像int、char),其中存储了另外一个数据的地址,因此将一个指针作为返回类型是完全可行的。但是,需要考虑的是,在什么情况下,我们会需要函数返回一个指针类型呢?
示例程序
考虑这样一个程序:

#include <stdio.h>
#include <stdlib.h>

int Add(int a, int b){
    int c = a + b;
    return c;
}

int main(){
    int x = 2, y = 4;
    int z = Add(x, y);
    printf("Sum = %d\n", z);
}

在Add函数返回之后,它在栈区上的栈帧也被程序自动释放了,这个时候,原来存放整型变量c的150这个内存地址中的值就已经是未知的了,我们之前说过,访问未知的内存是极其危险的。
返回被调函数在栈区的局部变量的指针是危险的。通常,我们可以安全地返回堆区或者全局区的内存指针,因为它们不会被程序自动地释放。

我们尝试在堆区分配内存:

#include <stdio.h>
#include <stdlib.h>

int* Add(int* a, int* b){
    int* c = (int*)malloc(sizeof(int));
    *c = (*a) + (*b);
    return c;
}

void printHello(){
  printf("Hello\n");
}

int main(){
    int x = 2, y = 4;
    int* ptr = Add(&x, &y);
    printHello();
    printf("Sum = %d\n", *ptr);
    free(ptr);
}

6、函数指针

6.1函数指针的定义和使用

下面这个程序定义和使用了一个函数指针:

#include <stdio.h>

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

int main(){
    int c;
    int (*p)(int, int);
    p = &Add;
    c = (*p)(2, 3);
    printf("%d\n", c);
}
  1. 声明函数指针的语法是:int (*p)(int, int),这条语句声明了一个接收两个整型变量作为参数,并且返回一个整型变量的函数指针。注意函数指针可以指向一类函数,即可以说,指针p指向的类型是输入两整型,输出一整型的这一类函数,即所有满足这个签名的函数,都可以赋值给p这个函数指针。
    另外,要注意指针要加括号。否则int *p(int, int),是声明一个函数名为p,接收两个整型,并返回一个整型指针的函数。
  2. 函数指针赋值:p = &Add,将函数名为Add的函数指针赋值给p。同样注意只要满足p声明时的函数签名的函数名都可以赋值给p。
  3. 函数指针的使用:int c = (*p)(2, 3),先对p解引用得到函数Add,然后正常传参和返回即可。
  4. 还有一点,在为函数指针赋值时,可以不用取地址符号&,仅用函数名同样会返回正确的函数地址。与之匹配的,在调用函数的时候也不需要再解引用。这种用法更加常见。
int (*p)(int, int);
p = Add;
c = p(2, 3);
  1. 再强调一下:注意函数指针可以指向一类函数,即可以说,指针p指向的类型是输入两整型,输出一整型的这一类函数,即所有满足这个签名的函数,都可以赋值给p这个函数指针。用不同的函数签名来声明的函数指针不能指向这个函数。

如以下这些函数指针的声明都是不能指向Add函数的:

void (*p)(int, int);
int (*p)(int);
int (*p)(int, char);
6.2函数指针的使用案例(回调函数)

这里使用函数指针的案例都围绕着这么一个概念:函数指针可以用作函数参数,而接收函数指针作为参数的这个函数,可以回调函数指针所指向的那个函数。

#include <stdio.h>

void A(){
    printf("Hello !\n");
}

void B(void (*ptr)()){
    ptr();
}

int main(){
    void (*p)() = A;
    B(p);
  
		B(A);	
  
    return 0;
}

或者我们可以直接在主函数中B(A),而不需要上面那写两句先复制给p,再调用p。

在上面的例程中,将函数A()的函数指针传给B(),B()在函数体内直接通过传入的函数指针调用函数A(),这个过程成为回调。这里函数指针被传入另一个函数,再被用函数指针进行回调的函数A()成为回调函数。

6.3回调函数的实际使用场景
#include <stdio.h>
#include <math.h>

void BubbleSort(int A[], int size){
    int i, j, temp;
    for (i=0; i<size; i++){
        for (j=0; j<size-1; j++){
            if (A[j] > A[j+1]){
                temp = A[j];
                A[j] = A[j+1];
                A[j+1] = temp;
            }
        }
    }
}

int main(){
    int A[] = {2, -4, -1, 3, 9, -5, 7};
    int size = sizeof(A) / sizeof(A[0]);
    // BubbleSort(A, size, greater);
    BubbleSort(A, size);
    int i = 0;
    for (i=0; i<size; i++){
        printf("%d ", A[i]);
    }
    printf("\n");
}

输出排序结果:

-5 -4 -1 2 3 7 9

对于这个排序函数,我们可能有时需要升序排序有时需要降序排序,即我们可能会根据具体使用场景有不同的排序规则。而由于实现不同的排序函数时,整个算法的逻辑是不变的,只有排序的规则会不同,总不至于为了不同的排序规则都单独写一个函数,这时我们就可以借助函数指针作为参数来实现不同的排序规则的切换。

即实现如下:

#include <stdio.h>
#include <math.h>

void BubbleSort(int A[], int size, int (*compare)(int, int)){
    int i, j, temp;
    for (i=0; i<size; i++){
        for (j=0; j<size-1; j++){
            if (compare(A[j], A[j+1]) > 0){
                temp = A[j];
                A[j] = A[j+1];
                A[j+1] = temp;
            }
        }
    }
}

int greater(int a, int b){
    if (a > b) return 1;
    else return -1;
}

int abs_greater(int a, int b){
    if (abs(a) > abs(b)) return 1;
    else return -1;
}

int main(){
    int A[] = {2, -4, -1, 3, 9, -5, 7};
    int size = sizeof(A) / sizeof(A[0]);
    BubbleSort(A, size,greater);
    int i = 0;
    for (i=0; i<size; i++){
        printf("%d ", A[i]);
    }
    printf("\n");
}

输出:

-5 -4 -1 2 3 7 9

五、指针与结构体

truct MyStruct 
    { 
        int a; 
        int b; 
        int c; 
    }; 
    struct MyStruct ss={20,30,40}; 
    //声明了结构对象ss,并把ss 的成员初始化为20,30 和40。 
    struct MyStruct *ptr=&ss; 
    //声明了一个指向结构对象ss 的指针。它的类型是 
    //MyStruct *,它指向的类型是MyStruct。 

通过指针ptr 来访问ss 的三个成员变量:

ptr->a; 
ptr->b;
ptr->c;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值