C语言的灵魂(函数)
关于C语言的基础知识:常量、流程控制等等
函数的定义
函数:一组一起执行一个任务的语句。
这里有必要说一下:通常一个较大的程序中会分为若干个模块,一个模块用来实现一个特定的功能,在大多数的编程语言中都有子程序的概念,通常都用子程序来实现模块的功能,在C语言中,子程序的作用就是由函数来完成。在程序设计中通常将一些常用的功能编写成函数,并放在函数库中供别人调用,例如C语言自带一些比较(strcmp)、拷贝(strcopy)等。
学习C语言有两个知识点是必须要学的:
1、函数:理解面向过程和面向对象的切入点
2、指针:帮助我们灵活操作数据,甚至访问硬件资源
函数的声明
函数声明会告诉编译器函数名称及如何调用函数,下面是函数声明的原型:
extern return_type func_name (para_list); // 1、extern:是C语言的关键字,表明一个函数的声明,可加可不加 // 2、return_type: 返回值类型
函数的参数
C语言中,参数分为实参和形参
实参:在调用是传递给函数的参数,实参可以是常量、变量、表达式、函数等。
形参: 它不是实际存在的变量,所以又称为虚拟变量,在定义函数名和函数体时使用,目的是用来接收调用该函数时传入的参数,形参是只在被调用时才分配内存单元,结束后会被释放回收。
形参是函数被调用时用于接收实参值的变量,在调用函数时,有3种向函数传递参数的形式:
1、传值调用:该方法把参数的实际值复制给函数的形参,这种情况修改函数内的形参不会影响实参。
2、传地址调用:通过指针传递方式,形参为指向实参地址的指针,当对形参做指向操作时,就相当于对实参本身进行操作。
3、传引用调用:引用“&”就是别名(相当于每个人都有大名和小名,这里区别于go语言,取址符号),所以程序对引用作出改动,其实就是对目标的改动。
实例:
#include <stdio.h> void swap (int a, int b) { // 传值交换 int temp; temp = a; a = b; b = temp; } void swap2(int *a, int *b) {// 传地址交换, int temp; temp = *a; *a = *b; *b = temp; } /* 特此说明C语言不支持这种引用传递,C++可以 void swap5(int &a,int &b) { int temp; temp = a; a = b; b = temp; } */ int main() { int a=1, b=2; // 值传递 swap(a,b); printf("a=%d,b=%d\n",a,b); int c=10,d=20; swap2(&c, &d); printf("a=%d,b=%d\n",c,d); // // int e=100, f=200; // swap5(e,f); }
函数的调用过程
首先完成一个简单的C程序:
#include <stdio.h> int plus(int a, int b) { int c = a + b; return c; } int main() { int a =1,b=2; int c=0; c = plus(a,b); printf("%d",c); return 0; }
使用:gcc -o demo demo.c;来生成可执行文件demo,再通过命令:objdump -d demo > demo.txt,得到该可执行程序的反汇编指令,如下
生成的汇编文件分析:
demo: file format Mach-O 64-bit x86-64 Disassembly of section __TEXT,__text: __text: 100000f20: 55 pushq %rbp // 在linux C中所有的main函数都是被__libc_start_main调用,这里把rbp的地址压栈,每次压栈后,rsp都是指向最新的栈顶 100000f21: 48 89 e5 movq %rsp, %rbp // rsp要么在内存空间中处于一个段的低地址,要么和rbp重合,这里rbp也指向栈顶,重一起表示函数的栈底地址 100000f24: 89 7d fc movl %edi, -4(%rbp) 100000f27: 89 75 f8 movl %esi, -8(%rbp) 100000f2a: 8b 75 fc movl -4(%rbp), %esi 100000f2d: 03 75 f8 addl -8(%rbp), %esi 100000f30: 89 75 f4 movl %esi, -12(%rbp) 100000f33: 8b 45 f4 movl -12(%rbp), %eax 100000f36: 5d popq %rbp 100000f37: c3 retq 100000f38: 0f 1f 84 00 00 00 00 00 nopl (%rax,%rax) 100000f40: 55 pushq %rbp 100000f41: 48 89 e5 movq %rsp, %rbp 100000f44: 48 83 ec 20 subq $32, %rsp 100000f48: c7 45 fc 00 00 00 00 movl $0, -4(%rbp) 100000f4f: c7 45 f8 01 00 00 00 movl $1, -8(%rbp) 100000f56: c7 45 f4 02 00 00 00 movl $2, -12(%rbp) 100000f5d: c7 45 f0 00 00 00 00 movl $0, -16(%rbp) 100000f64: 8b 7d f8 movl -8(%rbp), %edi 100000f67: 8b 75 f4 movl -12(%rbp), %esi 100000f6a: e8 b1 ff ff ff callq -79 <_plus> 100000f6f: 89 45 f0 movl %eax, -16(%rbp) 100000f72: 8b 75 f0 movl -16(%rbp), %esi 100000f75: 48 8d 3d 36 00 00 00 leaq 54(%rip), %rdi 100000f7c: b0 00 movb $0, %al 100000f7e: e8 0d 00 00 00 callq 13 <dyld_stub_binder+0x100000f90> 100000f83: 31 f6 xorl %esi, %esi 100000f85: 89 45 ec movl %eax, -20(%rbp) 100000f88: 89 f0 movl %esi, %eax 100000f8a: 48 83 c4 20 addq $32, %rsp 100000f8e: 5d popq %rbp 100000f8f: c3 retq // 以上都是完成_libc_start_main函数的汇编 _plus: // plus函数的汇编部分 100000f20: 55 pushq %rbp // 将rbp的地址压栈 100000f21: 48 89 e5 movq %rsp, %rbp // 将rsp和rbp重叠表示整个函数的栈底 100000f24: 89 7d fc movl %edi, -4(%rbp) 100000f27: 89 75 f8 movl %esi, -8(%rbp) 100000f2a: 8b 75 fc movl -4(%rbp), %esi 100000f2d: 03 75 f8 addl -8(%rbp), %esi 100000f30: 89 75 f4 movl %esi, -12(%rbp) 100000f33: 8b 45 f4 movl -12(%rbp), %eax 100000f36: 5d popq %rbp 100000f37: c3 retq 100000f38: 0f 1f 84 00 00 00 00 00 nopl (%rax,%rax) _main: // main函数部分汇编 100000f40: 55 pushq %rbp 100000f41: 48 89 e5 movq %rsp, %rbp // 初始化压栈和函数栈顶 100000f44: 48 83 ec 20 subq $32, %rsp // 为main函数开辟空间 100000f48: c7 45 fc 00 00 00 00 movl $0, -4(%rbp) // 将变量1压栈(a) 100000f4f: c7 45 f8 01 00 00 00 movl $1, -8(%rbp) // 将变量2压栈(b) 100000f56: c7 45 f4 02 00 00 00 movl $2, -12(%rbp) // 将变量3压栈(c) 100000f5d: c7 45 f0 00 00 00 00 movl $0, -16(%rbp) 100000f64: 8b 7d f8 movl -8(%rbp), %edi // 设置寄存器edi保存实参 100000f67: 8b 75 f4 movl -12(%rbp), %esi // 设置寄存器esi保存实参 100000f6a: e8 b1 ff ff ff callq -79 <_plus> 100000f6f: 89 45 f0 movl %eax, -16(%rbp) // 形参入栈 100000f72: 8b 75 f0 movl -16(%rbp), %esi 100000f75: 48 8d 3d 36 00 00 00 leaq 54(%rip), %rdi 100000f7c: b0 00 movb $0, %al 100000f7e: e8 0d 00 00 00 callq 13 <dyld_stub_binder+0x100000f90> // 调用plus函数 100000f83: 31 f6 xorl %esi, %esi 100000f85: 89 45 ec movl %eax, -20(%rbp) 100000f88: 89 f0 movl %esi, %eax 100000f8a: 48 83 c4 20 addq $32, %rsp 100000f8e: 5d popq %rbp 100000f8f: c3 retq Disassembly of section __TEXT,__stubs: __stubs: 100000f90: ff 25 6a 10 00 00 jmpq *4202(%rip) Disassembly of section __TEXT,__stub_helper: __stub_helper: 100000f98: 4c 8d 1d 69 10 00 00 leaq 4201(%rip), %r11 100000f9f: 41 53 pushq %r11 100000fa1: ff 25 59 00 00 00 jmpq *89(%rip) 100000fa7: 90 nop 100000fa8: 68 00 00 00 00 pushq $0 100000fad: e9 e6 ff ff ff jmp -26 <__stub_helper>
函数递归
我们以最常见的斐波拉契数列为例:
#include <stdio.h> long Fibonacci (int n) { if (n < 0){ return -1; }else if(0==n){ return 0; }else if(1==n){ return 1; }else{ return Fibonacci(n-1) + Fibonacci(n-2); } } int main() { int* n; printf("请输入n的值:"); scanf("%d",n); printf("第n项的值为:%ld\n",Fibonacci(*n)); return 0; }
小编第一次跑这个程序的时候输入了70,发现半天没有响应,打开性能统计cpu占了99%,后来发现在40的时候y都已经上千万了...
可变参数列表
当我们无法列出传递函数的所有实参类型和数目时,我们可以使用省略号参数表例如:
int printf(const char* format ...); // 这就是printf的函数原型
我们之前介绍过参数是以数据结构栈的方式进行存取的,从右到左入栈,如之间将的压栈图,如之前我们调用的额函数(先b参数入栈,再a参数入栈);理论上来说我们只要知道一个参数的地址,那么其他的变量地址都可以进行捕捉到,这里我们讲解哈如果实现可变参数的功能:因为参数是从右到左压栈,当我们不知道参数个数的时候,应该如何开辟栈?通过<stdarg.h>中定义的宏来解决:
typedef char* va_list // 该变量用于存储参数 void va_start (....) // 当前参数靠右边的一个参数 type va_arg (...) // 获取到参数 void va_end (...) // 左后一个参数
代码实例:
#include <stdio.h> #include <stdarg.h> int average(int n,...) { va_list arg; // 返回一个参数列表 int i = 0; int sum = 0; va_start(arg,n); // 制定起始值 for (i=0;i<n;i++){ sum += va_arg(arg,int); // 制定参数类型返回该参数 } va_end(arg); // 结束 return sum/n; } int main() { int a = 10; int b = 20; int c = 30; int avg1 = average(2,a,b); int avg2 = average(3,a,b,c); printf("avg1=%d\n",avg1); printf("avg2=%d\n",avg2); }