C语言程序的编译过程:
- 预处理
- 编译
- 汇编
- 链接
一、什么是预处理
预处理就是在源文件(如.c文件)编译之前,所进行的一部分预备操作,这部分操作是由预处理程序自动来完成;当源文件在编译时,编译器会自动调用预处理程序来完成对预处理指令的解析,预处理指令解析完成才能进入下一步的编译过程。
我们为了能够方便的看到这个编译细节,我们可以使用下面命令:
gcc 源文件 -E -o 程序名[.后缀]
二、预处理的功能
1、宏定义
-
不带参数的宏定义
语法:
#define 宏名 常量数据;
预处理:此时的预处理只做数据替换,不做类型检查
注意:我们定义的宏是不会占用内存空间,还没有到编译环节,就已经被替换成了我们宏中的常量数据。
宏展开:在预编译时将宏名替换成字符串的过程称为“宏展开”。
举例:
#include <stdio.h> #define PI 3.1415926 void main() { float l,s,r,v; printf("input radius:"); scanf("%f",&r); l=2.0*PI*r; s=PI*r*r; v=4.0/3*PI*r*r*r; printf("l=%10.4f\ns=%10.4f\nv=%10.4f\n",l,s,v); }
-
带参数的宏定义
语法:
#define 宏名(参数列表) 参数表达式
面试题:
#define multi(a,b) a*b
举例:带参数的宏定义
/** * 宏定义——带参数 */ #include <stdio.h> #define MULTI(a,b) a * b int main() { int result = MULTI(7+2,3); printf("%d\n",result);// 7+(2*3) = 13 return 0; }
-
带参数的宏和参数的区别:
举例:在宏定义中引用已定义的宏
#include <stdio.h> #define R 3.0 #define PI 3.1415926 #define L 2*PI*R #define S PI*R*R void main() { printf("L=%f\nS=%f\n",L,S); }
-
宏定义的作用域
- #define 命令出现在程序中函数的外围,宏名的有效范围为定义命令之后到本源文件结束。通常, #define 命令写在文件开头,函数之间,作为文件一部分,在此文件范围内有效;
- 可以用 #undef 命令终止宏定义的作用域。
举例:宏定义的作用域
1、用 #undef 结束宏定义
2、 #undef 后再次定义(结论:不能重复定义宏)
3.宏定义的作用域
/** * 宏定义的作用域 */ #define PI 3.14 #define DAY 20 void fun() { float r = 4; float s = PI * r * r; int day = DAY; } #undef PI // 结束PI的作用范围 #define PI 3.1415926 void fun1() { float r1 = 4; float s1 = PI * r1 * r1; int day = DAY; } void main() { fun(); fun1(); }
三、文件包含
1、概念
所谓“文件包含”处理是指一个源文件可以将另外一个源文件的全部内容包含进来。这个适用于多文件开发。
2、预处理
此时的预处理,是将文件中的内容替换,文件包含指令。
3、包含方式
-
#include <xxxx.h>
系统会到标准库头文件目录(Linux下 /usr/include )查找包含的文件;
-
#include "xxxx.h"
在当前工程路径下查找,如果未找到,仍然会到标准库头文件目录查找。
举例:双文件
algorithm.h——自定义头文件,专门用于存放被外部访问的函数的声明
/** * 自定义头文件,专门用于存放被外部访问的函数的声明 */ // 数组的累加和计算 extern int sum(const int *p,int len);
algorithm.c——实现数组元素的累加计算
/** * 实现数组元素的累加计算 */ int sum(const int *p,int len) { int sum = 0; register int i = 0; for(;i < len; i++) { sum += *(p+i); } return sum; }
app.c
// #include <stdio.h> // 引入自定义的头文件 #include "algorithm.h" // 如果有n多个外部函数,难道都要一个个的使用extern进行声明? // 引入外部函数声明 // extern int sum(const int*,int); int main() { int arr[5] = {12,33,14,55,34}; int res = sum(arr,5); printf("数组累和结果是:%d\n",res); return 0; }
编译命令:
gcc algorithm.c app.c -o app // 又包含关系的c文件要一起编译
四、条件编译
1、概念
根据设定的条件选择编译的语句代码。
2、预处理
将满足条件的语句进行保留,不满足条件的语句进行删除,交给下一步编译
3、语法
1)语法一:
#ifdef 标识 --> 判断标识符定义与否
..
#else
..
#endif
2)语法二:
#ifndef 标识 --> 判断标识符定义与否
..
#else
..
#endif
3)语法三:
#if 表达式 --> 根据表达式返回的结果:0-不成立输出else,1-成立输出if
..
#else
..
#endif
举例:预处理-条件编译——判断标识符定义与否
/** * 预处理-条件编译 */ #define MINUS 1 //判断 int main(void) { #ifdef MINUS // 判断标准是表示有没有定义,不是0或-1 int a = 7 - 4; #else int a = 7 * 4; #endif printf("计算结果为:%d\n",a); return 0; }
举例:根据表达式的返回值,判断输出。
预处理-条件编译:输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写输出,或全改为小写字母输出。
/** * 预处理-条件编译:输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写输出,或全改为小写字母输 出。 */ #include <stdio.h> // 定义一个标识 #define LETTER 0 void main() { // 测试用的字母字符数组 char str[20] = "C Language"; char c; int i; i = 0; while((c=str[i])!='\0') { i++; #if LETTER if(c >='a' && c <='z') { c -= 32; } #else if(c >='A' && c <='Z') { c += 32; } #endif printf("%c",c); } printf("\n"); }
1)#define LETTER 1;
2)#define LETTER 0;
4、避免头文件重复包含的方法
语法:
#ifndef __XXXX_H
#define __XXXX_H
...
#endif
举例:自定义头文件,专门用于存放被外部访问的函数的声明
/** * 自定义头文件,专门用于存放被外部访问的函数的声明 */ #ifndef __ALGORITHM_H #define __ALGORITHM_H // 数组的累加和计算 extern int sum(const int *p,int len); #endif
五、位运算
1、什么是位运算
针对数据的二级制位进行的相关操作,位运算在嵌入式开发领域有着非常重要的应用。
2、位运算常用的运算符
符号 | 说明 | 符号 | 说明 |
---|---|---|---|
& | 按位与 | ~ | 按位取反 |
| | 按位或 | << | 按位左移 |
^ | 按位异或 | >> | 按位右移 |
注意:参与位运算的运算量只能是整型或者字符型,不能是实型。
3、位运算符的运算规则
&:按位与:
-
运算规则:对于左右操作数,只有相应二进制位数据都为1时,结果数据对应位数据为1。
-
举例:
// 代表二进制位 1 & 1 = 1 1 & 0 = 0 0 & 1 = 0 0 & 0 = 0
-
作用:
- 获取某二进制位数据;
- 将指定二进制位数据清零;
- 保留指定位。
|:按位或:
-
运算规则:对于左右操作数,只要相应二进制位数据有一个为1,结果数据对应位数据为1。
-
举例:
// 代表二进制位 1 | 1 = 1 1 | 0 = 1 0 | 1 = 1 0 | 0 = 0
-
作用:
- 置位某二进制位数据;
^:按位异或:
-
运算规则:对于左右操作数,只要相应二进制位数据相同,结果数据对应位数据为0,不同为1。(相同为0,不同为1)
-
举例:
// 代表二进制位 1 ^ 1 = 0 1 ^ 0 = 1 0 ^ 1 = 1 0 ^ 0 = 0
-
作用:
-
翻转;
-
值交换
-
面试题:
~:按位取反:
-
运算规则:原操作数据将相应二进制位数据:0变1,1变0
按位左移:
-
运算规则:原操作数所有的二进制位数向左移动指定位;移出的数据舍弃,右边用0补全
注意:
1. 如果移出的高位都是0,我们可以这样理解:a ,可以看做:a * 2^n
2. 如果移出的高位包含1,我们是不能使用以上计算方式的。
按位右移:
-
运算规则:原操作数所有的二进制位数据向右移动指定位,移出的数据舍弃
-
如果操作数是无符号数,左边用0补全:
-
如果操作数是有符号数,左边用什么去补全,取决于计算机系统:
-
逻辑右移:用0补全;
-
算数右移:用1补全。
-
-
大部分情况下,系统是遵循“算数右移”的。
4、位运算赋值符
- 运算符:&=, |=, >>=,<<=,^=
- 举例:
a &= b 相当于 a = a & b a <<=2 相当于 a = a << 2
5、不同长度数据进行位运算
如果两个数据长度不同(例如long型和short型),进行位运算时(如a & b,而a为long型,b为short型), 系统会将二者按右端对齐。