07函数

基础笔记:07函数

1.函数定义、函数调用、函数原型

(1)定义:函数(Function)是C中模块化编程的最小单位;

  • 一个C程序由一个或多个源程序文件组成
  • 一个源程序文件由一个或多个函数组成

(2)函数的地位关系

  • 函数是平等的

  • main()稍微特殊一点,C程序的执行从main函数开始。调用其他函数后流程回到main函数,在main函数中结束整个程序的运行

(3)函数的分类

标准库函数

  • ANSI/ISOC定义的标准库函数

    • 符合标准的C语言编译器必须提供这些函数

    • 函数的行为也要符合ANSI/ISOC定义

  • 第三方库函数

    • 其他厂商自行开发的C语言函数库

    • 不在标准范围内,能扩充C语言的功能(图形、数据库等)

自定义函数

  • 用户自己定义的函数

    • 包装后,也可成为函数库,供别人使用

(4)程序设计中函数的功能

  • 不局限于计算,还有判断推理

(5)函数定义方法

C/C++main函数返回值以及return 0的作用

main函数的返回值

main函数的返回值用于说明程序的退出状态。如果返回0,则代表程序正常退出。返回其它数字的含义则由系统决定。通常,返回非零代表程序异常退出。

素养阅读

2.函数的参数传递与返回值

(1)函数的参数传递和返回值

  • 调用者通过函数名调用函数

  • 有返回值时,可放到一个赋值表达式语句中

  • 还可放到一个函数调用语句中,作为另一个函数的参数

(2)函数调用

  • 函数定义时的参数,形式参数(Parameter),简称形参

  • 函数调用时的参数,实际参数(Argument),简称实参

过程:每次执行函数调用时

  • 现场保护并为函数的内部变量(包括形参)分配内存

  • 把实参值复制给形参,单向传值(实参–>形参)

  • 实参与形参数目一致,类型匹配(否则类型自动转换)

  • 现场保护

执行函数内语句

  • 当执行到return语句或}时,从函数退出

从函数退出时

  • 根据函数调用栈中保存的返回地址,返回到当次函数调用的地方

  • 程序控制权交给调用者,返回值作为函数调用表达式的值

  • 收回分配给函数内所有变量(包括形参)的内存

(3)函数原型

  • 调用函数前先声明返回值类型、函数名和形参类型

  • 有助于编译器对函数参数类型的匹配检查

区别:

(4)函数封装(Encapsulation)

  • 外界对函数的影响仅限于入口参数

  • 函数对外界的影响仅限于一个返回值和数组、指针形参

如何增强程序的健壮性,使函数具有遇到不正确使用或非法数据输入时避免出错的能力?答:在函数的入口处,检查输入参数的合法性,同时,主程序增加对函数返回值的检验

(5)函数复用:就是用好几次

(6)断言(assert)

考虑使用断言的几种情况

  • 检查程序中的各种假设的正确性

  • 证实或测试某种不可能发生的状况确实不会发生

仅用于调试程序,不能作为程序的功能

  • Debug版有效

  • Release版失效

    Debug和Release的区别
    Debug:调试版本,包含调试信息,所以容量比Release大很多,并且不进行任何优化(优化会使调试复杂化,因为源代码和生成的指令间关系会更复杂),便于程序员调试。Debug模式下生成两个文件,除了.exe或.dll文件外,还有一个.pdb文件,该文件记录了代码中断点等调试信息
    Release:发布版本,不对源代码进行调试,编译时对应用程序的速度进行优化,使得程序在代码大小和运行速度上都是最优的。(调试信息可在单独的PDB文件中生成)。Release模式下生成一个文件.exe或.dll文件

    Debug 版本:
    /MDd /MLd 或 /MTd 使用 Debug runtime library(调试版本的运行时刻函数库)
    /Od 关闭优化开关
    /D “_DEBUG” 相当于 #define _DEBUG,打开编译调试代码开关(主要针对assert函数)
    /ZI 创建 Edit and continue(编辑继续)数据库,这样在调试过程中如果修改了源代码不需重新编译
    /GZ 可以帮助捕获内存错误
    /Gm 打开最小化重链接开关,减少链接时间

    Release 版本:
    /MD /ML 或 /MT 使用发布版本的运行时刻函数库
    /O1 或 /O2 优化开关,使程序最小或最快
    /D “NDEBUG” 关闭条件编译调试代码开关(即不编译assert函数)
    /GF 合并重复的字符串,并将字符串常量放到只读内存,防止被修改

3.递归函数

(1)函数的嵌套调用

  • C语言规定函数不能嵌套定义

    • 函数是相互平行的,该限制可以使编译器简单化
  • 但可以嵌套调用

    • 在调用一个函数的过程中又调用另一个函数
  • 函数直接或间接调用自己,称为递归调用(RecursiveCall),这样的函数,称为递归函数(Recursive Function)

(2)递归函数

  • 字典是递归定义的典型实例

  • 递归过程

  • 函数调用栈







(5)递归方法编写程序的优点

  • 符合人的思维习惯,逼近数学公式的表示

  • 从编程角度来看,简洁、直观、精炼,易编、易懂、逻辑清楚,结构清晰、可读性好

(6)递归方法编写程序的缺点

  • 增加了函数调用开销,每次调用都需参数传递、现场保护等,为函数使用的参数、局部变量等额外分配存储空间

  • 耗费更多的时间和栈空间,时空效率低

  • 重复计算多

4.变量的作用域与存储类型
  • 变量的作用域 (Scope):指在源程序中定义变量的位置及其能被读写访问的

    范围,分为:

    • 局部变量(Local Variable):在语句块内(函数、复合语句)定义的变量

    • 有效范围仅为该语句块(函数,复合语句)

      仅能由语句块内的语句访问,退出语句块时释放内存,不再有效

    • 全局变量(Global Variable ):在所有函数之外定义的变量

      有效范围是从定义变量的位置开始,到本程序结束

  • 假如变量同名…

    • 并列语句块内各自定义(不同作用域)的变量同名互不干扰

    • 形参和实参的作用域不同,互不干扰(改变形参对实参不影响)

    • 局部变量与全局变量同名:局部变量屏蔽全局变量

      只要作用域不同,新的声明屏蔽旧的声明

  • 编译器如何区分不同作用域的同名变量?

    • 一个变量名能代表两个不同的值,仅当它能代表两个不同的内存地址

    • 编译器通过将同名变量映射到不同的内存地址来实现作用域的划分局部变量和全局变量被分配的内存区域不同,因而内存地址也不同

    • 形参和实参的作用域、内存地址不同,所以形参值的改变不会影响实参

可划分为四大内存分区:堆、栈、静态存储区和代码区。

堆区:
由程序猿手动申请,手动释放,若不手动释放,程序结束后由系统回收,生命周期是整个程序运行期间。使用malloc或者new进行堆的申请,堆的总大小为机器的虚拟内存的大小。
说明:new操作符本质上是使用了malloc进行内存的申请,new和malloc的区别如下:
(1)malloc是C语言中的函数,而new是C++中的操作符。
(2)malloc申请之后返回的类型是void*,而new返回的指针带有类型。
(3)malloc只负责内存的分配而不会调用类的构造函数,而new不仅会分配内存,而且会自动调用类的构造函数。

栈区:
由系统进行内存的管理。主要存放函数的参数以及局部变量。在函数完成执行,系统自行释放栈区内存,不需要用户管理。整个程序的栈区的大小可以在编译器中由用户自行设定,VS中默认的栈区大小为1M,可通过VS手动更改栈的大小。64bits的Linux默认栈大小为10MB,可通过ulimit -s临时修改。

静态存储区:
静态存储区内的变量在程序编译阶段已经分配好内存空间并初始化。这块内存在程序的整个运行期间都存在,它主要存放静态变量、全局变量和常量。
注意:
(1)这里不区分初始化和未初始化的数据区,是因为静态存储区内的变量若不显示初始化,则编译器会自动以默认的方式进行初始化,即静态存储区内不存在未初始化的变量。
(2)静态存储区内的常量分为常变量和字符串常量,一经初始化,不可修改。静态存储内的常变量是全局变量,与局部常变量不同,区别在于局部常变量存放于栈,实际可间接通过指针或者引用进行修改,而全局常变量存放于静态常量区则不可以间接修改。
(3)字符串常量存储在静态存储区的常量区,字符串常量的名称即为它本身,属于常变量。
(4)数据区的具体划分,有利于我们对于变量类型的理解。不同类型的变量存放的区域不同。后面将以实例代码说明这四种数据区中具体对应的变量。

代码区:
存放程序体的二进制代码。比如我们写的函数,都是在代码区的。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 0;//静态全局变量区
char *p1; //编译器默认初始化为NULL
void main()
{
int b; //栈
char s[] = "abc";//栈
char *p2 = "123456";//123456在字符串常量区,p2在栈上
static int c =0; //c在静态变量区,0为文字常量,在代码区
const int d=0; //栈
static const int d;//静态常量区
p1 = (char *)malloc(10);//分配得来得10字节在堆区。
strcpy(p1, "123456"); //123456放在字符变量想串常量区,编译器可能会将它与p2所指向的"123456"优化成一个地方
}

以上所有代码,编译成二进制后存放于代码区,文字常量存放于代码区,是不可寻址的。

  • 变量的存储类型

    存储类型 数据类型 变量名;

    • C存储类型关键字:auto(自动变量)、static(静态变量)(包括:静态局部变量,静态外部变量)、extern(外部变量)(编译器并不对其分配内存,只是表明“我知道了”)、register(寄存器变量)
  • 全局变量(跟下面几类不是并列关系)

先看看定义:定义在函数外面的变量,就叫全局变量。

包括:普通全局变量 静态全局变量  跨文件引用全局变量(extern)

  • 关于自动变量的解释

又叫动态局部变量(缺省类型),存在栈内。进入语句块时自动申请内存,退出时自动释放内存,离开函数,值就消失

  • 关于外部变量的解释

全局变量在整个程序中起作用,如果程序包含多个程序文件模块,可以通过外部声明,使得全局变量的作用范围扩展到其他模块,也可以通过定义静态全局变量,使其作用范围仅限制在这个模块

全局变量只能被定义一次,如果其他模块要使用该全局变量,需要通过外部变量的声明。

外部变量声明格式: extern 变量名表;

只起说明作用,不分配存储单元,对应存储单元在全局变量定义时分配。

main.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int count ;
extern void write_extern();

int main()
{
count = 5;
write_extern();
}

support.c

1
2
3
4
5
6
7
8
#include <stdio.h>

extern int count;

void write_extern(void)
{
printf("count is %d\n", count);
}
  • 关于静态局部变量的解释
  1. 静态局部变量在静态存储区内分配存储单元,在程序整个运行期间都不释放。而普通局部变量属于动态存储类别,存储在动态存储区空间,函数调用结束后即释放;
  2. 静态局部变量只初始化一次,以后每次调用函数时保留上次函数调用结束时的值。而自动变量每调用一次函数重新执行一次赋值语句;
  3. 静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为0(对数值型变量)或空字符(对字符型变量)。而对自动变量来说,如果不赋初值,则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。
  4. 静态局部变量始终驻留在全局数据区,直到程序运行结束。但其作用域仍为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。
  • 关于静态全局变量的解释

当程序只有一个文件模块,其与一般全局变量作用相同; 当程序有多个模块时,C语言静态全局变量的作用范围局限于该模块。

1)全局变量是不显式用static修饰的全局变量,但全局变量默认是动态的,作用域是整个工程,在一个文件内定义的全局变量,在另一个文件中,通过extern 全局变量名的声明,就可以使用全局变量。

2)全局静态变量是显式用static修饰的全局变量,作用域是声明此变量所在的文件,其他的文件即使用extern声明也不能使用。

静态全局变量有以下特点:

1.该变量在全局数据区分配内存;

2.未经初始化的静态全局变量会被程序自动初始化为0

3.静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的;

4.静态变量都在全局数据区分配内存,包括后面将要提到的静态局部变量。

  • 局部变量初始化时机问题:

    首先,静态局部变量和全局变量一样,数据都存放在全局区域,所以在主程序之前,编译器已经为其分配好了内存,但在C和C++中静态局部变量的初始化节点又有点不太一样。

    在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。

    而在C中,初始化时在执行相关代码时才会进行初始化,~~*主要是由于C引入对象后,要进行初始化必须执行相应构造函数和析构函数,在构造函数或析构函数中经常会需要进行某些程序中需要进行的特定操作,并非简单地分配内存。所以C标准定为全局或静态对象是有首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。*~~ **所以在C中是可以使用变量对静态局部变量进行初始化的。**

  • 后面再来谈谈另一个问题,假如我们在一个循环中,定义了一个静态局部变量并进行初始化,循环过程中,编译器怎么知道当前的静态局部变量已经初始化过了呢?

    • 这个问题C和C++的处理方式也是不一样的。

    • C中编译器会直接跳过这一个语句,因为在编译的时候已经对静态局部变量进行过分配空间并初始化,所以代码执行过程中根本不需要再次执行。

    • 而在C++中,编译器会在编译器分配内存后,在全局区域(当前静态局部变量的地址)附近同样分配一块空间,进行记录变量是否已经进行过初始化。之所以说附近是根据编译器不同,处理方式不同导致的。

  • 关于寄存器变量的解释

在程序运行时,根据需要到内存中相应的存储单元中调用,如果一个变量在程序中频繁使用,例如循环变量,那么,系统就必须多次访问内存中的该单元,影响程序的执行效率。

因此,C语言\C++语言还定义了一种变量,不是保存在内存上,而是直接存储在CPU中的寄存器中,这种变量称为寄存器变量。

1
register int i=100;
  1. C编译程序会自动地将寄存器变量变为自动变量
  • 对于VC编译器会自动优化,即使没有声明寄存器变量,VC也会自动优化。

  • 对于GCC编译器就不会自动优化。

  1. 由于受硬件寄存器长度的限制,所以寄存器变量只能是char、int或指针型。

  2. 寄存器说明符只能用于说明函数中的变量和函数中的形参,因此不允许将外部变量或静态变量说明为"register"

  3. register变量使用的是硬件CPU中的寄存器,寄存器变量无地址,所以不能使用取地址运算符"&"求寄存器变量的地址。

5.模块化程序设计方法:自顶向下