Linux C: 函数
本篇进入了函数的部分,也是C语言进阶阶段的第二个重难点,本文主要从函数的定义、声明、调用三个方面来认识和学习函数的使用,并学习了计算机的底层原理的实现、局部变量、全局变量、变量的生存期来更好的理解代码的存储,函数的递归调用举例了汉诺塔游戏来加以证明,今天已经将数组作为参数传递等问题补充完整啦,在文章最后也通过不少例子方便更好的理解。
一、 函数基础常识
在C语言中,子程序的作用是由函数来完成的。一个C程序可由一个主函数和若干个其他函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次。
函数的好处:①降低了程序的耦合性
②提高代码的复用性
eg:
说明:
(1)一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件对较大的程序,一般不希望把所有内容全放在一个文件中,而是将它们分别放在若干个源文件中,再由若干个源程序文件组成一个C程序。
(2)一个源程序文件由一个或多个函数以及其他有关内容(如命令行、数据定义等)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
(3)C程序的执行是从main函数开始的,如是在main函数中调用其他函数,在调用后流程返回到 main函数,在main 函数中结束整个程序的运行。
(4)所有函数都是平行的,即在定义函数时是分别进行的,是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。函数间可以互相调用,但不能调用main
函数。main函数是系统调用的。 //函数的定义和函数的声明是不同的
(5)从用户使用的角度看,函数有两种。
①标准函数。标准函数即库函数,它是由系统提供的,用户不必自己定义而直接使用它们。应该说明,不同的C语言编译系统提供的库函数的数量和功能会有一些不同当然许多基本的函数是共同的。
②用户自己定义的函数。它是用以解决用户专门需要的函数。
(6)无参函数:参数个数为0个就可以被称为无参函数
二、函数的定义
2.1 有参函数定义的一般形式
类型标识符 函数名(形式参数表列) //函数的首部
{
声明部分
语句部分
}
注:类型标识符:所有数据类型都可使用
函数名: 标识符 ,需符合标识符的一般规则
形式参数表列:简称形参列表,该部分填写的是 函数在使用或被调用时所需要提供的额外条件
2.2 函数的调用
使用形式: 实参个数与形参个数需要匹配 实参与形参类型不一致时,会将实参类型转换为形参类型
函数名(实际参数)
//站在两个角度int add(int a, int b) //函数的设计者{ int ret; ret = a + b; return ret;}int main(void) //函数的使用者{ int i= 10,j= 20; int sum; sum = add(i, j); //add(i,j)即为:函数名(实际参数) printf(\"%d\\n\", sum); printf(\"%d\\n\", add(11, 33)); return 0;}
注意:
①实参名和形参名可以相同,但二者代表的含义不同
②C语言语法要求函数的形参和实参类型匹配(可以进行隐式转换即可),个数相同
③以上述代码为例:主函数(main函数)被称为主调函数,add函数被称为被调函数; 写代码时将被调函数写于主调函数之前
return语句的作用:
①程序执行到return语句时会立即终止函数并将函数返回到函数的调用并继续向下执行
注意:被调函数中必须通过return语句进行返回,且返回类型应与前所定义的数据类型相同
注意:返回值不能是数组,但返回值和形参可以是void,直接使用return;向void进行返回
若未定义返回值类型则在C语言当中默认返回值为int型
在写程序时,每个形参都要单独定义数据类型,中间用逗号隔开
2.3 函数的声明
声明函数的参数及特性 1. 声明方式: 如果被调函数在主调函数的下方定义需要再主调函数上方声明 如果被调函数在主调函数的上方定义,定义时已经完成函数的声明
三、底层实现原理
PC:指向所执行的下一条地址
栈:先进的后出,后进的先出
入栈:保护现场 出栈:恢复现场
代码运行时计算机内部空间分配:
内存:
文本段:存放函数、代码和指令 栈区:存放局部变量(auto) 栈区空间有上限的,默认(8M),不要定义太大空间 栈区是操作系统管理区域,频繁被申请释放,所以未经初始化值为随机值 代码执行到变量定义时开辟空间(栈空间),代码执行超过变量作用域回收空间(栈空间)数据区: 1. 特点: 存放全局变量和静态变量,未经初始化时会初始化为0值 程序编译时分配空间 程序结束时回收空间 2. 区域划分: 已初始化全局变量/静态变量区域(.data) 存放初始化的全局变量和静态变量 未初始化全局变量/静态变量区域(.bss) 存放未初始化的全局变量和静态变量 在程序运行时会对.bss端初始化为0值 字符串常量区(.rodata) 区域中的内容不能修改,修改的话会导致段错误 static关键字的作用: 1. 延长变量的生存周期,局部变量超过作用域被回收,但用static修饰,会在程序结束时回收空 间 2. static修饰变量,将变量存放在数据区中,未经初始化时值为0值 3. static限定全局变量作用域只能在本文件中使用 4. static防止全局变量或者全局函数重名
四、函数的递归调用
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。C语言的特点之一就在于允许函数的递归调用。例如:
int f(int x){ int y,z; z = f(y); return(2*z);}
Linux下栈区空间为8M,直接调用会造成崩溃(短时间内迅速堆满栈区),并不会呈现死循环现象
//直接调用void f1(void){ f1();}int main(void){ f1(); return 0;}//间接调用void f2(void){ f1();} void f1(void){ f2();}int main(void{ f1(); return 0;}
递归调用实现循环(若数据很大时)仍会导致栈区堆满,以消耗大量的内存空间为代价,造成程序崩溃【每次调用都需要保护现场和恢复现场,实际效率更差,低于for循环】
注意: 递归函数一定要有结束条件 避免深层次的递归调用
使用推荐:编写程序要求函数内部不的定义变量、不得使用循环(For/do While /While)/使用循环无法完成
汉诺塔游戏(递归)
void move(char pole1, char pole2){ printf(\"%c->%c\\n\", pole1, pole2);}void hanoi(int n, char pole1, char pole2, char pole3){ if(1 == n) { move(pole1, pole3); } else { hanoi(n-1,pole1,pole3,pole2); move(pole1,pole3); hanoi(n-1, pole2, pole1, pole3): }}int main(void){ hanoi(64,\'A\',\'B\',\'C\'); return 0;}
五、 局部变量、全局变量、变量的生存期
标识符的作用域和可见性问题
局部变量:在一对花括号内部起作用
特殊情况:所定义的函数的形参也具有局部作用的效果;作用域定义的函数内部
全局作用域:所有在花括号外部的区间:在所定义的行数及以后行数直到文件结束区间都有效
全局变量(g)
eg: int g_i;
C语言当中的所有的函数名都属于全局变量
全局变量的作用:为了实现类似于函数的传参
int g_a;int g_b;int add(void){ return g_a + g_b;}int main(void){ g_a = 10; g_b = 20; printf(\"%d\\n\",add()); return 0;}
该程序中g_a和g_b存在于全局区(静态区),其他定义的变量存放于栈区,而存在于全局区的变量如果没有进行初始化,C语言在开辟空间时会自动清零(打印结果必然为0!!),若进行初始化,则该值为所初始化的值
局部作用域包含于全局作用域
可见性:当程序运行到某一点时,对某个标识符是否可以访问或使用,称之为该标识符在程序某一点的可见性
总结:
1.标识符必须先定义再使用;
2.在同一作用域中不得定义同名标识符;
3.在没有包含关系的不同作用域中定义的同名标识符互不影响;
4.在两个或多个具有包含关系的作用域中定义的同名标识符,外层标识符在内层不可见;(就近原则)
变量的生存期:
1.静态生存期:该变量的生存期与程序运行的周期相同
所有的全局变量就具有静态生存期,全局变量的生存期与所在程序的运行周期是相同的
特殊情况:
void fn(void)
{
static int s_i;
}
\"static\"会使s_i的动态生存期强制变为静态生存期
static + 全局变量:限制该变量的适用范围,只能在当前文件中被使用不能被其他.c文件所使用
2.动态生存期:
所有的局部变量具有动态生存期
总结记忆:存在与栈区的变量具有动态生存期;
存在于全局区(静态区)的变量具有静态的生存期;
用static修饰的静态局部变量具有静态的生存期;(具有静态生存期但存在于局部作用域)
auto变量 : 变量空间的开辟和销毁是自动的
register变量(寄存器):开辟变量空间时从RAM中开辟到CPU内部 (建议) 目的 :提高对变量的读写速率 注意:被register修饰的变量不能被取地址
extern函数:对函数声明,声明的函数属于外部程序中的函数
在vi中实现多文件编写:
按esc后,输入 :vsp +加新文件名 回车(左右)
按esc后,输入 :sp +加新文件名 回车(上下)
再进行编译时所有参与的文件都要一起进行编译
eg:
gcc -oapp main.c func.c
在所在程序中调用其他文件程序:在所调用程序前要对所用函数进行声明
eg:
extern int add(int a , int b); //注意要加分号,加分号为声明
//int add(int a , int b) //不加分号为定义
再多文件编写时解除vi对鼠标的限制:
按esc后,输入 :set mouse=a
可以另开一个xxxx.h的头文件进行函数的声明 ,该头文件中只放声明,不放定义(否则会造成重复定义)
六、数组作为函数参数传递
6.1值传递
输出结果i的值不变,仍为10;
被调函数中无法修改由主调函数所定义的变量
当所设置的函数有两个或两个以上的参数时,该函数传参的顺序是自右向左传参
前面已经介绍了可以用变量作函数参数,显然,数组元素也可以作函数实参,其用法与变量相同。此外,数组名也可以作实参和形参,传递的是数组首元素的地址。
6.2 数组元素作函数实参
由于实参可以是表达式,而数组元素可以是表达式的组成部分,因此数组元素当然可以作为函数的实参,与用变量作实参一样,是单向传递,即“值传送”方式。
数组传递两个参数 : 数组名(该数组首元素地址) 和 数组长度
void printArray(int a[], int len){ int i; for(i = 0; i <len ;++i) { printf(\"%d\\n\", a[i]); }}//数组求和int sumOfArray(int a[], int len){ int sum = 0,i; for(i = 0;i < len;++i) { sum += a[i]; } return sum;}//数组求最大值int maxOfArray(int a[],int len){ int max = a[0]; int i; for(i = 1;i < len;++i) { if(max < a[i]) { max = a[i]; } return maX;}int main(void){ int a[] = {1,2,3,4,5,6,7,8,9,0,11}; int len = sizeof(a) / sizeof(a[0]); // printArray(a, len); printf(\"%d\\n\",sumOfArray(a,len); return 0;}
当我们使用数组作为参数传递时,由于传递的实参总是一个数组的首元素地址(更高效),所以恰好变成了一种指针传参,从而造成了可以在被调函数中去修改主调函数中的参数
6.2.1 一维数组作为参数练习
1.倒置
2.选择排序
3.冒泡排序
4.插入排序
5.二分查找
6.2.2 二维数组作为函数传数传递
1)要传三个参数:数组名、行号;(行号必须传,因为在函数内部行号无法正确计算出来,列数可以计算;数组名依旧可以省略行号,列数不能省略)
2)一个写定的二维数组函数,最多只能用于相同大小的;(在省略行号的情况下)
练习:交换数组中元素
6.2.3 字符型数组作为参数
字符型数组作为函数参数传递时,我们通常不传递数组元素个数,是因为字符型数组本质上是一种容器,是专门用来装字符串的,而字符串中总是含有“ /0 ”作为字符串结束标志,计算机在处理字符串时和数组容积无关,即可省略字符型数组元素个数
练习
1.strlen
2.strcpy
3.strcat
4.strcmp
5.字符串逆序