目录
预处理
文件的预处理
演示环境使用Linux下的GCC
gcc -E test.c -o test.i :(o的意思是output(输出的意思))
gcc -E test.c :将文件test.c操作后的结果输出到屏幕上
gcc -S test.c :将文件操作后默认生成一个名叫test.s的文件(打开这个文件后,里面全部装的是汇编代码)
gcc -S test.i -o test.s : 将文件操作后指定生成一个名叫test.s的文件(打开这个文件后,里面全部装的是汇编代码)
gcc -c test.s : 将文件指定步骤编译后默认生成一个叫test.o的文件(打开这文件后,你会在屏幕上看到一堆乱码,需要用指定的文本编辑器打开(二进制文本文件编辑器))
gcc -c test.s -o test.o: 将文件指定步骤编译后指定生成一个叫test.o的文件(打开这文件后,你会在屏幕上看到一堆乱码,需要用指定的文本编辑器打开(二进制文本文件编辑器)(readelf))
读取机器语言代码需要一个指令:readelf(工具解析elf格式的文件)(test.o是elf格式的文件)
readelf -a test.o :
readelf -s test.o :
1.预编译
(1)头文件的展开
(2)删除注释(注释被空格替换)
(3)宏删除与宏替换
2.编译 将C语言代码转化为了汇编代码(感兴趣的话可以去学习编译原理这门课)(相关书籍《程序员的自我修养》)
(1)语法分析
(2)词法分析
(3)符号汇总
(4)语义分析
3.汇编 将汇编代码转回为机器语言代码
注意:在Windows环境下的目标文件后缀为.obj 例如:test.obj
但在Linux环境下目标文件的后缀为.o 例如:test.o
(1)形成符号表
4.链接
(1)合并段表
(2)符号表合并和重定位
多个外部文件进行链接的时候会通过符号表来查看来自外部的符号是否真实存在
如果查看符号表有误的话外部的符号并非真实存在的,就会报错
运行环境
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的。
#define
#define 定义标识符
语法:
#define name stuff
for example:
#define MAX 1000 #define reg register //为 register这个关键字,创建一个简短的名字 #define do_forever for(;;) //用更形象的符号来替换一种实现 #define CASE break;case //在写case语句的时候自动把 break写上。 // 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。#define DEBUG_PRINT printf("file:%s\tline:%d\t \ date:%s\ttime:%s\n" ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )
那么在define定义标识符的时候,要不要加“ ;”?
不要。这样会导致存在一些潜在的问题。宏的本质是替换。例如:
#define MAX 1000;
if(condition)
max = MAX;
else
max = 0;
这样做会使得多带一个“ ; ”,致使这个条件变成一个错误的。
#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro)。
注意: 参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
#define MAX(x,y) x>y?x:y
int main()
{
int a = 10;
int b = 20;
printf("%d\n", MAX(a, b));
return 0;
}
宏的隐性问题:
#define DOUBLE(x) x+x
int main()
{
printf("%d\n", DOUBLE(3));
}
也许这样传参会没有问题,但我使用其他方式传参呢,还能得到我想要的结果吗?
#define DOUBLE(x) x+x
int main()
{
printf("%d\n", 10*DOUBLE(3+3));
}
因此宏后面的格式应当如何写呢?
#define DOUBLE(x) ((x)+(x))
int main()
{
printf("%d\n", 10*DOUBLE(3+3));
}
加上合适的括号,改变优先级,但也不要加太多
用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。
2.2.3 #define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先 被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。
注意: 1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。例:
#define M 100
int main()
{
printf("M = %d\n", M);
}
一种奇怪的宏
printf奇怪的用法
int main()
{
printf("hello world!\n");
printf("hello ""world!\n");
printf("hell""o w""orld!\n");
return 0;
}
“ # ”这种宏的奇怪用法:
#define PRINT(x) printf("the value of "#x" is %d\n",x)
int main()
{
int a = 10;
int b = 20;
PRINT(a);
PRINT(b);
return 0;
}
##
(见到知其意思即可)
##的作用:
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
#define CAT(C,NUM) C##NUM
int main()
{
int helloVS = 10000;
printf("%d\n", CAT(hello, VS));
return 0;
}
带有副作用的宏参数
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
int a = 3;
int b = 5;
int c = MAX(a++, b++);
int d = a++ > b++ ? a++ : b++;
printf("%d %d\n", c);
printf("%d %d\n", a, b);
return 0;
}
变量c和变量d不是相同的;因为无论是a++的操作还是b++的操作都影响了a和b的值;但MAX(a++,b++)的替换结果是a++>b++?a++:b++(这样的操作会对变量本身造成影响)(尽量不要使用带有副作用的宏参数,用其他的方式去替代)
宏和函数的对比
宏通常被应用于执行简单的运算
比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不用函数来完成这个任务?
#define MAX(a,b) ((a)>(b)?(a):(b))
int max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int a = 10;
int b = 20;
int c = max(a, b);
int d = MAX(a, b);
printf("%d %d\n", c, d);
return 0;
}
实现同样的逻辑功能,函数调用所需要的时间为:函数调用+逻辑运算+函数返回
宏所需要的时间为:逻辑运算
宏花费的时间远远小于函数(在只需要很短的代码就能就能实现的功能方面)
原因有二:
1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。 宏是类型无关的。 宏的缺点:
当然和函数相比宏也有劣势的地方:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
有些只有宏能做到的场景而函数做不到的场景
假如说,你想这样传参:一个参数传一个整数,另一个参数一个类型,这种做法,函数是做不到的,只有宏能做到,试想一下以下场景:
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
int* p1 = (int*)malloc(10 * sizeof(int));
int* p2 = MALLOC(10, int);
free(p1);
free(p2);
return 0;
}
宏的优点是不是体现出来了呢
宏和函数的比较
属 性 | #define定义宏 | 函数 |
代 码 长 度 | 每次使用时,宏代码都会被插入到程序中。除了非常 小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每 次使用这个函数时,都调用那个 地方的同一份代码 |
执 行 速 度 | 更快 | 存在函数的调用和返回的额外开 销,所以相对慢一些 |
操 作 符 优 先 级 | 宏参数的求值是在所有周围表达式的上下文环境里, 除非加上括号,否则邻近操作符的优先级可能会产生 不可预料的后果,所以建议宏在书写的时候多些括 号。 | 函数参数只在函数调用的时候求 值一次,它的结果值传递给函 数。 |
带 有 副 作 用 的 参 数 | 参数可能被替换到宏体中的多个位置,所以带有副作 用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一 次,结果更容易控制。 |
参 数 类 型 | 宏的参数与类型无关,只要对参数的操作是合法的, 它就可以使用于任何参数类型 | 函数的参数是与类型有关的,如 果参数的类型不同,就需要不同 的函数,即使他们执行的任务是 不同的。 |
调 试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递 归 | 宏是不能递归的 | 函数是可以递归的 |
命名约定
一般来讲函数的宏的使用语法很相似。
所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是: 把宏名全部大写 函数名不要全部大写
#undef
这条指令用于移除一个宏定义。
#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
#undef MALLOC
int* p1 = (int*)malloc(10 * sizeof(int));
int* p2 = MALLOC(10, int);
return 0;
}
#undef MALLOC 把MALLOC这个宏取消后,程序就报错了,显示未定义
命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假 定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)
#include<stdio.h>
int main()
{
int arr[SZ];
for(int i=0;i<SZ;i++)
{
arr[i]=2*i;
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}
注意:演示环境为Linux
指令:gcc file.c -D(中间可以有空格,也可以没有)SZ=100 -std=c99