C语言之容易被忽视的知识点——函数、指针和数组的关系(╰(‵□′)╯非常重要的知识点呐)

本文详细介绍了C语言中的函数、指针和数组的基础知识,包括函数定义、传参、指针定义、解引用、数组初始化、多维数组以及它们之间的关系。特别强调了指针和数组在内存表示上的相似性,以及如何在函数中使用指针改变外部变量的值。此外,还探讨了函数指针、指针数组和数组指针的概念,并展示了它们在函数参数传递中的应用。
摘要由CSDN通过智能技术生成

自从上一篇文章写了之后,已经过了有一个星期了(这就是摸鱼吗)。我最近是越来越懒了,这篇文章是拖了再拖,我觉得不能这么下去了。于是乎想写一篇总结函数、指针和数组的关系的一篇文章。

本文基于《C Primer Plus》加上我自己的一些整理而作

(强烈推这本书,非常好,适合初学者入门,就是五百多页有点难啃)

在总结之前先回顾一下它们的基础知识(讲的不会太过精细,请见谅)

该文章中的代码默认在32位机上运行!!!

重点:如果还记得基础的话,请直接跳转到“ 函数与指针 ”

警告:基础的我讲的不是很全面,我只讲了后面内容要用到的知识点

目录

一.函数基础

定义一个自己的函数

函数传参

函数的嵌套

二. 指针基础

如何定义一个指针?

如何取出指针存的地址中的值?

 请务必避免野指针

如果使用了不同类型的指针来储存不同类型值的地址,会怎么样?

使用了不同类型的指针来储存不同类型值的地址的使用例子

多级指针

三. 数组基础

请定义一个数组

数组初始化

怎么使用下标索引数组中的数据?

多维数组

五. 函数与指针

用函数改变函数外的变量的值

函数指针

六.指针与数组 

指针的加减法与数组的下标索引 

数组指针与指针数组

数组指针与二维数组

指针与数组在函数传参中的替换

函数指针数组


函数基础

什么叫做函数?

大家应该都知道c语言是一个模块化的语言,大大小小的一些功能实现都可以通过写一些函数来做。而且因为函数,c程序更加简洁,代码更具可读性

定义一个自己的函数

函数分为库函数和自定义函数

库函数:是c标准自带的库中的函数

自定义函数:顾名思义是用户自己写的一些函数

接下来,主要讲自定义函数,如果有想要学习库函数的小伙伴,可以进入下面的网站去学习

www.cplusplus.com

如何写一个自定义函数呢?

1、首先得定义返回类型

如:void、int、float、double、long等

2、在是函数名后加()

()里写输入参数

3、最后再是函数主体

如:

void print(void){
       printf("Hello World!");
    }

这个函数说明

返回值为void类型,即无返回类型

print为函数名

()为声明这是一个函数并定义它,里面的void指无参数

{}所包含就是函数体,用于执行任务

函数传参

现在讲函数传参

使用函数时传入的参数称为实际参数(实参)

函数原型中的参数叫做形式参数(形参)

函数的嵌套

函数还有一个性质

就是允许函数之间相互嵌套,即在一个函数中使用其他函数

当一个函数调用自己时,就是函数递归

函数递归我就不细讲了,以后有机会再说

 指针基础

什么叫做指针?

指针可以说就是一个变量,专门用来储存数据地址

(如果你想问地址是什么,那我还是不讲了,请移步百度)

任意类型的指针都占4字节内存(32位机标准)

如何定义一个指针?

先确定你所需的指针要指向什么类型的对象

那么就用此类型并加上

如:

​int a = 9;
int * pa = &a;
int* pa = &a;
int *pa = &a;
int*pa = &a;    //此四种形式是一样的意思
                //都表示定义一个名为 pa 指针
//*两面有没有空格是无关紧要的事情
//不过程序员一般喜欢用第二种形式用来表示
​

&是什么意思?

这是一个取地址符

上述代码表示取变量a的地址存入int*类型的指针中

如何取出指针存的地址中的值?

如果要这么做,请使用 * (解引用运算符)

还是上述代码,我们应该如何取出pa中地址的值

int b =0;
b = *pa;
b = *       pa;
//当然这样也行,只是最好不要这么做,我只是做个例子

 请务必避免野指针

什么是野指针指向一个非法地址的指针

一个未初始化的指针会随机在内存中存入一个地址,首先这个是一个非法的地址,因为系统根本没有允许这样的地址能够使用!

如果使用了呢?

在编译器使用的话,在运行程序时,编译器会立即报错

因为该指针使用的非法地址可能存了重要数据,使用野指针可能会破坏它

所以,珍爱程序,远离野指针

即使你想要创建一个指针,但是还没用到,也请初始化为 NULL (此宏已在stdio.h定义)

如果使用了不同类型的指针来储存不同类型值的地址,会怎么样?

是的,这个问题在一般意义上是可以肯定为错误的。但是,我既然提到了必定有一定用的

首先我们得了解地址与内存的关系

我们都知道 int 类型的变量占4字节(32位机上是这样的,64位机 int 类型变量占8字节)

而取一个int类型变量的地址是怎样的呢?

 如上图,取变量a的地址其实取的是该变量的第一个字节的地址

而*p这个解引用操作是取出存在四个字节的值

同样的,如果是一个char*类型的指针变量,那么解引用操作会使它取出一个字节的值(关键!!!)

如果用一个char*类型的指针来存一个int类型的变量的地址会怎么样?

当解引用操作时只会取出该地址所在字节的值,而不是该int类型变量的值

不同类型指针解引用时只会拥有取出该类型所占内存大小的数据,或者说当定义一个指针时,它的类型就是它解引用时拥有的内存权限

如:

可见对指针p解引用的值为44,占一个char类型(一个字节)

 那么会有什么用呢?

使用了不同类型的指针来储存不同类型值的地址的使用例子

使用前请了解一个有关于数据储存的定义:

大端模式,是指数据的高字节保存在内存的低地址中,数据的低字节保存在内存的高地址

小端模式,是指数据的高字节保存在内存的高地址中,数据的低字节保存在内存的低地址

如图:

 每个编译器的存储方式都不一样

那如何来判断呢?

​#include <stdio.h>
int main(){
    int a = 1;    //0x 00 00 00 01
    char* p = (char*)&a;//p得到a的处在低地址的字节地址
    if(*p==1)
       printf("小端存储");
    else
       printf("大端存储");
    return 0;
}

​

请复制到你的编译器试一下

多级指针

既然能定义一个指针,那么它肯定也有一个地址来储存它

这时我们想要定义一个指向它的指针该怎么做呢

在 类型* 后再加一个 * ,此时它就是一个二级指针,一个指向指针的指针

 同理还有三级,四级乃至N级指针

int a = 1;        //变量
int* p1 = &a;     //一级指针
int** p2 = &p1;   //二级指针
int*** p3 = &p2;  //三级指针
………………

 数组基础

什么叫做数组?

顾名思义,储存一组数据的集合,当然必须是同一种类型

请定义一个数组

先选择一个该数组类型

再写上数组名

数组名后面记得加 [ ] ,表示这是一个数组(一定要用英文的,不要用【】)

[ ] 内可填可不填常量表达式,填了表示该数组的大小

如:

int arr[];     //这是一个未定义大小的int型数组
int arr1[4];   //这是一个含4个int型变量的数组
char arr2[5];  //这是一个含5个char型变量的数组

数组初始化

一个数组在使用前一定要初始化,否则可能会出现一些不可预料的变故(当然,其他变量的使用也最好初始化)

现在就由我来讲一下数组初始化的规则

1.一个未定义大小的数组在初始化的时候以存入数据多少为准

2.一个指定大小的非字符数组在初始化时如果未存满,剩下的每单位都以 0 初始化

3.一个指定大小的字符数组在初始化时如果未存满,剩下的每单位都以 '\0' 初始化

4.如果要指定初始化某位置,请以下面代码形式执行

int arr[5] = {1,[4]=5};
//初始化后其为arr[5]={1,0,0,0,5}

如果在[4]=5后再设置一个数,则计算机会自动将其排在[5]处,当然在本例中这样子会超出数组大小,编译器报错

([4]=5的意思时在4下标位置初始化为5,下标马上就讲)

(太过详细也不讲了,自己用编译器去试一些就行了,毕竟计算机语言是一门实用性大于理论性的语言)

怎么使用下标索引数组中的数据?

首先当然是得有一个数组啦!😉

写出该数组的数组名,再用下标运算符 [ ],在下标运算符中写入想要找的值的下标,这样就完成啦

当然,你得记住,下标是从0开始的

例如:一个有5个元素的数组,第一个值的下标为0,最后一个就为4

多维数组

当某一个数组的元素为一个数组时,我们就可以把这么一个数组称作二维数组

那么我们该如何来声明这么一个二维数组呢?

int arr[4][4];

这就是一个4×4的二维数组

最前面的 [ ] 表示这个数组含4个元素,后面 [ ] 表示这四个元素是数组,每个子数组含4个元素,都为int型,所以总共含16个int类型的变量

那么,如果是一个三维数组呢?

int arr1[3][3][3];

这是一个3×3×3的三维数组

还是先从最前面的那个 [ ] 来看,该数组含三个元素,再后面的一个 [ ] 表示每个元素也是一个数组,含三个元素,最后一个 [ ] 表示这3×3个元素也是数组,含三个元素,都为int型,所以总共含27个int类型的变量

后面的四维数组,五维数组……都是按照这个套路来的😁

如果你不知道一个多维数组到底应该有多大,请如下面的规则定义

int arr[][4];  //正确
int arr1[4][]; //错误
int arr2[][];  //错误

只能空最接近数组名的那个 [ ] 的数字,后面的都必须写上确切的数

如某四维数组:

​int arr[][4][4][4];  //正确
int arr[][][4][4];   //错误
int arr[4][][4][4];  //错误

​

 基础的终于是讲完了,虽然不是太全面,如果想获悉更多请关注我的博客哦!😉我会努力更新奉献各位观众老爷的😎

 函数与指针

这个知识点的内容比较少,你只需要一点点的函数与指针知识就可以完全了解

用函数改变函数外的变量的值

如果想要改变一个变量的值,该怎么传参呢?

传递地址,以指针接收,在函数内解引用操作后就可以改变该地址的值了,也就是该变量的值

(我们可以这样理解,变量名其实是程序员给该变量所在地址起的别名)

#include <stdio.h>

void Add_one(int* num);
int main(){
    int a=1;
    printf("%d\n",a);
    Add_one(&a);
    printf("%d\n",a);
    return 0;
}

void Add_one(int* num){
     *num+=1;
}

/*   打印结果
**   1
**   2
*/

函数指针

什么是函数指针?

顾名思义当然是一个储存函数地址的指针,那么我们又该如何来创建呢?

如:

int fun(int y);  //这是一个函数原型

.............

​int (*pfun)(int x);
int (*pfun1)(int);
//两个都是一样的意思,是否写形参名都无关紧要

​pfun = fun;
pfun = &fun;
//函数名其实就可以表示该函数所在的地址

它们都代表命名一个pfun或pfun1的指针来储存一个函数地址,该函数必须有一个int型的形参

返回值为int类型

一个函数指针的创建必须要用一下形式创建

                  类型  (*函数名)( 形式参数);  

当我们去掉 *函数名 两边的() 会发生什么?

这样子是定义一个返回值是一个指针的函数的形式(也就是指针函数)

指针与数组 

我们可以说,数组其实与指针的区别就好像一对双胞胎一般,两者不同的地方可能就是一个叫指针,一个叫数组🤗🤗🤗

指针的加减法与数组的下标索引 

指针的加减法

int arr[3]={1,2,3};
int* p=a;   //数组名既是数组首元素地址
int* p1; 

//***********************************
p1 = p+1;
p1 = arr[1];//两个赋值语句的意思是一样的

//***********************************
p1 = p+2;
p1 = arr[2];//两个赋值语句的意思是一样的

按照上面的代码来看可以说

指针加法其实就是地址的加法,+1其实就是+一个int型大小

也就是地址+4(int 为4个字节嘛)

如果是减法则为地址-4

其他的无论是char*,double*指针加减法都是类似的,在此我就不多加赘述了,因为我相信我的观众老爷一定是顶级天才(应该是的吧~)

下面有一张图来表示:

 根据上面的概念来看,指针加减法与数组下标索引有区别吗?

int arr[3]={1,2,3};
int* p=arr;   //再提一遍数组名代表数组首元素地址

if(*(p+0)==arr[0])
   printf("相等,没什么区别");
if(*(p+1)==arr[1])
   printf("相等,没什么区别");
if(*(p+2)==arr[2])
   printf("相等,没什么区别");

/*输出结果:
**相等,没什么区别
**相等,没什么区别
**相等,没什么区别
*/

看吧!其实只是形式不同,其实差别不大

这样我们就可以用数组替代指针,用指针替代数组了,当然一定要注意非法越界哦!

int arr[3]={1,2,3};
printf("%d\n",*arr);   //再说一遍哦,数组名代表数组首元素地址
printf("%d\n",*(arr+1));
printf("%d\n",*(arr+2));

/*输出结果
** 1
** 2
** 3
*/

*********我是分界线*************

int arr[3]={1,2,3};
int* p=arr;
printf("%d\n",p[0];  
printf("%d\n",p[1]);
printf("%d\n",p[2]);  //是的,可以这么用,是不是很神奇啊!

/*输出结果
** 1
** 2
** 3
*/

数组指针与指针数组

什么叫数组指针?

一个指向数组的指针

什么叫指针数组?

数组内元素是指针类型

那它们的表现形式是怎么样的?(或者说是格式)

​int a=1;
int b=2;
int c=3;

int arr={a,b,c};        //没错,是可以这么用的
     数组
int (*arr1)[3]=&arr;    //注意这次一定要加&,否则编译器会发出警告
     数组指针
int* arr2[3]={&a,&b,&c};//原则上是一个数组,存入地址的数组
     指针数组
​

注意:如果直接使用arr给数组指针,其实只是给首元素地址,只有&数组名才是数组全地址 

你可能发现了,数组指针函数指针创建相似

先用()将 * 数组名先结合,确定它是一个指针

然后 [3] 再与之结合,定义它是指向一个元素里量为3个的数组

最后再看 int ,可知这个被指向的数组含的元素为 int 型

再看指针数组

[3] 优先级最高,先与数组名结合,确定它是一个数组

数组类型为 int* ,即整型指针

该数组可以存入三个地址

(请注意,()比 [ ] 的优先级更高,如果你想了解更多与优先级相关的内容,请关注我的下一篇博客😘😘😘点赞关注加收藏哦

数组指针与二维数组

我们先了解一下数组指针的加减法

int (*arr1)[3]=&arr;    //注意这次一定要加&,否则编译器会发出警告
     数组指针

还是这个数组指针

如果arr1加1会怎么样?(或者说跳过多大的内存)

跳过一个含三个int型元素数组,也就是3×4个字节的大小

了解这个后我们来看看下面一段示例

可见,确实是跳过了一个数组

int arr1[2][3] = { { 1,2,3 },{ 4,5,6} };
int (*arr)[3] = &arr1[0]; //如果不用&,会报警告

 这样我们又可以看出指针又可以与数组相互替换了

 arr1[0][3];
    ||
 (*arr)[3];
arr1[1][3];
   ||
*(arr+1)[3];

其他多维数组当然也可以这么替换,只是难道数组表现形式不香吗。硬要搞指针那么复杂的形式干嘛

指针与数组在函数传参中的替换

我们了解一下数组如何在函数中传参的

#define SizeNum 3  //宏定义一个常数

void wsmpbx(int arr[],int SizeNum);
     //我是冒泡排序
int main(){
     int arr[SizeNum]={1,2,3};//[]里只能是常量表达式
     wsmpbx(arr,SizeNum);
     return 0;
}

。。。。。函数实现我就不写了。。。。

用arr[ ]来接收数组( [ ] 里可写可不写)

此时我们是不是可以用指针来接受数组的首元素地址?

可以只要知道数组长度就好

之后使用这个指针来索引数组各元素无论是用指针加减法还是数组下标索引都可以,毕竟指针和数组可以互相转化的嘛😗😗

函数指针数组

我们先从名字来分析

数组为名词,函数指针为修饰词

所以这是一个存有函数指针的数组

那我们该怎么来创建呢?

int (*arr[3])(int );

啊!看着好复杂啊!

但是我们可以层层剖析它,找出它的本质

arr 先与[3]结合,确定它是一个数组

再与 * 结合,表明该数组存入的是一个指针类型

然后再与(int)结合,确定该指针类型为函数类型

指针指向的函数必须返回值为 int 类型,形参有一个 int 类型

这就是函数指针数组

用处我就不讲了,我现在从来都没用过

那么再来看看这个

​
​int (*arr[])(int (*[])(int) );

​

很复杂哦!

如果要剖析的话,我觉得还是省省吧!

这是一个函数指针数组,不过指针所指向的函数传参为函数指针数组

还有呢

​

​int (*(*arr[])(int (*[])(int) ))(int);

跟上一个最大的区别就是指针所指向的函数的返回类型也是函数指针数组

现在又来了一个问题,如何才能简化呢?

​typedef int (* [3])(int) funpnums;
//记得,[]和()内一定要有值和形参
​//这是错误命名方法
​typedef int (* funpnums[3])(int);
//正确
//创建一个函数指针数组
funpnums fpn ;
//创建了一个名为 fpn 的函数指针数组

是不是非常方便?

重点:在进行类型重命名时,[ ]和( )内必须有内容,否则创建的类型不完整

那么再上面的代码该如何简化,想必也有办法了吧!

​typedef int (*name[3])(int);

​int (*(*arr[3])(int (*[3])(int) ))(int);
        ||
        ||
name (*arr[3])(name);//意思一模一样


​

可能讲的不太详细,但希望能有所帮助吧!

 讲到这,我想表述的已经写的差不多了,此时就到了文章的结尾

如果你想了解更多,可以关注

我会在别的时间加油来写文给我的观众老爷们看的哦!🤗🤗🤗

                                       作于2022/3/5 11:52(3)

  • 13
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值