[C语言笔记]09、指针

目录

前言:

9-1指针的概念

9-2指针的定义

1、指针变量的定义

2、指针变量的初始化

3、打印指针变量的值

4、利用指针证明数组中元素在内存中是连续的

9-3指针的解引用

1、利用解引用获取数据

2、利用解引用修改数据

9-4指针变量占几个字节?与前面的数据类型有关吗?

9-5指针的运算

1、指针和整数的加减法:

2、指针在数组上的偏移:

3、指针与指针之间的减法:

9-6-1函数的址传递

9-6-2函数返回指针

9-6-3多返回值的应用

9-6-4数组参数退化

9-7-1空指针

9-7-2野指针

9-7-3悬空指针

9-7-4void类型的指针

9-7-5二级指针和多级指针

9-8-1指针常量

9-8-2常量指针

9-8-3常量指针常量

9-9-1指针与数组的关系

利用指针来访问数组元素

下面我们可以总结一下结论

9-9-2指针数组

9-9-3数组指针

9-10-1指针函数

9-10-2函数指针

9-10-3函数指针数组


前言:

继续更新c语言笔记!

专栏链接:

c语言笔记_1zero10的博客-CSDN博客

9-1指针的概念

指针就是内存地址,一串十六进制的数,比如:0x0000009B754FFA34,通过内存地址,我们可以找到存储在内存中的量所在的内存空间,还可以对这个量进行修改

指针在c语言中非常常见,我们可以用一个变量把那一串十六进制数存起来如

cpp 复制代码
int*p = 0x0000009B754FFA34;
//其中int*p叫做指针变量 简称指针

9-2指针的定义

1、指针变量的定义

格式: 数据类型 * 指针变量 名 其中的*是一个特殊符号,代表后面的变量是指针变量

*符号的前面和后面的空格可以省略,比如下面编译器默认让*靠近数据类型

cpp 复制代码
int* pa;

2、指针变量的初始化

格式: 数据类型 * 指针变量 名 = &某个变量 其中&叫做取地址符

cpp 复制代码
int b;//定义变量b
int* pb = &b;//定义了 一个指针变量pb,用来存储变量b的地址

&取地址符是单目运算符,&b的返回值是b的内存地址

一般指针变量都是p开头,然后第二个字母表示指向的变量 这里是b

p的意思是pointer 指针

对于其他数据类型的变量,也可以用指针变量存储内存地址

cpp 复制代码
double c;
double* pc = &c;

char d;
char* pd = &d;

指针变量 数据类型 要和指向变量的数据类型保持一致 如pc和c的数据类型都是double,pd和d的数据类型都是char

简而言之就是 pd=&c;不合法 因为pd指针变量和变量c的数据类型不同

注意指针变量指的是pa,pb,pc,pd而不包含前面的*,*只是一个标记,代表后面的变量是指针变量

指针变量 的值是一个十六进制的内存地址

3、打印指针变量的值

其中%p是任意指针变量的占位符

你会发现

pb的值/b的内存地址是:000000369F8FF6C4

pc的值/c的内存地址是:000000369F8FF708

pd的值/d的内存地址是:000000369F8FF744

前面几位数字和字母都是一样的,因为b,c,d的内存地址是在同一段内存空间里的

等学习到内存管理时,会仔细讲解这些东西

简单提一下,这三个十六进制数,比如000000369F8FF6C4叫做栈空间,它是在函数里面的

上面这几个内存地址非常的大,类似现实生活中我们的门牌号

4、利用指针证明数组中元素在内存中是连续的

首先取地址符&加上数组元素arr[0]或者arr[1]或者arr[2].....,用于获取数组中元素的地址

然后再打印出来

另外 每一次编译后显示的内存地址不一样,不用担心这个问题,因为每次存的地址不一样

先要介绍一下十六进制数的进位逻辑

因此我们来观察一下

arr[0]:0000008A2D4FFC38 C39 C3A C3B

arr[1]:0000008A2D4FFC3C C3D C3E C3F

arr[2]:0000008A2D4FFC40 C41 C42 C43

arr[3]:0000008A2D4FFC44 C45 C46 C47

arr[4]:0000008A2D4FFC48 C49 C4A C4B

你会发现由于我们定义的数组是int类型,四个字节 你会发现正好每个元素开辟了四个字节的空间,前一个元素arr[0]的四个字节的空间和后一个arr[1]的四个字节的空间是连在一起的

注意:每个 int 元素均占用4字节 :无论存储的数值大小(如5或1),int类型的内存空间始终被完整占用。

9-3指针的解引用

解引用就是获取指向变量的值的操作

1、利用解引用获取数据

格式 * 指针变量

以下是具体的演示

当a的值修改为11时,*pa的值也会变成11

因为*pa中的*是一个运算符,意思是对指针变量pa进行解引用。它的返回值始终与指向变量a的值相等

注意!

cpp 复制代码
#include <stdio.h>
int main() {
        int a = 10;
        int* pa = &a;
        printf("*pa=%d\n", *pa);
        a = 11;
        printf("*pa=%d\n", *pa);

        return 0;
}

第4行int* pa = &a;中的*是一个标记,代表后面的pa是指针变量

而第5、7行的*pa中的*是一个运算符,表示对指针变量pa解引用

2、利用解引用修改数据

格式: *指针变量名 = 值;

*pa的值变成了12

a的值也变成了12

指针解引用的作用就是修改变量的值

相比直接对变量的值进行修改 有好处

在后续章节会有体现

9-4指针变量占几个字节?与前面的数据类型有关吗?

之前我们学过的变量所占字节与前面的数据类型有关

而 指针则不同 具体演示如下

你会发现与数据类型无关,都是8个字节

原因是在64位的系统(即x64)中 每个指针变量电脑分配8个字节,来存储指向变量的地址,也就是一个16进制数

但是在32位系统(即调用x86系统)中 每个指针变量电脑分配4个字节,来存储指向变量的地址,也是一个16进制数,不过比较小相比64位系统

如0136FB50 0136FB34

x86系统可以兼容32位和64位系统,只不过在编译器中,默认x86表示32位系统

下面需要对我们上一节的一个点讲的更清楚一点

arr[0]:0000008A2D4FFC38 C39 C3A C3B

arr[1]:0000008A2D4FFC3C C3D C3E C3F

arr[2]:0000008A2D4FFC40 C41 C42 C43

arr[3]:0000008A2D4FFC44 C45 C46 C47

arr[4]:0000008A2D4FFC48 C49 C4A C4B

以下的内容很重要!!!

首先由于定义了一个int类型的数组,所以数组中的每个元素arr[0],arr[1],arr[2],arr[3],arr[4]占4个字节,开辟了四个字节的内存空间

每个元素对应的一个地址,如上图,就像门牌号一样,如0000008A2D4FFC38表示0000008A2D4FFC38 0000008A2D4FFC39 0000008A2D4FFC3A 0000008A2D4FFC3B都是arr[0]元素的了,共同存储元素arr[0]的值

每一个元素和其对应的地址并不在同一块内存空间里,数组元素arr[0],arr[1],arr[2],arr[3],arr[4]在内存中是连续的,在A位置比如

而他们的地址0000008A2D4FFC38,0000008A2D4FFC3C,0000008A2D4FFC40,0000008A2D4FFC44,0000008A2D4FFC48本质上是一个16进制的数,存储在另一个位置 比如位置B

以下举一个例子,以x64系统为例

int a=1;//在A位置开辟四个字节的空间,这个空间属于变量a,存储了数据1,a在内存里对应一个地址,比如0000008A2D4FFC38

int*pa=&a;//在B位置开辟一个八个字节的空间,用于存储a在内存中的地址,也就是0000008A2D4FFC38这一串16进制数

然后当你打印pa时,printf("%d\n",pa);实际上是打印出存储在pa里的一个数,0000008A2D4FFC38

然后当你解引用时 打印printf("%d\n",*pa); 实际上是解析存储在pa里的一个数,0000008A2D4FFC38的含义,然后发现是a的地址,然后找到a的内存空间,读取a内存空间内的值

9-5指针的运算

上一节我们讲了指针变量在x64系统中编译器是占8个字节的内存空间的

用来存储一个十六进制数

与指针变量前面的数据类型无关

那么为什么还需要标明数据类型,这一节我们就来揭示这个秘密

1、指针和整数的加减法:

以下我们进行一个小实验

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
        int a;
        int* pa = &a;//把a的内存地址(一个十六进制数)存进指针变量pa中
        printf("pa  =%p\n", pa);
        printf("pa+1=%p\n", pa + 1);
        //我们看一下直接对指针变量进行+1操作会有什么结果

        return 0;
}

你会发现对指针进行加法,实际上就是指向变量的内存地址往前走了一步,具体这一步走多大取决于指针的数据类型

这里是int类型,所以pa+1比pa的指向变量的内存地址往前走了4个字节,也就是大了4个字节

与数据类型所占的字节数有关

char 1个字节

short 2个字节

int 4个字节

double 8个字节

...

我们可以输出结果验证一下

那么指针的减法就是存储的内存地址后退一步,具体这一步走多大也是取决于指针的数据类型

2、指针在数组上的偏移:

所以利用指针的加法可以在数组上索引到任意一个元素

这种技巧在之后的章节会详细介绍

还有一个需要注意的点:

图上的三种写法含义一样

但是printf("pa[2] %d\n", pa[2]);的前提是,数组的第0个元素的内存地址已经赋值给了指针变量pal

像这样

cpp 复制代码
int arr[] = { 5,2,0,1,3,1,4 };
int* pa = &arr[0];//把第0个元素的十六进制的内存地址数赋值给指针变量pa
printf("pa[2]          %d\n", pa[2]);//输出0

3、指针与指针之间的减法:

9-6-1函数的址传递

1、我们先来回顾一下函数的值传递

在函数的值传递那一章,我们学了利用函数的值传递,

写了一个变量交换的代码,但是我们发现最终并没有完成变量的值的交换

我们看一下原来的代码

进入函数前a=1,b=2

出函数了,还是a=1,b=2

之前我们讲过了,出现这种情况是因为进入函数后,函数又开辟了新的a和b的内存空间,只不过新的a和b获得了进入函数前我们定义的a和b的值,1和2

因此在函数内是新的a和b的值进行了交换,变成了新的a=2,新的b=1

而函数结束后,这个新的a和新的b自动被从内存中清理掉了

实际上这个函数并没有对进入函数前的a和b所在的内存空间进行任何操作

因此最终打印出来a还是1,b还是2

我们用指针来验证一下以上这一点

我们对变量加上取地址符& 并且占位符是%p即可输出变量的内存地址

然后我们发现形参和实参并不在同一个内存地址上,这也验证了我们上面的猜想

下面我们用函数的址传递来对代码进行一下修改

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void swap(int*x, int*y) {
        printf("交换前x的值是:  %p y的值是:  %p\n", x, y);
        int tmp;
        tmp = *x, *x = *y, *y = tmp;
}
int main() {
        int a = 1, b = 2;
        printf("交换前a的地址是:%p b的地址是:%p\n", &a, &b);
        swap(&a, &b);
        printf("a=%d b=%d\n", a, b);

        return 0;
}

我们对代码分析一下

第3行,参数列表传的是指针x,指针y,到时候调用函数的时候需要传实参的内存地址给x和y

然后第6行,通过解引用获取x和y的值,并且值之间进行交换

第11行,传进a和b的值

通过运行以后我们发现,函数的址传递就是把变量的内存地址传给了形参,然后形参在函数中进行相关操作,由于形参与实参在同一内存地址,因此函数进行什么操作,最终实参也会进行相应的变化

这里函数对变量进行交换,最终a和b的值也发生了交换

9-6-2函数返回指针

函数返回指针的应用很多很多,由于很多应用需要用到以后的知识,所以这里只介绍最简单的应用

更多应用将在内存管理那一章进行介绍

本节介绍利用函数返回指针,实现获取任意数组的第任意个元素的操作

以下是具体代码,我们一步一步的分析

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int* get(int a[], int index) {
        return &a[index];
}


int main() {
        int arr[] = { 6,5,4,3,2,1 };
        int* p = get(arr , 2);
        printf("%d\n", *p);
        *p = 99;
        printf("%d\n", *p);
        return 0;
}

首先第3-5行,定义了一个叫get的函数,返回值是int指针类型

参数列表是一个未知长度的数组,以及一个索引

这样的形参可以保证任意一个数组都可以调用get函数

8行开始是主函数

9行,定义一个叫arr的数组,元素是6 5 4 3 2 1

定义一个int类型的指针,把get函数经过传入实参arr和2之后的返回值,也就是arr[2]的地址赋值给指针p

此时指针p就是指向arr数组第二个元素的指针

10行,打印出指针p解引用的值 此时为4

然后11行把指针p解引用的值改成99

此时p和arr[2]仍然是对应的

13行输出新的arr[2]的值,也就是p解引用的值 此时为99

9-6-3多返回值的应用

我们以前所学的函数,只能有一个返回值,比如我要实现一个函数,用来获取数组的最大值

怎么操作

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int max(int a[], int asize) {
    int ret = a[0];
    for (int i = 0;i < asize;++i) {
        if (a[i] > ret) {
            ret = a[i];
        }
    }
    return ret;
}

int main() {
    int arr[] = { 8,7,9,6,5,1,3,5,7,9,47,1,3,4,5,4,43,21 };
    int size = sizeof(arr) / sizeof(int);
    int m = max(arr, size);
    printf("数组arr中元素的最大值是:%d\n", m);
    return 0;
}

我们来解释一下

3-11行,实现获取数组元素最大值的功能

第3行,形参是int类型数组a[] 和asize,aszie表示数组的长度

第4行,定义整型变量ret,并把a[0]赋值给ret

第5-9行,遍历数组的每一个元素,并且与ret比较,如果更大,就把值赋值给ret

经过全部遍历之后,得到的ret就是最大值了

然后10行在for循环外返回ret

13行开始是主函数

14行 定义了arr数组,并且初始化了很多元素

15行 计算数组长度

16行 传参进入函数max并且把返回值赋值给m

17行 打印m,由此打印出数组元素的最大值

那么我想同时获得数组的最大值最小值怎么办

此时就得利用指针了

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void minmax(int a[], int asize,int*pmin,int*pmax) {
    int min = a[0];
    int max = a[0];
    for (int i = 0;i < asize;++i) {
        if (a[i] > max ) {
            max = a[i];//不断迭代最大值
        }
        if (a[i] < min) {
            min = a[i];//不断迭代最小值
        }
    }
    *pmin = min;
    *pmax = max;

}

int main() {
    int arr[] = { 8,7,9,6,5,1,3,5,7,9,47,1,3,4,5,4,43,21 };
    int size = sizeof(arr) / sizeof(int);
    int min, max;
    minmax(arr, size, &min, &max);
    printf("min是:%d max是:%d\n",min,max);
    return 0;
}

3-17行是求最大最小值的函数,我们介绍几个关键点

首先在传形参的时候,传人两个指针

然后14行,把经过迭代后的最小值赋值给传进来的指针pmin

15行,把经过迭代后的最大值赋值给传进来的指针pmax

这是因为

1、因为要返回多个值,所以只能选择没有返回值的void返回值类型

2、由于在函数内部设置的变量会随着函数调用结束而被清理,所以我们提前传进来两个指针

并且在函数调用结束前,把最大值,最小值赋值给这两个指针

只要人为传进来的指针还在,那么函数调用结束后,这两个指针不会消失,并且被赋予了最大值和最小值

这个就是利用指针来得到多个返回值的操作

下面的主函数就不用介绍了,就是定义和传入参数,就是要注意,因为min,max是要传的指针,所以要加取地址符

9-6-4数组参数退化

这个标题是什么意思呢?就是当数组作为函数的一个参数时,会退化成一个指针

我们拿上一节的代码举例

原因就是,当把一个数组作为参数传递给一个函数时,该数组会自动退化为一个指向数组首元素的指针,也可以达到把整个数组传入函数的目的

因为我们前面已经讲过了,只要把数组的首元素取地址赋值给一个指针pa,那么在指针就可以遍历数组的任意一个元素了

如传进去一个数组a[],那么进入函数后就会变成指向数组第0个元素的地址

同时上面的代码传形参时,可以写成这样

也就是把void minmax(int a[], int asize, int* pmin, int* pmax)

改成void minmax(int *a, int asize, int* pmin, int* pmax)

那为什么数组退化成指针呢?

1、效率考虑:在c语言函数调用的时候,如果传递指针,比传进去整个数组更加高效,尤其是大型数组

这样可以减少函数调用时的数据赋值开销,提高程序的运行速度

2、灵活性:将数字退化成指针,可以使函数更加的灵活,通过指针,函数可以访问不同大小的数组,而不仅仅局限于特定大小的数组,使得函数可以处理不同长度的输入数据,增加了代码的通用性

3、缺点就是函数内部无法获取到数组的大小,通常需要另一个参数来传递数组的大小,或者在数组的末尾添加一个特殊的标记,来表示数组的结束

9-7-1空指针

什么是空指针,就是定义一个指针,把它初始化为NULL

对于空指针不能进行解引用操作,因为是不合法的,空指针是没有对应的指向变量的,也就是它没有存储任何指向变量的内存地址,没有指向的内存空间,贸然解引用,是不合法的,返回值非0

那么空指针的作用是什么?

当一个指针变量没有指向任何内存时,需要有一个特殊的状态,这个状态就可以用空指针来表示

注意!未初始化的指针并不是没有指向任何内存的指针,c语法是不允许的,编译器会报错

空指针也可用于如下情况:

暂时不打算赋值某个变量的内存地址,等到用时再赋值

如何判断一个指针是空指针?

9-7-2野指针

野指针就是被赋值了内存地址(一串十六进制数),但是此内存地址所对应的内存空间还没有被某个变量使用

比如下面这样

指针pa进行加法后得到的地址赋值给了pb,pb由此得到了一串16进制数,但是此十六进制数并没有对应的指向变量,即在此内存地址对应的内存空间,还未被某个变量使用

野指针也不要进行解引用,否则是下面这种结果

得到一个很大的负数

野指针不是好东西,能避免就避免,如果程序出现野指针,可能导致程序崩溃

9-7-3悬空指针

悬空指针就是指针指向的空间曾经分配过内存,但是现在已经被释放了

我们举一个例子

你会发现我们设置了一个函数,求函数内部的变量a的地址,然后我们在函数外部也同时获得了a的地址,貌似有效的通过指针,使func函数与外部进行了通信

但是你会看到有一个warning C4172: 返回局部变量的地址或临时 : a

什么意思?就是函数在调用完成后,函数内部的变量会被在内存上清理,此时函数返回的地址并没有任何变量存储在该地址所对应的内存空间里了

因此我们此时解引用函数返回出来的地址,并不能得到a的值

此时指针p叫做悬空指针

你看,解引用得不到a的值

悬空指针也不好,能避免就避免

如果指针悬空了或者变成野指针了,我们要把它变成空指针

9-7-4void类型的指针

之前我们学过,指针可以有数据类型,比如int short double...数据类型决定了在进行指针运算时,走的步长

比如int类型的指针,每+1,表示内存地址的数值增加4,表示在内存上前进4个字节

那么void类型的指针,是不能进行运算的,void类型的指针只能记录地址的值,但是不能通过解引用获取该地址对应的内存空间里存储的变量的数据

本节介绍void类型的指针的应用

1、用来存储不同类型的指针赋值过来的地址

此时如果我想把a地址赋值给pb,也就是让pb指向变量a,编译器会 warning C4133: "=": 从"int *"到"double *"的类型不兼容

那么我就是想要把a的地址赋值给另一个指针怎么办

就得用到void类型的指针了,它可以接受任意类型的指针赋值过来的地址

你会发现

void* pc = &a;

void* pd = &b;

这样的赋值是没有问题的没报错

那么我们可以通过解引用获取变量的值吗?

你看我代码刚刚输完就报错了,说明这样不行

这是为什么?

解引用的目的是,通过指针的类型,从而知道有多少个字节的数据可以被我解析,

但是现在void没有类型,也就无法通过解引用解析指针指向变量的值了

同理也无法进行计算,因为不知道类型,也就不知道每一步走几个字节

总结:

void类型的指针是不能进行解引用的

也不能进行计算

2、那么void类型的指针如何使用呢?

我们举一个例子

写一个函数,实现如下功能

给定函数一个数据和数据类型,然后把它打印出来

我们一起来分析一下代码

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void print(void* data, char type) {
    switch (type) {
    case 'i':
        printf("%d\n", *(int*)data);
        break;
    case 'f':
        printf("%lf\n", *(double*)data);
        break;
    case 'c':
        printf("%c\n", *(char*)data);
        break;
    }
}

int main() {
    int a = 1314;
    double b = 520.1314;
    char c = 'o';
    print(&a, 'i');
    print(&b, 'f');
    print(&c, 'c');
    return 0;
}

3-15行为我们实现的函数

首先函数类型void 函数名print 参数列表传两个参数,void类型的指针用来代表数据(传实参的时候直接 &变量就行,没必要在写 void*pa=&a;然后把pa传进函数)和一个字符,用来代表数据类型

然后函数体就是一个switch case函数

分别对三种字符代表的数据类型进行讨论

关键在于理解如下这三个东西

*(int*)data

*(double*)data

*(char*)data

以*(int*)data举例

(*int)data表示将传进来的void类型的指针改成int类型,然后再*,也就是再解引用的意思

由此函数就实现了,

传进来一个任意类型的变量的地址和其数据类型所对应的字符(即函数里定义的'i','f','c'分别表示整型,浮点型,字符型)

按照数据类型 输出这个变量对应的数

以此就发挥了void的作用,一个函数实现多态的效果

在c++中我们叫泛型

9-7-5二级指针和多级指针

我们前面讲的指针,都是指针变量,认为是一级指针,一级指针的值是其他变量的内存地址,那么如果需要存储一级指针的地址呢?则需要二级指针,二级指针是一个变量,存储一级指针的内存地址

大致关系如图

一般超过二级的指针都不常用,因此下方仅介绍二级指针的语法,未来遇到多级指针再进行介绍

二级指针在指针数组和数组指针中会有广泛运用

cpp 复制代码
    //定义二级指针的格式
    //指向的指针的数据类型  * 指针名
//其实就是 数据类型*       * 指针名
 //比如 int**pa;

对ppa进行一次解引用,得到的是存储在二级指针变量ppa中的16进制数在内存中的具体指向,即pa存储的16进制数,即pa的值

具体过程就是 解引用,解析指针变量ppa存储的东西的含义,即000000C263F4FB68的含义,发现是pa的内存地址,然后把内存中000000C263F4FB68对应的位置存储的东西输出出来,内存中000000C263F4FB68存了什呢?存的是pa的值,也就是000000C263F4FB44,这一串16进制数

你看,ppa解引用的结果就是pa的值

对ppa进行两次解引用得到的是一级指针的指向变量的值,在这里就是a的值10

但是注意不能写成这样

int**pa=&&a;

因为&&是逻辑与运算符

也不能写成这样

int**pa=&(&a);

因为&a得到的是一串16进制数,此时&16进制数的&是位与的含义而不是取地址的含义

事实上,常量只有赋值给变量或者用const修饰才能在内存中存储,单个常量是不在内存中分配空间进行存储的

因此这里对常量,也就是对一个16进制数取地址符是没有意义的,因为它根本就没在内存里,也就没有地址了

9-8-1指针常量

从这一节开始花三节讲解指针和const关键字组合在一起的三个概念

指针常量

常量指针

指针常量指针

本节先介绍指针常量

指针常量就是指针的值是一个常量

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>


int main() {
    int a = 1;
    int b = 2;
    //1、定义指针常量
    // 指针常量:指针的值是一个常量
  //指针 常量 变量名 &指向变量
    int* const pa = &a;
//上面这句话的意思就是pa的值是一个常量,即pa的值只能是a的地址,不能修改成其他变量的地址
   //如 pa = &b;就是不合法的 赋值符号的左值是一个常量,不能被修改
    return 0;
}

但是注意!

a的值可以修改,即*pa的值可以修改

a的值变成了1314

9-8-2常量指针

常量指针就是指向常量的指针

但是注意!

可以把这样写

pa=&b;也就是把b的地址赋值给pa,此时pa的指向变量变成了b,以后b的值都必须是2了,无法进行修改

你看pa的指向变量变成b了,所以解引用出来才是b的值

9-8-3常量指针常量

常量指针常量就是指针的值是一个常量的同时,指针的指向变量的值也是一个常量

常量指针常量是一个指针 指针的值不可修改 指向变量的值也不可修改

下面用一个表格来总结前三节学的指针与常量之间的三种关系

9-9-1指针与数组的关系

利用指针来访问数组元素

先回顾一下以前如何访问数组中的元素的

如下 利用索引值

那么下面我们看看用指针怎么操作

1、先把数组首元素的地址赋值给指针变量p,也可以直接把数组名赋值给指针变量p

具体原因如下

此时 p既然获得了数组首元素的地址,那么通过解引用自然可以得到数组的首元素的值,下面我们来验证一下

由此可见*p的值确实是数组首元素的值,即*p与a[0]的值相等

那么我们我们可以大胆的想象,既然把首元素赋值给指针变量p了,并且对p解引用得到了数组首元素的值

我们都学过指针的偏移,指针加上1,就是在内存上往前走一步,具体这一步取决于指针的数据类型

我们也学过数组中元素是在内存中连续的,每个相邻元素相差的字节数取决于数组的数据类型

那么只要我们把指针的数据类型与数组设置成同一个数据类型

那么指针p加上1,就可以用来表示数组元素从a[0]到了a[1]

下面我们分别看看a[0],a[1]的地址和值 还有p和p+1存储的内存地址以及它们解引用的值

可以看到由于数组是int类型的数组,因此数组相邻元素之间的内存地址刚好差4个字节,B8要到BC需要经过B9,BA,BB,BC刚好四个字节

同时由于指针类型是int类型,因此p+1和p存储的内存地址刚好差4个字节

再看从*p到*(p+1),经过加1操作成功从数组的第0个元素,偏移到了数组的第1个元素

下面我们可以总结一下结论

1、先把数组名赋值给指针变量p,然后就可以利用下面的规律用指针在数组上遍历任何一个元素了

*(p+i)==a[i]; //p加i解引用的值等于数组第i个元素的值

2、

我们看到p++之后,p解引用的值从数组的第0个元素变成了数组的第1个元素

利用这个就可以修改指针p的初始值了,原来的指针是数组第0个元素的地址,那么经过p++操作,现在指针是数组第1个元素的地址了

只有指针可以这样操作,数组不行,这个就是指针和数组的区别,指针和数组并不等同,完全就是两个东西

9-9-2指针数组

指针数组就是一个数组,数组中每个元素都是一个指针

下面我们来具体看一下一些语法和格式

补充:上面的第2点,如果后面没有大括号初始化,那么中括号内的元素个数必须写

但是如果有初始化了,中括号内的元素个数可以不写,如果写了,必须与后面初始化元素的个数相同

我们可以看到最终*pa的值和a相等 *pb的值和b相等 *pc的值和c相等

那么指针数组到底有什么作用呢?

我们来看一下

利用指针数组,把多种数据类型的元素组织起来,存储在一个数组中

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
    int a = 1;
    double b = 3.14;
    char c = 'y';

    void* parr[] = { &a,&b,&c };
    char t[] = { 'i','f','c' };
    for (int i = 0; i < sizeof(parr) / sizeof(parr[0]); i++) {
        switch (t[i]) {
        case 'i':
            printf("a = %d\n", *(int*)parr[i]); // 强制转换为int*后解引用
            break;
        case 'f':
            printf("b = %.2f\n", *(double*)parr[i]); // 强制转换为double*
            break;
        case 'c':
            printf("c = %c\n", *(char*)parr[i]); // 强制转换为char*
            break;
        }
    }

    return 0;;

}

这里需要着重讲一下第10行条件判断语句的含义i < sizeof(parr) / sizeof(parr[0])

  1. sizeof(parr) 计算整个数组 parr 占用的内存总字节数。

    1. 例如:若 parr 是包含 3 个指针的数组,每个指针占 8 字节(64 位系统),则 sizeof(parr) = 3 * 8 = 24 字节
  2. sizeof(parr[0]) 计算数组中单个元素占用的字节数(即第一个元素的大小)。

    1. 例如:在 64 位系统中,parr[0] 是一个 void* 指针,占 8 字节,因此 sizeof(parr[0]) = 8 字节
  3. 除法运算 总字节数 / 单个元素字节数 = 元素个数

    1. 例如:24 字节 / 8 字节 = 3 → 数组有 3 个元素。

9-9-3数组指针

数组指针就是指向数组的指针

下面介绍一下格式和语法

你会发现arr的值和parr的值居然是一样的

我们来分析一下:

arr的值是数组首元素的内存地址

parr的值是整个数组的内存地址

原因就是数组中的元素在内存中是连续存储的,并且首元素的地址就是数组的起始地址

所以从数值上看arr和parr是相同的

但是arr和parr是有区别的,区别在两者的运算方式不一样

arr到arr+1 从0000009A7551F728到0000009A7551F72C (29,2A,2B,2C)刚好四个字节

parr到parr+1 从0000009A7551F728到0000009A7551F73C 8到C是4个字节 然后2到3是16个字节 一共20个字节

因为arr的值是数组第0个元素的地址,那么arr+1就是偏移到数组的第1个元素,就是加4个字节

而parr的值是整个数组的地址,那么parr+1就是偏移了整个数组,整个数组是5*4=20个字节

下面我们介绍数组指针的应用

利用数组指针遍历二维数组

首先解决几个问题:

1、二维数组指针中括号内的数字必须与二维数组的列数一致,所以这里二维数组指针是这样定义的

double (*pd)[5] = &d[0];//把二维数组的第一行的数组的地址赋值给指针pd

2、为什么pd的值与*pd的值以及d[0]的值相等

因为double (*pd)[5] = &d[0]的含义是把二维数组的第一行的数组的地址赋值给指针pd,那么pd存储的就是数组首元素的地址,即d[0]的值

然后*pd表示访问pd指向的数组(即第一行d[0]

数组名在表达式中会自动退化为指针d[0]作为一维数组名,其值等于&d[0][0](即第一行第0个元素的地址)。所以*pd表示第一行数组的首元素的值

所以总的来说他们三个的值相等都等于二维数组第一行的第0个元素的地址,也就是这里数字4的地址

那么现在pd+1,pd的值是第一行数组的地址,那么+1表示偏移整个数组,整个数组的字节数是8*5=40,8是double类型所占的字节 而5是第一行共有5元素

即pd+1表示第二行数组的地址,以此类推

那么我们想象一下用第一个for循环,执行3次,i=0,++i,每次执行 pd+i 的操作,pd是一个数组指针,pd +i 还是一个数组指针,假设把 pd+i 赋值给一个新的数组指针pdi 第一次i=0,pd+i指向的是第一行,第二次i=1,pd+i指向的是第二行,以此类推

然后由于对数组指针pd解引用,的值是第一行数组的首元素的值,在第一个for循环内在嵌套一个for循环,执行5次,输出每一行的5个元素,把pdi解引用的值赋值给一个新的指针p 第一次,j=0,把p[0]输出出来,以此类推

那么最终

当数组指针 pdi 遍历到第一行时,指针p遍历输出第一行的五个元素

当数组指针 pdi 遍历到第二行时,指针p遍历输出第二行的五个元素

当数组指针 pdi 遍历到第三行时,指针p遍历输出第三行的五个元素

下面就是具体遍历二维数组的过程

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
    double d[3][5] = {
        {4,3,2,1,0},
        {9,8,7,6,5},
        {-1,-2,-3,-4,-5}
    };
    double (*pd)[5] = &d[0];//把二维数组的第一行的地址赋值给指针pd
    for (int i = 0;i < 3;++i) {
        double(*pdi)[5] = (pd + i); 
    //上面这一行的意思是:
    // 第一次循环把第一行数组的地址赋值给数组指针pdi,第二次循环把第二行数组的地址赋值给数组指针pdi
    //以此类推
        double* p = *pdi;
    //每次循环对pdi解引用,第一次循环得到的就是第一行数组的首元素的地址
    //第二次循环得到的就是第二行数组的首元素的值
        for (int j = 0;j < 5;++j) {
            printf("%0.lf ", p[j]);
        }
        printf("\n");
    }
    return 0;;

}

9-10-1指针函数

就是返回值为指针的函数 在9-6-2中已经介绍过基础用法,可回看

在c语言中有很多系统函数都是需要返回指针的,到内存管理章节的时候会详细讲解

这里先介绍指针函数的几个注意事项

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
    //格式 返回值类型 * 函数名(参数列表);
    int* func(int a, int b);
    return 0;;

}

注意事项:

1、可以返回空指针,但是需要对空指针进行判空处理,否则解引用就会出错

2、不要返回一个野指针或者悬空指针,这样会导致程序出现未定义的行为

9-10-2函数指针

函数指针是一种特殊类型的指针,它的指向变量是一个函数,而不是我们以前见过的整型变量a=2之类的

我们先演示一下函数指针怎么用的,再介绍函数指针的语法

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void func1();//函数的声明
int func2(int a);//函数的声明
int main() {
    void(*p1)() = func1;
    int (*p2)(int a) = func2;
    p1();
    p2(15);
    return 0;;

}
void func1() {
    printf("func1\n");
}

int func2(int a) {
    printf("func2: %d\n", a);
    return a * a;
}

函数指针就是让 具有函数功能的指针 可以向函数一样调用如上面的第8行,第9行

那么函数指针的语法是怎么样的?

函数的声明语法: 函数返回值类型 函数名(参数列表);

函数指针的定义: 函数返回值类型 (*函数指针名)() =函数名

只有函数指针名要新写一个 其他的直接对着函数声明抄下来就行

下一节9-10-3进行讲解函数指针的应用

9-10-3函数指针数组

我们用一道题来介绍函数指针数组 也介绍函数指针的应用

题目:输入两个整数 输出加 减 乘 除 取模的结果

Input:

5 6

output:

5+6=11

5-6=-1

5*6=30

5/6=0

5%6=5

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
//分别定义加减乘除函数
int add(int a, int b) {
        return a + b;
}
int sub(int a, int b) {
        return a - b;
}
int mul(int a, int b) {
        return a * b;
}
int div(int a, int b) {
        return a / b;
}
int mod(int a, int b){
        return a % b;
}

int main() {
        int (*operation[5])(int, int) = { add,sub,mul,div,mod };
        char t[] = { '+','-','*','/','%'};
        //上面这一行是定义一个函数指针数组 
        // 格式与函数指针的区别
        // 1、在函数指针名后面加[数组元素] 2、多了大括号,里面包含五个函数 3、(参数列表里的变量名不用写)
        int a, b;
        scanf("%d %d", &a, &b);
        for (int i = 0;i < 5;++i) {
                int ret = operation[i](a,b);//调用第i个(0-4)函数指针,并把返回值赋值给ret
                printf("%d %c %d=%d\n", a, t[i], b, ret);
        }
        return 0;
}

由此我们可以知道函数指针的作用就是,可以打包成一个函数指针数组,每一个函数实现某一个小功能,在一些情况下可以避免使用一大堆的if else语句判断等等

相关推荐
褚翾澜4 分钟前
Haskell语言的NoSQL
开发语言·后端·golang
zh_xuan15 分钟前
LeeCode 57. 插入区间
c语言·开发语言·数据结构·算法
aoxiang_ywj20 分钟前
【Linux】内核驱动学习笔记(二)
linux·笔记·学习
2401_8534482320 分钟前
C嘎嘎类里面的额函数
c语言·开发语言·c++
巷北夜未央30 分钟前
数据结构之二叉树Python版
开发语言·数据结构·python
旧识君1 小时前
移动端1px终极解决方案:Sass混合宏工程化实践
开发语言·前端·javascript·前端框架·less·sass·scss
Moonnnn.1 小时前
运算放大器(五)电压比较器
笔记·学习·硬件工程
郝YH是人间理想1 小时前
OpenCV基础——傅里叶变换、角点检测
开发语言·图像处理·人工智能·python·opencv·计算机视觉
Tiger Z1 小时前
R 语言科研绘图第 36 期 --- 饼状图-基础
开发语言·程序人生·r语言·贴图
揣晓丹1 小时前
JAVA实战开源项目:校园失物招领系统(Vue+SpringBoot) 附源码
java·开发语言·vue.js·spring boot·开源