C语言函数详解

目录

函数的引入

函数的传值调用和传址调用

递归函数

main函数

函数库

输出型参数

内联函数


函数的引入

整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。
函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要,目的就是实现模块化编程。

函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数,即主函数 main() ,所有简单的程序都可以定义其他额外的函数。

您可以把代码划分到不同的函数中。如何划分代码到不同的函数中是由您来决定的,但在逻辑上,划分通常是根据每个函数执行一个特定的任务来进行的。

函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。

函数三要素:声明、定义、调用
函数声明是告诉编译器函数的原型、函数的定义就是函数体、函数调用就是使用函数

函数定义是函数的根本,函数定义中的函数名表示了这个函数在内存中的首地址,所以可以用函数名来调用执行这个函数(实质是指针解引用访问);函数定义中的函数体是函数的执行关键,函数将来执行时主要就是执行函数体。所以一个函数没有定义就是无基之塔。

函数原型就是函数的声明,说白了就是函数的函数名、返回值类型、参数列表。
函数原型的主要作用就是给编译器提供原型,让编译器在编译程序时帮我们进行参数的静态类型检查
必须明白:编译器在编译程序时是以单个源文件为单位的(所以一定要在哪里调用在哪里声明),而且编译器工作时已经经过预处理处理了,最最重要的是编译器编译文件时是按照文件中语句的先后顺序执行的。
编译器从源文件的第一行开始编译,遇到函数声明时就会收到编译器的函数声明表中,然后继续向后。当遇到一个函数调用时,就在我的本文件的函数声明表中去查这个函数,看有没有原型相对应的一个函数(这个相对应的函数有且只能有一个)。如果没有或者只有部分匹配则会报错或报警告;如果发现多个则会报错或报警告(函数重复了,C语言中不允许2个函数原型完全一样,这个过程其实是在编译器遇到函数定义时完成的。所以函数可以重复声明但是不能重复定义)

函数的声明

函数声明会告诉编译器函数名称及如何调用函数。函数的实际主体可以单独定义。

函数声明包括以下几个部分:

return_type function_name( parameter list );

比如:

int max(int num1, int num2);

在函数声明中,参数的名称并不重要,只有参数的类型是必需的,因此下面也是有效的声明:

int max(int, int);

当您在一个源文件中定义函数且在另一个文件中调用函数时,函数声明是必需的。在这种情况下,您应该在调用函数的文件顶部声明函数。

函数的定义

C 语言中的函数定义的一般形式如下:

return_type function_name( parameter list )
{
   body of the function
}

在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:

  • 返回类型:一个函数可以返回一个值。return_type 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void。如果没有写明返回值类型(是允许的),其返回值将默认为 int 型。
  • 函数名称:这是函数的实际名称。函数名和参数列表一起构成了函数签名。
  • 参数:参数就像是占位符,是形式参数。当函数被调用时,您向参数传递一个值,这个值被称为实际参数。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
  • 函数主体:函数主体包含一组定义函数执行任务的语句。

函数的调用

创建 C 函数时,会定义函数做什么,然后通过调用函数来完成已定义的任务。

当程序调用函数时,程序控制权会转移给被调用的函数。被调用的函数执行已定义的任务,当函数的返回语句被执行时,或到达函数的结束括号时,会把程序控制权交还给主程序。

调用函数时,传递所需参数,如果函数返回一个值,则可以存储返回值。

编译器对函数声明、调用和调用的缺失处理:
1、如果没有定义,只有声明和调用:编译时会报连接错误。undefined reference to `func_in_a'
2、如果没有声明,只有定义和调用:编译时一般会报警告,极少数情况下不会报警告。但是最好加上声明。
3、如果没有调用,只有定义和声明:编译时一般会报警告(有一个函数没有使用),有时不会报警告。这时候程序执行不会出错,只是你白白的写了几个函数,而没有使用浪费掉了而已。

函数书写的一般原则:
1、遵循一定格式。函数的返回类型、函数名、参数列表等。
2、一个函数只做一件事,不能太长也不宜太短,原则是一个函数只做一件事情。
3、传参不宜过多,不宜超过4个。如果传参确实需要多个则考虑结构体打包
4、尽量少碰全局变量,函数最好用传参返回值来和外部交换数据,不要用全局变量。

函数的实质是:数据处理器
程序的主体是数据,也就是说程序运行的主要目标是生成目标数据,我们写代码也是为了目标数据。我们如何得到目标数据?必须2个因素:原材料+加工算法。原材料就是程序的输入数据,加工算法就是程序。
程序的编写和运行就是为了把原数据加工成目标数据,所以程序的实质就是一个数据处理器。
函数就是程序的一个缩影,函数的参数列表其实就是为了给函数输入原材料数据,函数的返回值和输出型参数就是为了向外部输出目标数据,函数的函数体里的那些代码就是加工算法。
函数在静止没有执行(乖乖的躺在硬盘里)的时候就好象一台没有开动的机器,此时只占一些存储空间但是并不占用资源(CPU+内存);函数的每一次运行就好象机器的每一次开机运行,运行时需要耗费资源(CPU+内存),运行时可以对数据加工生成目标数据;函数运行完毕会释放占用的资源。
整个程序的运行其实就是很多个函数相继运行的连续过程。

小节补充和总结:

1、函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。
2、函数体是函数的关键,由一对{}括起来,包含很多句代码,函数体就是函数实际做的工作。
3、形参是函数的输入部分,返回值是函数的输出部分。对函数最好的理解就是把函数看成是一个加工机器,形参列表就是这个机器的原材料输入端;而返回值就是机器的成品输出端。
4、其实如果没有形参列表和返回值,函数也能对数据进行加工,用全局变量即可。用全局变量来传参和用函数参数列表返回值来传参各有特点,在实践中都有使用。总的来说,函数参数传参用的比较多,因为这样可以实现模块化编程,而C语言中也是尽量减少使用全局变量。
5、全局变量传参最大的好处就是省略了函数传参的开销,所以效率要高一些;但是实战中用的最多的还是传参,如果参数很多传参开销非常大,通常的做法是把很多参数打包成一个结构体,然后传结构体变量指针进去。

6、函数是动词、变量是名词(面相对象中分别叫成员方法和成员变量)
(1)函数将来被编译成可执行代码段,变量(主要指全局变量)经过编译后变成数据或者在运行时变成数据。一个程序的运行需要代码和数据两方向的结合才能完成。
(2)代码和数据需要彼此配合,代码是为了加工数据,数据必须借助代码来起作用。拿现实中的工厂来比喻:数据是原材料,代码是加工流水线。名词性的数据必须经过动词性的加工才能变成最终我们需要的产出的数据。这个加工的过程就是程序的执行过程。

7、关于函数重名:

在一个项目的两个.c文件中,分别定义一个名字相同的函数会如何?
编译报错 multiple definition of `func_in_a'
结论:在一个程序中,不管是一个文件内,还是该程序的多个文件内,都不能出现函数名重复的情况,一旦重复,编译器就会报错。主要是因为编译器不知道你调用该函数时到底调用的是哪个函数,编译器在调用函数时是根据函数名来识别不同的函数的。

8、从现代C语言(较新的C规范C99、C11)的规定来说,任何标识符(除了goto的label以及main()的main)在使用之前都一定要声明,函数也是如此。理论上这是必须的,但某些编译器为了迁就以前代码的一些写法放松了这个要求。所以还是应该写,尽管看起来貌似不是必须的。

函数的传值调用和传址调用

先说明,其实函数只有传值调用,只是因为传的如果是指针,因为指针的特性,所以为了稍加区别,就有了传址调用这个说法。

传值调用

函数调用的过程,其实就是实参传递给形参的一个过程。这个传递实际是一次拷贝。实参(本质是一个变量)本身并没有进入到函数内,而是把自己的值复制了一份传给了函数中的形参,在函数中参与运算。这种传参方法,就叫做传值调用。  

至于什么是传址调用

实际是传值的一种特殊方式,只是他传递的是地址,不是普通的赋值,那么传地址以后,实参和行参都指向同一个对象,因此对形参的修改会影响到实参。

看一个经典案例就明白了

先看看这个代码

#include

void swap(int n1,int n2)
{
    int temp;
    temp=n1;
    n1=n2;
    n2=temp;
}

int main()
{
    int a=10;
    int b=20;
    printf("a=%d\n",a);
    printf("b=%d\n",b);
    swap(a,b);
    printf("a=%d\n",a);
    printf("b=%d\n",b);
}

以上代码实现的功能好像是交换两个数的数值对吧!运行一下看看结果:

这里写图片描述

不对啊,和我们预想的不一样啊,可以看到a,b的值并没有被交换,怎么回事呢?
因为a和b虽然成功把值传给了n1、n2,n1、n2也完成了它们之间数值的交换,但是也仅仅是n1、n2之间交换了,和a、b没有关系。这是一次单向的传递过程,a、b能传给n1、n2,n1、n2能成功互换其数值,但n1、n2是定义在函数swap中的局部变量,当函数调用结束后,它俩就over了,被残忍抛弃了(子函数的生命期为子函数开始调用到结束调用,调用结束后就主动释放了),因此它们没有渠道把交换的值传回给a、b。所以看到的是如上图的结果。

有了以上的结果,我们再来看这样一段代码:

#include

void swap(int *p1,int *p2)
{
    int temp;
    temp=*p1;
    *p1=*p2;
    *p2=temp;
}

int main()
{
    int a=10;
    int b=20;
    printf("交换前a,b的值分别为:\n");
    printf("a=%d\n",a);
    printf("b=%d\n",b);
    swap(&a,&b);
    printf("交换后a,b的值分别为:\n");
    printf("a=%d\n",a);
    printf("b=%d\n",b);
}

 以上代码的功能同样是实现交换两个数的数值对吧!让我们再来看看运行结果:

这里写图片描述

很显然,实现了交换。

 这是调用swap函数后a、b的数值与其在内存中开辟的空间的地址以及开始调用函数时*p1、*p2的数值与其地址。
可以看到此时a、b与*p、*p2的地址空间是一样的,那么当*p1、*p2被修改时,a、b也会跟着发生变化,因为此时二者占用了同一块空间,当任意一者使空间里的内容发生变化时,二者都会做相同变化。

递归函数

递归函数就是函数中调用了自己本身这个函数的函数。
递归不等于循环
递归函数解决问题的典型就是:求阶乘、求斐波那契数列

函数的递归调用原理
1、实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一些栈内存。
2、栈内存的大小是限制递归深度的重要因素。

使用递归函数的原则:收敛性、栈溢出
收敛性就是说:递归函数必须有一个终止递归的条件。当每次这个函数被执行时,我们判断一个条件决定是否继续递归,这个条件最终必须能够被满足。如果没有递归终止条件或者这个条件永远不能被满足,则这个递归没有收敛性,这个递归最终要失败。
因为递归是占用栈内存的,每次递归调用都会消耗一些栈内存。因此必须在栈内存耗尽之前递归收敛(终止),否则就会栈溢出。
递归函数的使用是有一定风险的,必须把握好。

举个求阶乘的例子:

#include 
 
double factorial(unsigned int i)
{
   if(i <= 1)
   {
      return 1;
   }
   return i * factorial(i - 1);
}
int  main()
{
    int i = 15;
    printf("%d 的阶乘为 %f\n", i, factorial(i));
    return 0;
}

生成斐波那契数列:

#include 
 
int fibonaci(int i)
{
   if(i == 0)
   {
      return 0;
   }
   if(i == 1)
   {
      return 1;
   }
   return fibonaci(i-1) + fibonaci(i-2);
}
 
int  main()
{
    int i;
    for (i = 0; i < 10; i++)
    {
       printf("%d\t\n", fibonaci(i));
    }
    return 0;
}

main函数

在C语言程序中,main主函数是程序的入口,而且在整个项目的源代码中,有且只有一个main主函数。

main主函数的两种标准写法:

int main(void) 
{ 
    return 0; 
}
int main(int argc, const char *argv[])
{
	return 0;
}

其实还有其他一些非标准写法比如void main(void),不过为了代码的通用可移植性,建议采用标准提供的形式。如果一个函数确定无需传入任何参数,那么用void限定是一个不错的选择(建议没有参数的函数都加上void,以明确表示该函数没有任何参数)。返回值可以是void,但是推荐int。因为void的情况下,在程序退出后,想要获取其退出状态也就不可以了。

main函数返回给谁?函数的返回值就是给调用它的人返回一个值。
main函数被谁调用?
main函数是特殊的,首先这个名字是特殊的。因为C语言规定了main函数是整个程序的入口。其他的函数只有直接或间接被main函数调用才能被执行,如果没有被main直接/间接调用则这个函数在整个程序中无用。
main函数从某种角度来讲代表了我当前这个程序,或者说代表了整个程序。main函数的开始意味着整个程序开始执行,main函数的结束返回意味着整个程序的结束。
谁执行了这个程序,谁就调用了main。
谁执行了程序?或者说程序有哪几种被调用执行的方法?

以linux下某个程序的执行来探讨这个问题:
1、linux中在命令行中输入./xx执行一个可执行程序
2、我们还可以通过shell脚本来调用执行一个程序
3、我们还可以在程序中去调用执行一个程序(fork exec)
总结:我们有多种方法都可以执行一个程序,但是本质上是相同的。linux中一个新程序的执行本质上是一个进程的创建、加载、运行、消亡。linux中执行一个程序其实就是创建一个新进程然后把这个程序丢进这个进程中去执行直到结束。新进程是被谁开启?在linux中进程都是被它的父进程fork出来的。
分析:命令行本身就是一个进程,在命令行底下去./xx执行一个程序,其实这个新程序是作为命令行进程的一个字进程去执行的。
总之一句话:一个程序被它的父进程所调用。
结论:main函数返回给调用这个函数的父进程。父进程要这个返回值干嘛?父进程调用子进程来执行一个任务,然后子进程执行完后通过main函数的返回值返回给父进程一个答复。这个答复一般是表示子进程的任务执行结果完成了还是错误了。(0表示执行成功,负数表示失败)

谁给main函数传参

int main(int argc, char *argv[])

调用main函数的父进程给main函数传参,并且接收main的返回值。
为什么需要给main函数传参?
首先,main函数不传参是可以的,也就是说父进程调用子程序并且给子程序传参不是必须的。 int main(void)这种形式就表示我们认为不需要给main传参。
有时候我们希望程序有一种灵活性,所以选择在执行程序时通过传参来控制程序中的运行,达到不需要重新编译程序就可以改变程序运行结果的效果。

表面上:给main传参是怎样实现的?
给main传参通过argc和argv这两个C语言预定的参数来实现

argc arg count

argv arg vector
argc是int类型,表示运行程序的时候给main函数传递了几个参数;argv是一个字符串数组,这个数组用来存储多个字符串,每个字符串就是我们给main函数传的一个参数。argv[0]就是我们给main函数的第一个传参,argv[1]就是传给main的第二个参数····

本质上:给main传参是怎样实现的?
程序调用有各种方法,但是本质上都是父进程fork一个子进程,然后子进程和一个程序绑定起来去执行(exec函数族),我们在exec的时候可以给他同时传参。
程序调用时可以被传参(也就是main的传参)是操作系统层面的支持完成的。

给main传参要注意什么
main函数传参都是通过字符串传进去的。
程序被调用时传参,各个参数之间是通过空格来间隔的。
在程序内部如果要使用argv,那么一定要先检验argc。

char *argv[],这表示字符串数组,因为在c语言中,字符串用指针来表示,所以这个数组里存的是指针数组,也可以理解成字符串数组。如果是char argv[],那就表示的是字符数组。

从C99开始,规定main函数必须返回一个int变量值,其值是返回给系统用的。 main函数的返回值,用于说明程序的退出状态。如果返回0,则代表程序正常退出;返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退出。 但是由于历史原因,很多地方可以看到返回类型是int但是程序没写返回值的main,这时候编译器会帮你加上,不会报错。不过,还是建议手动添加。 return 0;

函数库

函数库就是一些事先写好的函数的集合,给别人复用。
函数是模块化的,因此可以被复用。我们写好了一个函数,可以被反复使用。也可以A写好了一个函数然后共享出来,当B有相同的需求时就不需自己写直接用A写好的这个函数即可。

函数库的由来
最开始是没有函数库,每个人写程序都要从零开始自己写。时间长了慢慢的早期的程序员就积累下来了一些有用的函数。
早期的程序员经常参加行业聚会,在聚会上大家互相交换各自的函数库。
后来程序员中的一些大神就提出把大家各自的函数库收拢在一起,然后经过校准和整理,最后形成了一份标准化的函数库,就是现在的标准的函数库,譬如说glibc。

函数库的提供形式:动态链接库与静态链接库

早期的函数共享都是以源代码的形式进行的。这种方式共享是最彻底的(后来这种源码共享的方向就形成了我们现在的开源社区)。但是这种方式有它的缺点,缺点就是无法以商业化形式来发布函数库。
商业公司需要将自己的有用的函数库共享给被人(当然是付费的),但是又不能给客户源代码。这时候的解决方案就是以库(主要有2种:静态库和动态库)的形式来提供。

静态链接库:
比较早出现的是静态链接库。静态库其实就是商业公司将自己的函数库源代码经过只编译不连接形成.o的目标文件,然后用ar工具将.o文件归档成.a的归档文件(.a的归档文件又叫静态链接库文件)。商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用;客户拿到.a和.h文件后,通过.h头文件得知库中的库函数的原型,然后在自己的.c文件中直接调用这些库文件,在链接的时候链接器会去.a文件中拿出被调用的那个函数的编译后的.o二进制代码段链接进去形成最终的可执行程序。

动态链接库:
动态链接库比静态链接库出现的晚一些,效率更高一些,是改进型的。

现在我们一般都是使用动态库。静态库在用户链接自己的可执行程序时就已经把调用的库中的函数的代码段链接进最终可执行程序中了,这样好处是可以执行,坏处是太占地方了。尤其是有多个应用程序都使用了这个库函数时,实际上在多个应用程序最后生成的可执行程序中都各自有一份这个库函数的代码段。当这些应用程序同时在内存中运行时,实际上在内存中有多个这个库函数的代码段,这完全重复了。而动态链接库本身不将库函数的代码段链接入可执行程序,只是做个标记。然后当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。

静态库与动态库的区别
1.命名方式不同:

动态链接库的后缀名是.so(对应windows系统中的dll),静态库的扩展名是.a
静态库libxxx.a:库名前加”lib”,后缀用”.a”,“xxx”为静态库名。
动态库libxxx.so:库名前加”lib”,后缀变为“.so”。

dll,dynamic linked library,比如:

C语言函数详解_第1张图片

2.链接时间不同:
静态库的代码是在编译过程中被载入程序中。 
动态库的代码是当程序运行到相关函数才调用动态库的相应函数。

3.链接方式不同:
静态库的链接是将整个函数库的所有数据在编译时都整合进了目标代码。
动态库的链接是程序执行到哪个函数链接哪个函数的库。

静态库与动态库的优缺点
静态库:

优点是:在编译后的执行程序不再需要外部的函数库支持,运行速度相对快些;
缺点是:如果所使用的静态库发生更新改变,你的程序必须重新编译。

动态库 :

优点是:动态库的改变并不影响你的程序,所以动态函数库升级比较方便;
缺点是:因为函数库并没有整合进程序,所以程序的运行环境必须提供相应的库。
 

gcc中编译链接程序默认是使用动态库的,要想静态链接需要显式用-static来强制静态链接。
库函数的使用需要注意3点:

1、包含相应的头文件;

2、调用库函数时注意函数原型;

3、有些库函数链接时需要额外用-lxxx来指定链接;如果是动态库,要注意用-L指定动态库的地址。

C链接器的工作特点:因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度想了一个折中的方案:链接器只是默认的寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。链接时可以用-lxxx来指示链接器去到libxxx.so中去查找这个函数。

输出型参数

函数传参中使用const指针
const如果用在函数参数列表中,一般用法是const int *p;(意义是指针变量p本身可变的,而p所指向的变量是不可变的)
const用来修饰指针做函数传参,作用就在于声明在函数内部不会改变这个指针所指向的内容,所以给该函数传一个不可改变的指针(char *p = "linux";这种)不会触发错误;而一个未声明为const的指针的函数,你给他传一个不可更改的指针的时候就要小心了。

C语言标准,明确规定,数组类型和函数类型不可以做为返回值。字符串是数组的一种,是字符数组,所以同样不可以作为返回值。(Java中是可以的)

结构体类型可以做为返回值,C语言设计者当时引入struct结构体的概念,目的是为了增加一种建立C语言新类型的机制,换句话说它希望通过struct建立的类型,像内置的int float类型一样使用方便。

函数需要向外部返回多个值时怎么办?可以返回结构体,但是比较麻烦。
一般来说,函数的输入部分就是函数参数,输出部分就是返回值。问题是函数的参数可以有很多个,而返回值只能有1个。这就造成我们无法让一个函数返回多个值。
现实编程中,一个函数需要返回多个值是非常普遍的,因此完全依赖于返回值是不靠谱的,通常的做法是用参数来做返回(在典型的linux风格函数中,返回值是不用来返回结果的,而是用来返回0或者负数用来表示程序执行结果是对还是错,是成功还是失败)。
普遍做法,编程中函数的输入和输出都是靠函数参数的,返回值只是用来表示函数执行的结果是对(成功)还是错(失败)。如果这个参数是用来做输入的,就叫输入型参数;如果这个参数的目的是用来做输出的,就叫输出型参数。
输出型参数就是用来让函数内部把数据输出到函数外部的。

下面详细说明:

先来看简单的题,我们有一个长度为10的int型数组

int arr[] = {1,8,10,2,-5,0,7,15,4,-5};

现在我们需要写一个函数,并且返回此数组中最大值和最小值。函数中只能返回一个值,那我们是不是非得写两个函数?其实我们完全可以通过指针的特性,从函数中返回多个我们需要的“值”。

我们在main函数中 定义我们需要用到的指针

	int *pmax,*pmin;

接下来 来写我们的功能函数:

void find_max_and_min(int **pmax,int **pmin, int arr[]) {
	*pmax = *pmin = arr;

	int i;
	
	for(i=0;i<10;i++) {
		if(**pmax < arr[i]) {
			*pmax = arr+i;
		}
		if(**pmin > arr[i]) {
			*pmin = arr+i;
		}
	}

}

此时我们注意到,功能函数中传入的参数分布为两个指向指针的指针,以及我们需要查找的数组。

主函数中

int *pmax,*pmin;
	
find_max_and_min(&pmax,&pmin,arr); 

printf("%d,%d",*pmax,*pmin);

即可在arr中找出我们需要的“返回值”。
敲重点,敲重点:我们将 指针 pmax和pmin的地址 传给了函数find_max_and_min。

其实,这就是传址调用的思想。

总结
看到一个函数的原型后,怎么样一眼看出来哪个参数做输入哪个做输出?函数传参如果传的是普通变量(不是指针)那肯定是输入型参数;如果传指针就有2种可能性了,为了区别,经常的做法是:如果这个参数是做输入的(通常做输入的在函数内部只需要读取这个参数而不会需要更改它)就在指针前面加const来修饰;如果函数形参是指针变量并且还没加const,那么就表示这个参数是用来做输出型参数的。譬如C库函数中strcpy函数:

char *strcpy(char *dest, const char *src)

内联函数

在C99中引入了内联函数(inline),联函数和宏的区别在于,宏是由预处理器对宏进行替代 ,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销。可以象调用函数一样来调用内联函数,而不必担心会产生于处理宏的一些问题。

内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。

指针函数

指针函数比较简单,就是返回值是指针的函数。

比如:

int *IsRight();

如果让星号*先跟变量结合,则变成了函数指针。

比如:

int (*IsRight)();

补充

1、

形式参数是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。局部变量(Local variables)指在程序中只在特定过程或函数中可以访问的变量。
两者都是所在函数体内有用,函数终止,则形参也被销毁,局部变量也被销毁。

事实上,形式参数也属于局部变量。

所以,虽然函数在声明时,形参可以只放类型不加变量名,但是在定义时,是当做局部变量来使用的,所以必须有一个变量名。

2、

函数定义时不会分配内存,而是在被调用时分配内存,调用结束后形参所占单元被释放。

3、

函数中可以用独立的大括号来限制变量的作用域。 

比如:

int a = 3;

int main()
{
    int s = 0;
    {
        int a = 5;
        s += a++;
    }
    s += a++;
    printf("%d\n", s);

}

注意

有种情况c++支持,但是c并不支持,需要注意别弄错了。

函数的形参也是一个局部变量;

在c++中
该变量可以有默认值,如下形式是支持的;
int fun(int i = 1, int j = 2){return (i + j);};
函数形参在有默认值的情况下再传实参,默认匹配原则为从左到右赋值形参。
当我们调用该函数,有如下情况:
fun(),使用默认值返回3;
fun(3),3赋值给第一个i,j使用默认值,返回5;
fun(3, 3),3和3依次赋值给i和j,返回6。

补充

附上一道题:

C语言函数详解_第2张图片

如果出现逗号表达式:
Y = (X1, X2, X3, X4, ... Xn)
那么会分别计算X1, X2, X3, X4, ... Xn表达式的值,但是最后赋给Y的值是Xn表达式的值。

如果在函数调用里出现:
 int add(int x, int y)
{
    ...
}
形参中的逗号只是用于分隔不同变量,不能算是逗号表达式。

如果还有其它变量需要用逗号分隔的情况下,要使用逗号表达式得加上括号,就像这题:
 exec((v1, v2),(v3,v4), v5, v6);

实际上等价于
 exec(v2, v4, v5, v6);
一共有4个实参。

实参和形参

1、形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。函数调用结束返回主调用函数后则不能再使用该形参变量。

2、实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使参数获得确定值。

3、实参和形参在数量上,类型上、顺序上应严格一致,否则就会发生类型不匹配的错误。

4、在一般传值调用的机制中只能把实参传送给形参,而不能把形参的值反向地传送给实参。因此在函数调用过程中,形参值发生改变,而实参中的值不会变化。而在引用调用的机制当中是将实参引用的地址传递给了形参,所以任何发生在形参上的改变实际上也发生在实参变量上。

个人总结:实参传递给形参的过程,是不是可以看做,形参定义了若干个变量,然后将实参的数值赋值给对应的形参变量,但是赋值行为是不会对右值产生副作用的,显然,对形参的操作是不会影响到实参的(不考虑传址调用)。 

个人总结(目前还未完全确定,尤其是后半句话,只是暂时这么理解):函数名只是一段代码集合的入口,通过函数名找到对应的入口地址后,执行的是里面的指令,并通过指令对相应的数据进行操作。 基于函数名所指示的首地址,函数内每条指令也会对应一个依次递增的地址。

当函数的返回类型是void时,可以直接使用return;作为返回语句。

当函数返回类型为void时,表示函数什么也不返回,因此返回语句return后面可以不加返回值。

有时候,函数里不直接使用形参,而是要把形参再赋值给某变量,再去操作这个变量,这通常见于操作非输入型形参,即函数中,不仅是使用该变量,还有可能改变该变量,但是,我们在函数中,也有可能要使用到原来的值,所以,就通过再赋值一次,将原来的值保存下来,便于操作。 

可重入函数和不可重入函数

重入(re-enter),是指当一个函数 func() 已经被 模块 A 调用时,还可以同时被其他模块B、C调用,并且保证 A、B、C 三个模块都能通过调用函数func() 获取正确的结果。也就是这个函数可以同时被很多模块调用,并且保证每个模块都能得到期望的结果。

可重入函数就是,可以被多个模块(如任务、线程、进程)同时使用,且能保证每个模块结果都正常的函数;由于这种可以被多个模块使用的特性,可重入函数也被称为(多)线程安全函数、(多)进程安全函数、可并发函数。

当两个或多个 task(也可称为多个模块、线程)虽然调用的是同一个函数,但函数内若存在共享的变量(如上述的 global_count),则该函数内的数据之间可能发生干扰,导致出现不可预知的错误。此时,该函数就属于不可重入函数,需要设法解决共享数据的互斥访问。

注意不能返回局部变量的地址

函数中创建的局部变量是保存在栈区的,函数执行完毕后,对应空间内的变量会自动销毁,所以是不能直接返回局部变量的地址的(值可以返回)。
那如果要返回呢,方法如下:

可以在该局部变量前加上static,这样该变量不再是局部变量,而是存储在静态区的变量,程序执行完毕才会销毁。
在函数外预先定义一个存放返回结果的全局变量。

你可能感兴趣的:(c语言,开发语言)