C语言--函数
文章目录
- 一、函数的定义
- 1、函数定义形式:
- 2、函数的参数
- 3、空函数
- 二、函数的返回值
- 三、函数的调用
- 四、函数的递归调用
- 五、内联函数_inline
- 六、函数指针
- 七、变参函数
- 八、裸函数"__declspec(naked)"
一、函数的定义
C程序是由一个主函数和其他若干函数构成,每个函数实现一定的功能,其中主函数闷是必须的,其他函数被主函数调用或者其他函数之间相互调用,c语言的函数可以分为三类,主函数main,库函数和用户自定义函数。
C程序总是从主函数开始执行,其他函数只有在被主函数或其他正在执行的函数调用时才能被程序执行,执行后返回调用函数,最后到达主函数。
所有的函数在定义时是相互独立的,他们之间是平行关系,所以不能在一个函数内部定义另外一个函数,也就是不能千套定义,函数之间可以互相调用,但是不能调用主函数。
1、函数定义形式:
类型标识符 函数名 (形式参数类型说明表列){//语名函数体;}double get(double x){retrun x;}
例如:.
float max(float x,folat y){float z;if(x>y)z=x;elsez=y;return z;}
需要注意的是类型标识符为函数的类型与return语句返回值的类型相同,可以理解为函数最终的结果的类型,它可以是任何一种有效的类型当函数类型标示符缺省时,默认是整形,上例中函数max的结果为两个浮点数的最大数,即同样是浮点数类型,其返回数值z也是浮点类型,所以函数的类型标示符为flat,如果函数无返回值时,类型标识符为void.
函数名要符合c语言规定的标识符的命名规则,函数名字必须唯一不能与函数体内变量或形式参数名相同,所以上类中如果变量z改为max将导致结果错误。
2、函数的参数
形式参数类型说明表中的形式参数,用于接受主调函数传递过来的数字。
A、例如max是求两个数的最大数,需要从主函数中传过来两个浮点数,所以形式参数有两个。
B、形式参数的命名,只要符合变量的命名规则即可无需与主函数中的变量名一致。
C、如果函数不需要从主函数数接收数据可以不带形式参数,此时形式参数类型说明表示空,但函数名后面的圆括号不能省。
在形式参数类型说明时一定要注意,每一个形式参数必须单独说明类型,行使参数说明之间用逗号间隔,例如以下两个例子是错误的
float max(float x,y);
float max(float x;float y);
3、空函数
函数体也可以是空的
void input(){}
此函数为空函数,什么都不做,没有任何实际作用,在组函数调用对应语句为input(), 它主要用于在编写程序时留的空缺,当需要添加该函数功能时补上内容即可,空函数的使用对设计程序或者调试都极为方便。
二、函数的返回值
在调用函数时,有时需要将运算结果返回主调函数,此时需要使用return语句返回一个值,称作函数返回值,return语句形式
return 表达式;
return 0;
函数返回值的类型应该与函数类型一致,如果不一致是函数类型决定返回值的类型,函数无返回值时函数类型说明为void,
例:
void star(){}
三、函数的调用
函数只有在被调用时才能执行,当一个函数调用另一个函数时,程序的执行流程就从主函数转移到被调函数,并且从被调函数的函数体起始位置开始执行,直到函数及执行结束,返回到主调函数的调用位置继续往下执行。
需要注意的是,实参和形参必须个数相同,类型一致,顺序对应,以确保顺利进行数据的值传递,实参可以是常量变量表达式或函数,函数通变量一样在调用前应该在主调函数中事先说明及声明声明的方法是在主调函数开始位置,加上被调函数的函数原型及函数定义的第1行。
下面两种情况可以不用声明,
当被调用函数的定义的位置在主调函数之前。
被调用函数的返回值是整型int。
四、函数的递归调用
在编写递归算法时要特别注意:必须保证被用于递归调用的函数,可以在满足一定条件时结束递归调用,否则无限递归调用将导致程序无法结束,例如 fn()当中的 i==1,是令结果唯一则可以结束递归调用,否则它将继续调用而无限循环下去。
例:
int fn(int i){if(i==1) //终止条件return 1;elsereturn i+fn(i-1);//递归调用}
必须保证函数可以在满足一定条件时结束递归调用.
五、内联函数_inline
内联函数从源代码层看,有函数结构,而在编译后,却不具备函数的性质.
内联函数不是在调用时控制移动,而是在编译时将函数体嵌入在每一个调用处.
在代码中用inline修饰
inline int Add(int nNumA,int nNumB){return nNumA+nNumB;}
不能使用内联函数的情况:
在以下情况中,编译器不进行函数内联,即使是使用__forceinline:
1、函数或其调用者使用/Ob0编译器选项进行编译(Debug模式下的默认选项)。也就是说在Debug模式下,是不会发生函数内联的。
2、函数和其调用者使用不同类型的异常处理。
3、函数具有可变数目的参数。
4、函数使用了在线汇编(即直接在你C/C++代码里加入汇编语言代码)。但使用了编译器关于优化的选项/Og,/Ox,/O1,或/O2的情况除外。
5、是虚函数并且是虚调用。但对虚函数的直接调用可以inline。
6、通过指向该函数的函数指针进行调用。
7、函数被关键字__declspec(naked)修饰。
六、函数指针
函数在系统内存中都占有一片连续的存储区域保存其二进制代码,而函数名在C语言中就代表这片连续存储区域的首地址,因此函数名可以使用指针代理,指向函数的指针变量一般形式为:
返回数据类型 (*指针变量名)(形参列表);
int (*FunAdd)(int,int);
数据类型指的是函数的返回值类型。
形参列表指的是指向函数的形式参数表列。
七、变参函数
C函数要在程序中用到以下这些宏:
va_list:
用来保存宏va_start、va_arg和va_end所需信息的一种类型。
为了访问变长参数列表中的参数,必须声明va_list类型的一个对象。
定义: typedef char * va_list;
va_start:
访问变长参数列表中的参数之前使用的宏,它初始化用va_list声明的对象,初始化结果供宏va_arg和va_end使用;
va_arg:
展开成一个表达式的宏,该表达式具有变长参数列表中下一个参数的值和类型。
每次调用va_arg都会修改用va_list声明的对象,从而使该对象指向参数列表中的下一个参数;
va_end:
该宏使程序能够从变长参数列表用宏va_start引用的函数中正常返回。
va在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件。
下面我们写一个简单的可变参数的函数,改函数至少有一个整数参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
参考:
#define va_list void*#define va_arg(arg, type) *(type*)arg; arg = (char*)arg + sizeof(type);#define va_start(arg, start) arg = (va_list)(((char*)&(start)) + sizeof(start))#include <stdio.h>; #include <string.h>; #include <stdarg.h>; /* ANSI标准形式的声明方式,括号内的省略号表示可选参数 */ int demo(char *msg, ... ) { va_list argp; /* 定义保存函数参数的结构 */ int argno = 0; /* 纪录参数个数 */ char *para; /* 存放取出的字符串参数 */ /* argp指向传入的第一个可选参数, msg是最后一个确定的参数 */ va_start( argp, msg ); while (1) { para = va_arg( argp, char *); /* 取出当前的参数,类型为char *. */ if ( strcmp( para, "/0") == 0 ) /* 采用空串指示参数输入结束 */ break; printf("Parameter #%d is: %s/n", argno, para); argno++; } va_end( argp ); /* 将argp置为NULL */ return 0; }void main( void ) { demo("DEMO", "This", "is", "a", "demo!" ,"333333", "/0"); }
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获取各个参数.
八、裸函数"__declspec(naked)"
裸函数是一个没有任何可执行代码的空函数,它在内存中仅仅是一条地址信息,裸函数使用关键字“__declspec(naked)"定义,一个可运行的最简单的裸函数如下所示:
void __declspec(naked) TestFun(){__asm ret}
使用场景:
写驱动;
内联汇编;
__declspec(naked) 是告诉编译器 不要对函数进行优化 函数的所有实现包括堆栈的平衡 参数的压栈 ebp的赋值 还原 都要我们来做,甚至是ret / ret n 都要我们来做。
一般来说,使用naked函数时需要注意以下问题:(以VC++编译器为例)
1、函数必须显式返回。
一般通过__asm ret的内嵌汇编指令返回。
2、不可以通过任何方式使用局部变量。
若声明一个局部变量,并在代码中为其赋值,则会更改父函数中相应位置的局部函数的值。
3、只能通过esp引用参数。
因为子函数继承了父函数的ebp寄存器,所以只能通过esp引用参数。
4、naked 属性仅与函数的定义相关,不能在函数原型中指定。不能用于函数指针,不能用于数据定义。
实例代码:
#include "stdafx.h"extern "C" int __declspec(naked) add2(int x,int y) //引用说明支持跨文件的调用{__asm{ //函数的环境初始化(升栈、保护现场、填充缓存区)PUSH EBPMOV EBP,ESPSUB ESP,0X40PUSH EBXPUSH ESIPUSH EDILEA EDI,DWORD PTR SS:[EBP-0X40]MOV EAX,0XCCCCCCCCMOV ECX,0X10REP STOSD}__asm{ //函数功能MOV EAX,DWORD PTR SS:[EBP+0X8]ADD EAX,DWORD PTR SS:[EBP+0XC]}__asm{ //恢复现存降栈返回POP EDIPOP ESIPOP EBXMOV ESP,EBPPOP EBPRET}}====================================================#include "stdafx.h"extern "C" int add2(int,int); //声明外部函数int _tmain(int argc, _TCHAR* argv[]){//testint sum=0;printf ("add2(50,76):%d\n",add2(50,76));return 0;}