在讲这节课之前,我们先回顾一下上一节课讲的内容。在上一节课,我们主要讲了三大程序流程语句。
1.顺序语句
顺序语句很简单,就是程序从上向下,一条一条地执行语句。
2.条件分支语句
条件分支语句一共有两大类,
第一个,if...else...语句,if语句可以单独使用,可以不用搭配else,但是else不能单独使用,必须搭配一个if语句。if语句可以形成连续判断语句,格式是: if...else...if...else... ,连续判断语句的执行流程必须要清楚。另外,if语句可以进行嵌套,在一个if语句中,还可以内嵌一个if语句,形成嵌套if语句。
第二个,switch...case语句,记住,每个case成立后,一定需要加一个break语句,用于退出switch语句,否则语句会一直执行,直到碰到break语句或者switch语句结束。
3.循环语句
第一个,while循环,while会一直判断while后的条件,如果条件为真,则执行while后的语句块;如果条件为假,则直接跳出while循环。
第二个,do...while循环,do...while循环和while唯一的不同之处在于,do...while语句会率先执行一遍语句块,然后再进入while的条件判断语句,如果条件为真,则执行语句块中的内容;如果条件为假,则跳出do...while循环语句。
第三个,for循环,for循环中,包含三个部分,一个是for循环开始之前需要执行的语句,一个是for循环的判断条件,在开始for循环前,首先会判断一遍条件是否成立,如果成立,则进入for循环语句块执行程序;如果不成立,将直接跳出for循环。在每次for循环结束之后,都会执行一条语句,这条语句属于for循环的第三个部分。
4.程序控制语句
在C语言中,程序控制语句一共有三个
第一个,break语句,在循环中,碰到break语句将立刻结束循环操作,break除了在循环中使用,在switch中也能使用。
第二个,continue语句,在循环中,碰到continue语句,将立刻结束本次循环。开始下一次循环,continue仅限用于循环操作中。
第三个,goto语句,goto语句可以用在程序的各个地方,我们只需要在使用之前,在程序中给一个标记,goto语句就能跳转到我们需要执行的程序。但是,我们不建议使用goto语句,它会打乱程序执行的顺序,导致程序的可读性变差。因此尽量少用或者不用goto语句,其实,goto语句完全可以由其他语句替代。
以上就是我们上节课讲的内容,今天这节课,我们来讲C语言中的函数,数组,局部变量和全局变量以及C语言中的存储类型。
一、函数
函数的作用就相当于将一段代码进行打包,我们调用某个函数,就相当于将这段打包的代码执行一遍,至于将什么代码进行打包,这完全取决于编程者自己的想法;我们一般习惯将具有某个特定功能的函数进行打包操作,这一步也叫做封装,封装有一个好处,别人不需要知道你内部实现的过程,只要知道函数的功能,就能调用函数完成某些事情;同时,封装可以更好的保护代码,如果你不想把实现过程分享给其他人的话。
1.函数的定义
函数定义的方法比较简单,一个函数包括四个部分
①、函数返回值:这个指的是在执行完这个函数之后,将会返回一个什么类型的数据出来。
②、函数名称:函数的名称可以自己定义,不过我们一般要求,见名思意,就是,我们一看到函数的名字,我们就知道这个函数有什么作用,比如说,我要执行一个加法操作,我可以将函数命名为add,这样,一来方便我们自己记住,二来也减少读函数所需要的时间,甚至有些开源项目。它们都具有自己的一套函数和变量的命名规范。我们如果想要快速看懂它们写的函数,了解这些命名规范是非常必要的,可以为你减少开发时间。
③、函数参数:函数的参数指的就是,我们要执行函数时,需要带什么变量进入我们的函数。
④、函数体:函数体就是我们函数需要执行的内容,我们所有函数实现的代码都放在函数体内。用{}进行括起来。
下面是函数定义的一个例子:
int add(int a,int b)
{
return a+b;
}
在这里,根据函数四大部分,我们来逐一分析:
①、函数返回值:这里函数的返回值的类型是int类型,所以在执行完这个函数后,函数会返回一个int类型的数据出来。
②、函数名称:这里的函数名称是add,根据见名思意的原则,这个函数是一个加法函数,在函数体中,的确,执行的是一个加法过程。
③、函数参数:这里的函数参数有两个,一个a和一个b,它们都是int类型的数据,说明在执行这个函数的时候,我们需要代入两个int类型的数据进入函数执行。
④、函数体:函数体就是我们函数实现的具体代码,这里比较简单,用return返回a+b的值
这里我们讲一下return的用法,return专门用于函数中,用于在函数结束的时候,将值返回出来,同时它还有一个功能,直接结束函数的运行,所以return语句后面不要写逻辑代码,因为就算你写了逻辑代码,程序也不会执行。我们举一个例子
int add(int a,int b)
{
return a+b;
return a-b;
}
这个例子充分说明了return的用法,retrurn语句可以在函数结束的时候返回值出来,同时结束了函数,虽然后面还有一条语句return a-b;但是,这条语句在return a+b;语句之后,不会再执行了,因为执行完return a+b;语句后整个函数就已经结束了,如果不相信,大家可以亲自试一试,用于尝试,大家会有不一样的感受。
如果返回值类型为void空类型,也就是说,我们不需要返回值,我们可以用return进行函数的提前退出吗?当然可以,我们来看下面一个例子
#include <stdio.h>
void compare(int,int);
int main(void)
{
compare(10,20);
compare(20,10);
return 0;
}
void compare(int a,int b)
{
if(a>b)
{
printf("a>b\r\n");
}else{
printf("a<=b\r\n");
return;
}
printf("compare函数执行完毕!\r\n");
}
这里定义了一个compare函数,见名思意,就是比较两个数的大小。比较a和b两个数的大小,如果a大于b则打印出a>b在屏幕上,否则打印出a<=b在屏幕上,然后执行完之后还有一条语句,用于指示函数所有语句都执行了一遍。我们来执行函数试一试
第一次调用compare函数,a=10,b=20;
第二次调用compare函数,a=20,b=10;
执行结果如下图所示
从执行结果可以看出,a<=b时并没有执行printf("compare函数执行完毕!\r\n")这条语句。因为这里有一个return语句,它可以提前结束函数的运行。而当a>b的时候,语句中没有retrurn语句,所以就继续执行下列语句,控制台中打印出compare函数执行完毕!
2.函数的调用
函数的调用方法非常简单,只要根据我们定义的函数,如果需要带入参数,我们就带入相同类型的参数,如果不需要,直接空着就好。我们拿上面的程序作为例子:
#include <stdio.h>
void compare(int, int);
int main(void) {
compare(10, 20);
compare(20, 10);
return 0;
}
void compare(int a, int b) {
if (a > b) {
printf("a>b\r\n");
} else {
printf("a<=b\r\n");
return;
}
printf("compare函数执行完毕!\r\n");
}
在这里,我定义的compare函数需要带入两个参数,且没有返回值。在主函数中,我们就按照我们定义函数的方式进行调用。这里调用了两次函数,第一次带入的a为10,b为20;第二次带入的a为20,b为10,函数的调用就是这么简单。
3.函数的声明
函数的声明和函数的定义不一样,函数的定义是定义一个具体的函数,函数具有函数体,具有详细的执行过程,但是函数的声明,只是向程序说明,接下来的程序,可以调用这个函数。就像变量一样,变量必须要先声明再使用,函数也是一样,同样需要先声明后使用。同样拿上面的程序作为例子:
#include <stdio.h>
void compare(int, int);
int main(void) {
compare(10, 20);
compare(20, 10);
return 0;
}
void compare(int a, int b) {
if (a > b) {
printf("a>b\r\n");
} else {
printf("a<=b\r\n");
return;
}
printf("compare函数执行完毕!\r\n");
}
在这里,因为compare函数定义在main函数下面,因为程序是顺序执行的,main函数执行不能调用下面的函数,只能调用上面的函数。因此我们如果需要在main函数中使用compare函数,我们首先需要声明一下我们定义的compare函数,这样main函数才能够调用。
函数的声明也很简单,我们把除了函数体的部分全都复制下来,后面加一个分号即可。就像上面程序那样。那有人会问,为什么声明的函数没有a和b呢?我们来解释一下这个问题,由于是函数的声明,声明只是告诉编译器,我们已经定义了这个函数,并且告诉编译器函数的参数类型、返回类型、以及函数名,不需要告诉编译器执行过程,参数名只是在函数执行过程有用,声明过程并没有什么作用。所以,我们可以省去参数名,直接写每个位置对应的数据类型即可。当然,我们也可以写上参数名,这个不会有什么影响。我们通常也是这么做的,直接将函数头复制下来,末尾加个分号就好了。
4.函数传值调用的执行机制
函数的传值调用,指的是我们在执行函数的时候,带入的是某个具体数值,而不是指针,函数的地址调用我们在讲到指针一节的时候再进行说明。
函数传值调用的执行机制说简单点,就是对参数进行拷贝,将参数拷贝一份到我们的函数执行过程中。我们拿一个例子来说明;
#include <stdio.h>
void swarp(int a, int b);
int main(void) {
int a = 10, b = 20;
printf("调用函数前a=%d,b=%d\r\n", a, b);
swarp(a, b);
printf("调用函数后a=%d,b=%d\r\n", a, b);
return 0;
}
void swarp(int a, int b) {
int temp;
temp = a;
a = b;
b = temp;
}
这里我们定义了一个swarp函数,参数为两个int类型的变量a和b,没有返回值,很明显,这个函数目的是用来交换两个数的值。然后我们在主函数中调用swarp函数试试,看看是否成功交换两个数的值了呢?
执行结果告诉我们,并没有,函数并没有达到我们的想要的功能,这是为什么呢?正如我们之前所说的,函数的传值调用,首先会拷贝一份参数,这里是拷贝,我们现在函数中使用的值不再是我们代入函数的值,而是另外一个变量。只不过这个变量的值与你代入函数的值是一样的罢了。所以我们在函数中交换的变量并不是我们代入的变量,而是进入函数时拷贝的那份变量,同时,我们拷贝的那份变量在函数结束之后就会被系统销毁。所以函数没有达到我们预期的目的。这是函数传值调用特别需要注意的地方!
二、数组
我们在第一讲说了,C语言中有许多的内置数据类型,今天我们讲的数组,就是其中一种内置数据类型。数组就是相同类型数据的一种集合,它用来存储多个同类型的数据,可以方便我们管理多个数据。数组包括一维数组和多维数组,多维数组中,我们常用的是二维数组,三维及其以上数组使用频率比较少。
1.一维数组的定义
一维数组,就是只有一行同样类型的数据。用以下方式来定义一个一维数组。
int array[10];
定义数组,主要包括三个部分
①、数组的数据类型:数组的数据类型写在最前面,表明这个数组用于存储什么类型的数据。可以是C语言内置的数据类型,也可以是我们自定义的数据类型,比如结构体,共用体,指针等等
②、数组的名称:数组的名字同样遵循见名思意的原则,命名时尽量体现出这个数组的作用。
③、数组元素的个数:在数组名之后有一对中括号,中括号中表示的就是数组元素的个数。
这里我们定义了一个保存10个int类型数据的数组,名字叫做array。
2.一维数组的初始化
数组和变量一样,在定义的时候就可以对其进行初始化,我们来看看如何初始化数组。
int array[5] = {1,2,3,4,5};
如上面的代码所示,初始化数组,我们只需要在后面给予一个赋值即可,接着用大括号把你需要赋值的数据放在其中,每一个元素用逗号隔开。
当然,我们也可以不用全部赋值,我们可以给数组部分赋值,这是允许的,如下所示:
int array[5] = {1,2,3};
但是需要注意,我们不能给出超出数组元素个数的赋值,比如下列所示,这是不允许的!请不要这样做。
int array[5] = {1,2,3,4,5,6};
3.一维数组的使用
现在,数组的定义和初始化讲完了,接着我们讲讲如何调用数组的元素。在C语言中数组元素的调用是从标号0开始的。如下所示
int a,array[5] = {1,2,3,4,5};
a = array[0];
我们想要使用数组第一个元素,我们必须从0开始调用,使用不同的数组标号,我们就能够调用不同的数组元素,这里是5个元素的数组,那么最后一个元素应该是array[4],而不是5。
4.一维数组的内存分布
数组的定义和使用,相信大家应该都学会了吧,那数组在内存中是如何存储的呢?我们首先定义几个数组看看:
char char_array[5] = {1,2,3,4,5};
int int_array[5] = {1,2,3,4,5};
在这里,我们定义了两个都包含5个元素的数组,其中一个数组是char类型的,另一个数组是int类型的。让我们来看看,这些数组在内存中的分布情况,这里我们假设所有数组的初始地址为0
之前说了,计算机中内存是由一个个存储器单元构成,每个存储器单元可以存8位二进制数据。 每一个存储器单元都有对应的存储地址。上面两张图,充分展示了数组在内存中的存储方式,也就是连续式存储,所谓连续,就是每个元素的地址是连续的,中间没有间断。如我们上面定义的char_array数组,它是char类型的数据,每个数组元素占一个字节,所以分布如图。再如我们的int_array数组,它是int类型的数据,每个数组元素占四个字节,所以分布如图所示,每四个字节算一个数组元素的存储地址。这就是一维数组在内存中的存储方式。
5.二维数组的定义
刚刚,我们已经学习完了一维数组,接下来我们讲讲二维数组如何定义,初始化以及使用。
二维数组的定义相对一维数组来说,仅仅多了一个中括号而已,如下所示:
int array[3][4];
这里的二维,我们可以理解为行和列的模式,在这个例子中,我们定义了一个二维数组,这个数组具有三行四列的数据,第一个中括号表示的行数,第二个中括号表示的是列数,如下图所示:
如图,我们这样就定义了一个三行四列的矩阵,前面的中括号表示的是行,后面中括号的数据表示的是列。
6.二维数组的初始化
如一维数组一样,二维数组同样可以初始化时赋值。如下所示:
int a[3][4] = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
这里我们给这个三行四列的矩阵赋予了初始值,同样,我们可以部分赋初始值,如下所示:
int a[3][4] = {
{1,2},
{3,4},
{5,6}
};
在这里,我们没有给所有的元素都赋值,而是给a[0][0],a[0][1],a[1][0],a[1][1],a[2][0],a[2][1]进行赋值了。
7.二维数组的使用
二维数组和一维数组一样,如果需要调用数组元素,我们就像上述一样,指明行和列,我们就可以获得数组某个位置的元素。如下所示:
int a,b[3][4];
a = b[1][2];
这里,我们就给a赋值了b数组的第二行第三列的元素。注意,数组元素标号,是从0开始的!
8.数组与函数结合的用法
第一种:函数的参数可以代入数组,我们来看一个例子
#include <stdio.h>
double getAverage(int arr[], int size);
int main ()
{
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
avg = getAverage( balance, 5 ) ;
printf( "平均值是: %f ", avg );
return 0;
}
double getAverage(int arr[], int size)
{
int i;
double avg;
double sum=0;
for (i = 0; i < size; ++i)
{
sum += arr[i];
}
avg = sum / size;
return avg;
}
这里我们定义了一个函数,叫做getAverage用于求数据的平均值,这个函数第一个参数,与我们之前使用的参数不一样,在变量名后面我们添加了一对中括号,这里就代表,我们要传入一个数组到这个函数中。在主函数中,我们直接将我们需要代入的数组名字作为第一个参数即可。这就是将数组作为函数参数的用法。
第二种:数组作为函数的返回值
第三种:指向数组的指针
第二种和第三种,我们在指针一节中详细讲解,其实第一种方法也属于指针的内容,我们在这里先讲述了。
三、C语言中的存储类
在这之前,我们已经讲述了函数和数组的使用方法,在这里我们讲述几个关键字。用于修饰变量类型,指定变量存储在什么地方。
1.auto关键字
auto关键字,仅用于函数中,它用来修饰局部变量,表示这个变量是局部变量,在执行完函数之后就会自动销毁。我们在函数中定义的变量,默认就是auto类型的。我们给不给auto关键字都可以。看一个例子:
void function()
{
int a;
auto int b;
}
在上面,a没有用auto修饰,但是b用auto修饰了,我们说了,函数中定义的变量默认就是auto类型的,所以这两个的存储地方是一样的,都存在栈中。(学到单片机原理,我们再详细讲述栈这种数据结构)
2.register关键字
register关键字,用于表示这个变量保存在寄存器中,在我们的硬件中,寄存器变量访问的速度比从RAM中访问变量的速度要快,所以,对于一些频繁调用的变量,一个变量要调用1万次或者几十万次,我们使用这个关键字,可以大大节约访问时间,当然,这个关键字不能保证变量一定会保存在寄存器中,只是尽量保存在寄存器中,很有可能修饰的变量还是保存在RAM中。
3.static关键字
static关键字,在C语言中一共具有三种用法
第一种:static修饰函数内的局部变量
众所周知,我们在执行函数的时候,在函数内定义的变量都是局部变量,在这个函数执行完之后,局部变量就会被销毁,但是有一种情况,局部变量不会被销毁,那就是使用static关键字修饰这个变量的时候,static修饰函数内局部变量时,在函数执行完毕之后,不会被销毁,仍然保持上一次的值,但是这个变量只能在对应的函数中使用,其他地方无法使用这个变量。我们来看一个例子:
void i_add(void)
{
static int i;
i++;
}
我们这里定义了一个函数,这个函数内部定义了一个静态的局部变量,我们每次执行函数后,i的值都会保存下来,我们每一次调用这个函数的时候,i都会自增1,而且这个变量只能在这个函数中使用,其他地方不能调用。
第二种:static修饰全局变量
static关键字还能够修饰全局变量,在修饰全局变量的时候,表示这个全局变量只能在当前文件中被调用,其他文件无法调用static修饰的全局变量。
第三种:static修饰函数
static除了能够修饰全局变量,还能修饰函数,在函数定义之前,我们用static进行修饰,表示这个函数只能被当前文件调用,其他文件无法调用使用static修饰的函数。这个用法和第二种类似。我们来看一个例子 :
static void function()
{
}
我们这里定义了一个静态函数function,使用static修饰的为静态函数,表示这个函数只能在定义这个函数的文件中使用,其他文件中无法使用这个函数。
4.extern关键字
extern关键字我们在以后会比较常见,这个关键字的作用就是对外部文件声明全局变量或者函数。我们在一个文件中定义的函数,在另一个文件中是无法直接使用的,必须在另一个文件中使用extern关键字去修饰这个全局变量或者函数,这样另一个文件就知道我们在其他文件中已经有这个函数或者全局变量了,系统就会自动搜索这个函数并执行函数的内容或者访问这个全局变量。
四、C语言的作用域
经过上面的讲述,这一节就显得很简单了,C语言中所谓的作用域,就是各个变量能使用的范围,比如,对于局部变量,它只能在函数内使用,对于全局变量,它可以在任何地方使用,但是在不同文件中使用时,需要用extern关键字先声明这个变量,再去使用。另外,在函数中代入的参数,同样也只能用于函数执行过程中,在执行完之后,就会被系统销毁。