一、程序编译和执行
我们写的程序是文本文件,而计算机不能识别文本文件,可识别二进制文件(可执行文件)
文本文件如何转换成可执行文件?
程序成功运行要经过编译和链接。
编译阶段可分为预编译(预处理),编译,汇编,链接。
预处理阶段是文本操作:注include头文件的包含,#define预处理符号的替换
编译过程:主要是语法,语义,词法分析,符号汇总
汇编:汇编指令转换成程序可识别的二进制指令,形成符号表
链接:段表的合并,符号表的合并和重定位

二、Linux下观察编译和链接
1.预处理
指令;gcc text.c -E -o text.i 将text.c文件预处理的结果输出到text.i文件中
2.编译
指令:gcc -s text.i 编译text.i文件形成text.s编译文件.当然我们也可以一步到位直接对text.c文件进行编译成汇编文件
3.汇编
指令:gcc -c text.i 对text.i文件进行编译形成text.o二进制的汇编文件。同理也可一步到位直接对text.c文件进行汇编编译处理
4.链接
指令:gcc text.o -o text对文件text.o编译链接形成text可执行文件
5.符号汇总和符号表合并重定位
就是全局函数变量等,记录下来形成符号表,列如全局函数Add,定义在add.c文件中,声明在test.c中,两个都有出现,就把它记录下来(符号+地址),合并和重定位就是变成一个,它的地址是有效的那一个,声明函数是虚拟地址,保留定义的那个函数有意义的地址。

这也就可以很好理解为什么我们声明函数却没有定义函数时编译链接的时候会报错,因为Add真正的地址找不到
6.合并段表
首先汇编文件text.o和可执行文件是有格式的 。Linux系统下gcc编译的汇编文件和可执行文件是按ELF格式存放的,根据格式可以分为不同的段比如全局变量分为一块,全局函数分为一块,多个文件有多个段,合并段表就是把多个段相同区域的信息整合起来,变成一个。
elf格式的文件可以用工具readelf识别:readelf text.o -s

三,预处理详解
1.预定义符号
这些符号是c语言内置的
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);//编译的源文件
printf("%d\n", __LINE__);//文件被编译所在的行数
printf("%s\n", __DATE__);//文件被编译的时间
//printf("%s", __STDC__);如果编译器遵循ANSI C,其值为1,否则未定义
return 0;
}
2. 定义标识符
#define MAX 6 //设置标识符的值
#define ret register//为关键字起别名
#define forever for(;;)//用简短的符号代替一种简单的实现
#define CASE break;case //很多时候会忘记写break,这种方式在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define print printf("%s,%d,%s",\
__FILE__,\
__LINE__,\
__DATE__)
int main()//以下是等价的,但是不友好
{
switch (2)
{
case 0:printf("0");
CASE 1:printf("1");
CASE 2:printf("2");
}
switch (2)
{
case 0:printf("0");
break;
case 1:printf("1");
break;
case 2:printf("2");
}
return 0;
}
2.1标识符注意点
#define m 6; error 不建议在最后添加分号,因为替换的时候也会连带分号替换,有时候会出现问题,比如以下这种情况(else总是与它最近的if匹配,多了个分号就找不到和它最近的了)

3.#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义 宏(define macro),也就是
定义宏最好加括号,避免因为优先级的问题导致结果不匹配,有时替换的是算数表达式,它是一个整体。
#define sum1 a+b
#define sum2 ((a)+(b))
int main()
{
int a = 2;
int b = 3;
int b1 = sum1 * sum1;//我们的期望是两个相乘,不加括号就会变成 2+3*2+3=11
printf("%d ", b1);
int b2 = sum2 * sum2;//加括号((2)+(3))*((2)+(3))=25
printf("%d", b2);
return 0;
}
3.1规则

//#define M 8
//#define Sum ((M+b)*(c))
//#define print printf("7等于n + %d",vale)
//int main()
//{
// int c = 9;
// int b = 9;
// int sum = Sum;//Sum宏定义中发现有M的宏定义,预编译时先进行替换((8+b)*(c)),然后再次替换到原来的位置,然后再进行扫描再次进行替换(int sum=((8+b)*(c)))
//
//
//}
4.宏定义中符号#和##
4.1#作用
C语言中双引号,具有连接字符串的功能,列如以下打印函数打印“hello word”.
平时如果我们要打印变量的值,要知道变量名,也要知道正确的打印格式。不同类型的值,打印的格式也是不同的,就像如下函数print2(),print3()。而宏可以实现根据字符串连接的同时也可指定打印格式,不用因为打印格式的不同,而需要写另一个打印格式的函数。
#define print(n,format) printf("the sumber "#n" is "format"\n",n)
void print2(int n)
{
printf("the sumber n is %d\n", n);
}
void print3(float n)
{
printf("the sumber b is %f\n",n);
}
int main()
{
//printf("hello""word\n");
//printf( "helo word\n");
int n = 90;
float b= 35.5;
print(n, "%d");
print2(n);
print(b,"%f");//对b以%f形式打印数据
print3(b);
}

4.2.##的作用
##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
就是预编译后就已经被合成一个符号
#define add(x,y) x##y
//#define add(x,y) (x)##(y) error,不要受前面宏定义的优先级的影响,这里是替换然后合并,加了括号就
变成(Number)(One)
int main()
{
int NumberOne = 123;
printf("%d", add(Number, One));
add(print, f)("hello,word");// 预编译阶段符号合并等价printf("hello,word");
}

5.宏定义存在副作用
当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能 出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。 例如:b++,++a
#define MAX(x,y) (x)>(y)? (x):(y)
int main()
{
int a = 1, b = 2;
int c = MAX(a++, b++);
//如果不了解宏的副作用,你可能认为输出的c=3,a=2,b=3
printf("a=%d b=%d ", a, b);//实际(a++)>(b++)? (a++):(b++)-----1<2?
//1小于2,由于后置++这时a=2,b=3,1<2返回y,但是y=b++,b又加一,这时b=4.
return 0;
}

6.宏和函数的区别
1.宏参数不看类型,只是替换,缺少类型检查,而函数形参必须有,也让宏做了函数做不了的事情
#define malloc(date,type) (type*)malloc(sizeof(type)*date)
int main()
{
int* a = malloc(4, int);//直接传类型,函数做不到
return 0;
}
2.效率方面,简单的功能用宏比用函数高点比如求两个数值大小,因为函数要用到函数调用,函数栈帧创建,计算逻辑,计算返回结果。宏直接计算得出结果。
3.宏在参数是算数表达式,不进行计算直接替换,函数的是需要计算的
#define M(a,b) (a)>(b)? (a):(b)
int Max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int a = 4 + 2;
int b = 8 + 1;
int c = M(a, b);//在linux下gcc进行预编译结果中int c=(4+2)>(8+1)?(4+2):(8+1);
int m = Max(6, 9);
return 0;
}
4.代码长度方面,如果宏定义很长,多次替换的时候,造成代码过长,而函数使用就调用它就可以了。
5.优先级问题。如果不多加注意,容易出现结合性和自己的逻辑不匹配问题。
6.宏不方便调试,它是预编译就替换了,与实际我们看到代码不同,而函数可以调式。
7.宏不可以递归,而函数可以。
既然他们都有优点于就有了内联函数,函数前加了个 inline关键字
7.函数和用宏命名规则
宏最好大写,函数不全部大写
8.#undef
这个取消宏定义
#define M 9
int main()
{
int b = M;
#undef M;
// int c = M;//宏定义已经失效
return 0;
}
7.命令行定义

就是说内存小,用指令指定数组大小,数组长度就短点,内存大点就,数组长度就大点,比较灵活。
这里我们没有对数组的大小进行定义,程序运行不了,,但是许多编译器可以实现对符号的定义,liunx系统下指令,gcc test,c -D sz=100 -o test.o。这里在预编译时标识符sz被替换成100.程序就正常运行了
7.条件编译
在预编译时,我们可以决定代码参不参与编译。
#if 常量表达式 ... #endif //常量表达式由预处理器求值
表达式为真值就参与编译(也就是程序运行阶段,有这个代码,这和条件语句不同,条件语句不管是真值还是假值都进行了编译)

2.多个分支的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif
如果前面不不为真就执行最后那个

3.判断有没有定义
#define M 8
int main()
{
#if defined(M)//方式一
printf("window");
#endif
#ifdef M//方式二
printf("yes");
#endif
#if !defined(m)//方式三等同四,如果m没被定义,就执行
printf("no");
#endif
#ifndef m//四
printf("no");
#endif
}

嵌套
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
8.文件包含
#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样
这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次。
8.1文件包含的形式
库文件包含:例如#include<stdio.h>
本地文件包含:例如#include“test.h”
区别:它们的作用都是相同的,但是查找的方式不同。我们自己定义的头文件“test.h”首先是从当前源文件下所在的目录下查找,找不到才到库里面查找,找不到就报错。<stdio.h>是直接从库里面查找。
8.2嵌套文件的包含
嵌套文件的包含会出现,多次包含头文件的情况,导致编译错误。在vs下我们创建一个头文件它会创建预处理指令,,在一些老的编译器并不支持(#pragma once ),我们必须自己处理,结果如下




被折叠的 条评论
为什么被折叠?



