在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序
库,将其需要的函数也链接到程序中。
其中编译又分了三个过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ElcXD6q3-1645852663476)(C:/Users/Allen/AppData/Roaming/Typora/typora-user-images/image-20220224155532284.png)]
那么编译期间具体发生了什么呢?如图
预编译的过程发生了文本操作
头文件包含
删除注释
#define
宏替换
把C语言代码转换成了会汇编代码,设计的过程在上图可见
把汇编指令转化为二进制指令,形成符号表,-o的文件格式是elf格式文件
合并段表
符号表的合并和重定位
符号表的意义:作为目标代码生成阶段地址分配的依据
每个符号变量在目标代码生成时需要确定其在存储分配的位置(主要是相对位置)。语言程序中的符号变量由它被定义的存储类别或被定义的位置(如分程序结构的位置)来确定。
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
下面是内置的预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
记录日志时可以使用
int main()
{
int i = 0;
//记录日志
FILE* pf = fopen("log.txt", "w");
if (pf == NULL)
{
//....
return 1;
}
for (i = 0; i < 10; i++)
{
fprintf(pf, "%s %s %s %d i=%d\n", __DATE__, __TIME__, __FILE__, __LINE__, i);
}
fclose(pf);
pf = NULL;
return 0;
}
语法
#define name stuff
在define定义标识符的时候,最好不要在最后加上;
比如下面就会出问题
#define MAX 1000;
#define MAX 1000
if(condition)
max = MAX;
else
max = 0;
#define name( parament-list ) stuff
//其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
⚠️ 参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
注意区分下面两行代码
#define DOUBLE(x) ((x)+(x))
#define DOUBLE (x) ((x)+(x))
定义宏的时候最好带上括号,免得发生运算顺序错误
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 10;
int b = 20;
int m = MAX(a+2, b);
printf("%d\n", m);
return 0;
}
下面是一个易错的代码
#define DOUBLE(x) x+x
//应该改成#define DOUBLE(x) ((x)+(x))
int main()
{
printf("%d\n", 10*DOUBLE(3+M));
//printf("%d\n", 10 * 3+3);//33
printf("M = %d\n", 10);
return 0;
}
所以不要吝啬括号的使用
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意事项:
宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
以下用的不多
把参数加入字符串中怎么加,可以用到#
#define PRINT(n) printf("the value of "#n" is %d\n", n)
int main()
{
int a = 10;
PRINT(a);
int b = 20;
PRINT(b);
return 0;
}
#n
就是表示参数
两个#号放在一起的话就表示连接符号,下面的代码输出时10000
#define CAT(C, num) C##num
int main()
{
int Ababa = 10000;
printf("%d\n", CAT(A, baba));//Ababa
return 0;
}
当宏参数在宏的定义中出现超过一次的时候,可能带有副作用的代码会使得产生问题
x+1;//不带副作用
x++;//带有副作用
宏的优点:
宏比函数在程序的规模和速度方面更胜一筹。
宏是类型无关的。
宏的缺点:
⚡️ 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
⚡️ 宏没法调试。
⚡️ 宏由于类型无关,也就不够严谨。
⚡️ 宏可能会带来运算符优先级的问题,导致程容易出现错。
另外对于宏来说有着特殊的命名要求就是要全大写
用于移除一个宏定义。
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define MAX(x, y) ((x)>(y)?(x):(y))
int Max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
#undef MAX
int m = MAX(2, 3);//“MAX”未定义
printf("%d\n", m);
return 0;
}
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假
定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一
个机器内存大写,我们需要一个数组够大。)
#include
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]);
}
printf("\n" );
return 0;
}
命令行中这样输入
gcc -D ARRAY_SIZE=10 programe.c
通过条件编译指令,在编译一个程序的时候我们可以将一条语句(一组语句)编译或者放弃是很方便的。
对那些暂时不需要但又弃之可惜的代码可以使用条件编译
常见的条件编译指令:
//1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
//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(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
和循环很像
#define M 150
int main()
{
#if M<100
printf("less\n");
#elif M==100
printf("==\n");
#elif M>100&&M<200
printf("more\n");
#else
printf("hehe\n");
#endif
return 0;
}
下面的定义方式是一个意思
#define M 0
int main()
{
//1.define
#if defined(M)
printf("hehe\n");
#endif
#ifdef M
printf("haha\n");
#endif
//2.!define
#if !defined(M)
printf("hehe\n");
#endif
#ifndef M
printf("haha\n");
#endif
return 0;
}
利用条件编译来实现代码裁剪,这样的目的是能够在一份代码中实现两个版本,也就是减少维护两份代码的开支
举个例子就比如说,一个编译器有两种版本的话,社区版肯定比完整版功能少,这就可以一份代码中,设置条件编译,使得某些功能不执行,来实现版本的不同~
我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于#include 指令的地方
一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
⚡️ 包含本地文件
#include "filename"
⚡️ 包含库文件
#include
为了防止文件的重复包含可以使用条件编译解决
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
或者是
#pragma once
以避免头文件的重复引入
由多个源文件组成的C程序,经过编辑、预处理、编译、链接等阶段会生成最终的可执行程序。下面哪个阶段可以发现被调用的函数未定义?( )
预处理
编译
链接
执行
预处理只会处理#开头的语句,编译阶段只校验语法,链接时才会去找实体,所以是链接时出错的
预处理:相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
编译:将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。
以下关于头文件,说法正确的是( )
#include
,编译器寻找头文件时,会从当前编译的源文件所在的目录去找
#include“filename.h”
,编译器寻找头文件时,会从通过编译选项指定的库目录去找
多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,是好的编程习惯
在大型项目开发中,把所有自定义的数据类型、函数声明都放在一个头文件中,各个源文件都只需要包含这个头文件即可,省去了要写很多#include语句的麻烦,是好的编程习惯。
尖括号是直接去库找,双引号是先从当前目录找,再去库里找。头文件不能定义全局变量,否则如果有多个文件,那链接时会冲突。D也不是十全十美,在大型项目的开发中,这也并不是一个很好的编程习惯,分类放在不同的头文件并根据特点命名是更好的选择,因为这样更加方便代码的管理和维护,算是一个好习惯吧。
写一个宏,可以将一个整数的二进制位的奇数位和偶数位交换(32位)
奇数位拿出,那就是要&上010101010101……,偶数位拿出,就是要&上101010101010……
奇数位左移一位就到了偶数位上,偶数位右移一位就到了奇数位上,最后两个数字或起来,就完成了交换。
#define Swap(x) (((n) & 0x55555555) << 1 | ((n) & 0xaaaaaaaa) >> 1)
StructType是结构体类型名,MemberName是成员名。具体操作方法是:
1、先将0转换为一个结构体类型的指针,相当于某个结构体的首地址是0。此时,每一个成员的偏移量就成了相对0的偏移量,这样就不需要减去首地址了。
2、对该指针用->访问其成员,并取出地址,由于结构体起始地址为0,此时成员偏移量直接相当于对0的偏移量,所以得到的值直接就是对首地址的偏移量。
3、取出该成员的地址,强转成size_t并打印,就求出了这个偏移量。
#define offsetof(StructType, MemberName) (size_t) &(((StructType *)0)->MemberName)