C语言完全听懂系列 - day5
- 我们之前实现了一段代码,可以计算狗狗的岁数:
#include <stdio.h>
int main() {
int dog_age;
printf("Input the age of dog:\n");
scanf("%d", &dog_age);
float human_age;
if (dog_age <= 2) {
human_age = dog_age * 10.5;
} else {
human_age = 2 * 10.5 + (dog_age - 2) * 4;
}
printf("%f", human_age);
}
- 如果我们想每次运行计算两只狗的岁数呢?最简单的办法就是把代码复制一遍:
#include <stdio.h>
int main() {
//1
int dog_age;
printf("Input the age of dog:\n");
scanf("%d", &dog_age);
float human_age;
if (dog_age <= 2) {
human_age = dog_age * 10.5;
} else {
human_age = 2 * 10.5 + (dog_age - 2) * 4;
}
printf("%f", human_age);
//2
int dog_age;
printf("Input the age of dog:\n");
scanf("%d", &dog_age);
float human_age;
if (dog_age <= 2) {
human_age = dog_age * 10.5;
} else {
human_age = 2 * 10.5 + (dog_age - 2) * 4;
}
printf("%f", human_age);
}
然后我们就遇到了我们编程生涯的第一个报错:
- redeclaration of blabla的大概意思是这个变量重复声明了,在同一个代码块内(被同一个大括号括起来的代码),我们取的变量名应该是唯一的,毕竟重名的东西总是要被区分一下的。所以我们给第二段代码的两个变量名,后面分别加个2,现在代码没问题了。
- 变量名取名的规则是:
- 仅由字母,数字,下划线组成。
- 不可以与关键字重名。关键字就是像if, int, float这种,有特殊含义的单词,编译器需要区分。
- 必须以字母或者下划线开头。
- 想一下为什么不能以数字开头?
- 我们在给变量取名的时候约定一个习惯:单词只用小写,单词之间用下划线区分,比如dog_age。这个习惯跟编译器就没关系了,单纯是为了代码可读性更好。这种习惯只是三大流派之一,你之后还会遇到不同流派的程序员,都是些没什么意义又永远没办法统一的日经争议话题了,暂时先跟我保持一致吧。
- 假如说我们想算四只狗呢?那代码还要再复制两遍,然后变量名要改出3和4,情况看起来不太对了;假如说算每只狗的岁数的时候,dog_age<=3的部分要乘10.5,而不再是2了呢?那四处代码都要改了。这样写的话,代码的屎山味就呼之欲出了。客户都是一天一个样张嘴就要改的**,需求变动是非常频繁的,所以我们就要防着点客户,让代码尽量不怕改,要不然客户改需求我们改代码,改着改着改出bug了,锅就全到我们自己头上了,还会被客户吊我们技术不行,代码全是bug。
- 所以重构上面这段代码很简单,把“算人的岁数”这件事,抽出来只写一份,然后每次想算的时候,都“调用”这同一处代码,就算要改,也只改这一份:
#include <stdio.h>
float get_human_age(int dog_age) {
float human_age;
if (dog_age <= 2) {
human_age = dog_age * 10.5;
} else {
human_age = 2 * 10.5 + (dog_age - 2) * 4;
}
return human_age;
}
int main() {
int dog_age;
printf("Input the age of dog:\n");
scanf("%d", &dog_age);
float human_age = get_human_age(dog_age);
printf("%f", human_age);
int dog_age2;
printf("Input the age of dog:\n");
scanf("%d", &dog_age2);
float human_age2 = get_human_age(dog_age2);
printf("%f", human_age2);
}
- 上面这部分,我们在“定义”一个函数,定义函数的意思是,描述一个函数该做什么。我们把最核心的岁数换算的逻辑拿了出来,而输入和输出,还是由主函数来完成。我们在主函数的视角来看,从用户输入拿到了一个岁数,丢给了get_human_age这个函数之后,就可以拿到换算后的岁数,赋值给human_age这个变量,后面就拿去输出了。主函数不知道是怎样做的岁数换算,只需要知道这件事被完成了就好。
- 你可以认为每个函数都是不同的人,不同人之间的记忆是不共享的,全靠主动沟通来共享一部分记忆,比如说现在你想叫我下楼拿快递,那你就需要把取件码发给我,我拿到了取件码,经过了一段时间执行一些步骤,最后把快递拿给你,这是现实世界的逻辑;现在我们来看
get_human_age
的定义,小括号里面的叫参数列表,参数的意思就是传递给这个函数的数据,很显然他需要一个dog_age
,而且既然doge_age
是一个数字,就需要确定一下类型是小数还是整数。最前面的float
是返回值类型,也就是这个函数最后会返回给你什么,大括号括起来的代码块,都属于这个函数,用于具体说明“怎么做”。 - 我们“定义”好了一个函数之后,我们来捋一下执行逻辑。要记住程序永远是从主函数的第一句开始执行,执行到最后一句退出,所以当他在主函数执行完输入之后,走到了
float human_age = get_human_age(dog_age);
这一行,主函数读取自己的变量dog_age
的值,将数值本身传递给get_human_age
,“调用”这个函数,这个时候代码执行会进入这个函数的内部,函数接收到了参数,定义了一个自己的变量,也叫dog_age
,来保存接收到的参数数据,然后从函数定义的第一句开始,执行到最后一句,返回了一个float
值。返回的float
也是在这一行,被主函数用human_age
这个变量存起来了,这一行结束。- 不同人之间的记忆是不共享的,不同函数定义的变量也是不共享的。两次调用分别传递了主函数的
dog_age
和dog_age2
作为get_human_age
的参数,他们的“值”被传递进去之后,在get_human_age
内部,都被保存到了他自己的dog_age
变量里面,进行后续的计算。主函数的dog_age
和get_human_age
的dog_age
只是重名而已。
- 不同人之间的记忆是不共享的,不同函数定义的变量也是不共享的。两次调用分别传递了主函数的
- 我们现在这个函数,有输入(参数),有输出(返回值),只关心岁数换算这一件事。函数也可以没有输入,没有返回,类似你叫我扫个地,不需要给我传递信息,我也不需要给你什么,我做完了你就能直接看到地扫完了就行了,写起来像这样:
#include <stdio.h>
float get_human_age(int dog_age) {
float human_age;
if (dog_age <= 2) {
human_age = dog_age * 10.5;
} else {
human_age = 2 * 10.5 + (dog_age - 2) * 4;
}
return human_age;
}
void calculate_a_dog() {
int dog_age;
printf("Input the age of dog:\n");
scanf("%d", &dog_age);
float human_age = get_human_age(dog_age);
printf("%f", human_age);
}
int main() {
calculate_a_dog();
calculate_a_dog();
}
- 我们定义了一个新函数
calc
什么什么的,把每一只狗相关的输入和输出都放在了这个函数里,这个函数每次就可以处理一只狗。void是空的意思,表示我什么都不会返回,空的参数列表表示我什么都不需要。更标准的写法应该是void calculate_a_dog(void)
,没有参数的时候,参数列表也标一下void,不过我懒得管,又不是不能跑,懒得多写一个词。返回值的void要是能省了就好了,可惜不行。现在我们想处理几只狗,就在主函数调用几次就好了,不需要复制粘贴一坨代码,更不怕逻辑变动,因为他们都在复用同一块代码。编程语言的发展,就是不断的在研究怎么复用更多的代码,为了省事,为了不怕改。- 可以尝试从主函数开始捋一下执行逻辑,能理解函数调用时的跳转就好。
- 函数的参数可以传入多个值,但每次只能返回一个值。
int do_someting(int x, int y) { ... }
。我们后面会讲怎么返回多个值。定义函数的时候,要给参数取名字,比如说x和y,因为后续的逻辑要读取这两个值,甚至可能读取多次,但是返回值不用取名字,因为函数一旦return了,就结束了,函数内部不会再有其他的代码读取这个值了,取了名字也不会被用到。- 假如说你在return 语句后面写了代码,这些代码不会执行的。return 的意思就是函数到这里就结束了。
- 不返回值的代码可以不写return,编译器会在最后一行代码之后,自动补一个
return;
。如果你想提前返回,可以写出这样的逻辑:
void do_something() {
if (...) {
return; //只有if满足的时候才触发这个return,没满足不触发return
} else {
// do something...
// do something...
}
}
- 假如说a用到了b,b用到了c,那写的时候c的定义应该在最上面,函数是“先定义,后使用”的。
- 其他所有函数,都是你自己写好了定义,然后直接或者间接的,在主函数里调用了,现在回头看看主函数,其实这也只是一个函数的“定义”,至于是谁在哪调用的主函数呢,是操作系统,操作系统会找到每个程序的主函数,调用这个函数。int作为主函数的返回值,也是返回给了操作系统,返回0时表示你的程序正常执行结束了,返回任何非0的数字,操作系统都会理解为程序运行有问题,并记录下来这件事。你可以在主函数最后加一句return 1,并运行看看。
- 作业:
- 暂时没有别的饮品,都是奶茶,主函数要求最多三行代码,主函数不需要写return 0.
- 像美团,饿了么这些软件,表示全糖,半糖,其实也是这样的逻辑。任何现实里,有限数量的选项,加冰少冰,全糖半糖,其实在代码里都是映射到了某个数字而已。
- 提示
- 输出的时候,如果我分别写
printf("a ")
和printf("b")
,a后面没加换行符,最终a和b会显示到同一行。这道题可以在不同的位置调用printf输出少冰、全糖、奶茶这三个文本,只要不加换行,他们最终会显示在同一行。
- 输出的时候,如果我分别写