P 91 函数基本语法和快速入门 2022/12/30
一、需求引入
引入:请大家完成这样一个需求:输入两个数,再输入一个运算符(+,-,*,/),得到结果.。
c
#include<stdio.h>
void main(){
// 输入两个数,再输入一个运算符(+,-,*,/),得到结果
int num1 = 10; //第一个数
int num2 = 20;//第二个数
double res = 0.0;//结果
char oper = '-'; //运算符
int num3 = 60;
int num4 = 80;
double res2 = 0.0;
char oper2 = '*';
switch(oper) {
case '+' :
res = num1 + num2;
break;
case '-':
res = num1 - num2;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num1 / num2;
break;
default :
printf("你的运算符有误~");
}
printf("%d %c %d = %.2f\n", num1, oper, num2, res);
// 比如:执行了其他代码
// ......
// ......
// 到这里我们此时又要接收两个变量和运算符,我们怎么完成?
// 这里定义需要写在前面
switch(oper2) {
case '+' :
res2 = num3 + num4;
break;
case '-':
res2 = num3 - num4;
break;
case '*':
res2 = num3 * num4;
break;
case '/':
res2 = num3 / num4;
break;
default :
printf("你的运算符有误~");
}
printf("%d %c %d = %.2f\n", num3, oper2, num4, res2);
// 传统的方法就是把前面的代码结构又复制过来
getchar();
}
问题:
-
代码冗余(即有过多重复的代码)
-
不利于代码的维护
-
引出---->函数
二、函数基本介绍
-
为完成某一功能的程序指令(语句)的集合 ,称为函数。
-
在C语言中,函数分为: 自定义函数、系统函数(查看C语言函数手册)
-
函数还有其它叫法,比如方法等,在本视频课程中,我们统一称为 函数。
基本语法:
c
返回类型 函数名(形参列表){
执行语句...; // 函数体
return 返回值; // 可选
}
-
形参列表:表示函数的输入
-
函数中的语句:表示为了实现某一功能代码块
-
函数可以有返回值,也可以没有, 如果没有返回值,返回类型 声明为 void 。
入门案例:使用函数来解决以上问题------>函数
c
#include<stdio.h>
// 说明:
// 1.函数名命名为cal
// 2.没有返回值 double 类型
// 3.形参列表为(int n1,int n2,char oper)
// 4.在函数中,我们使用的变量名需要和形参列表中的变量名称一样
double cal(int n1,int n2,char oper){
// 定义一个变量保存运算的结果
double res = 0.0;
switch(oper) {
case '+' :
res = n1 + n2;
break;
case '-':
res = n1 - n2;
break;
case '*':
res = n1 * n2;
break;
case '/':
res = n1 / n2;
break;
default :
printf("你的运算符有误~");
}
printf("%d %c %d = %.2f\n", n1, oper, n2, res);
return res; // 返回一个结果
}
void main(){
// 输入两个数,再输入一个运算符(+,-,*,/),得到结果
int num1 = 10; //第一个数
int num2 = 20;//第二个数
double res = 0.0;//结果
char oper = '-'; //运算符
int num3 = 60;
int num4 = 80;
double res2 = 0.0;
char oper2 = '*';
// 使用函数来完成两个计算任务:
printf("使用函数来解决计算任务\n");
res = cal(num1,num2,oper);// 调用函数,使用函数;调用之后赋给res
printf("cal函数返回的结果%.2f\n",res);
// 同样可以完成第二个计算任务
res2 = cal(num3,num4,oper2);
printf("cal函数返回的结果%.2f\n",res2);
getchar();
}
P 92 头文件的工作原理和案例 2022/12/31
一、需求引入
-
在实际的开发中,我们往往需要在不同的文件中,去调用其它文件的定义的函数,
比如hello.c中,去使用myfuns.c 文件中的函数,如何实现? ---->头文件
二、基本概念
-
头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件 和C标准库自带的头文件
-
在程序中要使用头文件,需要使用
C 预处理指令
#include 来引用它。前面我们已经看过 stdio.h 头文件,它是C标准库自带的头文件 -
#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。
-
#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。
-
建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。
三、工作原理图
四、案例演示
- myfun.h头文件
c
// 声明函数
int myCal(int n1,int n2,char oper);
- myfun.c源文件
c
#include<stdio.h>
// 实现int myCal(int num1,int num2,char oper)
int myCal(int n1,int n2,char oper){
// 定义一个变量保存运算的结果
double res = 0.0;
switch(oper) {
case '+' :
res = n1 + n2;
break;
case '-':
res = n1 - n2;
break;
case '*':
res = n1 * n2;
break;
case '/':
res = n1 / n2;
break;
default :
printf("你的运算符有误~");
}
printf("%d %c %d = %.2f\n", n1, oper, n2, res);
return res; // 返回一个结果
}
- hello.c源文件
c
#include<stdio.h>
// 引入我们需要的头文件
#include "myfun.h"
void main(){
// 使用myCal完成计算任务
int n1 = 10;
int n2 = 50;
char oper = '+';
double res = 0.0;
// 调用myfun.c中定义的函数 myCal
res = myCal(n1,n2,oper);
printf("\nres=%.2f",res);
getchar();
}
结果输出:
P 93 头文件的注意事项和细节说明 2022/12/31
一、注意事项
- 引用头文件相当于复制头文件的内容。
c
#include<stdio.h>
// 引入我们需要的头文件
//#include "myfun.h"
int myCal(int n1,int n2,char oper); // 这里是myfun.h的内容,这样也是可以的,相当于引用头文件
// 所以说引用头文件相当于复制头文件内容
-
源文件的名字 可以不和头文件一样(myfun.c和myfun.h名字可以不一样),但是为了好管理,一般头文件名和源文件名一样,一看就可以看出来这是一组。
-
C 语言中 include <> 与include "" 的区别:★★★(重要)
include<> :引用的是编译器的类库路径里面的头文件,默认用于引用系统头文件。
include "" :引用的是你程序目录的相对路径 中的头文件,如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文件,用于引用用户头文件。(这种方式既可以引用用户自定义的头文件,也可以引用系统头文件)
总结:
- 引用 系统头文件 ,两种形式都会可以,include <> 效率高。
- 引用 用户头文件 ,只能使用 include " "。
-
一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
-
同一个头文件如果被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制 [举例]
c
#include "myfun.h"
#include "myfun.h"
#include "myfun.h" // 复制多份,运行也不会报错,因为头文件在代码层面有防止重复引入的机制
- 在一个被包含的文件(.c)中又可以包含另一个文件头文件(.h)
c
// myfun.h中还可以引入其他的头文件
#include<stdio.h> // 引入<stdio.h>
// 声明函数
int myCal(int n1,int n2,char oper);
// 但是hello.c里面又有引入的头文件<stdio.h>
#include<stdio.h> // 这里相当于再次引入了<stdio.h>,是不会报错的
// 引入我们需要的头文件
#include "myfun.h"
- 不管是标准头文件,还是自定义头文件 ,都只能包含变量和函数的声明 ,不能包含定义,否则在多次引入时会引起重复定义错误(!!!)
c
#include<stdio.h>
// 声明函数
int myCal(int n1,int n2,char oper);
void sayHello(){ // 定义函数,此时一次引入不会出现问题,但是如果进行多次引入,会发生报错,函数相当于发生了重名。
printf("say hello");
}
//在hello.c中,多次引入
#include<stdio.h>
// 引入我们需要的头文件,下面多次引入,上面定义的函数则会重复,发生报错
#include "myfun.h"
#include "myfun.h"
#include "myfun.h"
P 94 函数调用机制图解 2023/1/1
一、概念理解
- 拉登同志给特工小组下达命令:去炸美国白宫,特工小组返回结果
- 程序员调用方法:给方法必要的输入,方法返回结果.
二、调用过程案例
- 为了更好的理解函数调用过程, 看两个案例,并画出示意图(机制),这个很重要:
- 传入一个数+1 test函数
c
#include<stdio.h>
// 说明
// 1. 函数名字test
// 2. 函数没有返回,void
// 3. 完成功能:传入一个数+1
void test(int n){
int n2 = n + 1;
printf("n2=%d",n2);
}
void main(){
int num = 6;
test(num);
getchar();
}
- 计算两个数,并返回 getSum函数
c
int getSum(int n1,int n2){
return n1 + n2; // 也可以这样写,返回n1+n2的值,返回给接受者(接收变量)
}
void main(){
int res = getSum(1,9); //这里就可以理解为返回一个10
printf("res=%d\n" ,res);
getchar();
}
-
示意图-机制:(案例一)
解释:程序开始执行,当执行到main函数,会在计算机内存里面的栈中建立一个main栈,在main栈先执行前面的语句,当执行到调用函数的时候,就会在栈中重新建立一个test栈(这里就是函数),此时就会将mun给到形参n,n就等于6,然后在执行下面的语句,理所应当的就输出了7,执行完最后一条语句后,就使用完成了,相当于销毁了,然后又回到了main栈。
总结:函数调用的规则--->也同样适用java,c++,php
- 当调用(执行)一个函数时,就会开辟一个独立的空间(栈)
- 每个栈的空间是相互独立的。
- 当函数执行完毕后(或者执行到return),会返回到函数调用的位置,继续往下执行。
- 如果函数有返回值,则将返回值赋给接收的变量。
- 当一个函数返回后,该函数对应的栈空间也就销毁了。
三、return语句
- 基本语法
c
返回类型 函数名(形参列表){
语句...;
return 返回值;
}
- 细节说明
- 如果没有返回值,返回类型写为void
- 如果有返回值,返回类型要明确,要求返回值和返回类型要匹配,或者可以相互转化(自动或者强制)
c
int getSum(int n1,int n2){
return n1 + n2 + 1.0; // int --> double,会有警告,丢失数据;但是强制转换就不会生成警告了
//return (int)(n1 + n2 + 1.0); // 这里进行强制转换就不会警告了
// 也可以自动转换,int-->double,低精度向高精度自动转换。
}
P 95 函数调用机制应用举例 2023/1/2
一、案例说明
- 案例一:
c
char* getSum (int num1 , int num2 ) { // 这里是指针类型
int res = num1 + num2; // 这里是int类型,很显然不匹配
return res;
}
//错误,原因是类型不匹配
- 案例二:
c
double getSum (int num1 , int num2 ) {
int res = num1 + num2;
return res; // 返回一个int,原先是double,直接自动转换
}
//ok int -> double 自动转换
- 案例三:
c
int getSum (int num1 , int num2 ) {
int res = num1 + num2;
return res;
}
//ok int -> int,同类型,可以
- 案例四:
c
int getSum (int num1 , int num2 ) {
int res = num1 + num2;
return 0.0;
}
//可以运行,但是有警告 double -> int 有精度损失
- 案例五:
c
int getSum (int num1 , int num2 ) {
int res = num1 + num2;
return (int)0.0; // 强制转换成int类型,返回类型相同
}
//ok , 因为做了强制转换
P 96 函数递归调用机制 2023/1/3
一、基本介绍
介绍:一个函数在函数体内又调用了本身,我们称为递归调用。
递归调用快速入门案例:考虑分别输出什么?画出对应原理图和代码说明:
c
#include<stdio.h>
void test(int n){
if(n>2){
test(n-1); // 这里进行了递归调用
}
printf("n=%d",n);
}
void main(){
test(4); // 这里调用了函数test,最后会输出啥呢?
}
二、原理图讲解
注意:调用函数时,开辟的函数栈,都会完整的执行函数代码。
解释说明:当代码执行到test(4),会重新建立一个test栈,此时形参为4,在新栈中继续执行相应的代码,然后执行到test(n-1)时,这里n=4,就是test(3),然后在建立一个test栈,继续执行但此时n=3,test(n-1)就是test(2),然后在建立第三个test栈,继续执行,但是到第三个栈这里n=2,就无法进入到if语句中了,此时就只会执行输出语句,执行完就会销毁,接着就返回到上一个位置继续执行,直到返回主栈,所以最后输出n=2,n=3,n=4。
问题解决:将代码改成如下,会怎样输出?
c
#include<stdio.h>
void test(int n){
if(n>2){
test(n-1); // 这里进行了递归调用
}else{ // 将这里改成if-else语句,会怎样输出?
printf("\nn=%d",n);
}
}
void main(){
test(4); // 这里调用了函数test,最后会输出啥呢?
getchar();
}
// 很显然只有最后一个栈会进入else里面,所以最后只会输出n=2。
三、递归重要原则
-
执行一个函数时,就创建一个新的受保护的独立空间(新函数栈)。
-
函数的局部变量是独立的,不会相互影响(相当于就是每个栈上的 n都是独立的)。
-
递归必须向退出递归的条件逼近,否则就是无限递归(栈溢出),死龟了:)
-
当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁。
P 97 函数递归练习题 2023/1/4
一、题目练习
- 题目一:斐波那契数,请使用递归的方式,求出斐波那契数1,1,2,3,5,8,13...给你一个整数n,求出它的斐波那契数是多少?
斐波那契数列规律:通过斐波那契数列的定义可以发现规律是:F(n) = F(n-1)+F(n-2),但前两项都是1
c
#include<stdio.h>
int fib(int n)
{
if(n == 1 || n == 2) // 因为前两项都是1,所以直接返回1
return 1;
else
return fib(n-1) + fib(n-2); // 根据斐波那契数列的规律进行返回
}
int main()
{
int n;
// 请勿输入过大的数字导致卡死
scanf("%d", &n);
printf("%d\n", fib(n));
return 0;
}
斐波那契数列原理图:
- 题目二:求函数值已知 f(1)=3; f(n) = 2*f(n-1)+1; 请使用递归的思想编程,求出 f(n)的值?
c
#include<stdio.h>
//题目:求函数值
//已知 f(1)=3; f(n) = 2*f(n-1)+1;
//请使用递归的思想编程,求出 f(n)的值?
// 分析:因为该题的公式已经给出,所以直接使用即可
int f(int n){
if(n==1){
return 3;
}else{
return 2*f(n-1)+1;
}
}
void main(){
int res2 = f(30);
printf("res = %d",res2);
getchar();
}
-
题目三:猴子吃桃子问题有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有1个桃子了。
问题:最初共多少个桃子?
c
#include<stdio.h>
//题目:猴子吃桃子问题有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!
// 以后每天猴子都吃其中的一半,然后再多吃一个。
// 当到第十天时,想再吃时(还没吃),发现只有1个桃子了。
//问题:最初共多少个桃子?
// 分析:
// 1. day = 10有1个桃子
// 2. day = 9 有(day 10 + 1)*2 = (1+1)*2 = 4
// 3. day = 8 有(day 9 + 1)*2 = (4+1)*2 = 10
// ............
//day10 = day9-(day 9/2)-1----> 1=x-x/2-1 ---> 2=2x -x -2--->x-4=0--->x=4
int peach(int day){
if(day == 10){
return 1;
}else{
return (peach(day+1)+1) * 2; // 所求天数的桃其实是下一天加一,乘以2
}
}
void main(){
int peachNum;
peachNum = peach(1);
printf("第一天的桃子为%d",peachNum); // 第一天桃子有1534个桃子
getchar();
}
P 98 函数注意事项和细节讨论 2023/1/5
一、注意事项
- 函数的形参列表可以是多个。
- C语言传递参数可以是值传递(pass by value),也可以传递指针(a pointer passed by value)也叫引用传递。
- 函数的命名遵循标识符命名规范 ,首字母不能是数字,可以采用 驼峰法 或者 下划线法 ,比如 getMax() get_max()。
- 函数中的变量是局部的,函数外不生效【案例说明】
c
#include<stdio.h>
//●函数中的变量是局部的,函数外不生效
void f1(){
int num = 10; // 比如这里定义的变量只能在f1中使用
}
void main(){
printf("num=%d",num); // 如果在这里使用,是不会生效的!
}
- 基本数据类型默认是值传递 的,即进行值拷贝 。在函数内修改,不会影响到原来的值。【案例演示】
c
#include<stdio.h>
// ●基本数据类型默认是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
int f2(int n){
n++;
}
void main(){
int n = 9;
f2(n); // 这里调用函数了,下面的n会输出几呢?
// n = f2(n),这样才会输出10
// 依然会输出9,因为函数中的n,没有赋值给主方法的n。(值传递,就是拷贝了一分过去!)
printf("main函数中n=%d",n);
getchar();
}
- 如果希望函数内的变量能修改函数外的变量 ,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用(即传递指针) 【案例演示】
c
#include<stdio.h>
// ●如果希望函数内的变量能修改函数外的变量,
// ●可以传入变量的地址&,函数内以指针的方式操作变量。 (不懂可以看前面指针基本介绍笔记)
void f3(int *p){
(*p)++; // 修改会对函数外的变量有影响
}
void main(){
int n = 9;
f3(&n); // 这里加一个&符号
printf("\nn的值为%d",n); // 此时这里会输出10
getchar();
}
执行原理图解:
自我理解:首先执行就会重建一个main栈,当执行到f3(&n),此时就会在重建f3栈,这时传递的是一个地址,形参是int *p,然后接着执行( *p)++, *p表示的是取值符,得到的是p指针指向的变量,此时++,所以就改变了函数外面的值。
- C语言 不支持函数重载,即不能通过参数的个数不同,或者类型不同来区别函数
c
void f2(int n){
n++;
}
void f2(int n1,int n2){ // 即使这里有两个参数,也是不可以的,因为C语言不支持重载,它只人这个名称
n++; // error C2084: 函数"void f2(int)"已有主体
}
- C语言支持可变参数函数(做一个简单了解即可)
案例:
(1) int num 表示传递的参数个数 和 数据类型, 需要引入 include<stdarg.h> 。
(2) 案例演示: 编写一个函数fun ,可以求出 1到多个int的和
c
#include<stdio.h>
#include<stdarg.h>
int fun(int num, ...) // 可变函数,即参数的个数可以不确定,使用...表示
// 说明:
// 1.num表示传递的参数格式
// 2. ...表示后面可以传递多个参数和num对应一致即可
{
int i, totalSum=0; //totalSum 一定要初始化
int val = 0;
va_list v1; //v1实际是一个字符指针,从头文件里可以找到
va_start(v1, num); //使v1指向可变列表中第一个值,即num后的第一个参数
printf("*v = %d\n",*v1);
for(i = 0; i < num; i++) //num 减一是为了防止下标超限
{
val = va_arg(v1, int); //该函数返回v1指向的值,并使v1向下移动一个int的距离,使其指向下一个int
printf("res = %d\n", val);
totalSum += val;
}
va_end(v1); //关闭v1指针,使其指向null
return totalSum;
}
void main(){
int res = fun(3,10,30,60); // 第一个3表示传入参数的个数
printf("和是%d",res);
getchar();
}
二、课堂练习
- 题目:请编写一个函数 swap(int *n1, int *n2) 可以交换 n1 和 n2的值
c
#include<stdio.h>
// 请编写一个函数 swap(int *n1, int *n2) 可以交换 n1 和 n2的值
// 说明:
// 1. 函数名为swap
// 2. 形参是两个指针类型int *
int swap(int *n1,int *n2){
int temp = *n1; // 表示将n1这个指针指向的变量的值赋给temp
*n1 = *n2; // 表示将n2这个指针指向的变量的值赋给n1这个指针指向的变量
*n2 = temp; // // 表示将temp这个值赋给n2这个指针指向的变量
}
void main(){
int n1 = 1;
int n2 = 2;
swap(&n1,&n2);
printf("main的n1=%d,n2=%d",n1,n2);
getchar();
}
- 机制原理图(理解非常重要)
自我解析:首先调用函数就会创建栈,建立swap栈,main栈里面传入n1,n2的地址(函数中是int *的指针类型),在swap栈中,将n1这个指针指向的变量的值赋给temp,后面同样如此,然后就完成了值的交换。
P 99 函数传递参数特点小结 2023/1/9
一、函数传递
- 基本介绍
我们在讲解函数注意事项和使用细节时,已经讲过C语言传递参数可以是值传递(pass by value) ,也可以传递指针(a pointer passed by value)也叫传递地址或者 引用传递。
- 两种传递方式
-
值传递
-
引用传递(传递指针、地址)其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝 ,引用传递的是地址的拷贝 ,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
二、值传递和引用传递的特点
- 值传递:变量直接存储值,内存通常在栈中分配【案例: 示意图】
-
默认是值传递 的数据类型有 1. 基本数据类型 2. 结构体 3. 共用体 4. 枚举类型
-
引用传递 :变量存储的是一个地址,这个地址对应的空间才真正存储数据(值)。
-
默认是引用传递 的数据类型有:指针和数组。
-
如果希望函数内的变量能修改函数外的变量 ,可以传入变量的地址&,函数内以指针的方式操作变量(*指针)。从效果上看类似引用,比如修改结构体的属性。
P 100 变量作用域基本规则 2023/1/9
一、基本介绍
概念:所谓变量作用域(Scope),就是指变量的有效范围。
- 函数内部声明/定义的局部变量,作用域仅限于函数内部。
c
#include<stdio.h>
void sayHello() {
char name[] = "tom"; // 这里name就是局部变量,作用域仅限于在sayHello函数中!!!
printf("hello %s \n", name);
}
void main() {
sayHello();
//这里我们不能使用到sayHello的name变量
printf("name= %s", name); //这里将提示,没有定义name
}
- 函数的参数,形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用局部变量(编译器使用就近原则)
c
#include<stdio.h>
int n = 20; // 函数外部定义的变量,就是全局变量(作用域在整个程序中都可以使用)
// 函数的形参,会被视为f10的局部变量
// 说明:当局部变量与全局变量同名时,以局部变量为准(就近原则)
void f10(int n){
printf("\nn=%d",n);
}
void main() {
f10(10); // 当函数外定义了同名变量,依然还是输出10
getchar();
}
- 在一个代码块,比如 for / if中 的局部变量,那么这个变量的的作用域就在该代码块。
c
void main() {
int i = 0;
for (i = 0; i < 10; i++){
int k = 90; //k 的作用域在for代码块中
printf("i=%d k = %d\n", i, k);
}
printf("k=%d", k); // 这里不能使用for中定义的k变量,因为k的作用域范围只在for循环中。
getchar();
}
- 在所有函数外部定义的变量 叫全局变量,作用域在整个程序有效。(通常将全局变量放在头文件中)
c
myFun.h // 在myFun.h中定义了全局变量,使用只需要引入头文件就行,但是只能引入一次,引入多次会重复定义。
double money = 1.1; //定义了全局变量
hello.c
#include "myFun.h"; // 这里引入即可