复习补充遗漏知识点
虽然即使几乎是照抄,抄一遍也是好的
C编译全流程
- 预处理
命令: gcc -E hello.c -o hello.i
执行工具: 预处理器 (cpp, C Preprocessor)
工作内容: 预处理器处理源代码中以 # 开头的指令,生成一个纯粹的C代码文件(.i 文件)。具体操作包括:
头文件包含:将 #include <stdio.h> 替换为 stdio.h 文件的实际内容。
宏展开:将所有出现 MSG 的地方替换为字符串 “Hello, World!”。
条件编译:处理 #if, #ifdef, #endif 等指令,根据条件保留或删除代码。
删除注释:将所有注释(如 // 和 /* … */)替换为一个空格或直接删除。
结果: 生成一个没有预处理器指令、没有宏、没有注释的“干净”的C代码文件 hello.i。
- 编译
命令: gcc -S hello.i -o hello.s
(也可以直接从 .c 开始:gcc -S hello.c -o hello.s)
执行工具: 编译器 (cc1)
工作内容: 这是整个流程中最核心、最复杂的部分。编译器将预处理后的C代码(.i 文件)翻译成特定处理器的汇编代码(.s 文件)。这个过程又包含多个子步骤:
词法分析:将源代码的字符流拆分成一个个有意义的标记(Tokens),如关键字、标识符、运算符等。
语法分析:根据C语言的语法规则,将标记组合成语法结构树(抽象语法树,AST)。
语义分析:检查程序逻辑是否正确,如类型匹配、变量是否已声明等。
中间代码生成与优化:生成与硬件无关的中间代码,并进行各种优化(如常量传播、死代码消除等)。
代码生成:将优化后的中间代码转换为目标平台的汇编代码。
结果: 生成一个人类可读的汇编语言文件 hello.s。
- 汇编
命令: gcc -c hello.s -o hello.o
(也可以直接从 .c 开始:gcc -c hello.c -o hello.o)
执行工具: 汇编器 (as)
工作内容: 汇编器将人类可读的汇编代码(.s 文件)翻译成机器可以执行的指令,即二进制目标代码(.o 文件,在Windows下是 .obj 文件)。这个文件包含了所有函数的二进制指令。
重要特点: 此时,代码中调用的外部函数(如 printf)的地址还没有被确定。这些地址在后续的链接阶段被填充。
结果: 生成一个目标文件 hello.o。这个文件已经是二进制格式,但还不能直接运行。
- 链接
命令: gcc hello.o -o hello
执行工具: 链接器 (ld)
工作内容: 这是生成可执行文件的最后一步。链接器将一个或多个目标文件(例如你的 hello.o)和所需的库文件(如C标准库 libc.a)组合在一起,解析并填充所有的外部引用地址。主要工作包括:
地址和空间分配:为所有目标文件中的代码和数据段分配最终的内存地址。
符号解析:解决目标文件之间的交叉引用。例如,你的 hello.o 中有一个对 printf 函数的调用,但 printf 的实现在C标准库中。链接器需要找到 printf 的定义,并将其地址填入 hello.o 的调用指令中。
重定位:根据符号解析得到的地址,修改目标文件中所有对未知地址的引用。
结果: 生成最终的可执行文件 hello(在Windows下是 hello.exe)
关键字
extern 关键字 全局变量或函数声明
register 关键字 将数据存储在寄存器(比RAM读取快但没有地址)
static 关键字 只被初始化一次 想要同时全局,需提供私有变量读取接口
?:运算符 exp1?exp2:exp3 1为true计算2 反之计算3
int name[num1][num2][num3] 多维数组
*指针 数组名在大多数情况下会自动转换为指向数组第一个元素的指针,因而不需要添加&进行获取,而代表int等的变量名在scanf赋值时需要&指向地址
回调
函数指针typedef int (*fun_ptr)(type);
回调函数 通过函数指针调用的函数 函数指针可作为参数传入函数,调用此函数返回的结果
/*回调函数的例子*/
#include <stdlib.h>
#include <stdio.h>
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
for (size_t i=0; i<arraySize; i++)
array[i] = getNextValue();
}
// 获取随机值
int getNextRandomValue(void)
{
return rand();
}
int main(void)
{
int myarray[10];
/* getNextRandomValue 不能加括号,否则无法编译,因为加上括号之后相当于传入此参数时传入了 int , 而不是函数指针*/
populate_array(myarray, 10, getNextRandomValue);
for(int i = 0; i < 10; i++) {
printf("%d ", myarray[i]);
}
printf("\n");
return 0;
}
为什么传入指针而不直接运行getNextRandomValue()——1populate_array期待的是一个指针 2回调函数延迟执行,传入函数则相当于直接执行,丧失灵活性
比如说
// 可以根据条件动态选择不同的操作
fun_ptr op;
if (condition) {
op = add;
} else {
op = subtract;
}
int result = calculate(10, 5, op); // 运行时决定执行哪个函数
如果改为
int op;
if (condition) {
op = add();
}
即使不符合条件add()也会立即执行
1 strcpy(s1, s2);
复制字符串 s2 到字符串 s1。
2 strcat(s1, s2);
连接字符串 s2 到字符串 s1 的末尾。
3 strlen(s1);
返回字符串 s1 的长度。
4 strcmp(s1, s2);
如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。
5 strchr(s1, ch);
返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
6 strstr(s1, s2);
返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。
共用体union
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
举例
union Data
{
int i;
float f;
char str[20];
} data;
\*函数内调用*\
union Data data;
printf( "Memory size occupied by data : %d\n", sizeof(data));
始终只有一个(最后被赋值的)成员占有空间,其余成员空缺或损坏
位域 unsigned int a : 1;表示只为a分配一位,作为0/1使用 一般在struct或union中使用
typedef 关键字,为类型取新名字
输入输出
int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。
int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。
int puts(const char *str); puts() 函数用于将一个字符串输出到标准输出设备,并自动在末尾添加换行符。(只接受一个字符参数)
int fputs(const char *str, FILE *stream); fputs() 函数用于将字符串输出到指定的流(如标准输出、文件等),但不会自动在字符串末尾添加换行符。
文件读写
fopen() 函数
fopen() 函数用于打开一个文件。
语法:
FILE *fopen(const char *filename, const char *mode);
参数:
filename:要打开的文件名。
mode:打开文件的模式,如”r”(只读)、”w”(只写)、”a”(追加)等。
返回值:
成功时返回指向FILE对象的指针,失败时返回NULL。
fopen("路径","w")
fclose() 函数
fclose() 函数用于关闭一个已打开的文件。
语法:
int fclose(FILE *stream);
参数:
stream:指向FILE对象的指针。
返回值:
成功时返回0,失败时返回EOF。
r 打开一个已有的文本文件,允许读取文件。
w 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,文件内容会被清空(即文件长度被截断为0)。
a 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。
r+ 打开一个文本文件,允许读写文件。覆盖写入。
w+ 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+ 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。
如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:
“rb”, “wb”, “ab”, “rb+”, “r+b”, “wb+”, “w+b”, “ab+”, “a+b”
int fprintf(FILE *stream, const char format, …);文件写入 …为已定义char参数
注意 可用stdout stderr等流代替stream 这样就是选择流输出而不局限于文件
int fscanf(FILE *stream, const char *format, …); 文件读取 其中每次读取为 char参数赋予值 流的使用同fprintf
c预处理器(cpp)为编译的一个阶段,在编译之前,对源代码文本进行一系列转换操作,输出一个“纯净”的、不包含任何预处理指令的文本文件(通常称为“翻译单元”),然后这个文件才会被真正的编译器处理。
#include 为包含文件,<>从系统库提取 “”从本地提取
#define #undef 定义和取消定义
#ifndef pi
#define pi 3
#endif
条件判断
#define MAX(a, b) ((a) > (b) ? (a) : (b)) 定义函数举例,每个参数和整个表达式都要用括号括起来/参数化的宏模拟函数
#if defined(_WIN32)
// Windows特定代码
#elif defined(__linux__)
// Linux特定代码
#elif defined(__APPLE__)
// macOS特定代码
#endif //跨平台开发
#if !defined(MACRO_A) && !defined(MACRO_B)
// 当 MACRO_A 和 MACRO_B 都未定义时编译
#endif //可以检查多个未定义,检查一个可用#ifndef
#ifndef HEADER_FILE
#define HEADER_FILE
the entire header file file
#endif
//#ifndef包装器,可以防止头文件被重复引入
//判断宏语句
#if
//
#elif
//
#endif
错误处理
全局变量errno为错误码 通过 extern int errno ; 引入
perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。
strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。
#include <stdio.h>
#include <errno.h>
#include <string.h>
extern int errno ;
int main ()
{
FILE * pf;
int errnum;
pf = fopen ("unexist.txt", "rb");
if (pf == NULL)
{
errnum = errno;
fprintf(stderr, "错误号: %d\n", errno);
perror("通过 perror 输出错误");
fprintf(stderr, "打开文件错误: %s\n", strerror( errnum ));
}
else
{
fclose (pf);
}
return 0;
}
exit
exit() 是标准库函数,不同于return只能在函数中调用 它可在程序任何地方调用,用于立即终止程序
在main() 末尾使用return 0 或exit(0)几乎是等价的,因为前者会隐式调用后者
也可用 exit(EXIT_SUCCESS); 其中EXIT_SUCCESS宏定义为0 EXIT_FAILURE为-1 表示出现错误
可变参数
C接受可变参数定义为 int func_name(int arg1, …); 其中arg1 参数传入可接受参数的数目,...并非某种缩写,而是需要写在定义里的,表示可接受参数列表
函数内部可调用且常用的宏有:
va_start(ap, last_arg):初始化可变参数列表。ap 是一个 va_list 类型的变量,last_arg 是最后一个固定参数的名称(也就是可变参数列表之前的参数)。该宏将 ap 指向可变参数列表中的第一个参数。
va_arg(ap, type):获取可变参数列表中的下一个参数。ap 是一个 va_list 类型的变量,type 是下一个参数的类型。该宏返回类型为 type 的值,并将 ap 指向下一个参数。(实际作用就是逐个获取相同类型的值)
va_end(ap):结束可变参数列表的访问。ap 是一个 va_list 类型的变量。该宏将 ap 置为 NULL。
内存管理
void free(void *address);
该函数释放 address 所指向的内存块,释放的是动态分配的内存空间。
void *malloc(int num);
在堆区分配一块指定大小的内存空间,用来存放数据,但不会提前将这部分内存清零。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。所以分配内存后要记得及时释放
void calloc(int num, int size);
在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 numsize 个字节长度的内存空间,并且每个字节的值都是 0。同样需要free()手动释放
void *realloc(void *address, int newsize);
该函数重新分配内存,把内存扩展到 newsize。
C标准库
<stdio.h> 标准输入输出库,包含 printf、scanf、fgets、fputs 等函数。
<stdlib.h> 标准库函数,包含内存分配、程序控制、转换函数等,如 malloc、free、exit、atoi、rand 等。
<string.h> 字符串操作函数,如 strlen、strcpy、strcat、strcmp 等。
<math.h> 数学函数库,包含各种数学运算函数,如 sin、cos、tan、exp、log、sqrt 等。
<time.h> 时间和日期函数,如 time、clock、difftime、strftime 等。
<ctype.h> 字符处理函数,如 isalpha、isdigit、isspace、toupper、tolower 等。
<limits.h> 定义各种类型的限制值,如 INT_MAX、CHAR_MIN、LONG_MAX 等。
<float.h> 定义浮点类型的限制值,如 FLT_MAX、DBL_MIN 等。
<assert.h> 包含宏 assert,用于在调试时进行断言检查。
<errno.h> 定义了错误码变量 errno 及相关宏,用于表示和处理错误。
<stddef.h> 定义了一些通用类型和宏,如 size_t、ptrdiff_t、NULL 等。
<signal.h> 定义了处理信号的函数和宏,如 signal、raise 等。
<setjmp.h> 提供非本地跳转功能的宏和函数,如 setjmp、longjmp 等。
<locale.h> 定义了与地域化相关的函数和宏,如 setlocale、localeconv 等。
<stdarg.h> 提供处理可变参数函数的宏,如 va_start、va_arg、va_end 等。
<stdbool.h> 定义布尔类型和值 true 和 false。
<stdint.h> 定义了精确宽度的整数类型,如 int8_t、uint16_t 等。
<inttypes.h> 提供与整数类型相关的格式化输出宏和函数。
<complex.h> 提供复数运算的函数和宏,如 cabs、carg 等。
<tgmath.h> 为泛型数学函数提供宏,以简化对不同类型数据的数学运算。
<fenv.h> 提供对浮点环境的控制,如舍入模式和异常状态。
此方悬停