1.程序的翻译环境
2.程序的执行环境
3.c语言程序的编译+链接
4.预定义符号介绍
5.预处理指令#define
6.宏和函数的对比
7.预处理操作符#和##的介绍
8.命令定义
9.预处理指令#include
10.预处理指令#undef
11.条件编译
在ANSI C的任何一种实现中,存在两个不同的环境,第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,第二种是执行环境它用于实际执行代码
详解编译+链接
翻译环境
程序的编译过程
1.组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)
2.每个目标文件由连接器(linker)捆绑在一起,形成一个单一而完整的可执行程序
3.连接器同时也会引入标准c函数库中任何被该程序所用到的函数,而且它还可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
通俗讲上面这段话就是 首先翻译环境分为两个阶段,一个叫编译 ,一个叫链接,编译会依赖编译器 编译完了会生成目标文件 目标文件再经过连接器进行链接最终处理生成可执行程序
编译又可以分为几个阶段
第一个阶段 预编译
做的事情是
1. #include头文件的包含,把头文件的内容全部都包含放到.c文件里面去
2.删除注释,使用空格来替换注释
3.将#define定义的符号 全部换成所对应的值
总归一句话预处理做的事情叫做文本操作
test.c文件经过编译器处理会生成一个test.i的文件
第二个阶段 编译
做的事情是
编译是把test.i翻译成test.s把c语言代码翻译成了汇编代码
1.语法分析,看语法有没有错误
2.词法分析
3.语义分析
4.符号汇总 比如全局变量 函数 main g_val
其实就是把c语言代码转换为汇编代码
第三个阶段 汇编
将汇编代码转换生成test.o文件 test.o在windows下是test.obj文件也就是目标文件
做的事情是
1.把汇编代码转换成二进制的代码,也称二进制指令 指令就是代码
2.上一个阶段不是进行符号汇总了,在汇编阶段形成符号表
符号表就是各自的文件中的符号 比如函数名 和全局变量 和它们的地址的组成
全局变量和函数名 #include<stdio.h> extern int Add(int x,int y) int main() { return 0; } 编译阶段不是已经产生符号汇总了 所以汇编阶段就会形成符号表 这里面会产生符号是全局变量ADD和main函数 符号汇总会形成 ADD 0x112233 main 0x223344 将符号和它的地址对应起来就会形成符号表
1.预处理选项 gcc-E test.c -0 test.i 预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中
2.编译 选项 gcc-S test.c 编译完成之后就停下来,结果保存在test.s中
3.汇编 gcc-c test.c汇编完成之后就停下来,结果保存在test.o中 .o文件在windows下就是.obj文件
连接发生的事情
1.合并段表
首先目标文件也就是.obj文件它会有自己的格式 会自己分成几个段
将多个目标文件连接在一起 对应段的数据合并在一起,这就是合并段表最终生成一个可执行程序
2.符号表的合并和符号表的重定位
将各自目标文件的符号表合并成为一个符号表
//符号表合并如果符号名相同肯定会用地址有效的符号表
整个翻译环境所做的事情完成 生成可执行程序
运行环境
程序执行的过程
1.程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统完成,在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
2.程序的执行便开始,接着便调用main函数
3.开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值
4.终止程序,正常终止main函数,也有可能是意外终止
预编译阶段也叫预处理阶段
预处理详解
int main() { printf("%s\n", __FILE__);//文件名符号 d:\桌面\c\date11_2\test11_2\test11_2\today.c printf("%d\n", __LINE__);//行号 101查看 你在多少行 printf("%s\n", __DATE__);//获取日期 Nov 2 2021 printf("%s\n", __TIME__);//获取时间 17:39:36 printf("%s\n", __STDC__);//如果编译器遵循ANSI C 其值为1 否则未定义 //写日志文件 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 data:%s time:%s i=%d\n", __FILE__,__LINE__,__DATE__,__TIME__,i); } fclose(pf); pf = NULL; for (i = 0; i < 10; i++) { printf("%d ", arr[i]); } return 0; }
预处理指令都有哪些
#define
#include
#if
#end
#end if
#pragma pack(4)
#pragma
#ifdef
#line
以#开头的都叫预处理指令
#define 预处理指令
语法 #define name stuff
实例
#define MAX 1000 #define reg register //为register这个关键字,创建一个简短的名字 #define do_forever for(;;)//用更形象的符号来替换一种实现 #define CASE break;case//再写case语句的时候自动把break写上 //如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续航符) #define DEBUG_PRINT print("file:%s\t line:%d\t \ date:%s\t time:%s\n", \ __FILE__,__LINE__, \ __DATE__,__TIME__)
define 定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)
#define name(parament-list)stuff 其中的parament-list是一个由逗号隔开的符号表他们可能出现在stuff中
注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在参数列表就会被解释为stuff的一部分
#define SQUARE(X) ((X)*(X)) //SQUARE就是宏
#define SQUARE(X) ((X)*(X)) 这里将5给x 然后后面的替换掉x就是5x5了 牢牢记住这里不是把5传给x而是把5跟x进行了替换,也就是写宏的时候永远 不要忘记括号 int main() { int ret = SQUARE(5); printf("%d\n", ret);//25 return 0; } #define DOUBLE(X) (X)+(X) int main() { int a = 5; int ret = 10 * DOUBLE(a); printf("%d\n", ret);//55 /* 因为这里a替换掉了x,所以回来就是10*a+a所以就是55了 */ return 0; }
总结定义宏的时候 #define DOUBLE(X) ((X)+(X))一定不要忘记多写括号
提示: 所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用
#define替换规则
1.调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换
2.替换文本随后被插入到程序中原来文本的位置对于宏,参数名被它们的值替换
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号,如果是就重复上述过程
注意:
1:宏参数和#define定义中可以出现其他#define定义的变量,但是对于宏,不能出现递归
2:当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
#define MAX 100 #define DOUBLE(x) ((x)+(x)) int main() { int a = 5; int ret = 10 * DOUBLE(MAX); //也就是说这里首先将MAX进行替换,然后在对宏进行替换 printf("MAX =%d\n", MAX); //也就是说这里的字符串里面的""里面的MAX是不能被替换的它是字符串本身的内容 return 0; }
#和##
如何把参数插入到字符串中
int main() { printf("hello world\n"); //hello world printf("hello " " world\n");//hello world printf("hel" "lo " " world\n");//hello world return 0; }
#的作用
#define PRINT(X) printf("the value of " #X " is %d\n", X) #X的作用就是把参数插入到字符串中 #X也就是找到X所对应的字符并把#X换成 "所对应的字符" 比如"a" "b" int main() { int a = 10; int b = 20; 多个字符串放在一起天然的会连接在一起 PRINT(a);被替换成 printf("the value of " "a" "is %d\n", a) 它的结果就是: the value of a is 10 PRINT(b);被替换成 printf("the value of " "b" "is %d\n", b); /它的结果就是: the value of b is 20 return 0; }
##的作用
##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符
#define CAT( X , Y) X##Y //这里就会变成Class##84 ##就把它两边的符号连到一起 int main() { int Class84 = 2019; printf("%d\n", CAT(Class ,84 )); //2019 return 0; }
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能
出致不可预测的后果,副作用就是表达式求值的时候出现的永久性效果,例如
x + 1; 不带副作用
x++; 带有副作用
MAX宏可以证明具有副作用的参数所引起的问题
int main() { int a = 10; int b = a + 1; //这里的a赋给b的时候自己本身没有改变 int c = ++a; //这里的a先自身改变,然后在赋值给c return 0; }
#define MAX(X,Y) ((X)>(Y)?(X):(Y)) int main() { int a = 10; int b = 20; int max = MAX(a++, b++); /*首先因为宏的参数完全是替换那么就会变成 int max = MAX((a++)>(b++)?(a++):(b++)) a++先使用后自增 所以a是10 b++先使用后自增 所以b是20 10不大于20所以执行后面b++ b此时是21 先使用传给MAX,然后在自增变成22所以 MAX此时是21 b就会变成22 a是11 */ printf("%d\n", max);//21 printf("%d\n", a); //11 printf("%d\n", b); //22 return 0; }
宏和函数求解两个数的较大值
#define MAX(X,Y) ((X)>(Y)?(X):(Y)) int Max(int x, int y) { return (x > y ? x : y); } int main() { int a = 10; int b = 20; float c = 3.0f; float d = 4.0f; //函数在调用的时候 会有函数调用和返回的开销 //int max1 = MAX(a, b); //printf("max1 = %d\n", max1); //int max2 = Max(a, b); //printf("max2 = %d\n", max2); //int max1 = MAX(c, d); //printf("max1 = %d\n", max1);//从“float”转换到“int”,可能丢失数据 float max2 = MAX(c, d); printf("max2 = %f\n", max2); //预处理阶段就完成了替换 //没有函数的调用和返回的开销 return 0; }
宏和函数进行对比
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个
#define MAX((a,b)(a)>(b)>(a):(b))
那为什么不用函数来完成这个任务,原因有二
1.用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,所以宏比函数在程序
的规模和速度方面更胜一筹
2.更为重要的是函数的参数必须声明为特定的类型,所以函数只能在类型合适的表达式上使用,反之这个宏
咱可以适用与长整型,整型,浮点型,等可以用来>来比较的类型,宏是类型无关的
当然和宏相比函数也有劣势的地方
1.每次使用宏的时候,一份宏定义的代码将插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度
2.宏是没法调试的
3.宏由于类型无关,也就不够严谨
4.宏可能会带来运算符优先级的问题,导致程序容易出现错误
宏有时候可以做函数做不到的事情,比如:宏的参数可以出现类型但是函数做不到
#define SIZEOF(type) sizeof(type) int main() { int ret = SIZEOF(int); //这里就会被替换成 int ret = sizeof(int) printf("ret = %d\n", ret);//4 return 0; }
用宏来实现malloc
#define MALLOC(num, type) (type*)malloc(num* sizeof(type)) int main() { int* p = (int*)malloc(10 * sizeof(int)); //这样就开辟了10个整型的空间 //那么如何用宏来实现呢看下面 int* p = MALLOC(10,int); //这样就用上面宏来实现了 //这代码就会被替换成 int* p = (int*)malloc(10* sizeof(int)); return 0; }
宏和函数的对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方每次使用这个函数时都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则临近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多谢括号 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数表达式的求值结果更容易预测 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果更容易控制 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使它们执行的任务是不同的 |
调试 | 宏是不方便调试的 | 函数可以逐语句调试 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
函数和宏的命名约定
一般来讲函数和宏的使用语法很相似,所以语言本身没法帮我们区分二者,那我们平时的一个习惯是把宏名全部大写,函数名不要全部大写
#undef
这条指令用于移除一个宏定义
#define MAX 1000 int main() { printf("MAX = %d\n", MAX); #undef MAX printf("MAX = %d\n", MAX); //这里的MAX已经用不了了,因为已经被移除了 如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除 return 0; }
命令行定义
许多c的编译器提供了一种能力,允许在命令行中定义符号用于启动编译过程,例如:当我们根据同一个源文件要
编译出不同的一个程序的不同版本的时候,这个特性有点用处,(假定某个程序中声明了一个某个长度的数组,如果
机器内存有限,我们需要一个很小的数组但是另外一个机器内存大写,我们需要一个数组能够大写)
int main() { int array[ARRAY_SIZE];//这里的值现在没有给定明确大小,但是在编译的时候可以给定明确大小 int i = 0; for (i = 0; i < ARRAY_SIZE; i++) { array[i] = i; } for (i = 0; i < ARRAY_SIZE; i++) { printf("%d ", array[i]); } return 0; } 编译指令 gcc -D ARRAY_SIZE = 10 program.c 在这里可以给ARRAY_SIZE进行赋值
条件编译
在编写一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令
比如说:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译
常见的条件编译指令
(1)
#if 常量表达式
//…
#end if
//常量表达式由预处理器求值。
(2)多个分支的条件编译
#if 常量表达式
//…
#elif 常量表达式
//…
#else
//…
#endif
(3).判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
(4).嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(0S_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
#ifdef #endif
#define DEBUG //这里是对DEBUG进行定义 int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int i = 0; for (i = 0; i < 10; i++) { arr[i] = 0; #ifdef DEBUG //如果DEBUG定义 那么就执行下面的代码,如果DEBUG没定义,那么久不执行下面代码 printf("%d ", arr[i]); #endif } return 0; }
指令实例
int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; int i = 0; for (i = 0; i < 10; i++) { arr[i] = 0; #if 1 //这里如果if后面的常量表达式如果为真就执行下面的代码,如果为假就不执行 printf("%d ", arr[i]); #endif } return 0; }
int main() { #if 1==2 //如果这条语句为真,执行haha如果为假到下一步 printf("haha\n"); #elif 2==2 printf("hehe\n");//如果上面语句为假,这条语句为真,那么执行hehe否则到下一步 #else printf("嘿嘿\n");//如果上面两步代码都为假,那么执行嘿嘿 #endif return 0; }
#define DEBUG 0 //这里就是对DEBUG进行定义 int main() { #if defined(DEBUG) //只要DEBUG定义过就执行hehe这条语句,如果没定义过,就不执行这条语句 printf("hehe\n"); #endif
#ifdef DEBUG //这个跟上面的方法一摸一样 printf("hehe\n"); #endif return 0; }
int main() { #if !defined(DEBUG) //这里和上面相反,如果没有定义DEBUG反而要执行下面语句,如果定义了就不执行 printf("hehe\n"); #endif
#ifndef #endif
#ifndef DEBUG 这里的ifndef里面多了一个n就表示 没有定义DEBUG就执行下面语句, 定义了就不执行 printf("hehe\n"); #endif return 0; }
文件包含
我们已经知道,#include指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样
这种替换的方式很简单:预处理先删除这条指令,并用包含文件的内容替换,这样一个源文件被包含10次
那就实际被编译10次
头文件被包含的方式
#include"filename"
查找策略:先在源文件所在目录下查找如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置
查找头文件,如果找不到就提示编译错误,
linux环境的标准头文件的路径。 /usr/include
windows按照自己安装路径进行查找
库函数包含
如果直接引的是库函数文件那么直接用<>就可以了
#include<filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
包含就是把它所对应的文件拷贝一份过来
如何解决一段代码被重复包含 答案 : 条件编译
第一种方法
每个头文件的开头写
#ifndef TEST__H
#define TEST__H
//头文件的内容
#endif
第二种方法
#pragma once
防止下面这段代码被重复多次包含
#ifndef TEST__H
#define TEST__H
int ADD(int x, int y);
#endif
#pragma once
int ADD(int x, int y);
问题1:头文件中ifndef/define/endif是干什么用的
答案:防止头文件被重复多次包含
问题2:#include<filename.h>和#include"filename.h"有什么区别
答案:应用场景不一样,查找策略不一样
预处理阶段
1.条件编译指令
2.#include
3.#define
#包含的
…
#pragma pnce
#pragma pack()
#line
offsetof
请编写宏,计算结构体中某变量相对于首地址的偏移,并给出说明
offsetof的用法
#include<stddef.h> struct S { char c1; int a; char c2; }; int main() { printf("%d\n", offsetof(struct S, c1));//0 printf("%d\n", offsetof(struct S, a)); //4 printf("%d\n", offsetof(struct S, c2));//8 //offsetof是计算结构体变量相对于首地址的偏移量 return 0; }
模拟实现offsetof
struct S { char c1; int a; char c2; }; #define MYOFFSETOF(struct_name,member_name) (int)&(((struct_name*)0)->member_name) int main() { printf("%d\n", MYOFFSETOF(struct S, c1)); printf("%d\n", MYOFFSETOF(struct S, a)); printf("%d\n", MYOFFSETOF(struct S, c2)); return 0; }
面试题1
有如下宏定义和结构定义 #define MAX_SIZE A+B struct _Record_Struct { unsigned char Env_ALARM_ID : 4; unsigned char Paral : 2; unsigned char state; unsigned char avail : 1; }*Env_Alarm_Record; struct _Record_Struct * pointer = (struct _Record_Struct*)malloc (sizeof(struct _Record_Struct)* MAX_SIZE); 当A=2,B=3时pointer分配(9)个字节的空间
面试题2
#include<stdio.h> int main() { unsigned char puc[4]; struct tagPIM { unsigned char ucPim1; unsigned char ucData0 : 1; unsigned char ucData1 : 2; unsigned char ucData2 : 3; }*pstPimData; pstPimData = (struct tagPIM*)puc; memset(puc, 0, 4); pstPimData->ucPim1 = 2; pstPimData->ucData0 = 3; pstPimData->ucData1 = 4; pstPimData->ucData2 = 5; printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]); //02 09 00 00 return 0; }
面试题3 计算联合体大小
union Un { short s[7]; int n; }; int main() { printf("%d\n", sizeof(union Un)); //16 return 0; }
面试题4
在X86环境下,有下列程序 下面代码结果是多少 int main() { union { short k; char i[2]; }*s, a; s = &a; s->i[0] = 0x39; s->i[1] = 0x38; printf("%x\n", a.k);//0x3839 return 0; }
面试题5
enum { X1, //0 Y1, //1 Z1=255, A1, //256 B1 //257 }; enum enumA = Y1; //1 enum enumB = B1; //257 printf("%d %d\n", enumA, enumB); //1 257