飒飒
- 3. 编译及预处理
- define定义宏常量可以出现在代码的任何地方。
- define从本行开始,之后的代码都可以直接使用这个宏常量。
- define定义的宏常量本质为字面量
- define表达式有函数调用的假象,却不是函数(只是替换)。
- define表达式可以比函数更强大(函数数组参数会退化成一个指针)。
- define表达式比函数更容易出错(不要再宏参数中写表达式)。
- define f (x) ((x)-1)代表什么意思?
- include的本质
- include的本质是将已经存在的文件内容嵌入到当前文件中
- include的间接包含同样会产生嵌入文件内容的操作,若间接包含同一个头文件会产生编译错误
- if…#else…#endif被预编译器处理;而if…else语句被编译器处理,必然被编译进目标代码
- error用于生成一个编译错误消息,并停止编译。
- error编译指示字用于自定义程序员特有的编译错误消息。类似的,#warning用于生成编译警告,但不会停止编译。(\”字符串\”)
- error是一种预编译器指示字,可用于提示编译条件是否满足
- line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号。
- line编译指示字的本质是重定义__LINE__和__FILE__
- pragma简介
- pragma是编译器指示字,用于指示编译器完成一些特定的动作
- pragma所定义的很多指示字是编译器特有的
- pragma在不同的编译器间是不可移植的
- pragma message
- pragma once 用于保证头文件只被编译一次
- pragma once效率上比#ifdef..#define…#endif高,但与编译器相关的,不一定被支持,可以如下使用,保证可靠与实用
- pragma pack(能够改变编译器的默认对齐方式)
- 运算符
- 运算符用于在预处理期将宏参数转换为字符串,因此只在宏定义中有效
- 运算符在宏中的妙用
3. 编译及预处理
(1). 编译过程
-
预编译
-
处理所有的注释,以空格代替
-
将所有的#define删除,并且展开所有的宏定义
-
处理条件编译指令#if,#ifdef,#elif,#else,#endif
-
处理#include,展开被包含的文件
-
保留编译器所需要使用的#pragma指令
示例:gcc –E file.c –o file.i
-
-
编译
-
对与处理文件进行词法分析,语法分析和语义分析
-
词法分析:分析关键字,标识符,立即数等是否合法
-
语法分析:分析表达式是否遵循语法规则
-
语义分析:在语法分析的基础上进一步分析表达式是否合法
-
-
分析结束后进行代码优化生成相应的汇编代码文件
示例:gcc –S file.i –o file.s
-
-
汇编
-
汇编器将汇编代码转变为机器的可以执行指令
-
每条汇编语句几乎都对应一条机器指令
示例:gcc –c file.s –o file.o
-
(2). 链接过程
链接器的主要作用是把各个模块之间相互作用的部分处理好,使得各个模块之间能够正确的衔接
-
静态链接
-
由连接器在链接时蒋库的内容直接加入到可执行程序中
-
Linux下静态库的创建和使用
-
编译静态库源码: gcc –c lib.c –o lib.o
-
生成静态库文件: ar -q lib.a lib.o
-
使用静态库编译: gcc main.c lib.a –o main.out
-
//20-1.c #include <stdio.h> extern char* name(); extern int add(int a, int b); int main() { printf("Name: %s\n", name()); printf("Result: %d\n", add(2, 3)); return 0; }//slib.c char* name() { return "Static Lib"; } int add(int a, int b) { return a + b; } -
-
动态链接
-
可执行程序在运行时才动态加载库进行链接
-
库的内容不会进入可执行程序当中
-
Linux下动态库的创建和使用
-
编译动态库源码: gcc –shared dlib.c –o dlib.so
-
使用动态库编译: gcc main.c –ldl –o main.out
-
关键系统调用
dlopen: 打开动态库文件
dlsym: 查找动态库中的函数并返回调用地址、
dlclose: 关闭动态库文件
-
//20-2.c #include <stdio.h> #include <dlfcn.h> int main() { void* pdlib = dlopen("./dlib.so", RTLD_LAZY); char* (*pname)(); int (*padd)(int, int); if( pdlib != NULL ) { pname = dlsym(pdlib, "name"); padd = dlsym(pdlib, "add"); if( (pname != NULL) && (padd != NULL) ) { printf("Name: %s\n", pname()); printf("Result: %d\n", padd(2, 3)); } dlclose(pdlib); } else { printf("Cannot open lib ...\n"); } return 0; }//dlib.c char* name() { return "Dynamic Lib"; } int add(int a, int b) { return a + b; } -
(3). 宏的定义和使用分析
-
定义宏常量
-
define定义宏常量可以出现在代码的任何地方。
-
define从本行开始,之后的代码都可以直接使用这个宏常量。
-
define定义的宏常量本质为字面量
-
-
定义宏表达式
-
define表达式有函数调用的假象,却不是函数(只是替换)。
-
define表达式可以比函数更强大(函数数组参数会退化成一个指针)。
-
define表达式比函数更容易出错(不要再宏参数中写表达式)。
-
-
宏代码块的定义(宏强于函数的优势)
-
宏表达式在预编译期被处理,编译器不知道宏表达式的存在。
-
宏表达式用“实参”完全代替形参,不进行任何运算。
-
宏表达式没有任何的“调用”开销。
-
宏表达式中不可能出现递归定义。
-
宏参数可以是任何C语言实体
-
宏编写的_MIN_参数类型可以是int, float等等
-
宏的参数可以是类型名
-
#include <stdio.h> #include <malloc.h> #define MALLOC(type, x) (type*)malloc(sizeof(type)*x) #define FREE(p) (free(p), p=NULL) #define LOG_INT(i) printf("%s = %d\n", #i, i) #define LOG_CHAR(c) printf("%s = %c\n", #c, c) #define LOG_FLOAT(f) printf("%s = %f\n", #f, f) #define LOG_POINTER(p) printf("%s = %p\n", #p, p) #define LOG_STRING(s) printf("%s = %s\n", #s, s) int main() { int* pi = MALLOC(int, 5); char* str = "test demo"; LOG_STRING(str); LOG_POINTER(pi); FREE(pi); LOG_POINTER(pi); return 0; } -
-
宏定义的常量或表达式没有作用域的限制,若想让宏只在定义的函数中起作用,在函数最后加上#undef 宏名
-
定义日志宏
-
内置宏
-
日志宏(time函数得到当前系统时间ms,localtime转换为可识别的本地时间,asctime把时间转换为ASC码)
-
-
define f (x) ((x)-1)代表什么意思?
代表把f定义为 (x) ((x)-1),宏定义对空格敏感,但宏调用对空格不敏感。
(4). 条件编译使用分析
-
基本概念
-
条件编译的行为类似于C语言中的if…else
-
条件编译是预编译指示命令,用于控制是否编译某段代码
-
执行条件:#if 条件成立,#Ifdef 宏定义存在
-
-
条件编译的本质
-
预编译器根据条件编译指令有选择的删除代码,编译器不知道代码分支的存在
-
If…else…语句在运行期进行分支判断,而条件编译指令在预编译期进行分支判断
-
可以通过命令行定义宏
gcc –Dmacro=value file.c
或 gcc –Dmacro file.c
-
-
include的本质
-
include的本质是将已经存在的文件内容嵌入到当前文件中
-
include的间接包含同样会产生嵌入文件内容的操作,若间接包含同一个头文件会产生编译错误
-
-
条件编译的意义
-
条件编译使得我们可以按不同的条件编译不同的代码段,因而可以产生不同的目标代码
-
if…#else…#endif被预编译器处理;而if…else语句被编译器处理,必然被编译进目标代码
-
实际工程中条件编译主要用于一下两种情况:
-
不同的产品线共用一份代码
-
区分编译产品的调试版和发布版
-
-
(5). #error和#line使用分析
-
自定义错误消息
-
error用于生成一个编译错误消息,并停止编译。
-
用法:#error message (注:message不需要双引号包围)
-
error编译指示字用于自定义程序员特有的编译错误消息。类似的,#warning用于生成编译警告,但不会停止编译。(\”字符串\”)
-
error是一种预编译器指示字,可用于提示编译条件是否满足
(__cplusplus c ++内置宏,判断编译器是不是c ++)
``` #ifndef __cplusplus #error This file should be processed with c++ compiler #endif ```- ++编译过程中的任意错误信息,都无法生成最终的可执行程序++
-
-
重定义行号和文件名
-
line用于强制指定新的行号和编译文件名,并对源程序的代码重新编号。
-
用法:#line number filename (注:filename 可省略)
-
line编译指示字的本质是重定义__LINE__和__FILE__
(原本用于防止不同人员编写同一程序时出错,方便查找。)
-
(6). #pragma预处理分析
-
pragma简介
-
pragma是编译器指示字,用于指示编译器完成一些特定的动作
-
pragma所定义的很多指示字是编译器特有的
-
pragma在不同的编译器间是不可移植的
-
预处理器将忽略它不认识的#pragma指令
-
两个不同的编译器可能以不同的方式解释同一条#pragma指令
一般用法:
#pragma parameter (++注:不同的parameter参数语法和意义各不相同++)
-
-
-
pragma message
-
message参数在大多数的编译器中都有相似的实现
-
message参数在编译时输出消息到编译输出窗口中
-
message可用于条件编译中可提示代码的版本信息
-
与 #error和 #warning不同,#pragma message仅仅代表一条编译信息,不代表程序错误
-
-
pragma once
-
pragma once 用于保证头文件只被编译一次
-
pragma once效率上比#ifdef..#define…#endif高,但与编译器相关的,不一定被支持,可以如下使用,保证可靠与实用
-
-
内存对齐
-
什么是内存对齐?
不同类型的数据在内存中按照一定的规则排列;而不是顺序的一个接一个的排放,这就是对齐。
-
为什么需要内存对齐?
-
CPU对内存的读取不是连续的,而是分成块读取的,块的大小只能是1、2、4、8、16字节
-
当读取操作的数据未对齐,则需要两次总线周期来访问内存,因此性能会大打折扣
-
某些硬件平台只能从规定的相对地址处读取某些特定类型的数据,否则抛出硬件异常
-
-
-
pragma pack(能够改变编译器的默认对齐方式)
struct占用的内存大小
-
第一个成员起始于0偏移处
-
每个成员按其类型大小和pack参数中++较小++的一个进行++对齐++
-
++偏移地址必须能被对齐参数整除++
-
++结构体成员的大小取其内部长度最大的数据成员作为其大小++
-
-
结构体总长度必须为所有对齐参数的整数倍
-
结构体大小的计算(Intel和微软的面试题)
注:gcc编译器默认4字节对齐,不支持8字节对齐
#include <stdio.h>
#pragma pack(8)
struct S1
{ // 对齐参数 偏移地址 大小
short a; // 2 0 2
long b; // 4 4 4
};
struct S2
{ // 对齐参数 偏移地址 大小
char c; // 1 0 1
struct S1 d; // 4 4 8
double e; // 8 16
};
#pragma pack()
int main()
{
printf("%d\n", sizeof(struct S1)); // 8
printf("%d\n", sizeof(struct S2)); // 24
return 0;
}
(7). #和##运算符使用解析
-
运算符
-
运算符用于在预处理期将宏参数转换为字符串,因此只在宏定义中有效
-
运算符在宏中的妙用
#include <stdio.h> #define CALL(f, p) (printf("Call function %s\n", #f), f(p)) int square(int n) { return n * n; } int func(int x) { return x; } int main() { int result = 0; result = CALL(square, 4); printf("result = %d\n", result); result = CALL(func, 10); printf("result = %d\n", result); return 0; }
-
-
运算符
-
运算符用于在预处理期粘连两个标识符,因此只在宏定义中有效
-
利用##定义结构类型(高通公司用法)
#include <stdio.h> #define STRUCT(type) typedef struct _tag_##type type;\ struct _tag_##type STRUCT(Student) { char* name; int id; }; int main() { Student s1; Student s2; s1.name = "s1"; s1.id = 0; s2.name = "s2"; s2.id = 1; printf("s1.name = %s\n", s1.name); printf("s1.id = %d\n", s1.id); printf("s2.name = %s\n", s2.name); printf("s2.id = %d\n", s2.id); return 0; }
-
撒打算
撒打算
打撒所

浙公网安备 33010602011771号