![741dbf9a9a095016f52bac0116e37ada.png](https://i-blog.csdnimg.cn/blog_migrate/1cab97a52e8116f584b0842e49b41b20.jpeg)
大哥的眼睛变了,是万花筒?
又想给我下幻术吗?
《火影之雪殇》——纹路著
通知
这么多天来感谢各位的关注!!!
五月初,我会写一篇关于今年全国一、二、三卷的感想,以及更加有质量的数学文章。期待能够和大家一样,感受着那份属于数学的独特的美~
数组与函数
初见数组
先思考一个问题,如何记录很多数?
难道要不断的定义int num1 num2 num3.................?
所以,我们开始学新的手段——数组。
题目:需要算输入的数的平均值,并且列出所有大于平均值的数。
#include
这个程序存在安全隐患,是什么? 后面说
定义数组
定义数组
- <类型>变量名称[元素数量];
- int grades[100];
- double weight[20];
- 元素数量必须是整数
- C99之前:元素数量必须是编译时刻确定的字面量
数组
- 是一种容器(放东西的东西)。特点:
- 其中所有的元素具有相同的数据类型;
- 一旦创建,不能改变大小
- *(数组中的元素在内存中是连续依次排列的)
一个茶杯就如同一个数组,专门用来放东西的,你可以放咖啡,放茶叶,放果汁。并且C99之后,数组就可以是变量了!!
int a[10]
- 一个int的数组
- 10个单元:a[0],a[1],…,a[9]
![dfb1df032a6236777cfa842008b66c87.png](https://i-blog.csdnimg.cn/blog_migrate/386787f3590b11614984a40ffdca94de.png)
- 每个单元就是一个int类型的变量
- 可以出现在赋值的左边或右边(左边的叫写入,右边的叫读入)
- a[2]=a[1]+6;
- *在赋值左边的叫做左值
数组的单元
- 数组的每个单元就是数组类型的一个变量
- 使用数组时放在[]中的数字叫做下标或索引,下标从0开始计数:
- grades[0]
- grades[99]
- average[5]
有效的下标范围
- 编译器和运行环境都不会检查数组下标是否越界,无论是对数组单元做读还是写
- 一旦程序运行,越界的数组访问可能造成问题,导致程序崩溃
- 出现segmentation fault可能是因为数组出错了
- 但是也可能运气好,没造成严重的后果
- 所以这是程序员的责任来保证程序只使用有效的下标值:[0,数组的大小-1]
来看下面这一段代码
#include
在文章最前面的一个代码里,我们说我们写的那个代码(为方便观看:代码在下面)有安全隐患,这个安全隐患就是,如果用户输入的数大于99,那么就会导致程序出错。
#include
那么,怎么解决这个问题呢?
我们有2个办法。
- 当用户输入完第99个数,就停止读取。
- 可已让用户提前输入,要输入的数的个数。
我们就用方法2来修改代码
#include
长度为0的数组?
- int a[0];
- 可以存在,但是无用
因为,此时下标(下标必须大于0)的最大值为0-1,为-1,负值,而我们读数都是从0开始读,所以长度为0 的数组没有任何意义。但是呢,在运行程序的时候他能通过。
用数组做散列计算
写一个程序,输入数量不确定的[0,9]范围内的整数,统计每一种数字出现的次数,输入-1表示结束。
#include
初见函数
现在呢,我们要求所有素数的和。我们写出下面这一段代码:
#include
我把其中的一段代码单独拿出来:
int
这一段代码所做的事情就是去求素数。但是呢,这一段代码使得整个for循环看着很大,而他的功能又很单一 。
我们可以这样做,我们把这一段代码取出来(具体看下面的代码)
#include
这样呢,我们就写出了一个属于我们自己的函数。并且当我们需要这个函数的时候,就可以直接调用。其实,我们在最初写hello world的时候就用到了函数——printf,printf是C语言标准库里的函数。
对于一大段代码里,如果某一小段的功能很单纯,那么我们就可以单独把它拿出来,下次直接调用就行了。这个被调用的函数,我们就可以把它称为函数。
我们来看这样一个例子:求出1到10、20到30和35到45的三个和。
按照我们传统的写法,我们可以这样来写。
#include
我们发现对于for循环的3段代码几乎一模一样!他们的功能是完全一样的。这件事情,我们在程序里称为“代码复制”。“代码复制”是程序不良的表现,这说明你代码的质量不好!因为,你将来要去做修改,要去做维护的时候,你很可能遇到你不是只要维护一处的问题,而是维护很多处。这就是代码不良的表现。
我们可以这样来改
void
此时,我们的主函数很简单了。我们制作了一个自己的函数,当我们后面需要的它的时候,就可以直接调用了。
函数的定义和使用
什么是函数?
- 函数是一块代码,接受零个或多个参数,做一件事情,并返回零个或一个值
- 可以先想象成数学中的函数:
- y=f(x)
函数定义
void
void sum(int begin,int end)为函数头;大括号内的代码称为函数体;void称为返回类型,并且这个void表示返回0个值;sum表示函数名;{int begin,int end}表示参数表。
调用函数
- 函数名(参数值);
- ()起到了表示函数调用的重要作用
- 即使没有参数也需要()
- 如果有参数,则需要给出正确的数量和顺序
- 这些值会被按照顺序依次用来初始化函数中参数
函数返回
- 函数知道每一次是哪里调用它,会返回到正确的地方。
从函数中返回
我们之前有一个程序是能够返回函数值的。不知道你有没有注意到
#include
在这个代码里,我们定义了ret并进行了赋值,最后又return ret。其实,我们很早很早就接触了return。之前见到的形式是return 0;上面代码中的isPrime代表着要返回一个值,同时前面我们也说过,如果是void就意味着返回零个值。
return的作用是停止函数的执行,并送回一个值。它的写法有两种:
- return;
- return 表达式;
第一种也就是不返回值,第二种即返回一个函数值。
一个函数中可以有多个return。但是这就不满足我们前面所说的单一出口,如果有多个return,就意味着可以出来很多种值,这对我们后期的修改和维护都是有很大影响的。
从函数中返回的值
- 可以赋值给变量
- 可以再传递给函数
- 甚至可以丢弃(因为有时我们并不需要返回一个值,仅仅需要它的副作用,这个很常见)
在没有返回值的函数中,我们用
- void函数名(参数表)
- 不能使用带值的return
- 可以没有return
- 调用的时候不能做返回值的赋值(因为他不返回东西,所以你不可能把它赋给某样东西)
- 如果函数有返回值,则必须使用带值的return·······
函数原型
我们在写自己的函数时一定要写在main函数的上方,因为C的编译器是自上而下顺序分析你的代码,如果你写到后面,在这之前出现的函数,他就不知道那是什么。所以,你必须要把调用的函数写在前面,这样它才能检查你对函数的调用是否正确。
但有时,我们想先写一个主体函数,把各个细节放在主体函数的下面,这时怎么办?
我们可以在主函数上面做一个声明。
#include
这样编译器就提前知道有这样的一个函数了。
函数原型就是把函数头取出来,以分号“;” 结尾,就构成了函数的原型。函数原型的目的是告诉编译器这个函数长什么样,比如名称;参数(数量及类型);返回类型。其次,原型里可以不写参数的名字,但是一般仍然写上,这是为了方便我们人类读者去读懂它。
参数传递
- 如果函数有参数,调用函数时必须传递给它数量、类型正确的值
- 可以传递给函数的值是表达式的结果,这包括:
- 自变量
- 变量
- 函数的返回值
- 计算的结果
如果调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞;编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的;后续的语言,C++/Java在这方面很严格。
以前我们做过交换a和b的值的代码,但看下面这样的代码能交换a和b的值吗?
#include
结果
![8386743686810e92cafa5dbbe6348c20.png](https://i-blog.csdnimg.cn/blog_migrate/f898cff4a6eaafecbfdd8dbde178ede8.jpeg)
swap里面的a,b和主体函数里面的a,b没有任何关系。 这里仅仅把5给了a,把6给了b,后面交换后跟主体函数没有任何关系,显然这样的代码不能交换a和b的值。当然,实在不行,还能这样理解:既然返回值最多返回一个,很明显我这里超过1个,肯定不能完成转换功能。
C语言在调用函数时,永远只能传值给函数。
- 每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
- 过去,对于函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫做“实际参数”
- 由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数的时候把变量而不是值传进去了,所以我们不建议继续用这种古老的方式来称呼它们
- 我们认为,它们是参数和值的关系
本地变量
- 函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数的这次运行所独有的,称作本地变量
- 定义在函数内部的变量就是本地变量
- 参数也是本地变量
变量的生存期和作用域
- 生存期:什么时候这个变量开始出现了,到什么时候它消亡了
- 作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
- 对于本地变量,这两个问题的答案是统一的:大括号内——块
我们用上次运行的程序swap来解释上面这一段话。
![1df7a8098bd716a0ef7387f50e713f18.png](https://i-blog.csdnimg.cn/blog_migrate/c8ea0ee57cc35444fdd1393ff9c12eeb.jpeg)
![31e236e3f935658f48ba877f2fdad8fc.png](https://i-blog.csdnimg.cn/blog_migrate/07896c320816e27e3340227638bd3fd9.jpeg)
刚开始,我们发现在程度读取到第7行之前,a=5,b=6,t=not found in current context;
![9f05232ba77d8e5f3216ef6ccb661619.png](https://i-blog.csdnimg.cn/blog_migrate/d064e700f86980d78feab7f6186c0172.jpeg)
![23cd0b1c68ff15d649aedd2b9a6a8efe.png](https://i-blog.csdnimg.cn/blog_migrate/238a13f6c59109afe19b1c52ee6d9a53.jpeg)
这个时候,我们发现a=5,b=6,t=0;继续点击“下一步”程序开始读取第14行
![9e37613f3358723e99c8e33902034842.png](https://i-blog.csdnimg.cn/blog_migrate/badf2d646d4adb3f44952321f8717a43.jpeg)
这个时候,a=5,b=6,t=5;再点击“下一步”
![58d54b863d808fbd4fbeec612e3685f2.png](https://i-blog.csdnimg.cn/blog_migrate/3b4c0aa5e7206288aae8f85256f16ab3.jpeg)
继续点击“下一步”
![3e213dae5ecd382d4cdfcb00a0716362.png](https://i-blog.csdnimg.cn/blog_migrate/d7d2482cc0a209726f85b8ff96e52cba.jpeg)
![25f7a21153b2f7bddb67699665e3f314.png](https://i-blog.csdnimg.cn/blog_migrate/3cbb15181f2bc28ed6e42db3c3c4ba7d.jpeg)
我们发现printf并没有可以输出的值。这是因为当a和b进入了swap后,在这个小的函数中它有了生存期和作用域,当它离开的时候,这些也都随之没有了。
![9124cf8a9e726be71c4174c46681dd89.png](https://i-blog.csdnimg.cn/blog_migrate/f1462a709113a4e354dd8bb32e61b81b.jpeg)
一小段总结:上面表述这么多,就是想说一个单独函数内部的变量称为“本地变量”,因为只有进入这个函数中,他们才有可能会出现(之所以说可能,是因为有可能你在这个函数外定义了一个和他一模一样的变量),这时他们也就才有了生存期和作用域,当你离开这个函数时,他们的生存期和作用域也就随之消失了,并且,每一个单独函数都会有一个“{}”,我们又称这个“大括内{}”为“块”。而这些变量都在“{}”里,也就是在“块”内。
上面没看懂没关系(突然感觉到自己总结能力有点差,抱歉)~下面有具体的总结
本地变量的规则
- 本地变量是定义在块内的
- 它可以是定义在函数的块内
- 也可以定义在语句的块内
例:
![cd734a9cd04537f7419cba8505762291.png](https://i-blog.csdnimg.cn/blog_migrate/cfc325e8d9bf9acfdcf09c87f894f0a7.jpeg)
我们现在加一个if语句,这个i在这个块(大括号)内,当进入这个if语句里,你才能找到i,i才有效也即有生存期,接着它发挥作用,也就有了作用域;当你a>b,也就不进入if语句里,这个i也就起不到作用了;
![c1460794d6fa6a20db15ca1d5b90ac33.png](https://i-blog.csdnimg.cn/blog_migrate/1de6ffd37d0270128f2a2ea7e1ae0c9e.jpeg)
你看,我在块外面写一个i++,编译器就告诉我,我并没有定义i。所以当离开了块,i就不存在了。
- 程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了。
- 块外面定义的变量在里面仍然有效
例:
![d0190fcee59645d0c66077348b9c083a.png](https://i-blog.csdnimg.cn/blog_migrate/6e69975629c992abd4856a804b04d39f.jpeg)
- 块里面定义了和外面同名的变量则掩盖了外面的
例:
![e800c5be7b425cd143e3551157e0968a.png](https://i-blog.csdnimg.cn/blog_migrate/3d81f67130ed570c6089f0af8d95aaa6.jpeg)
- 不能在一个块内定义同名的变量
例:
![e6db79ab68cf14f38c4359dc6dd3b076.png](https://i-blog.csdnimg.cn/blog_migrate/34b71821ea51be4001f73716aa309db1.jpeg)
- 本地变量不会被默认初始化
- 参数在进入函数的时候被初始化了
我们还有一些其他事需要去琢磨:
当没有参数时,我们是写成
- void f(void);
- 还是
- void f();
在传统C中,它表示f函数的参数表未知,并不表示没有参数。
当你写void f(void)时,等于直接告诉编译器,我的这个函数不接收任何参数。
所以,我们在写程序时,最好明确参数表中参数的类型。
其次,我们要注意参数表中的“逗号”,不是“逗号运算符”而是“标点符号”。具体例子看下面:
- f(a,b)
- f((a,b))
1中的逗号就是“标点符号”;而2中的逗号为“逗号运算符”,这是因为它在外面又加了一层括号,此时代表输出一个参数。
C语言不允许函数嵌套定义:我们可以在一个函数里放一个函数的声明,但不能放他的定义(body)
再举个例子:
- int i,j,sum(int a,int b); //这个在C语言里面是允许的
- return (i); //我们说return后面应该跟一个表达式,这里i加了一个括号,也变成了表达式,但是会让人误解,误解return是一个函数。
关于main:
- int main()也是一个函数
- 要不要写成int main(void)? 这个可以写,代码并不会出错
- return的0有人看吗?在函数结束时,要把这个0返回给要调用它的那个地方,返回给那一小段代码,那一小段代码会检查main到底返回了一个什么样的东西,然后他会报给你的操作系统,报告给你的windows到底返回了一个什么样的值,这个值对有些人来说是有意义的。在Windows中你可以在P处理文件调用你写的那个程序,然后跟上一句if erroelevel 1。如果你的程序返回的是1的话,去做什么什么事情。传统上一个程序返回0,表示他正常的运行结束了,如果返回一个非0的值表示它运行什么地方出现了错误;对于Unix,你可以用echo$?,来看返回的值。所以return的0是有人看的,是可以看的,是可以起作用的。
- Windows:if errorlevel 1…
- Unix Bash : echo$s?
- Csh:echo $status
二维数组
我们前面说了一位数组,这个数组只有一个下标的,所以叫做一位数组,这个数组是线性的。除了一维的,二维的,三维的,甚至跟多维度的数组都可以做。我们来看看二维数组怎么做?
- int a[3][5];
- 通常理解为a是一个3行5列的矩阵(当然你也可以说是5行3列。我们用3行5列是考虑到计算机内部的排列,习惯性的说这是一个3行5列的矩阵)
![9c479ac7b57a0957b992d029c8cb0a2c.png](https://i-blog.csdnimg.cn/blog_migrate/6b4ebb591ac08dc0e89842d2fcf8b002.jpeg)
二维数组的遍历:
for
- 和一维数组一样,我们需要遍历,这里是每行每列的遍历。
- a[i][j]是一个int
- 表示第i行第j列上的单元
二维数组的初始化
int
- 列数是必须给出的,行数可以由编译器来数
- 每行一个{},逗号分隔
- 最后的逗号可以存在,有古老的传统
- 如果省略,表示补零
- 也可以用定位(* C99 ONLY)
看一个二维数组的例子:大家玩过tic-tac-toe的游戏吗?(反正我之前没玩过)
- 读入一个3×3的矩阵,矩阵中的数字为1表示该位置上有一个X,为0表示O
- 程序判断这个矩阵中是否有获胜的一方,输出表示获胜一方的字符X或O,或输出无人获胜
![e33c324d1b484a4aafa9c99b51e6ccb3.png](https://i-blog.csdnimg.cn/blog_migrate/ea9a2dd3aab5e994b4fb8fcd9fee9e4d.png)
这个游戏中文里我们叫他井字棋。就是有一个3×3的矩阵,如果有一行或者一列或者斜着的窦唯O或者X,就表示你赢了。(规则和五子棋一样!)所以呢,我们要做的很简单,我们并不是要去实现这个游戏,只是说,如果程序读到了这个矩阵,能不能判断出来,现在这个矩阵上面的情况,它的XX和OO的分布是不是有一方赢了,然后输出谁赢了,或者都没赢。就是做这样一件事情。
我们来看一下代码
const