C语言16之什么是函数?

时间:2018.3.11  作者:Tom   工作:HWE 说明:如需转载,请注明出处。
说明:本文主要参考朱有鹏老师linux嵌入式C语言高级篇笔记,已注明转载。

1.函数的本质

1.1 C语言为什么会有函数

C语言中,一个程序无论大小,总是由一个或多个函数构成,这些函数分布在一个或多个源文件中。每一个完整的C程序总是有一个main函数,它是程序的组织者,程序执行时也总是由main函数开始执行(main函数的第一条可执行语句称为程序的入口),由main函数直接或间接地调用其他函数来辅助完成整个程序的功能。

函数充分而生动地体现了分而治之和相互协作的理念。它可以将一个大的程序设计任务分解为若干个小的任务,这样便于实现、协作及重用,有效地避免了做什么都要从头开始进行。同时,大量经过反复测试和实践检验的库函数更是提高了程序的开发效率、质量,有效地降低了开发成本。这体现了程序设计中分工协作的思想。程序用于模拟客观世界,函数抽象了现实生活中能相对独立地进行工作的人或组织,函数间的相互协作正好映射了现实生活中人或组织间的相互协作。另外,函数还体现了封装的思想。它有效地将函数内部的具体实现封装起来,对外只提供可见的接口(传入的形式参数与返回的函数值)。这样,调用函数时就不用关心该函数内部具体的实现细节,而只需关注其接口即可调用和使用它来辅助完成所需功能。另外,利用函数还可以大大降低整个程序总的代码量。

1)整个程序分成多个源文件,一个文件分成多个函数,一个函数分成多个语句,这就是整个程序的组织形式。这样组织的好处在于:分化问题、便于编写程序、便于分工。

2)函数的出现是人(程序员和架构师)的需要,而不是机器(编译器、CPU)的需要。

3)函数的目的就是实现模块化编程。说白了就是为了提供程序的可移植性。

1.2函数书写的一般原则:

第一:遵循一定格式。函数的返回类型、函数名、参数列表等。

第二:一个函数只做一件事:函数不能太长也不宜太短,原则是一个函数只做一件事情。

第三:传参不宜过多: 在ARM体系下,传参不宜超过4个。如果传参确实需要多则考虑结构体打包

第四:尽量少碰全局变量:函数最好用传参返回值来和外部交换数据,不要用全局变量。

1.3函数是动词、变量是名词(面相对象中分别叫方法和成员变量)

1)函数将来被编译成可执行代码段,变量(主要指全局变量)经过编译后变成数据或者在运行时变成数据。一个程序的运行需要代码和数据两方向的结合才能完成。

2)代码和数据需要彼此配合,代码是为了加工数据,数据必须借助代码来起作用。拿现实中的工厂来比喻:数据是原材料,代码是加工流水线。名词性的数据必须经过动词性的加工才能变成最终我们需要的产出的数据。这个加工的过程就是程序的执行过程。

1.4函数的实质是:数据处理器

1)程序的主体是数据,也就是说程序运行的主要目标是生成目标数据,我们写代码也是为了目标数据。我们如何得到目标数据?必须2个因素:原材料+加工算法。原材料就是程序的输入数据,加工算法就是程序。

2)程序的编写和运行就是为了把原数据加工成目标数据,所以程序的实质就是一个数据处理器。

3)函数就是程序的一个缩影,函数的参数列表其实就是为了给函数输入原材料数据,函数的返回值和输出型参数就是为了向外部输出目标数据,函数的函数体里的那些代码就是加工算法。

4)函数在静止没有执行(乖乖的躺在硬盘里)的时候就好象一台没有开动的机器,此时只占一些存储空间但是并不占用资源(CPU+内存);函数的每一次运行就好象机器的每一次开机运行,运行时需要耗费资源(CPU+内存),运行时可以对数据加工生成目标数据;函数运行完毕会释放占用的资源。

5)整个程序的运行其实就是很多个函数相继运行的连续过程。

2. 函数的基本使用

2.1 函数三要素:定义、声明、调用

1)函数的定义就是函数体、函数声明是函数原型、函数调用就是使用函数

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

3)函数声明的主要作用是告诉编译器函数的原型

4)函数调用就是调用执行一个函数。

2.2 函数原型和作用

1)函数原型就是函数的声明,说白了就是函数的函数名、返回值类型、参数列表。

2)函数原型的主要作用就是给编译器提供原型,让编译器在编译程序时帮我们进行参数的静态类型检查

3)必须明白:编译器在编译程序时是以单个源文件为单位的(所以一定要在哪里调用在哪里声明),而且编译器工作时已经经过预处理处理了,最最重要的是编译器编译文件时是按照文件中语句的先后顺序执行的。

4)编译器从源文件的第一行开始编译,遇到函数声明时就会收到编译器的函数声明表中,然后继续向后。当遇到一个函数调用时,就在我的本文件的函数声明表中去查这个函数,看有没有原型相对应的一个函数(这个相对应的函数有且只能有一个)。如果没有或者只有部分匹配则会报错或报警告;如果发现多个则会报错或报警告(函数重复了,C语言中不允许2个函数原型完全一样,这个过程其实是在编译器遇到函数定义时完成的。所以函数可以重复声明但是不能重复定义)

3.递归函数

3.1什么是递归函数

1)递归函数就是函数中调用了自己本身这个函数的函数。

2)递归函数和循环的区别。递归不等于循环

3)递归函数解决问题的典型就是:求阶乘、求斐波那契数列

//计算一个数的阶乘

C语言16之什么是函数?_第1张图片

3.2函数的递归调用原理

1)实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一些栈内存。

2)栈内存的大小是限制递归深度的重要因素。

3.3使用递归函数的原则:收敛性、栈溢出

1)收敛性就是说:递归函数必须有一个终止递归的条件。当每次这个函数被执行时,我们判断一个条件决定是否继续递归,这个条件最终必须能够被满足。如果没有递归终止条件或者这个条件永远不能被满足,则这个递归没有收敛性,这个递归最终要失败。

2)因为递归是占用栈内存的,每次递归调用都会消耗一些栈内存。因此必须在栈内存耗尽之前递归收敛(终止),否则就会栈溢出。

3)递归函数的使用是有一定风险的,必须把握好。

4. 函数库

4.1什么是函数库?

1)函数库就是一些事先写好的函数的集合,给别人复用。

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

4.2函数库的由来

1)最开始是没有函数库,每个人写程序都要从零开始自己写。时间长了慢慢的早期的程序员就积累下来了一些有用的函数。

2)早期的程序员经常参加行业聚会,在聚会上大家互相交换各自的函数库。

3)后来程序员中的一些大神就提出把大家各自的函数库收拢在一起,然后经过校准和整理,最后形成了一份标准化的函数库,就是现在的标准的函数库,譬如说glibc。

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

1)早期的函数共享都是以源代码的形式进行的。这种方式共享是最彻底的(后来这种源码共享的方向就形成了我们现在的开源社区)。但是这种方式有它的缺点,缺点就是无法以商业化形式来发布函数库。

2)商业公司需要将自己的有用的函数库共享给被人(当然是付费的),但是又不能给客户源代码。这时候的解决方案就是以库(主要有2种:静态库和动态库)的形式来提供。

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

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

4.4函数库中库函数的使用

1)gcc中编译链接程序默认是使用动态库的,要想静态链接需要显式用-static来强制静态链接。

2)库函数的使用需要注意3点:第一,包含相应的头文件;第二,调用库函数时注意函数原型;第三,有些库函数链接时需要额外用-lxxx来指定链接;第四,如果是动态库,要注意-L指定动态库的地址。

5.字符串函数

5.1什么是字符串

字符串就是由多个字符在内存中连续分布组成的字符结构。字符串的特点是指定了开头(字符串的指针)和结尾(结尾固定为字符'\0'),而没有指定长度(长度由开头地址和结尾地址相减得到)。

5.2 为什么要讲字符串处理函数

1)函数库为什么要包含字符串处理函数?因为字符串处理的需求是客观的,所以从很早开始人们就在写很多关于字符串处理的函数,然后逐渐形成了现在的字符串处理函数库。

2)面试笔试时,常用字符串处理函数也是经常考到的点。

5.3常用字符串处理函数

1)C库中字符串处理函数包含在string.h中,这个文件在ubuntu系统中在/usr/include中

2)常见字符串处理函数及作用:

    memcpy    void *memcpy(void *dest, const void *src, size_t n);

必须保证src和dst内存域不会overlap,则使用memcpy效率高

C语言16之什么是函数?_第2张图片

    memmove    void *memmove(void *dest, const void *src, size_t n);

确定会overlap或者不确定但是有可能overlap,则使用memove比较保险

    memset    void *memset(void *s, int c, size_t n);一般使用在申请堆内存之后

C语言16之什么是函数?_第3张图片

    memcmp    int memcmp(const void *s1, const void *s2, size_t n);

    memchr    void *memchr(const void *s, int c, size_t n);

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

C语言16之什么是函数?_第4张图片

    strncpy        char *strncpy(char *dest, const char *src, size_t n);

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

    strncat        char *strncat(char *dest, const char *src, size_t n);

    strcmp        int strcmp(const char *s1, const char *s2);

C语言16之什么是函数?_第5张图片

    strncmp    int strncmp(const char *s1, const char *s2, size_t n);

    strdup        char *strdup(const char *s);

    strndup    char *strndup(const char *s, size_t n);

    strchr        char *strchr(const char *s, int c);

    strstr

    strtok

    •••••••

6. 数学库函数

6.1 math.h

1)真正的数学运算的函数定义在:/usr/include/i386-linux-gnu/bits/mathcalls.h

2)使用数学库函数的时候,只需要包含math.h即可。

6.2 计算开平方

(1)库函数: double sqrt(double x);

注意区分编译时警告/错误,和链接时的错误:

编译时警告/错误:

math.c:9:13: warning: incompatible implicit declaration of built-in function 'sqrt' [enabled by default]

double b = sqrt(a);

链接时错误:

math.c:(.text+0x1b): undefined reference to `sqrt'

collect2: error: ld returned 1 exit status

分析:这个链接错误的意思是:sqrt函数有声明(声明就在math.h中)有引用(在math.c)但是没有定义,链接器找不到函数体。sqrt本来是库函数,在编译器库中是有.a和.so链接库的(函数体在链接库中的)。

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

6.3 链接时加-lm

1)    -lm就是告诉链接器到libm中去查找用到的函数。

2)    实战中发现在高版本的gcc中,经常会出现没加-lm也可以编译链接的。

你可能感兴趣的:(C,函数)