C语言数组和函数详解
目录
引言:
本篇讲述的是数组与函数方面的知识,因为数组和函数方面需要注意的细节比较多,拓展也比较多,所以专开一篇来讲解下,当然,和之前一样,最基本的创建我并不会讲,毕竟没什么需要注意的地方。
那接下来就进入正文啦————>

数组
这里我就把数组分为一维数组和二维数组来讲啦,三维,四维等多维数组本质上与二维数组没什么区别,就是一个叠加,所以就不讲下去了
一维数组
数组的创建
一维数组创建时[]里可以不写数字,但后面要初始化值,创建完的数组的长度就为初始化值的个数,如下图代码,该数组的长度就为5.
int a[] = {0,1,2,3,4};
数组的初始化
若数组创建在局部变量中,要进行初始化,不然是随机值,如图

若数组创建在全局变量中,可以不进行初始化,若不初始化,数组内元素默认为0,如图

数组的初始化一般使用大括号,将数据放在大括号中,分为完全初始化和不完全初始化(剩余元素的值都默认初始化为0)
完全初始化就是将数组里的每个位置都进行初始化
int a[10] = {0,1,2,3,4,5,6,7,8,9};
不完全初始化就是只初始化前面一部分的数组,然后剩余的部分默认初始化为0,如图

数组的类型
数组也是有类型的,数组算是一种自定义类型,去掉数组名留下的就是数组的类型,这跟变量一样,去掉变量名就是变量类型,如下方代码,变量a的类型就是去掉变量名即为int,数组b的类型就是去掉数组名b,即为int [10],而不是int,int是数组元素的类型,数组c的类型同理,就是int [],二维数组也一样,就顺带直接讲了,下面就不讲了,d数组的数据类型就是int [10][10],arr数组的数据类型就是int [][10]
int a;
int b[10];
int c[];
int d[10][10];
int arr[][10];
数组使用
这个就没有什么需要注意的了,需要提一嘴的就是,[]这个也是一个操作符,叫下标引用操作符。然后数组下标是从0开始的。
一维数组在内存中的存储
在讲解一维数组是如何在内存中存储之前,我们先拓展一下内存相关的知识
首先,可以把内存当作是个大区域,然后栈区,堆区和静态区就是这个大区域中不同的三个区域,而每个区域为了更好的管理和使用内存,会把内存划分成一个个很小的内存单元,一个内存单元的大小是1个字节,内存单元的编号就是地址(就跟门牌号一样),那就先略微讲这些,大致理解就可以了
数组其实在内存中是连续存放的,随着下标的增长,地址是由小(低)到大(高)变化的,至于数组俩个下标临近的地址的差值就是数据类型的值,如图,给他强转成整形后输出会发现地址前后那个地址的差值就是int所占字节的个数,即为4

二维数组
我们可以把二维数组当成n个一维数组来看,一个一维数组就相当于他的一个元素,这样比较好理解
二维数组的创建
二维数组创建时,行号可以省略,但列号不能省略,
正确创建
int arr1[10][10];
int arr2[][10];
错误创建
int arr1[][];
int arr2[3][];
int arr3[3][] = {{0,1},{1,2},{2,3}};
错误创建中可能有人会觉得第三个arr3创建没问题,因为已经给它初始化了,但是,若给数组初始化时,若没有完全初始化,数组是会给剩余的元素默认初始化为0的,但列号不固定的情况下,就无法进行上述的剩余初始化操作,就会导致出问题,编译器应该也会报错
既然讲到了初始化,那接下来我们来讲下二维数组的初始化
二维数组的初始化
首先我们在讲完全初始化和非完全初始化前,我们先讲一讲按行初始化
按行初始化:
只需要加上{}即可,每行一个{}
注:{}里面也可以什么都不写,比如想初始第三行,前面俩行不想给他赋初值,前面俩个{}里面就可以空着不写,大括号里面初始化就正常 " , " 隔开就可以了
int a[3][10]={{..},{..},{..}};
接下来我们讲完全初始化和不完全初始化
完全初始化:
只要像这样一直写下去就可以了,不用在里面加{},因为完全初始化就是初始化所有的数组元素,从左往右,从上往下依次进行初始化,所以不用加{}
int a[10][10]={1,2,3,4,...}//(写到数组元素全部赋完初值为止)
不完全初始化:
数组元素没有全部进行指定初始化,就为不完全初始化。
可以像上面完全初始化的方式写,只要别写满就是不完全初始化,也可以用按行初始化的方式进行初始化。
二维数组在内存中的存储
二维数组在内存中也是连续存储的,且是从左往右,从上往下依次递增,如图

拓展:假设一数组arr[][10]
该数组的第一行数组的数组名是arr[0]
第二行数组的数组名是arr[1]......(对后续指针和深入理解二维数组很重要)
接下来,我们讲一下计算数组元素个数是时的易错点
sizeof计算数组元素个数
用sizeof算出整个数组所占字节再除以数组内单个元素所占字节即为数组元素个数,用下述代码即可求出数组元素个数
int arr1[10];
int len = sizeof(arr1)/sizeof(arr1[0]);
注:若用sizeof来算字符串,就会导致得到的结果比字符串长度多1,因为字符串的结束标志是\0,但sizeof会将这个也读进去进行计算,而用strlen函数则可以准确返回字符串的长度,因为strlen函数是遇到\0就停止,不算\0,就分析下面这个代码来具体讲清楚,这俩种初始化就使得他们的sizeof与strlen都不相同
arr1的sizeof的返回值是4,因为一个字符占一个字节,\0也被算在其中,而arr2中只有a,b,c三个字符,所以arr2的sizeof返回值是3。
arr1的strlen的返回值是3,因为遇到\0截止了,但arr2的strlen返回值是未知数,因为并不知道这个数组的下一个\0出现在哪里
char arr1[] = "abc";
char arr2[] = {"a","b","c"};
加长数组
C99中引入了变长数组的概念,这时创建数组的时候可以使用变量来指定大小
变长数组局限性:不能初始化
变长数组的意思是数组的大小可以使用变量指定(不是大小可以变化),但是数组一旦创建后,大小就无法改变了
C99之前数组的大小只能使用常量或者常量表达式指定
但是遗憾的是VS中不支持变长数组
使用gcc编译器的IDE可以使用变长数组,具体怎么使用就是把常量换成变量就好了
int arr[n];
数组应用
数组可以应用于很多场景,最简单的就是在有序序列里找一个数,这个时候可以用二分来找,可以另时间复杂度从n次下降为log(n)次
至于使用二分时,left+(right-left)/2 比 (left+right)/2 好,因为left+right可能产生数据越界情况
当然也有其他应用,这里就不过多叙述了
函数
首先提一嘴,函数也是可以作为参数的,OK,接下来进入正文
函数的概念
函数想来我们都不陌生,数学里就很常见,C语言也引入了函数的概念。有些翻译为:子程序,子程序更准确一些,因为C语言中的函数就是完成某项特定任务的一小段代码
C语言的程序就是由函数组成的,同时一个函数如果能完成某项特定任务的话,这个函数也是可以复用的,提升了开发软件的效率
在C语言中有俩类函数:库函数和自定义函数
库函数
在讲库函数之前,我们先要讲一讲标准库,那什么是标准库呢,标准库就是C语言标准中采纳了printf,scanf等函数,但不直接实现函数,而是给定标准:函数名,参数是什么,功能是什么。
编译器的厂商会按标准的约定去实现这些函数,这些函数就被称为库函数(因为是各个厂商去实现,所以具体代码肯定会有差异性,但功能是一样的)
库函数都是现成的,用库函数时要带上对应的头文件
因为库函数有很多,这里就不细讲了,可以去网站上自己学习研究,我推荐俩个网站,分别是cppreference.com和cplusplus.com
在这些网站里介绍库函数的文档都是类似的,即
1.函数原型
2.函数功能介绍
3.参数和返回类型说明
4.代码举例
5.代码输出
6.相关知识链接
自定义函数
(返回的数据类型) 函数名(参数)(参数要表明参数的类型和名字)
{
函数体
}
这便是自定义函数的格式
注:不设置参数时,可以(),也可以(void),(void)是不能有参数,()是传参数也行,不传参数也行,但一般表示的是不要传参数,如图
若为()时,不管()里写什么,都会进入函数

若为(void)时,则不能传参数


特别注意:函数设计时应该追求高内聚(高效的功能单元,比如要求和,那这个函数只求和,不干别的(不要让一个函数实现很多功能,越单一越好,这样复用性就高了))低耦合(模块间不要有强关联)
高内聚:功能独立
低耦合:比如写了一个项目,里面有20个函数,你修改了其中一个,若其他的都要跟着改,就说明耦合度过高了,要尽量避免这种情况
形参和实参
在函数使用过程中,参数分为实参和形参
实参:真实传递给函数的参数
形参:定义函数时的参数
如图

那为什么叫形式参数呢
因为不调用时候,函数的参数只是形式上存在,不会向内存申请空间,即不真实存在,所以叫形参。
形参只有在函数被调用的过程中为了存放实参传递过来的值,才会向内存申请空间,这个过程就是形参的实例化
实参和形参的关系
名字可以一样
形参和实参是完全不同的内存空间
形参是实参的一份临时拷贝(临时是因为出函数形参就没了,拷贝是因为形参数据由实参得到)
形参的改变不会影响实参
return语句
return语句很简单,就是返回一个东西,就是有些注意事项要讲一下
return后边可以是一个数值,也可以是一个表达式,如果是表达式则先执行表达式,再返回表达式的结果
return后边也可以什么都没有,直接写return; 用于直接结束该函数的进程,不执行该函数下面的代码,这种写法适合函数返回类型是void的情况
return返回的值和函数返回类型不一致时候,系统会自动将返回的值隐式转换为函数的返回类型,若强转会发生裁断,比如浮点转整型不会四舍五入,如图

return语句执行后,该函数后面的代码就不执行了
如果函数中存在if等分支语句,若不是void类型的函数,要保证每种情况都要有return返回,否则虽然能运行,但跑的时候会跳出提示说有隐患,如图

数组做函数参数
数组作为参数只要传数组名就可以了
但形参的数组要写成 数组名[],[]里面可写可不写,数组传过去时候,形参的数组的地址和实参的空间的地址是一样的,所以改变函数里的数组,原数组也会跟着改变,如图

数组传参的几个重要知识点:
函数的形参要和函数的实参个数匹配
函数的实参是数组,形参也要写成数组形式
形参如果是一维数组,数组大小可以忽略不写
形参如果是二维数组,行可省略,列不能省略
数组传参,形参是不会创建新的数组的
形参操作的数组和实参的数组是同一个数组(重点)
嵌套调用和链式访问
嵌套调用:
函数之间互相调用被称为嵌套调用
函数之间可以嵌套调用,但不能嵌套定义
链式访问:
将一个函数的返回值作为另外一个函数的参数,像链条一样将函数串起来就是函数的链式访问,这个比较难理解,用形象点的代码来说,如下图代码即为链式访问
printf("%d",strlen(a));
目录的拓展大纲中会拓展printf的链式访问,这里就不说啦
函数的声明和定义
一般我们常见的是教科书上的单个文件的声明和定义,那我们就先讲这个作为踏板再讲多个文件的声明和定义
单个文件的声明和定义
如果在定义一个函数前,就调用该函数,就要声明函数(除开函数体的其余部分加;)如图

这即是声明,函数声明就是告诉编译器函数名是什么,参数类型是什么,返回类型是什么,但具体有没有这个函数就要看函数的定义了
注:函数在使用前,一定要先声明再使用,函数的定义本身就是一种特殊的声明
多个文件的声明和定义
那么,单文件的声明和定义已经讲完了,接下来就要讲多个文件的声明和定义了(这个非常重要,一般公司都是用多个文件(因为代码很多))
一般情况下,新建文件,然后函数的声明,类型的声明放在头文件(.h)中,函数的实现放在源文件(当然是新建的源文件)中(库函数就跟这种很相似了)
自己写的头文件声明时不要用 "<>",而是用 " " 双引号来声明,如#include "add.h",这样
那么这么做的好处是什么呢
1.逻辑清晰
2.方便多人协同(如果都写在一个文件里,只能一个人来写,但如果多文件,就可以各自都能写代码了)
(拓展)3.适当的隐藏代码(可以让真正实现该功能的主体函数变成静态库)(了解就行)
如图
函数声明放头文件

函数主体放源文件

调用函数时候只需写上对应头文件即可

这样代码是不是就简洁很多,下一篇的扫雷游戏就会采用多个文件的形式来制作。
static和extern
staic和extern都是C语言中的关键字
static可以用来修饰局部变量,修饰全局变量,修饰函数
extern是用来声明外部符号的
在讲这俩个关键字之前,要先知道作用域和生命周期
作用域
作用域是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效的,而限定这个名字的可用性的代码范围就是这个名字的作用域(限定使用范围)
局部变量的作用域就是变量所在的局部范围
全局变量的作用域就是整个工程
生命周期
生命周期指的是变量从创建到销毁之间的一个时间段
局部变量的生命周期是:进入作用域变量创建,生命周期开始,出作用域生命周期结束
全局变量的生命周期就是整个程序的生命周期
都挺通俗易懂的概念,就不用图助理解了,那么接下来,我们正式开讲static和extern俩个关键字
static
static修饰局部变量
static 数据类型 变量名;
static修饰局部变量改变了变量的生命周期,生命周期改变的本质是改变了变量的存储类型(存储类型跟存储位置本质上讲的一个东西),本来一个局部变量是存储在内存的栈区的,但被static修饰后存储到了静态区,存储在静态区的变量的生命周期和全局变量是一样的。
局部变量被static修饰后变为静态变量
static修饰的变量生命周期变了,但作用域没变
具体例子如图,这个代码若不将 i 转为静态变量,输出肯定为1 1 1 1 1,这是毋庸置疑的,那为什么转为静态变量后却变成1 2 3 4 5了呢,因为 i 经过static修饰变成静态变量后,他的生命周期就已经是整个程序了,若程序未结束,这个 i 就不会销毁,既然不会销毁,他从第一次定义成0后,就不会再次定义变成0 ,而是基于原有的值进行累加了。所以最后输出为1 2 3 4 5

static修饰全局变量
一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用
本质原因是全局变量默认是具有外部链接属性的,在外部的文件中想使用,只要适当的声明就可以使用,但全局变量被static修饰后,外部链接属性就变成了内部链接属性,只能在自己所在的源文件内部使用了,其他源文件,即使声明了,也无法正常使用
使用建议:如果一个全局变量,只想在所在的源文件内部使用,不想被其他文件发现,就可以用static修饰
static修饰函数
与修饰全局变量效果一模一样,函数也默认具有外部链接属性
extern
函数跟全局变量一样,都是可以在整个工程中使用的,若要用别的源文件内的全局变量或函数,只需要在当前源文件里用extern声明一下就可以了
extern 数据类型 变量或函数;
这个就主要用于多个文件时候外部字符的声明了,若只用一俩个函数,就用extern,若用多个函数,就用#include "头文件"即可
注:如果是被static修饰了,就不能在别的源文件声明使用了
拓展
这次拓展三个简单的知识
printf函数的链式访问
先说前面提到过的printf()的拓展:printf()返回的是int类型的值,返回的是输出字符的个数
这个其实很容易理解,只需要俩个代码就可以领悟透彻了
先看第一个,这个代码为什么输出4321呢,因为首先链式访问肯定先访问最深层的那个嘛,然后最深层的输出43,是2个字符,所以最深层的printf返回值为2,所以又紧接着输出2,变为432,因为输出了一个2,所以返回值为1,最终输出4321

再看下一个,为什么这次输出是43 3 2呢,因为首先他先输出“43 ”,为3个字符,所以返回3,然后输出“3 ”,返回2个字符,最后输出2,即得到43 3 2

Sleep函数
头文件是windows.h(括号里面写数字,单位是毫秒)可以让代码睡眠(运行到这停顿)一段时间
system("cls")的作用
头文件是stdlib.h,用于清屏
结语
那么到这里,数组与函数部分也讲完啦,希望内容对你们有帮助,觉得好的话可以分享给朋友哟,毕竟虽然一个人走的快,但一群人走的远,谢谢观看啦,下一篇应该是用这些知识和前面C语言的知识(就这个C语言基础知识细节-CSDN博客)来做一个小小的扫雷游戏,那么,回见

更多推荐


所有评论(0)