C 碎片六 函数
一、程序编译执行过程
程序的编译执行过程分为4个阶段:预处理阶段、编译阶段、汇编阶段、连接阶段
1. 预处理阶段:预处理器(cpp)处理以头文件、宏、条件编译(字符#开头)等内容的替换。此阶段不进行语法检查,只进行简单的替换工作,修改原始的C程序,得到另一个C程序,通常以.i作为文件扩展名,产生的.i文件会变大(PS:增加了替换后的内容)。
gcc -o hello.i -E hello.c
2. 编译阶段:编译器(ccl)进行词法分析和语法分析之后,将文件hello.i翻译成文件hello.s。它包含一个汇编语言程序。汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言为不同编译器提供了通用的输出语言。
gcc -o hello.s -S hello.i
3. 汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,并将结果保存在目标文件hello.o中。hello.o是一种二进制文件,它的字节编码是机器语言指令而不是字符。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件,目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成,通常一个目标文件中至少有两个段:
3.1 代码段:顾名思义就是存放程序代码的段,主要存放一系列的指令。
3.2 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
gcc -o hello.o -c hello.s
4. 连接阶段:连接器(ld)将有关的目标文件彼此连接起来,因为程序源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等),也可能调用了某个库文件中的函数,都需要经链接程序的处理方能得以解决。也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。用下面的gcc命令后会生成一个test的可执行文件,执行的时候直接 ./test 即可。
gcc -o test hello.o
总结:
-E Preprocess only; do not compile, assemble or link
-S Compile only; do not assemble or link
-c Compile and assemble, but do not link
-o Place the output into
二、内存布局
在内存中,布局从低地址到高地址,依次是只读区(代码区),读写区(数据区),堆区,栈区,如图左上。地址增长方式如图右上。其中,堆区,栈区也叫动态区域;只读区,读写区也叫静态区域。
只读区:存放程序编译后的代码,和只读变量。特点:只读
读写区:存放全局变量,静态变量和字符串常量。特点:可读写
堆区:调用malloc函数会在堆区开辟空间。特点:主动去释放free
栈区:普通函数调用,压栈/出栈会在栈区开辟/释放空间。特点:先进后出FILO
三、变量的生命周期及作用域
参考上图:
1. C语言中的每个变量有两个属性:数据类型(整形、浮点型、字符型),还有数据存储类别,分别为自动的(auto),静态的(static),寄存器的(register)和外部的(extern),下面逐一说明:
auto类型:函数中的局部变量不加特殊声明都是auto变量,但是关键字"auto"可以被省略。这些变量在函数被调用时分配存储方式,函数调用结束后这些存储空间就被释放了
static类型:被static声明的变量为静态(局部/全局)变量,静态局部变量函数调用结束后,这些变量不消失,而保留当前数据,下一次调用时变量的值为上一次调用完成后的值
register类型:Register修饰符暗示编译程序相应的变量将将被频繁使用,如果可能的话,应将其保存在CPU的寄存器中,以指加快其存取速度。但是,使用register修饰符有几点限制:
(1)只有局部自动变量和形式参数可以作为寄存器变量,其他(如全局变量)不行。
(2)一个计算机系统中的寄存器数目是有限的,不能定义任意多个寄存器变量。
(3)局部静态变量不能定义为寄存器变量。
其实这个变量已经过时,因为现在的计算机处理速度够快,所以很少使用
extern类型:它不是一个定义,而是一个声明,他表示这个变量或者函数的定义在别的文件中。extern使用时,告诉编译器去其他文件找对应变量。
在C语言中,只有extern,static可以修饰函数。函数被默认定义为extern;static函数只能被本文件中的函数调用,而不能被同一程序其它文件中的函数调用。
2. 下面是 全局变量,静态全局变量,静态局部变量,局部变量 的比较,作用域全局变量 > 静态全局变量 > 静态局部变量 > 局部变量
局部变量:只要在{}内,包括代码块、函数体内,声明的变量就是一个局部变量,如果不给初始化默认是随机的数
(1)作用域(在代码中的使用范围):从包含变量声明的{}内开始到这个{}结束
(2)内存位置:在所在函数的函数栈空间中
(3)生命周期:从声明开始到当前函数栈释放
全局变量:如果不给初始化那么默认是0
(1)作用域:在整个程序整个工程,所有函数所有文件都可以使用,共享这个变量空间
(2)内存位置:数据段(跟栈段没有关系,内存中的堆段,栈段,代码段,数据段,都是相互独立的,数据段不会伴随函数栈的释放而释放)
(3)生明周期:编译代码的时候大小就确定了,程序一旦开始全局变量的空间就会创建,程序结束那么数据段全局变量空间才会释放
静态变量:static修饰的变量(在编译的时候就已经执行了,在函数运行时就不会执行了)
静态局部变量:在{}中用static修饰的变量
(1)作用域:在包含声明静态变量的大括号内
(2)内存:数据段
(3)生明周期:编译的时候就确定大小了,程序运行开始创建空间,程序结束空间释放
静态全局变量:在函数外用static修饰的变量
(1)作用域:在当前声明静态全局的文件内用
(2)内存:数据段
(3)生明周期:编译的时候就确定大小了,程序运行开始创建空间,程序结束空间释放
四、函数初步
#include <stdio.h> //自定义函数add int add(int a, int b) { return a+b; } //自定义函数printStar void printStar(void) { printf("****\n"); return; } //main函数 int main(int argc, const char * argv[]) { //函数只有调用了才会执行里面的代码 //调用函数了才会进行压栈push操作 int ret = add(3, 5); printf("ret:%d\n",ret); printStar();
//函数执行完会进行pop出栈操作 return 0; }
分析:上述程序代码的执行过程:
1. 先编写代码,编写完之后进行编译,编译会产生一个可执行文件(可执行文件:就是生成一个二进制文件),这时代码源码文件(.c)和可执行文件会放在硬盘上
2. 执行/运行可执行文件(二进制文件),cpu首先会把这个二进制文件的内容拷贝到内存中的代码段;然后cpu开始执行代码段中这个二进制文件的内容
(1)cpu 会从二进制文件中的main函数标号开始,调用main函数(这时会在栈段压一个main栈)执行main函数中的代码(从上至下)
(2)先执行第一句代码,遇到了int ret = add(3,5);先调用add(3,5) (这时会压/push一个add函数栈)执行add里面的代码,执行中遇到return函数返回到调用的地方(add函数栈就会出栈/pop,栈会释放),会返回值给ret空间
(3)接着执行printf函数(压printf栈),执行完之后返回调用的地方(printf 出栈)
(4)接着执行下面的printStar()(压一个printStar栈),printStar执行中遇到了return 这时printStar函数返回到调用的地方(printStar栈出栈 栈释放)
(5)最后main中遇到了return 那么main函数返回 (main栈出栈 释放)整个程序结束退出
1. 什么是函数
(1)函数是一个可以实现一个具体功能的代码块
(1)有名的代码块
2. 函数的分类
(1)库函数、printf scanf pow abs
(1)自定义函数 自己实现的
3. 函数定义
函数声明格式:返回值类型 函数名(参数);
函数调用格式:函数名(参数)
函数三要素:返回值 函数名 参数
五、函数作用
1. 函数的作用
(1)函数使我们的程序清晰明白
(2)为开发人员提供解决问题的方法:细化
(3)一次定义,处处使用,利用以有的代码
(4)抽象出公共的部分,隔离开易变部分
2. 函数用法
(1)使用之前必须先定义
(2)通过函数调用来使用,类似上下级管理形式
(3)调用时指定函数名字和所需要的信息(参数)
(4)调用完成后向老板报告工作,递交报告(返回值)
六、自定义函数
1. 什么情况下自定义函数
(1)需要一个功能相对独立的子模块
(2)一段代码多次使用
2. 如何自定义函数
(1)明确函数功能,起一个有意义的函数名(标识符)
(2)参数和返回值类型:考虑清楚,需要几个参数;是否需要返回值,什么类型?返回值最多一个
(3)声明函数原型,建议放在头文件中
(4)定义函数体内容
七、递归函数
自己调用自己的函数就是递归函数,递归函数一般解决数学推理问题
递归函数调用过程如图
汉诺塔问题:有三根柱子 A B C . A柱子上有N个盘子,要求把这N个盘子从A可以借助柱子B移动到柱子C上
1. 这个N个盘子大小不同,必须是小盘子在大盘子上面
2. 每次只能移动一个盘子