按照中国大学MOOC上浙江大学翁恺老师主讲的版本所作,B站上也有资源。原课程链接如下:
https://www.icourse163.org/course/ZJU-9001
由于是大三抽空回头整理的,所以可能前五章会记的内容比较简略。此外,作为选学内容的A0:ACLLib的基本图形函数和链表两章也没有做。西电的考试是机试,理论上学到结构体就能够应付考试了,但为了以后的学习考虑建议全学。
其他各章节的链接如下:
C语言程序设计笔记(浙大翁恺版) 第一周:程序设计与C语言
C语言程序设计笔记(浙大翁恺版) 第二周:计算
C语言程序设计笔记(浙大翁恺版) 第三周:判断
C语言程序设计笔记(浙大翁恺版) 第四周:循环
C语言程序设计笔记(浙大翁恺版) 第五周:循环控制
C语言程序设计笔记(浙大翁恺版) 第六周:数据类型
C语言程序设计笔记(浙大翁恺版) 第七章:函数
C语言程序设计笔记(浙大翁恺版) 第八周:数组
C语言程序设计笔记(浙大翁恺版) 第九周:指针
C语言程序设计笔记(浙大翁恺版) 第十周:字符串
C语言程序设计笔记(浙大翁恺版) 第十一周:结构类型
C语言程序设计笔记(浙大翁恺版) 第十二周:程序结构
C语言程序设计笔记(浙大翁恺版) 第十三周:文件
其他各科笔记汇总
什么是函数?
函数是一块代码,接收零个或多个参数,做一件事情,并返回零个或一个值
可以先想象成数学中的函数: y = f ( x ) y=f(x) y=f(x)
函数定义
调用函数
函数名(参数值);
()
起到了表示函数调用的重要作用,即使没有参数也需要()
如果有参数,则需要给出正确的数量和顺序,这些值会被按照顺序依次用来初始化函数中的参数
示例:
#include
void cheer()
{
printf("cheer\n");
}
int main()
{
cheer;
cheer();
return 0;
}
cheer
没有进行函数调用,编译时会警告表达式cheer
没有被使用
示例2:
#include
void sum(int begin, int end)
{
int i;
int sum = 0;
for ( i=begin; i<=end; i++ ) {
sum += i;
}
printf("%d到%d的和是%d\n", begin, end, sum);
}
int main()
{
sum(1,10);
sum(20,30);
sum(35,45);
return 0;
}
1到10的和是55
20到30的和是275
35到45的和是440
在函数调用sum(1,10)
处设置断点,运行到该语句之前会暂停。此时如果还用“下一步”就直接完成sum
函数的运行到下一行语句,并不会进入到函数调用,想要程序离开main
进入到sum
函数内需要用“单步进入”
如果不想循环多遍,可以直接在循环后设一个断点然后用“跳过”
函数返回
函数知道每一次是哪里调用它,会返回到正确的地方
从函数中返回值
return
停止函数的执行,并送回一个值
return
有两种写法:return;
和return 表达式;
一个函数里可以出现多个return
语句,这些return
不一定要放在最后。但这样不符合单一出口的理念
return
的值可以赋值给变量,可以再作为参数传递给函数,甚至可以丢弃而不会有任何警告
示例:
#include
int max(int a, int b)
{
int ret;
if ( a>b ) {
ret = a;
} else {
ret = b;
}
return ret;
}
int main()
{
int a,b,c;
a = 5;
b = 6;
c = max(10,12);
c = max(a,b);
c = max(c, 23);
c = max(max(23,45), a);
c = max(23+45, b);
max(23,45);
printf("%d\n", max(a,b));
return 0;
}
在printf
处单步进入会进入max
,因为在printf
之前要先把传给printf
的参数都准备好
没有返回值的函数
void 函数名(参数表)
不能使用带值的return
,可以没有return
,函数运行到最后一行就会自行返回
调用的时候不能做返回值的赋值。如果函数有返回值,就必须使用带值的return
用来告诉编译器这个函数长什么样
函数先后关系
像之前这样把sum()
写在上面,是因为C的编译器自上而下顺序分析你的代码,在看到sum(1,10)
的时候,它需要知道sum()
的样子,也就是sum()
要几个参数,每个参数的类型如何,返回什么类型,这样它才能检查你对sum()
的调用是否正确
如果不知道,也就是把要调用的函数放到下面了,LLVM编译时会先警告然后报错。在C99以前,C的编译器不知道函数的样子会猜函数长什么样,但当遇到实际的sum()
函数定义时发现与猜的不匹配就会报错
将函数头拷贝到main
函数前加分号作为函数原型声明,函数定义放在下面以通过编译
有了声明之后,编译器就知道函数长什么样子,根据原型判断对函数的调用是否正确,确保函数定义和声明是否一致
函数原型
函数头,以分号“;”结尾,就构成了函数的原型
函数原型的目的是告诉编译器这个函数长什么样(名称、参数数量及类型、返回类型)
原型里可以不写参数的名称,编译器检查时不会检查参数名称,但是一般仍然写上
示例:
double max(double a, double b);
int main()
{
int a,b,c;
a = 5;
b = 6;
c = max(10,12);
printf("%d\n", c);
return 0;
}
double max(double a, double b)
{
// ...
}
会发生自动类型转换,将12转换为double
交给a
调用函数的时候,是用表达式的值来初始化函数的参数
如果函数有参数,调用函数时必须传递给它数量、类型正确的值
可以传递给函数的值是表达式的结果,这包括:字面量、变量、函数的返回值、计算的结果
调用函数时给的值与参数的类型不匹配是C语言传统上最大的漏洞,编译器总是悄悄替你把类型转换好,但是这很可能不是你所期望的。后续的语言,C++/Java在这方面很严格
示例:
#include
void cheer(int i)
{
printf("cheer %d\n", i);
}
int main()
{
cheer(2.4);
return 0;
}
不同编译器不一定会出现警告,如果改为cheer(2.0)
,课程中使用的编译器就不出现警告
C语言在调用函数时,永远只能传值给函数
示例:
void swap(int a, int b);
int main()
{
int a = 5;
int b = 6;
swap(a,b);
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
swap
和main
里的a
和b
存在不同的地方,是没有任何联系的变量,不能实现交换a
和b
的值
传值
每个函数有自己的变量空间,参数也位于这个独立的空间中,和其他函数没有关系
示例:
在main
中,swap
里的x
和y
不存在。同样在swap
中,main
里的a
和b
也不存在
Dev C++中变量不存在会提示“Not found in current context”,如果存在会给出值
过去,对于包括原型声明和函数定义的函数参数表中的参数,叫做“形式参数”,调用函数时给的值,叫作“实际参数”
由于容易让初学者误会实际参数就是实际在函数中进行计算的参数,误会调用函数的时候把变量而不是值传进去了,所以我们不建议继续用这种古老的方式来称呼它们。我们认为,它们是参数和值的关系
定义在函数内部的变量是本地变量(局部变量,自动变量),参数也是本地变量
本地变量
函数的每次运行,就产生了一个独立的变量空间,在这个空间中的变量,是函数的这次运行所独有的,称作本地变量
定义在函数内部的变量就是本地变量
参数也是本地变量
变量的生存期和作用域
生存期:什么时候这个变量开始出现了,到什么时候它消亡了
作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
对于本地变量,这两个问题的答案是统一的:大括号内 —— 块
示例:
当离开main
函数调用swap
函数时会离开main
的变量空间进入swap
的变量空间。离开a
和b
的作用域,不能在当前上下文访问a
和b
,但作为生存来说a
和b
还存在
swap
函数运行完返回后swap
的变量空间就没有了,x,y
和t
不存在
本地变量的规则
本地变量是定义在块内的。它可以定义在函数的块内,也可以定义在语句的块内,甚至可以随意拉一对大括号来定义变量
程序运行进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了
块外面定义的变量在里面仍然有效,块里面定义了和外面同名的变量则掩盖了外面的
不能在一个块内定义同名的变量
本地变量不会被默认初始化,参数在进入函数的时候被初始化了
示例:
void swap(int a, int b);
int main()
{
int a = 5;
int b = 6;
swap(a,b);
if ( a<b ) {
int i = 10;
}
i++;
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
编译报错说i
未声明。i
是定义在块内的变量,它的生存期和作用域就仅限于这个块,在这个块内i
存在,离开就不存在了。程序的每次运行i
是否存在取决于a
和b
的大小关系
在对i
进行初始化之前,i
已经存在
示例2:
void swap(int a, int b);
int main()
{
int a = 5;
int b = 6;
swap(a,b);
{
int i = 0;
printf("%d\n",i);
}
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
0
a=5,b=6
这对大括号没有依附于任何语句,在块内i
可以参与运算和输出
如果将printf("%d\n",i)
改为printf("%d\n",a)
,得5
示例3:
void swap(int a, int b);
int main()
{
int a = 5;
int b = 6;
swap(a,b);
{
int a = 0;
printf("a=%d\n",a);
}
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
a=0
a=5,b=6
main
是什么?
没有参数时
当函数没有参数时,是在参数表里加void
写成void f(void);
还是void f();
?
前者明确告诉编译器该函数不接收任何参数,后者在传统C中,它表示f
函数的参数表未知,并不表示没有参数
示例:
// void swap(int a, int b);
void swap();
int main()
{
int a = 5;
int b = 6;
swap(a,b);
{
int a = 0;
printf("a=%d\n",a);
}
printf("a=%d b=%d\n", a, b);
return 0;
}
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
编译通过。这样写告知编译器只知道有swap
,但不确定swap
的参数,编译器遇到swap(a,b);
会猜测swap
的参数是两个int
如果将void swap(int a, int b)
改为void swap(double a, double b)
,并在int t = a
后添加printf("in swap,a=%f,b=%f\n", a, b)
,得
实际上swap
的参数是两个double
,但编译器认为swap
的参数是两个int
,为swap
函数调用安排两个int
的传递就会出错
调用函数时的逗号和逗号运算符怎么区分?
调用函数时的圆括号里的逗号是标点符号,不是运算符
示例:
f(a,b)
传两个参数
f((a,b))
再加一层括号表明要先做括号里的运算,这时,
就是逗号运算符,只传一个参数
C语言不允许函数嵌套定义
可以在一个函数里放另一个函数的声明,但是不能放一个函数的定义
int i,j,sum(int a, int b);
定义int
型变量i
和j
,声明sum
函数需要两个int
作为参数并且返回一个int
return (i);
的圆括号没有意义
关于main
int main()
也是一个函数,如果main
不需要任何参数,可以写成int main(void)
main
是C语言程序的入口,但main
也是一个函数。main
成为C语言的入口函数其实和C语言本身无关,它虽然是你写的代码当中第一个被执行的地方,但并不是程序运行起来第一条运行的代码,你的代码是被一小段叫做启动代码的程序所调用的,它在main
函数之前会为程序的运行做准备,做完准备工作后再调用main
函数
操作系统把你的可执行程序装载到内存里,启动运行执行编译器给你的启动代码,启动代码里会寻找main
函数并执行,所以main
才成为启动代码的入口函数。如果编译器采用另外的方式来编译程序,启动代码就可能不是去寻找main
了,如对于Win32API,这时要寻找的就是WinMain
return 0
是有意义的,main
函数结束时会把0返回给调用它的地方,返回给的一小段代码会检查main
返回什么,然后报告给操作系统
传统上一个程序返回0表示正常运行结束,返回任何非零值表示在程序运行过程当中出现错误
示例:
如果在Windows的批处理(.bat)文件调用你编写的程序,然后跟上一句if errorlevel 1 ...
,如果程序返回1就会做相关处理
对于Unix,如果使用的是bashell,可以使用echo $?
查看程序返回的结果