目录
基本知识框架
课堂笔记
在ANSI C标准的实现中,存在两种环境:
- 编译环境:用于将源代码转换为可执行代码的环境
- 运行环境:用于实际执行代码的环境
运行环境
C语言程序运行基本示意图
程序载入内存
在有操作系统的环境中:这个步骤一般由操作系统去执行
在无操作系统的环境中:这个步骤需要手动去执行,但也可以是通过将可执行代码置入只读内存的形式来完成
执行主函数
执行main函数
启用运行时堆栈,静态变量区初始化
对于函数参数,返回值,局部变量,在内存中使用栈来保存
对于内存操作函数(malloc,free,calloc,realloc等)申请的内存,使用堆来保存
对于全局变量、静态变量和常量字符串一般存储在静态内存中
其他变量都存放在对应数据段中
结束程序运行
终止main函数运行,既可能是正常终止,也可能是错误终止
编译环境
C语言程序的基本编译流程
预处理
预处理的目的主要是:
- 处理预定义符号
- 处理伪指令
- 处理特殊符号
- 去除注释
预定义符号
预定义符号指的是C语言内置的,代表着特殊意义的符号包括:
__FILE__ // 当前进行编译的源文件
__LINE__ // 当前行号
__DATE__ // 当前日期
__TIME__ // 当前时间
__STDC__ // 当前编译器是否遵循ANSI C标准,如遵循,则为1,否则为未定义
伪指令
主要指以’#'开头的指令,如#define,#include等
宏定义
宏定义标识符
#define MAX_NUM 1000
功能:在预处理时,会用后者直接去替换前者
注意:
- 可以大大增加程序的可读性。例如:
- 直接使用立即数会使得程序可读性变差,将其宏定义为便于理解的单词
- 将过长的变量宏定义为简单的单词,便于理解
是否要在宏定义后加 ;
一般来说不建议加,容易出现重复’;',从而引发一些逻辑问题
宏定义功能
#define MAX(a,b) ((a)>(b)?(a):(b))
功能:可以根据需求,将要实现的功能定义为宏
注意:
- 使用宏定义时,有约定俗成的习惯,宏名全部大写
可以有效的将宏定义与其他功能区分开
- 可以使用
#undef
取消宏定义 - 使用宏定义时,由于参数是直接无条件替换的,所以经常使用括号进行运算顺序以及参数完全隔离,以免出现意料之外的情况
例如:
#define MUX(a,b) a+b
MUX(5+6,6+7); //展开后就是5+6*6+7
- 宏定义可以嵌套使用,但不能出现递归
嵌套使用宏定义时,要遵循下列的展开规则:
- 宏定义展开时,要先检查参数是否包含宏定义
- 如果参数包含宏定义,先展开参数的宏定义,再将展开后的参数带入到原宏定义中
- 重复上述步骤,直至参数不可以再展开
- 宏定义中最好不要有带副作用的参数,例如对宏定义参数进行++运算
例如:
#define MAX(a,b) ((a)>(b)?(a):(b))
MAX(x++,y++). //展开后就是((x++)>(y++)?(x++):(y++))
- 宏定义如果太长,可以用反斜杠’\'续行
例如:
#define PRINT_CHECK_NUM(a,max) printf("This Num is %d,%s than %d" \
, a, ((a)>(max))?("Bigger"):("Smalle \
r"), max) \
- 字符串作为宏定义的参数
例如:
#define PRINT(FORMAT,VALUE) printf("String value is "FORMAT"\n",VALUE)
PRINT("%d",12) //展开后就是printf("String value is " "%d" "\n",12)
注意:字符串是可以自动连接的,所以输出的结果是String value is 12
- 宏定义功能通常能起到和函数类似的作用,我们可以对比一下宏定义功能和函数的两者的优缺点
属性 | 宏定义 | 函数 |
---|---|---|
代码长度 | 宏定义在使用时,会将宏定义出现处的代码替换。这样的替换增加了代码量 | 函数在使用时,仅需定义一次,之后的调用都不增加代码量 |
运行速度 | 仅仅是代码的简单替换,速度快 | 存在函数调用开销,更慢些 |
运算优先级 | 由于宏定义直接替换的特性,使用时要经常使用括号来保证运算优先级,结果更具不可预料性 | 函数传参时,即完成参数的运算,结果更容易预料 |
带副作用的参数 | 由于宏定义直接替换的特性,运算结果容易受参数影响 | 函数传参时,即完成参数的运算,结果更容易预料 |
参数类型 | 宏定义的参数与参数类型无关,不进行参数类型检查,使用灵活但也很危险 | 函数调用时会对参数进行类型检查,使用受限但是更加安全 |
调试 | 宏定义不可以调试 | 函数可以调试 |
递归 | 宏定义不能使用递归 | 函数可以使用递归 |
条件编译
某段代码,当满足条件的时候才进行编译
例如调试类代码。在大多是情况下我们不需要编译,只有在进行调试时需要编译。使用条件编译可以应对这类情况
单分支条件编译
#if (判断条件)
// 代码段1
#endif
功能:判断条件为真时,编译代码段1
多分支条件编译
#if (判断条件1)
// 代码段1
#elif (判断条件2)
// 代码段2
#else
// 代码段3
#endif
功能:判断条件1,条件2真假后,根据判断结果选择编译代码段
判断是否被定义
// 如果定义了MAX
#if defined(MAX)
#ifdef MAX
// 如果没定义MAX
#if !defined(MAX)
#ifndef MAX
条件编译的用法与分支语句中的if语句的用法类似
头文件包含
头文件包含可以使头文件中的内容替换到头文件包含处,从而实现引用头文件中定义的函数,变量等等
#include “private.h” // 用于包含个人定义的头文件
#include <stdio.h> // 用于包含C语言标准库的头文件
头文件查找的策略
- 对于"个人头文件名"这样的包含方式,会先在当前源文件根目录下寻找此头文件,找不到的话再到C语言标准库中查找
- 对于<C语言标准库头文件>这样的包含方式,直接到C语言标准库中进行查找
头文件重复包含的问题
当一个头文件被包含很多次时,也会被编译器相应的编译很多次,造成很多不必要的冗余。为了解决头文件被多次包含的问题,我们采用条件编译
头文件内容的一般格式为
#ifndef __TEST_H__
#define __TEST_H__
// 头文件内容
#endif
特殊符号
- 使用#将宏定义参数转换成纯字符串
例如:
#define PRINT(FORMAT,VALUE) printf("#VALUE is "FORMAT"\n",VALUE)
PRINT("%d",1+1) //展开后就是printf("1+1 is " "%d" "\n",1+1)
注意:输出的结果是1+1 is 2
- 使用##进行字符串拼接
例如:
#define ADD(code, num) sum##code+=num
int sum5 = 5;
ADD(5,10); //展开后就是sum5+=10;
注意:这里的sum##code拼接后的到的标识符sum5一定要是已经定义好的,否则就是未定义的
去除注释
C语言中的注释包括
- //开头的注释
// 我是注释
- 被/* */包括的注释
/* 我是注释 */
编译
在这一步,编译器会将预处理完毕的代码转换成汇编代码
要进行的工作主要包括:
- 词法分析
- 语法分析
- 语义分析
- 性能优化
这些都是很复杂的过程,具体可以去看《编译原理》这本书
汇编
在这一步,编译器会将汇编代码转换为机器码
链接
链接就是建立目标文件之间联系,最终打包得到可执行文件的过程
链接的意义
因为对于编译器而言,对某个源文件进行编译,得到的是对应目标文件。如果当前目标文件有调用库或者其他其他目标文件的话,那么是要建立它们之间联系的,链接就是建立这种联系的过程,这个过程是通过链接器完成的
链接器是一个独立的程序,可以将库文件或者多个目标文件打包到一起生成可执行文件
链接还分为
- 静态链接:链接器直接将要调用的函数或者过程打包到可执行文件中,成为可执行文件的一部分。静态链接不依赖于外部文件,使得程序可以独立运行
- 动态链接:链接器仅将要调用文件的信息或定位打包到可执行文件中,当文件执行时,再去调用相应的库。动态链接依赖外部文件,在使用时要确保外部文件存在且正确
基本知识框架Xmind文件下载
链接: 资源下载