函数
一、两种定义方法
(1)先声明再定义
在调用函数之前进行声明操作。
(2)直接定义
这种方式就要求,要在调用之前的位置进行好定义,不可写在调用位置的后面。
(3)错误用法
仅仅在调用位置之后进行定义,并没有在调用之前进行函数的声明,就会报错,故不可以这么使用。
那么再深入一点探索,我们看到了报错Conflicting types for 'func1',说func1这个函数类型冲突,仅仅是定义错了位置,为什么会说函数类型冲突呢?
这就要说到C程序的运行步骤了:
1.首先我们编写的代码叫做源程序,保存后,文件后缀为.c,即xxx.c文件。
2.对源程序进行编译,编译的目的就是生成目标程序,目标程序是什么呢?
C语言是高级语言,高级语言对于开发者是友好的,但是计算机其实并不能识别高级语言程序,他只能识别机器指令,编译就是将我们的高级语言程序,“翻译”成计算机能看懂的机器指令的程序,也就是目标程序。
而编译过程可以分为两部分:预编译和正式编译两步。
首先的预编译,即是先对程序中的预处理指令进行编译预处理。如我们日常引入的头文件使用的#include就是预编译指令,它的作用就是将其后面的文件内容读进来,例如#include <stdio.h>,就是相当于把stdio.h文件的内容读进来,替换掉#include <stdio.h>这一行,可以简单理解为把stdio.h文件内容复制到了该位置。而后预程序的其他部分,共同组成了可用来正式编译的源程序。
正式编译就是对源程序进行检查,帮助编程人员捡错。无错后,编译程序就会将源程序转化为二进制形式的目标程序(后缀为.obj)。
3.连接处理,得到了.obj文件后,计算机还不能直接执行,因为一个程序可能会包含很多个源程序文件,编译是以源程序文件为对象的,可以理解为一次编译只能得到于一个源程序文件对应的目标文件(.obj),而将这些目标文件(多个.obj文件)连接组装起来,再与库函数连接,就形成了可执行文件(.exe文件)。
4.执行
了解了编译过程,我们再回到上面的错误,报错Conflicting types for 'func1'的原因。
在编译程序编译到main函数中的调用func1()时(此时还没有连接),由于之前并没有对func1()的声明,所以会默认该函数是有返回值的,且返回值是int类型。这时候在编译下方的函数定义,编译程序会发现该定义使用了与前面main函数中相同的函数名称“func1”,但是类型却不同,对应不上,所以产生冲突。
了解了原理,那么如果我们将下方的函数定义,返回值改为int类型会怎样呢?上图
可以看到,成功编译并且运行,而并没有在调用之前进行声明操作,因为我们改了返回值类型,与默认的函数返回值对应上了,便可成功调用,虽然这样可以运行,但是不符合规范,日后使用仍然要按照前两种定义方法使用。
二、头文件的使用
当我们的项目较大时,会有非常多的函数,也会有某些函数在不同文件中的重复调用的需求。比如我在A文件里写了一个print函数,为了打印某些数据。而后我在B和C文件中也要使用同种功能的函数,这时就重新在B和C中进行函数的声明和定义,即重新写一次函数的声明和实现,再调用。这样是不是很繁琐,而且效率低下?那么如何避免重复的声明和定义操作而提高效率呢?能不能在B和C文件中直接调用A文件中的print函数呢?
这时候我们就可以创建一个头文件,而在其他文件中要调用该函数的时候,就直接将这个声明文件作为头文件导入一下,便可以进行跨文件的方法调用了,免去了重复声明定义函数的繁琐操作,极大提高效率。接下来例子展示一下。
首先我们新创建一个func.c文件和一个head.h头文件。
首先我们可以将func.c文件和main.c文件中的#include <stdio.h>删掉,而将#include <stdio.h>写入到head.h头文件中,这时main和func文件直接导入head.h头文件就可以了。
这时如图,我们在func.c文件中定义了一个函数,然后将该函数的声明写入head.h头文件,而后直接在main.c中就可以调用func_print()函数了,并且免去了在main.c中重新进行函数声明和定义的繁琐操作,其他有相同需求的文件,都可以通过直接导入head.h头文件,进行func_print()的调用操作。
成功打印。
这样当项目较大,功能较繁琐的时候,就可以通过创建多个.c文件,每个.c文件中封装不同的功能模块函数,而将这些功能函数的声明部分都放入一个头文件中,之后在任何文件中想使用头文件中包含的方法时,都可以通过导入头文件,而后直接调用相应函数,方便至极。
你是否注意到了,正常一个函数调用之前,都是要有声明的,才可以正常调用。而我们日常使用的printf()和scanf()函数,为什么都是可以直接使用,而没有在调用之前进行函数的声明操作呢?这其实就是头文件的作用,#include <stdio.h>命令,导入了stdio.h头文件,而printf()和scanf()函数的声明便包含在其中,所以我们可以直接调用。
我们平时常用的一些函数,都是通过导入库函数头文件,来进行使用的,如stdio.h,stdlib.h,math.h,string.h等等。
在此多提一嘴,#include后面使用""和<>两种符号导入头文件的区别。
可以看到,在本文的头文件示例中,导入使用的是"",而不是导入库函数时的<>,那么这两种导入方式有什么区别呢?
使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:
使用尖括号< >,编译器会到系统路径下查找头文件;
而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
故在导入库函数文件时,我们两种符号都可以使用,但是一般考虑到导入库函数头文件时的效率,都采用<>符号,避免不必要的路径检索。而在导入我们自定义的函数时,便需要使用""。
三、嵌套调用(协程)
正常我们如果嵌套调用函数,如main调用a,a调用b,b调用c......,当返回的时候,是从内向外一层一层返回的,而一旦嵌套了很多层,里面的某一层发生了错误,也将一层一层向上返回。而编译器对此进行了处理,即当里层(假设是第z层函数)发生了某些错误,或者有需求需要返回的时候,便可以不用一层一层返回,而是可以直接由z函数返回到main函数,然后通过判断条件,判断继续执行还是结束。
在这里我们通过jmp_buf,setjmp(),longjmp(),来实现理解这个过程。
使用之前先导入<setjmp.h>头文件。
jim_buf是一个数组类型,它是用来存储进程的上下文信息的
上下文:处理器运行进程期间,运行进程的信息被存储在处理器的寄存器和高速缓存中,执行的进程被加载到寄存器的数据集被称为上下文
setjmp(),longjmp()就是C给我们提供的库函数了
setjmp()就是让我们传入一个jmp_buf类型的参数,获取当前进程的上下文,赋给参数,并返回一个0。
longjmp()就是让我们传入保存了上下文的参数,和一个非零的数(用来判断返回后如何执行的条件),然后跳回到保存上下文的地方。恢复保存的上下文对应的状态。
可以类比goto,但是goto只能在一个函数中跳,而setjmp()和longjmp()不受限制。
接下来用一个小例子展示用法,
可以看到,我们成功的在b函数直接跳到了main函数,而不是b->a->main的返回路径。(详细点说是跳回到了第31行的位置,而后根据我们调用longjmp()函数时传入的数字,进行判断是否继续执行a()函数)。
四、变量及函数的作用域
说到变量自然是想到局部变量和全局变量了。
(1)局部变量
很多地方对于局部变量的作用域的解释都是“作用域只局限于函数内”,这其实是不准确的说法。
可以看到我们在同一个函数中,在代码体中定义一个变量j,而在代码体外进行对j打印,编译器直接编译不通过,说没有声明j变量。这就说明了其实局部变量的作用域并不是函数内,而是离它最近的一对大括号内。一旦超出了大括号,就超出了作用域。
(2)全局变量
在这里首先介绍一下内存分区。有内核区、栈区、堆区、数据段和代码段。
局部变量是存储在自己所处函数的栈空间的,函数结束执行结束后,函数内局部变量分配到的空间就会被释放。
而全局变量是存储在数据段的,不会因为某个函数执行结束而消失,在整个进程的执行过程中始终有效。
如果局部变量与全局变量重名,那么将采取就近原则,即实际获取和修改的值是局部变量的值,如图
需要注意的是,全局变量的有效范围,是从定义变量的位置开始到本源文件结束。
即如果在定义位置的前面企图使用该变量,会报未声明的错误。
全局变量在程序的全部执行过程中,都占用存储单元,而不是仅在需要时才开辟单元,且由前面的示例可知,各个函数执行时都有可能改变外部变量的值,程序容易出错,因此要有限制的使用全局变量
(3)extern(外部变量)关键字
全局变量只能在本文件中使用,在其他源文件中是访问不到的,那么如果想在其他文件中也访问本文件中的全局变量,就需要用到extern 关键字了。如图。
注意是要在要使用的那个文件使用extern,且函数默认都是extern修饰的。
(4)static(静态变量)关键字
如果用static修饰全局变量,那么该全局变量将不能被其他文件引用,例如上面例子的main.c中,如果在int i=10;前加上static,那么func.c中即便使用了extern关键字也无法引用main.c中的全局变量i了,且编译无法通过。
如果用static修饰函数,那么该函数将不能被其他文件引用。
用static修饰局部变量。静态局部变量是在编译时赋初值的,且只赋值一次。存储在全局区(数据区的静态区),而不是在栈区了,故函数执行结束时,局部变量并不会被释放,且随后的赋值操作也不会有效果,如图。
static修饰的局部变量i并没有被释放并且置0.
(5)寄存器变量(register)
变量的值是存储在内存中的,当程序中用到某个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中并进行运算,如果需要存储数据,那么再从运算器中将数据送到内存存储。
寄存器变量的使用场景:如果有些变量使用频繁,如在一个循环中,那么存取变量的值要花费不少时间。为提高运行效率,C语言允许将局部变量的值放在CPU的寄存器中,需要时直接从寄存器中取出并参与运算,而不必到内存中去存取。由于寄存器的存取速度远高于内存的存取速度,因此这样做可以提高执行效率。
但是由于寄存器的数量是有限的,因此用C语言进行服务器编程时,采用register提升性能非常的少,但嵌入式编程可能会用到。