尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

C语言数组与函数篇

C语言数组与函数篇 目录1.引言2.数组2.1.一维数组2.2.二维数组2.3.变长数组2.4.数组应用3.函数3.1.函数基础3.2.函数的形参与实参3.3.函数的 return 语句3.4.数组做函数参数3.5.函数的嵌套调用及链式访问3.6.函数的声明与定义4.函数递归5.结语1.引言在上篇博客中我们详细介绍了C语言中常用的操作符其中也提到了作用于数组的下标引用操作符与作用于函数的函数调用操作符那么本篇博客就详细讲解有关数组与函数的具体知识及使用方法。2.数组数组是一组相同类型元素的集合所以数组中可以存放一个或多个数据且这些数据属于同一数据类型。数组可分为一维数组与多维数组多维数组中常见的是二维数组。2.1.一维数组存放在数组中的值称为数组元素数组在创建时可以指定数组大小与数组元素类型。一维数组的语法格式如下。type 指数组中存放数据的类型可以是我们前面讲过的 intchar 等内置类型也可以是自定义的 struct 结构体类型。数组名与变量名一样应起的有意义尽量让别人一下就能知道这个数组是用于存放什么数据的。常量值用于指定数组大小数组大小一般根据实际需求去估量。同样数组也能和变量一样进行初始化使用 { } 将数据包裹起来各数据中间使用逗号隔开。数组的初始化分为完全初始化即为每一个数组元素赋上初值与不完全初始化即只为部分数组元素赋上初值。当我们使用完全初始化时数组的大小可以不指定此时数组大小就是赋值的元素个数。而使用部分初始化时除我们手动赋值的数组元素外其余元素均初始化为零值或空值。但是在使用浮点型数组时需要注意由于小数的二进制表示特性浮点数在计算机中未必能够做到精确保存。我们可以通过按F10进入调试状态在右侧监视窗口中观察刚刚样例的 arr3 看出这一点。关于小数部分是怎么从十进制转二进制的感兴趣的小萌新可以自行搜索一下。不过在实际生活中我们往往是对数组进行部分初始化然后根据实际需要通过 scanf 向数组中传入值就像下图代码这样。这里需要注意的是我们曾经说过在使用 scanf 获取变量的值时需要在变量名前使用取地址操作符 而数组不用。这里的数组是指整一个数组传入时对于数组元素而言可以认为其等同于变量也要用 。关于第二个字符数组为什么只输入10个字符数组大小却要指定为11本篇博客后面就会讲到各位小萌新无需着急。那么可能有小萌新会对上述代码有疑问关于数组的数值存入与数值获取为什么是一一对应的这实际上与数组在内存中的储存有关更多详细内容会在后续博客展开我们这里暂做了解。利用 取地址操作符除了可以让 scanf 将值储存在该变量或数组元素所在的内存地址中还可以让 printf 打印出该值所在的内存地址如下图代码所示我们发现对于同一个数组而言数组元素的储存地址是连续的相邻元素的地址间隔了4个字节的宽度而这4个字节的差值实际上是因为每个元素都是 int 整型需要占据4字节的长度。地址的打印格式是 %p。我们在基础知识篇讲过 sizeof 可以用来计算数据类型的长度那如果我们使用他来计算数组的长度通过下图的比较我们可以看出数组的每一个数组元素可以认为是一个变量其数据类型长度就是对应数据的类型长度。而数组作为一个集合其数据类型长度为所有数组元素的数据类型长度之和所以 type[n] 就是数组的数据类型。那么通过数组的数据类型长度与数组元素的数据类型长度我们就可以反推出数组的长度。那知道了这个知识之后有什么用如果我们需要频繁变动数组大小利用 sizeof 计算数组元素个数的方式就能使循环中的循环次数 n 随数组大小同步变化。在知道如何正确定义及初始化数组后还要知道如何正确使用数组。数组是有下标的而从前面的分析我们知道数组的地址是连续的数组下标本质是地址的偏移量所以数组的下标默认从0开始也就是说第一个数组元素的下标为0而最后一个数组元素的下标为 n-1。如果想要使用数组元素比如前面展示时使用的打印我们就要用到之前所讲的下标引用操作符 [ ]。利用下标引用操作符结合数组名与下标索引值我们就能访问任意一个已定义的合法数组的数组元素。值得注意的是除字符数组外其余所有数组都是以循环语句加下标引用操作符的方式获取整个数组的每一个元素而字符数组可使用 %s 一口气打印本质上也是因为内嵌了循环语句。但是在上述例子中我们的 arr 数组大小指定了10按理来说部分赋值无法占据整个数组打印时 - 却紧跟在 Hello 后面。这是因为字符数组与整型数组不同对于整型数组若部分初始化则其余未赋值元素全部初始化为0而对字符数组部分初始化其余元素填充 \0。而我们之前讲字符与字符串时说过\0 是字符串结束的标志所以打印 arr 数组时只会打印 Hello。在知道字符数组的填充特性后再来看下面这个例子。我们发现对于同样的数组元素使用循环打印时没有出现问题但在使用 %s 打印时 arr1 数组出现乱码的情况这是为什么其实还是 \0 惹的祸虽然两个数组看上去长得一样但之前讲字符串时说过当我们传入一个字符串时在字符串末尾是有一个 \0 隐藏的而 arr1 数组容量只有5这个 \0 就没有随字符串一起存入数组而 %s 打印时以 \0 识别字符串是否结束那么对于 arr1 数组就会存在识别失败的问题。如果我们打印一下两个数组的容量 n1 与 n2 就会发现不同。同样通过按F10调试在右侧监视窗口观察也能看出不同。所以在使用字符数组时一定要注意 \0 这个问题那么建议大家在平时使用数组时可以适当将数组开大一点原因我们其实也分析过了对于一个容量大于传入字符串长度的字符数组而言其余数组元素均会填充为 \0而 %s 在遇到第一个 \0 时就会结束输出。或者直接使用循环的方式来控制输出的长度。2.2.二维数组二维数组与一维数组类似如果将一维数组作为数组元素所得的就是二维数组将二维数组作为数组元素所得的就是三维数组以此类推对于二维及以上的数组统称为多维数组我们这里只讲二维数组。通过下图可以快速理解一维数组与二维数组的区别二维数组的语法格式如下。相比于一维数组用一个常量值表示数组容量二维数组需要两个常量值来表示常量值1表示行数常量值2表示列数二维数组的容量为常量值1 * 常量值2。和一维数组一样在定义完二维数组后同样需要对二维数组进行初始化二维数组也有完全初始化与不完全初始化但二维数组的初始化相比于一维数组要更复杂一些。由于二维数组的数组元素是一维数组所以除了按顺序初始化外还能按行初始化具体看下图代码。这里就不通过打印的方式呈现的直接按F10调试在右侧监视窗口查看。值得注意的是二维数组完全初始化时是可以省略行数即常量值1但不能省略列数即常量值2。其实原因也很清楚当列数明确时所有值依据列数摆放就能知道行数有空缺未能赋值的初始化为0或空但当行数明确时所有值通过填充一定可以满足行数但一列摆放多少元素永远是未知数。那么二维数组的使用方法也与一维数组类似同样需要利用下标引用操作符 [ ]。不过二维数组既有行又有列所以需要两个下标索引值加数组名才能正确访问二维数组的数组元素。同样如果使用循环的方式读入或者输出整个二维数组就需要嵌套使用循环具体代码如下。同样的我们也可以通过检验二维数组地址的方式来观察二维数组的一些特性。通过地址的打印比较可以发现虽然二维数组书写形式上是按行储存了多个一维数组但实际的地址存放与一维数组一样所有的数组元素都是连续存放的并不存在分区的情况。2.3.变长数组从前面对一维数组与二维数组的学习中我们知道在定义数组时往往需要指定数组大小且数组大小指定只能用常量值。但在 C99 标准中提供了一个变长数组的新特性这使得我们可以根据需要通过变量的方式来指定数组大小但也因其特性导致变长数组不能初始化。其语法格式如下。但当我们真的尝试运行代码时编译器会报错。这是因为 VS2022 虽然支持大部分 C99 标准中新增的语法但不支持变长数组大家可以试试 VS2026 是否已经能够支持。如果不能也无需着急我们改用 clang 编译器就可以使用变长数组。首先右击项目名称然后选择最底下的属性在弹出窗口中选择常规然后右侧的平台工具集右侧下拉框里更换成 clang然后点击最下面的应用并关闭窗口现在重新运行刚刚的代码就不再报错了。如果之前安装时没有下载 clang 也不用担心打开菜单栏搜索 Visual Studio Installer然后右击以管理员身份运行进来后点击修改右侧下拉找到 clang右下角点击修改并等待安装安装完成后操作上述步骤即可。一维变长数组及二维变长数组无论定义还是用法均与普通的一样只是改变了必须用常量值指定数组大小这一特性下面直接附上示例代码细节就不再重复了。值得注意的是变长数组只是说可以在程序运行时利用变量来指定数组大小不需要在创建数组时就指定但变量一旦指定完成该数组大小就固定了后续再改变变量大小也不会影响数组大小。2.4.数组应用那么掌握了数组之后我们可以利用数组解决什么问题来看下面这个例子。从上图的结果我们不难推测出这个程序的作用是把一个乱序的数组通过循环的方式变成一个从小到大排列的升序数组。那除了结合循环对数组排序外我们还能查找一个数是否在一个数组中具体代码如下。然后我们思考一下利用循环的方式如果我们要找的数在数组的后面那么我们前面的每一次循环都增加了整个程序最终的运行时间。那有没有更高效的查找方法有的我们可以利用二分查找。原理就是对于一个完成排序的升序数组来说如果我们要找的 x 比数组中某一位元素要大那么他也一定比这一位元素前面的所有元素要大。基于这个逻辑我们每次用 x 与中间那个元素做比较如果 x 大于这个元素就再次比较后半段的中间元素不断缩小间距直至找到 x 的所在位置或者最终发现 x 不在数组中。但是这里有一个小问题针对于下图这行代码如果我们按照这个逻辑运算表达式会先对 l与 r 进行求和然后再除以 2。而我们之前在操作符篇讲过程序进行操作时默认以 int 整型参与运算而 int 整型也是存在数据范围的当两个极大的整型数据相加时就会导致数据溢出使得最后计算结果错误。为了避免这种情况我们可以将代码改成第二张图的那样。两者查找逻辑相同但改变运算逻辑之后就避免了最开始的数据溢出问题。3.函数3.1.函数基础在C语言中函数就是完成某项特定任务的一小段代码这段代码有特殊的写法及调用方式。C语言程序可以认为是由无数个小的函数组合而成的因此会使用函数就显得尤为重要。而在C语言中所有函数可以分为库函数与自定义函数关于库函数在前面的博客已经介绍过了这里就简单重提一下本篇博客着重讲解自定义函数该如何去书写及使用。库函数是那些编译器厂家根据C语言标准已经封装完的函数我们可以直接拿来使用。库函数在标准库中对应的头文件中声明的因此在使用库函数前需要包含对应的头文件。有关C语言库函数的头文件可以在这里查到https://zh.cppreference.com/w/c/hearder。虽然库函数很方便但是库函数并不能解决所有问题我们往往需要根据实际问题自己去书写自定义函数所以自定义函数相比于库函数就更重要了。自定义函数的语法格式如下由函数头与函数体两部分构成函数头包含返回值类型函数名与参数函数体包括执行语句。一个完整的自定义函数必须包含返回值类型函数名及函数体内的代码语句形参是否需要及形参个数视情况而定。我们之前讲过操作符也知道有算术操作符可以专门用于计算那如果要实现两个数的加法操作就可以直接使用 算术操作符代码大体如下。如果 xy 这个加法表达式我们需要反复使用那么每一次使用都要重写一遍代码此时我们就可以自定义一个加法函数专门用于处理加法操作那么每次只需要调用这个自定义函数即可在这里可能看不出自定义函数的优势但如果实现的代码很长复用的地方很多那么使用封装的自定义函数就能极大的简化程序代码提高我们的书写效率。3.2.函数的形参与实参我们前面说了在定义函数时函数名后面 ( ) 里所存放的参数就是是形参。那么在调用函数时真实传递给函数进行具体操作的参数就是实参。如果我们只是定义了函数而没有去调用他那么函数定义那里的 x 与 y 只能形式上存在因为他们并没有向内存申请空间也就不会真实存在。而当我们调用函数并传值后为了存放实参的值才会向内存申请空间这个过程就叫形参的实例化。为了检验形参与实参的区别我们同样可以按F10进行调试。这里为了做区别我们使用不同的变量代表形参与实参。当我们调试进入函数前此时实参 a,b 的值已被记录接下来按F11而不是F10就可以进入函数内部而当我们进入函数之后形参 x,y 此时才变为实参 a,b 的值。那么可能会有小萌新问就不能是形参 x,y 被实参 a,b 替换了那么我们可以比较形参 x,y 在接收后地址是否与实参 a,b 地址相同因为所有变量的值都是存放在这个变量所在的内存地址中。而通过下图的比较我们可以看出形参 x,y 与实参 a,b 并不是同一个变量。如此看来虽然形参与实参紧密相连但两者都有自己独立的储存空间。3.3.函数的 return 语句在定义函数时我们往往需要他帮忙完成某些功能同时也往往需要他返回运算的结果此时就要用到 return 语句。但在自定义函数中return 语句也有自己的一些特性。1.return 后面可以跟一个值也可以跟一个表达式如果跟的是表达式则先对表达式求值再返回结果。return 后面还可以不跟任何东西这种情况仅适用于返回值类型为 void。2.return 语句执行后直接返回结果并结束函数后续语句不再执行因此需要注意 return 语句的摆放位置。3.函数的返回值类型如果没写默认返回 int 整型。return 的返回值类型若与函数的返回值类型不符自动隐式转换为函数的返回值类型。若函数有返回值类型但没写 return 语句此时返回值是未知数。4.如果函数中存在 if 等分支语句则要保证每条分支语句都有对应的 return 语句否则会发生编译出错。这些特性就不一一演示了各位小萌新在自定义函数里使用 return 语句时注意即可。3.4.数组做函数参数除了可以用变量作为实参进行传参还可以用数组作为实参进行传参。在使用数组作为参数传参时也有一些注意点。首先使用数组与使用变量不同使用数组时形参不会创建一个新的数组通过形参接收进入函数的还是原来作为实参的那个数组这一点通过调试发现形参与实参实际地址一致可以看出。其次形参如果是一维数组则可以省略数组大小形参如果是二维数组则可以省略行数但必须注明列数。3.5.函数的嵌套调用及链式访问函数的嵌套调用指的是函数之间的互相调用就如下面这个代码一样。函数的链式访问指的是把一个函数的返回值作为另一个函数的参数进行传参就如下面这个代码一样。3.6.函数的声明与定义我们一般都是先定义函数再调用函数那能不能先调用函数再定义函数其实也是可以的只是这样需要用到函数声明。如果只是单纯的颠倒函数定义与函数调用的顺序编译器运行检验时会报函数未定义的错误。从上面的示例中我们也能看出函数声明的语法格式其实就是将自定义函数的函数头剥离出来加上 ; 这样等于告诉编译器其实存在这么一个函数就可以解决编译报错的问题了。函数声明相比于单文件内使用更多还是使用在跨文件的函数调用上。因为当一个函数复用度高时直接将其抽离出来单独放置会便于代码的复用多个源文件只要通过包含头文件的形式就能获得函数声明进而调用该函数就不用每次使用前都去定义一遍该函数。而一般情况下都是将函数的声明放在头文件.h文件中将函数的实现放在源文件.c文件中。如下图所示这样我们通过在头文件这里添加新建项的方式创建一个.h的头文件用来存放判断闰年函数的函数声明在源文件这里添加新建项的方式创建一个.c的源文件用来存放判断闰年函数的函数实现这样就实现了对判断闰年函数的函数封装。在对自定义函数完成函数声明与函数实现之后我们就可以在其他源文件里通过包含头文件的方式来获得函数声明进而调用函数。不同于库函数的头文件引入自定义函数的头文件引入使用 的形式具体格式如下图。那么除了函数可以跨文件调用变量其实也可以跨文件使用这里就要讲到 extern 关键字。在正式讲解 extern 之前我们先要了解一下作用域与生命周期的概念。一般性而言在一个程序中并不是所有名字都是永久可用的限制这个名字可用的代码范围就是这个名字的作用域。局部变量的作用域是这个变量所在的局部范围全局变量的作用域是整个项目工程。而生命周期则是一个变量创建出来到变量销毁之间的时间段。局部变量的生命周期是从进入作用域开始到离开作用域结束全局变量的生命周期就是整个程序的生命周期。extern 的作用是声明外部符号利用 extern 修饰全局变量我们就可以在一个源文件里使用另一个源文件的全局变量。值得注意的是extern 只是声明变量不可用于定义变量。利用 extern 修饰函数也是如此我们可以在一个源文件里使用另一个源文件的函数。与 extern 这个关键字对应的是 static 关键字。static 关键字可以修饰局部变量全局变量与函数。static 修饰局部变量时改变了局部变量原有的生命周期将原本储存于栈区的局部变量储存到静态区之后变得与全局变量类似其生命周期等同于整个程序但作用域不变。从前面利用 extern 可以声明其他源文件的全局变量我们可以推测出全局变量是具有外部链接属性的想要在外部文件使用时只要声明一下即可而 static 修饰全局变量后外部链接属性变成了内部链接属性只能在自己所在的源文件内部使用了外部文件即使声明了也无法使用。static 修饰函数与 static 修饰全局变量作用相同函数原本也有外部链接属性被 static 修饰后外部链接属性变成了内部链接属性只能在自己所在的源文件内部使用了外部文件即使声明了也无法使用。4.函数递归我们前面讲过函数的嵌套调用函数递归与其类似函数递归就是函数自己调用自己的过程。同时递归本身也是我们解决问题的一个重要思路利用递归就能把一个复杂的问题层层转化为一个与原问题相似但规模较小的子问题进行求解直至子问题不再能被拆分此时递归结束。递归往往是有终止条件的与循环类似我们使用递归是为了解决问题而不是无意义的无限递归。所以每一次递归调用后都应该越来越接近终止条件而当程序运行到满足终止条件时递归终止。在知道递归定义之后我们就通过一些实际的例子来熟悉函数递归的使用方法以及遇到何种类型的问题时我们可以采用递归的方式去解决。我们知道阶乘就是从 1 开始累乘到 n 的过程且 0 的阶乘为 1那么我们就能利用循环实现这个过程。​​​​​​​ ​​​​​​​ ​​​​​​​那除了用循环的方式其实还可以改成递归的方式。如果我们仔细观察阶乘的求值表达式就会发现数字 n 的阶乘可以认为是由前一个数 n-1 的阶乘乘以 n 得到那么归纳一下就可以得到下图这个递归方程。我们将递归方程改用代码的形式表示如下图。然后我们来分析一下这个递归函数就可以看出递归的特点。首先我们说递归本质是将大问题分解为小问题所以对于求解数字 n 的阶乘而言我们通过分解不断先获取数字 n-1 的阶乘结果。其次我们说递归是有终止条件的在这个递归函数里 n0 就是我们的终止条件当 n 不断分解到 0 时就结束递归且每一次分解都是在接近这个终止条件。下面就是整个递归函数的运行模拟其中红线为递就是将问题不断向下分解递推的过程蓝线为归就是从最底层逐层回归结果的过程。通过草图的演示我们可以更清楚的了解递归函数的运行逻辑。那么就从上述这个例子来看递归似乎比循环还要麻烦那递归有什么可取之处我们前面讲操作符时说过结合 % 取模操作符与 / 除法操作符我们可以通过循环在屏幕上倒序打印一个数的每一位。那如果我们现在想要正序打印一个数的每一位那我们就可以使用递归的方式。这里的草图画的可能不太规范简单讲解一下程序实际的运行流程。自定义函数包括了函数递归与打印两部分操作将数字 1234 传入后原本会执行函数递归 print(123) 打印数字 4 操作但中途优先进行了函数递归最开始的打印操作并没有执行一直将问题分解递推到 n10 这个终止条件结束递归此时执行后续打印操作最后一层打印数字 1回归时由于一开始是从函数递归进入的所以打印完数字 1 后回归上一层会继续执行整个递归函数的剩余语句即打印数字 2然后继续逐层回归完成打印数字 3 及打印数字 4。通过上述两个例子的讲解以及草图的运行模拟相信各位小萌新已经对递归函数的运行逻辑有一个较为清楚的认知也明白了递归函数处理问题的特点。那么再来分析下面这个有关斐波那契数列的例子就能更深入的理解函数递归了。关于斐波那契数列是什么这里就不介绍了感兴趣的小萌新可以去查一下。简单来讲就是第一个数与第二个数都是 1从第三个数开始每个数都是他前面两个数相加的和所以第三个数是 2第四个数是 3以此类推。现在我们想要知道斐波那契数列的第 n 个数为多少。那么根据定义我们不难推导出下图这个递归方程然后以代码形式表示就是第二张图那样。看似用递归函数轻易解决了问题那我们思考一下如果想要知道更后面的数递归函数还能快速给出答案吗当我们尝试输入一个较大的数时就会变成下图这样。这是因为程序死循环了吗还是我们书写的代码存在漏洞其实都不是我们耐心等待一会就会跑出最终结果。那这是为什么其实是因为在C语言中程序的每一次函数调用都需要为其申请一块空间用于保存函数调用期间各种局部变量的值这块空间叫函数栈帧。但由于递归函数的特性每一次调用都会开辟新的栈帧空间直到回归时才逐层释放。这样就导致函数递归的层次较深时会占据大量的栈帧空间除了降低程序的运行效率有时还会导致栈溢出的报错问题。下图这个代码是不可取的我们也不应该这样去使用只是为了演示而这样使用。那怎么解决这个问题我们前面讲过循环所以我们可以利用循环的方式来替代递归函数的使用具体代码如下。从上面这些例子可以看出有些问题利用循环的方式不容易一下想到解题思路而利用递归方程可能很容易形成解题思路且递归往往可以使用少量的代码完成复杂的运算但递归又有自身的局限性。所以递归与循环各有优劣我们需要根据实际合理选用解决问题的方法。5.结语本篇博客到这里就结束了希望能够帮到各位刚接触C语言的小萌新们下篇博客将继续讲解C语言的相关知识。
返回列表