【函数基础】
函数用于将程序代码进行分类管理,将实现一些功能的代码放在一个函数内,方便管理和修改,并且其它函数禁止使用本函数的数据(可通过指针参数绕过这一限制)。
函数外只能定义数据,不能修改数据,所有涉及程序执行逻辑相关的代码必须放在函数内部。
【资料图】
函数外定义的数据称为全局数据,所有函数都能使用。
函数内定义的数据称为局部数据,只有本函数可以使用。
● 函数之间的通信
一个函数可以跳转到另一个函数执行,调用者使用call指令跳转到一个函数,被调用者执行完毕后使用ret指令返回,当然有些函数可能无需返回。
两个函数之间跳转执行时,可能有通信需求,调用者需要向被调用者传递一些数据、或者被调用者执行完毕后需要向调用者传递一些数据,函数之间的通信可以有多种方式,最简单的方式就是通过全局变量中转,一方将通信数据写入全局变量,另一方读取全局变量获得通信数据,但是这种方式无法保密,所有函数都可以读写全局变量,为了解决此问题,C语言提供了参数和返回值,实现两个函数之间的保密通信。
参数是函数内的特殊局部数据,它由当前函数定义、调用者进行赋值,用于实现调用者向被调用者传递数据,参数可以没有,有可以有多个,可以是变量也可以是常量。
返回值用于实现被调用者向调用者传递数据,返回值只能有一个,通过寄存器传递。
main函数是自定义代码的执行入口,我们编写的代码从main函数开始执行,main函数的返回值为int类型,一般返回一个0,作用是告知操作系统此程序正常执行完毕,没有出现异常情况。
【输入输出函数】
操作系统的主要作用有两个:
1.管理程序的运行。
2.提供程序需要的基础功能,供程序调用执行。
操作系统提供了一些可被程序调用的基础功能,称为系统调用,Linux程序使用终端输出功能时可通过如下代码实现:
rax、rdi,设置需要使用的系统调用,这里使用终端输出功能。
rsi,设置要输出数据的指针。
rdx,设置输出字符串的长度,不包含末尾空字符,UTF8字符集中汉字占3个字节。
syscall指令,发出一个内中断,系统内核接收到内中断后,根据寄存器中的参数执行系统调用。
创建一个源代码文件a.c,将上述代码保存在文件中,之后在终端内输入如下命令:gcc -masm=intel a.c,之后输入 ./a.out 执行程序,将会在终端内输出“阿狸”。
在高级编程语言中通过内嵌汇编代码的方式使用系统调用显然太过繁琐,C语言编译器提供了一个函数库,其实有些函数封装了系统调用,我们可以使用这些函数实现输入输出。
● 输出函数 - printf
输出函数可以向终端发送一些字符数据,终端接收到这些数据后进行显示,最常用的是printf函数,原型如下:
参数format,设置输出字符串的指针。
执行成功返回输出字符的数量,执行失败返回-1。
使用终端输出功能时,往往需要将一些数值数据转换为对应的字符编码、然后合并到输出字符串中,此时可以在输出字符串中添加一些格式字符,之后将需要输出的数值数据使用额外的参数传递,常用格式字符如下:
%c,将一个字符合并到输出字符串中,不进行转换。
%s,将一个字符串合并到输出字符串中,不进行转换。
%hd,将2字节的有符号数转换为对应的字符、并合并到输出字符串中。
%d,同上,操作4字节数据。
%ld,同上,操作8字节数据。
%hu,将2字节的无符号数转换为对应的字符、并合并到输出字符串中。
%u,同上,操作4字节数据。
%lu,同上,操作8字节数据。
%f,将float浮点数转换为对应的字符、并合并到输出字符串中,小数部分默认显示6位。
%lf,同上,操作double浮点数。
有些字符编码起控制作用,不用来显示,比如回车、换行,这些字符需要使用转义字符表示。
有些字符与C语言关键词重复,不能直接编写,这些字符也需要使用转义字符表示。
常用转义字符如下:
\r,表示回车
\n,表示换行,在linux中可以使用换行表示“换行+回车”两种功能。
\0,表示空字符
\t,表示水平空白
\v,表示垂直空白
\b,表示退格
\',表示 ' 符号
\",表示 " 符号
\?,表示 ? 符号
\\,表示 \ 符号
● 输入函数 - scanf
用户可以在终端内输入一个字符串传递给程序,程序可以使用输入函数读取这些数据,最常用的输入函数是scanf,原型如下:
参数format,指定一个字符串,存储格式字符(与printf对应、功能相反),用于将用户输入的字符数据转换为对应的数值数据,设置了几个格式字符就需要额外添加几个变量指针作为参数,接收用户输入的数据。
Linux会为每个进程创建一个标准输入文件,用于存储用户在终端内输入的数据,用户的输入以换行结束,当scanf函数中使用了%c格式字符时、或者使用getchar输入函数时,标准输入文件内的数据并不会完全读取,下次执行输入函数时会继续读取上次剩余的数据,若不想使用之前剩余的数据,可以使用如下代码清空标准输入文件。
【数据存储方式】
● 全局数据
全局变量会分配一段专用的虚拟地址存储,可以称其为全局变量段,全局变量段中未使用的部分操作系统全部设置为0,所以定义全局变量未赋值的话,默认值为0。
全局常量也会分配一段专用的虚拟地址,可以称其为全局常量段,此段虚拟地址对应的内存页会被操作系统设置为只读,修改其中的数据会被CPU禁止。在开启编译优化后,某些常量也会转换为立即数寻址,增加执行速度。
● 局部数据
局部变量(包括参数)不需要在程序执行期间一直存在,函数执行完毕既删除,操作系统为程序分配一个栈空间来存储,所有函数共用此栈空间,栈空间不会进行初始化操作,定义局部变量不赋值的话,默认值无法预测。
局部常量默认使用栈存储,开始编译优化后会转换为立即数寻址,对于长度较大的常量字符串,为了增加程序的执行速度,会存储在全局常量段中。
● 静态局部变量
可以在定义局部变量时添加static关键词,表示定义静态局部变量,静态局部变量在全局变量段中存储,所以它在程序执行期间一直存在,它本质上是在函数外定义的全局变量,但禁止其它函数使用,这个限制是由编译器提供的,对程序进行逆向分析并修改时,并不存在任何限制。
静态局部变量的作用是保存函数的执行结果,当此函数再次执行时,会直接取上次保留的结果使用。
上述代码等同于如下代码,只不过编译器禁止其它函数使用变量a。
【数组作为参数和返回值】
参数和返回值只能是单个的数据,C语言将数组当做多个数据的集合,而将结构体当做一个独立的数据对待,所以两者在作为参数和返回值时会有不同的命运。
● 结构体作为参数
结构体作为参数时会转换为其所有成员作为参数。
编译器会转换为类似如下的代码:
● 数组作为参数
数组作为参数时,编译器会自动转换为其指针作为参数,就比如printf的第一个参数为字符指针,但是我们直接使用字符串名为参数赋值也可以,编译器会自动转换为指针。
因为编译器自动将数组参数转换为指针,所以f函数操作的其实是main函数内的数组,f函数修改数组后,main函数调用的也是修改后的值,最终main函数输出9。
若我们不希望f函数的修改影响main函数内的数组,可以在传递参数时创建数组的副本,将副本传递给f函数,也可以将数组放在一个结构体内,此时编译器不会将数组转换为指针,而是将数组内所有元素当做参数。
● 结构体作为返回值
返回值只能是单个数据,在64位Linux程序中,整数型返回值使用rax寄存器保存,若返回一个结构体,则编译器自动转换为返回其指针,但函数执行完毕后其使用的栈空间将被其它函数使用,此时局部结构体的存储地址将会被覆盖,对于不同编译器会使用不同方式解决此问题。
在GCC中,若返回一个局部结构体实例,编译器会自动调整栈空间的使用情况,下一个函数会自动避开此结构体使用的栈空间,防止被覆盖。
在VC++中,若返回一个局部结构体实例,会自动为此函数创建一个结构体指针参数,通过指针修改接收返回值的结构体,从而实现结构体的返回。
VC++编译器会转换为类似如下的代码:
● 数组作为返回值
C语言将数组当做一个数据的集合,而返回值只能是单个数据,所以数组不能直接返回,若需返回数组,可以使用如下方式:
1.通过全局数组进行通信,无需返回。
2.函数内定义一个指针参数,接收调用者定义的一个局部数组进行使用,无需返回。
3.函数内定义一个指针参数,通过指针修改对方函数内的数组实现返回,就像VC++编译器返回结构体那样。
4.将数组放在结构体内返回。
通过指针参数返回:
通过结构体返回:
注:在VC++中可以直接返回局部数组的指针,在开启内联函数功能时会正确执行,但是这种方式是错误的,不要被内联函数所迷惑。