1、函数入门介绍
在C语言中,函数意味着功能模块,一个典型的C语言程序,就是由一个个的功能模块(函数)拼接起来的整体,也正因为如此,C语言也叫做模块化语言。
函数是C语言的基本单位,被调用的函数可以是系统提供的库函数,也可以是用户根据需要自己定义的函数。
2、函数的基本语法
2.1、函数定义
2.1.1、函数头
:函数对外提供的接口,由函数返回值类型,函数名,参数列表组成
函数名:跟变量名的命名规则一样,一般取与函数实际相关的符合意思的单词,简称顾名思义。
函数参数列表:调用函数的时候,传入数据的列表,一个函数可以有一个或者多个参数,也可以不需要参数
函数返回值:函数执行完成之后,返回的数据,一个函数最多只能返回一个数据,也可以不返回数据
2.1.2、函数体
:函数功能的内容实现
函数语法说明:
返回值类型 函数名(参数列表) { 函数体; }
函数举例:求给定的两个整数的最大值
// 返回值类型 函数名(参数列表) // { // 函数体; // } int max(int a, int b) { return a>b?a:b; }
语法:
1、当函数的参数列表为void时候,表示该函数不需要任何参数(参数列表的void可以省略)
int func(void)//int func() { return 0; }
2、当函数的返回值为void时,表示不返回任何数据
void func(void)//函数没有返回值 { return ; } void *func(void)//函数返回值为void指针 { return NULL; }
3、关键字return表示退出函数,若函数中规定函数有返回值类型,则写return需要携带一个与返回值类型相匹配的数据,若函数头中规定函数返回值为void时,则写return不需要携带参数。
注意:如果函数头有返回值类型,但是函数体里面没有写return返回对应的匹配数据类型,那么会返回eax寄存器里存放着函数最后一次赋值的变量的值。
eax寄存器:形参数据初始化的位置。
2.2、函数声明
当我们要使用一个函数的时候,得保证这个函数已经定义或者声明
在做函数声明的时候,参数列表的变量名可以省略,(整个形参列表都可以省略,但是不建议)
函数定义在函数调用之后,会有经过,就要先做函数声明
//函数声明:就是告诉后面用这个函数的人,在那个地方去找 int max(int , int ); int main(int argc, char const *argv[]) { int a = 100; int b = 200; int c; c = max(b, a); printf("最大值是:%d\n", c); return 0; } int max(int a, int b)//在Linux里面,函数调用,实参给形参做赋值从右到左 { return a>b?a:b; }
3、实参和形参
概念:
函数定义中的参数列表,被称为形参
函数调用中的参数,被称为实参
实参和形参的关系:
实参与形参位于不同的内存区域,彼此独立
形参变量只有在函数被调用的时候,才会分配内存,在函数调用结束(函数退出的时候),形参就会自动释放(所有形参在函数体内是有效的,不能在函数体外面使用)
实参可以是变量,常量,表达式,函数调用,但是要求在函数调用的时候,它们必须都有确定的值,以便将这些值传递给函数做形参初始化(实参赋值形参)
注意:
实参和形参在进行传递的时候,参数的类型必须要一致(当然,能隐式转换就另说),顺序,数量也要一致。
函数调用的时候,数据传送是单向的,不可逆
函数调用的时候,多个形参的初始化顺序从右往左
4、函数调用的内存逻辑
函数调用时,进程的上下文会切换到被调函数,当被调函数执行完毕之后再切换回去
5、局部变量与栈内存
全局变量:定义在函数体外面的变量,全局变量在程序退出之前都是有效的,但是仅仅在定义之后有效,而且全局变量还可以定义多个同名的。
局部变量:定义在函数体内的,(形参也是局部变量),局部变量在函数退出时释放。
局部变量:凡是被一对花括号{}包含起来的变量,称为局部变量
{ int a = 4; printf("a = %d\n", a);//4 { int a = 5; //这个变量a被这个花括号括起来的,相当于是这个大括号的局部变量,出了这个大括号就被释放了 printf("a = %d\n", a);//5 } printf("a = %d\n", a);//4 }
局部变量的特点:
某一函数内部的局部变量,存储在当前函数的特定的栈内存中
局部变量只能在该函数内部可见,在该函数外部不可见
当该函数退出后,局部变量所占的内存立即被系统回收,因此局部变量也称为临时变量
函数的形参虽然不被花括号所包含,但依然属于该函数的局部变量
栈内存的特点:
每当一个函数被调用时,系统就自动分配一段栈内存给该函数,用于存放其局部变量
每当一个函数退出时,系统将自动回收其栈内存
系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配的原则
技术要点:
栈内存相对而言是比较小的,不适合用来分配尺寸太大的变量
return之后不可在访问函数的局部变量,因此返回一个局部变量的地址通常是错误的
6、静态函数与static关键字
6.1、静态变量
1.static修饰的变量就叫做静态变量
2.static修饰的局部变量:这个变量从定义位置开始就被分配到内存的数据段里面,直到程序退出才会释放,并且这个静态局部变量只会初始化一次。
int func() { static int b = 0;//静态局部变量,存放在数据段中,只会初始化一次 // b = 0; //赋值 printf("b = %d\n", b++); return 0;//函数退出 } int main(int argc, char const *argv[]) { func();//b=0 func();//b=1 func();//b=2 return 0;//程序退出 }
3.static修饰的全局变量:没有修饰的全局变量,存放在数据段中,全文件可见,修饰的静态全局变量,存放在数据段中,这个全局变量只在当前文件有效。
cesi.c static int l; static_quanju.c //外部声明 ,声明不过来的 extern int l; int main(int argc, char const *argv[]) { l = 100; printf("l = %d\n", l); return 0;//程序退出 }
4.static修饰的函数:没有修饰的函数,跨文件使用,修饰的静态函数,这个静态函数只在当前文件可见。
cesi.c #include <stdio.h> static void function() { printf("%s\n", __FUNCTION__); } static_func.c #include <stdio.h> //外部声明,声明函数用不了 extern void function(); int main(int argc, char const *argv[]) { function(); return 0;//程序退出 }
背景:普通函数,普通的全局变量是可以跨文件使用,即a.c里面定义的函数和全局变量,能够在b.c中被使用
要点:
静态全局变量,静态函数主要是为了缩小它的可见范围,减少与其他文件中函数重名的冲突。
静态函数跟静态全局变量一般定义在头文件中,然后被各个源文件包含
7、递归函数
7.1、递归思想
一个函数或这个过程在定义或者说明中有直接或间接调用自身的一种方法,递归通常用来将一个大问题层层转化为与原来问题一样的小问题。
(一般来说,只要能把问题描述成“每个。。。都。。。直到。。。”,那么这个问题就可以用递归来解决)
int add(int n)//5 4 3 2 1 { if (n == 1)//边界条件 { return 1; } return n+add(n-1);//递归公式 // 5 + add(4) // 4 + add(3) // 3 + add(2) // 2 + add(1) // 1 } int main(int argc, char const *argv[]) { int d = add(2); printf("d = %d\n", d); return 0; }
7.2、使用递归的条件
(1)子问题的逻辑与原问题一样,并且更为简单
(2)不能无限制重复调用本身,必须要有一个出口,化简为非递归状态出来
7.3、设计一个递归算法
(1)确定问题变量
(2)确定递归公式(化简过程,比如:n-1)
(3)确定边界条件(递归的出口)
注意:递归是一个函数嵌套调用的过程,递归次数越多,运行时间会大幅度增加,内存空间也会被大量使用,所以,一般能用循环解决的,我们就不考虑递归。
缺点:效率低,还比较占内存
7.4、递归的一般应用
(1)数据的结构形式按照递归定义的(比如:二叉树)
(2)执行过程是按照递归定义的(比如:函数嵌套调用)
(3)问题的解决办法按照递归算法实现(比如:年龄问题,台阶问题,汉诺塔问题,分鱼问题)
递归调用是,函数的栈内存变化如下图所示,可见,随着递归函数的层层深入,栈空间逐渐往下增长,如果递归的层次太深,很容易把栈内存耗光。
层层递进时候,问题的规模会随之减小,减小到可直接退出的条件时,函数开始层层回归。
8、内联函数
8.1、内联使用的原因分析
函数切换过程中,保护现场和恢复现场等工作有一定的时间消耗,这个时间大约是一两行语句的执行时间,假如被调用的函数本身很简短,只包含一两条语句,函数切换保护现场和恢复现场的时间和函数本身代码的执行时间在同一个数量级,那么这种切换耗费的时间就不能忽略。
inline关键字
8.2、内联函数的语法
inline 返回值 函数名(参数列表)
{
函数体;
}
inline void func() { printf("hello world\n"); } inline关键字就将普通函数变成内联函数
这里要注意一个问题:inline关键字不属于C语言原来的32个关键字,是后面扩展进去的,所以我们目前有些编译器(比如:Ubuntu里面的gcc)是不支持inline关键字的程序编译,需要找一个支持的编译器(比如:Ubuntu里面的g++,用来编译C++的代码)
8.3、什么时候用内联
(1)该函数代码简短,简短到跟函数切换所用到的时间具有可比性
(2)该函数被频繁调用
(3)函数里面没有循环的时候
当满足 以上条件的时候,我们可以考虑使用内联,实际上在大型项目中,真正使用到内联方式优化代码只能是热点代码。
9、变参函数
概念:参数可变的函数
比如:
scanf("%d", &n); scanf("%d%d", &n, &m); printf("%d", n); printf("%d%d", n, m);
一般在实际开发中,用到的地方其实不多,因为在实际开发中,写一个函数,会直接就把参数给固定好
10、回调函数
概念:回调函数就是一个通过函数指针调用的函数,简称函数调用函数
用处:
程序分层设计
11、main函数的传参和返回
11.1、main函数返回
普通函数返回时是回到这个函数的调用位置,main函数的返回,就是返回到main函数调用的位置,也就是执行这个程序的位置
11.2、main函数的传参
执行该程序,调用main函数所在的程序的进程,给main函数传递数据,这些是可以被main函数形参进行接收
给main函数传参的格式:
int main(int argc, char const *argv[])
int main(int argc, char *argv[])
int main(int argc, char const **argv)
int main(int argc, char **argv)
./可执行文件 参数1 参数2 ...
可以不传参:
int main(void)
参数含义:
argc:代表的是传入参数的个数
argv:代表的是每个参数的内容,用一个char *argv[],指针数组来保存的
注意:main函数传参会把后面的数据都当做字符串来处理,那如果传入的参数是一个整数,对于main函数来讲,它是按照字符串的方式来接收,如果当做整数来处理,需要用到atoi函数来进行转换
#include <stdlib.h> int atoi(const char *nptr);//转成int long atol(const char *nptr);//转成long long long atoll(const char *nptr);//转成long long
atoi,atol,atoll在进行转换的时候,只能转换数字字符,遇到非数字字符,结束转换