程序的翻译环境和运行环境
从.c -> .exe
翻译环境
翻译分为编译(编译器)和链接(链接器)两个过程
每个源文件都会经过编译器的 单独处理 生成目标文件(.obj文件object)
所有目标文件和链接库经过链接器生成可执行文件
链接器可以搜索所需的标准C库文件和个人的程序库
编译分为预编译,编译,汇编三个过程
以下在Linux下演示
创建test.c
预编译:
gcc -E
预编译命令
gcc -E test.c > test.i
将预编译产生的内容重定向到test.i中
Linux中头文件存放在/usr/include
中
预编译过程中:
头文件展开
去除注释(使用空格替换注释)
完成预处理指令,例如对#define
定义的值进行替换
例如:
1 2 3 4 5 6 7 8 #define MAX 100 int main(void) { int i = MAX; return 0; }
替换为:
1 2 3 4 5 6 int main(void) { int i = 100; return 0; }
总结:文本操作
编译:
gcc -S test.i
生成test.s
文件
把C代码翻译成汇编代码
具体操作有:语法分析,词法分析,语义分析,符号汇总
符号汇总:函数名,全局变量等
涉及编译原理
《程序员的自我修养》
汇编:
gcc -c test.s
生成test.o
Windows下是.obj
操作:把汇编代码转换成二进制代码(指令),形成符号表
符号表:一个表中放置符号(函数名,全局变量等)和符号的地址
链接过程
合并段表
每个.o文件都分为相同格式的段(段里面的内容不同)(elf文件格式)
链接时不同文件的相同段合并到一起,最终生成的可执行文件的格式也是elf格式
符号表的合并和重定位
不同文件的符号表合并,表中遇到相同的符号,选地址有效的那个
例如:
main.c
中引用外部函数add
, add.c
中定义了add
函数的实现,main.o
的符号表中有add
,地址是无效地址,add.o
的符号表中也有add
,表中的地址有效,链接时选取add.o
符号表中add
的地址
运行.exe
运行环境
程序载入内存中:
在有操作系统的环境中一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
程序的执行:
从main函数开始
开始执行程序代码:
程序使用一个运行时程序堆栈,存储局部变量和返回地址,也可以在堆空间里开辟动态内存,在静态内存中存放静态变量
终止程序:
可以是正常终止,也可以是意外终止
预处理
预定义符号
FILE
获取当前文件名称和路径
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { printf("%s\\n", __FILE__); return 0; }
输出:D:/Study/StudyNotes-C/Pretreatment/main.c
LINE
获取当前行号
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { printf("%d\\n", __LINE__); return 0; }
输出:4
DATE
获取当前日期
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { printf("%s\\n", __DATE__); return 0; }
输出:Mar 11 2023
TIME
获取当前时间
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { printf("%s\\n", __TIME__); return 0; }
输出:20:51:56
FUNCTION
获取当前位置所在的函数名称
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { printf("%s\\n", __FUNCTION__); return 0; }
输出:main
STDC
如果编译器遵循ANSI C,其值为1,否则未定义
用来检测是否遵循ANSI C
1 2 3 4 5 6 7 8 #include <stdio.h> int main(void) { printf("%d\\n", __STDC__); return 0; }
输出:1
如果是MSVC那么会报错
用处:写日志文件
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> int main(void) { int i = 0; int arr[10] = { 0 }; FILE* pf = fopen("log.txt", "w"); for (i = 0; i < 10; i++) { arr[i] = i; fprintf(pf, "file:%s line:%d date:%s time:%s i=%d\\n", __FILE__, __LINE__, __DATE__, __TIME__, i); } for (i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0; }
输出:0 1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9 10 11 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=0 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=1 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=2 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=3 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=4 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=5 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=6 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=7 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=8 file:D:/Study/StudyNotes-C/Pretreatment/main.c line:12 date:Mar 11 2023 time:20:58:03 i=9
预处理指令
#开头的指令都是预处理指令#
#define#
#include#
#pragma#
#if#
#endif#
#ifdef#
#line#
#define#
#define定义标识符#
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define MAX 100 int main(void) { int max = MAX; printf("%d\\n", max); return 0; }
1 2 3 4 5 6 7 8 #include <stdio.h> #define STR "hello" int main(void) { printf("%s\\n", STR); return 0; }
#define reg regist#define do_forever for(;;)#define CASE break;case
如果如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)
1 2 3 4 5 #define DEBUG_PRINT printf("file:%s\\tline:%d\\t \\ date:%s\\ttime:%s\\n" , \\ __FILE__,__LINE__ , \\ __DATE__,__TIME__ )
define语句不要加分号,相当于定义的内容就有分号
#define定义宏#
#define# 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的parament-list
是一个由逗号隔开的符号表,它们可能出现在stuff中
注意: 参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
例如:
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define SQUARE(X) X*X int main(void) { int ret = SQUARE(5); printf("%d\\n", ret); return 0; }
输出为25
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define SQUARE(X) X*X int main(void) { int ret = SQUARE(5 + 1); printf("%d\\n", ret); return 0; }
输出为11
宏不是传参而是替换,例如:int ret = SQUARE(5 + 1)
被替换成
int ret = 5 + 1 * 5 + 1
所以说是11
如果改成
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define SQUARE(X) (X)*(X) int main(void) { int ret = SQUARE(5 + 1); printf("%d\\n", ret); return 0; }
或者
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define SQUARE(X) X*X int main(void) { int ret = SQUARE((5 + 1)); printf("%d\\n", ret); return 0; }
那么输出就是36
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #define DOUBLE(X) X+X int main(void) { int a = 5; int ret = 10 * DOUBLE(a); printf("%d\\n", ret); return 0; }
输出是55
在写宏时不要少些括号
#define的替换规则#
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> #define MAX 100 #define DOUBLE(X) ((X)+(X)) int main(void) { int a = 5; int ret = 10 * DOUBLE(MAX); printf("%d\\n", ret); return 0; }
注意:
宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
例如printf(MAX = %d\\n, MAX)
中第一个MAX不被替换
#和##
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> void print(int a) { printf("the value of a is %d\n", a); } int main(void) { int a = 10; int b = 20; print(a); print(b); return 0; }
无法做到替换a
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main(void) { printf("hello world\\n"); printf("hello " "world\\n"); return 0; }
C语言可以自动连接两个字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> #define PRINT(X) printf("the value of " #X " is %d\\n", X) int main(void) { int a = 10; int b = 20; PRINT(a); //printf("the value of " "a" " is %d\\n", X) PRINT(b); //printf("the value of " "b" " is %d\\n", X) return 0; }
#把一个宏参数直接插入到字符串中#
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define CAT(X, Y) X##Y int main(void) { int a1 = 1; printf("%d\\n", CAT(a, 1)); return 0; }
##会把两端的值连接到一起
带有副作用的宏参数
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> int main(void) { int a = 10; int b = a + 1;//a不变 int b = ++a;//a加一 //a变化了可能会影响后续代码,有副作用 return 0; }
a+1没有副作用
a++有副作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> #define MAX(X, Y) ((X) > (Y) ? (X) : (Y)) int main(void) { int a = 10; int b = 11; int max = MAX(a++, b++); printf("%d\\n", max); printf("%d\\n", a); printf("%d\\n", b); return 0; }
输出12 11 13MAX(a++, b++)
替换为((a++) > (b++) ? (a++) : (b++))
宏和函数的区别
宏的优势
函数调用的时候会有函数调用和返回的开销,效率较低
宏在预处理时就完成了替换,没有函数调用和返回的开销,效率高
函数的参数必须声明成特定类型,只能在类型适合的表达式上使用,宏的使用与类型无关
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> #define MAX(X, Y) (X)>(Y)?(X):(Y) int Max(int x, int y) { return x > y ? x : y; } int main(void) { int a = 0; int b = 1; float c = 1.0f; float d = 2.0f; int max = Max(a, b); printf("%d\\n", max); max = MAX(a, b); printf("%d\\n", max); max = Max(c, d); printf("%d\\n", max); max = MAX(c, d); printf("%d\\n", max); return 0; }
宏的劣势
每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度,函数只有一份
宏是没法调试的
宏由于类型无关,也就不够严谨
宏可能会带来运算符优先级的问题,导致程序容易出现出错
宏不能递归,函数可以递归
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define SIZEOF(type) sizeof(type) int main(void) { int ret = SIZEOF(int); printf("%d\\n", ret); return 0; }
1 2 3 4 5 6 7 8 9 10 #include <stdlib.h> #define MALLOC(num, type) (type*)malloc(num*sizeof(type)) int main(void) { int* p = MALLOC(10, int); return 0; }
命名约定
一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。那么我们平时的一个习惯是:
宏命名全部大写,函数名不要全部大写
#undef#
移除一个宏定义
1 2 3 4 5 6 7 8 9 10 11 #include <stdio.h> #define MAX 100 int main(void) { printf("MAX = %d\\n", MAX); #undef MAX return 0; }
#命令行定义#
许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> int main( void) { int arr[SZ] = { 0 }; int i = 0; for(i = 0; i < SZ; i++) { arr[i] = i; } for(i = 0; i < SZ; i++) { printf("%d\\n", arr[i]); } return 0; }
编译时输入gcc main.c -D SZ=10
则可以编译成功
在预编译阶段替换
条件编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #define DEBUG int main(void) { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; int i = 0; for (i = 0; i < 10; i++) { arr[1] = 0; #ifdef DEBUG printf("%d ", arr[i]); #endif//DEBUG } return 0; }
常见的条件编译指令:
#if和#endif
1 2 3 4 #if 常量表达式 //... #endif
#if判断常量表达式的真假,非零执行,零不执行#
多分支的条件编译
1 2 3 4 5 6 7 8 9 10 #if 常量表达式 //... #elif 常量表达式 //... #elif 常量表达式 //... #else //... #endif
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> int main(void) { #if 1==1 printf("1"); #elif 2==1 printf("2"); #else printf("3");#endif return 0; }
判断是否被定义
1 2 3 4 5 6 7 8 #if defined(symbol) //等价于#ifdef symbol #if !defined(symbol) //等价于#ifndef symbol #endif
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> #define DEBUG int main(void) { #ifdef DEBUG //#if defined(DEBUG) printf("hello world"); #endif//DEBUG return 0; }
嵌套指令
1 2 3 4 5 6 7 8 9 10 11 12 13 #if defined(OS_UNIX) #ifdef OPTION1 unix_version_option1(); #endif #ifdef OPTION2 unix_version_option2(); #endif #elif defined(OS_MSDOS) #ifdef OPTION2 msdos_version_option2(); #endif #endif
文件包含
#include在包含本地文件时用#" "
,如果包含库文件时用< >" "
本地文件查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件,如果找不到就提示编译错误
< >
库文件查找策略:直接去标准路径下去查找,如果找不到就提示编译错误
库文件也可以用" "
包含,但是查找效率低
嵌套文件包含
头文件重复包含导致代码冗余
1 2 3 4 5 6 7 #ifndef __TEST_H__ #define __TEST_H__ //... #endif
或者
#pragma once
offsetof()的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <stdio.h> #define OFFSETOFF(struct_name, member_name) ((int)&(((struct_name*)0)->member_name)) struct S { char c1; int a; char c2; }; int main(void) { struct S s; printf("%d\\n", OFFSETOFF(struct S,c1)); printf("%d\\n", OFFSETOFF(struct S,a)); printf("%d\\n", OFFSETOFF(struct S,c2)); return 0; }