目录

1.环境

2.翻译环境

2.1编译

a.预处理

1.#define 定义常量

2.#define 定义宏(参数)

3.使用宏时不合适参数的副作用

4.宏替换的规则

5.宏与函数

6.两个奇葩的操作符# 和 ##

7.命名 与函数的不同

8.#undef

9.命令行定义

10.条件编译

11.头文件的包含

12.嵌套包含

b.编译

c.汇编

2.2链接

3.运行环境 


1.环境

在ANSI定义的c语言国际标准中,存在两个不同的环境:翻译环境和执行环境

 机器只能理解二进制语言,翻译环境就让源代码变成二进制文件(后缀为.exe的文件,如果强制用记事本打开会看到一堆乱码)

2.翻译环境

Windows中,从.c文件生成目标文件(.obj)(linux环境下的目标文件后缀是.o)的过程叫编译,VS使用的编译器名字是cl

目标文件中的内容已经是二进制了。

• 多个.c⽂件单独经过编译出编译处理⽣产对应的⽬标⽂件。
• 多个⽬标⽂件和链接库⼀起经过链接器处理⽣成最终的可执⾏程序。
• 链接库是指运⾏时库(⽀持程序运⾏的基本函数集合)或者第三⽅库。

2.1编译

编译可分为 预处理(预编译) 编译   汇编 三个步骤。

a.预处理

预处理生成的是临时文件(后缀为.i),使用完后会删除

        预编译的文本操作包括但不限于:处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif (#是C语言预处理的标志,会在下文介绍) ,把头文件的内容包含进去,并且替换#define的内容,删除掉所有的注释。整个过程可能是递归进行的(头文件里包含了其他的头文件)。

被预编译的文件应当不再包含宏,头文件都被插入,注释也已经被删除。

接下来我们对预处理详细介绍(包含宏等内容,可直接跳过阅读编译相关内容)。

1.#define 定义常量

先名字后内容 如:#define name stuff的意思就是name代表stuff了

预处理之后stuff就会被换成name了。还可以写成 #define str "hiehiehie"   

#define m 100+2       等写法,预处理之后都是进行单纯的文本替换

注意:#define之后不建议加上分号,因为分号也会被当作文本内容替换进去。

2.#define 定义宏(参数)

#define机制包括了⼀个规定,允许把参数替换到⽂本中,

这种实现通常称为宏(macro)或定义宏
注意 :
参数列表的左括号必须与参数名紧邻 ,如果两者之间有任何空⽩存在,参数列表就会被解释为stuff的⼀部分。


如下图

#define SQUARE(X) X*X

int main()
{
   int a=5;
   printf("%d",SQUARE(a));
//预处理之后就应该是printf("%d",a*a);

   return 0;
}

但是注意,这样的话会有宏的通病:宏是简单的文本代换,并不会对参数进行计算。

如下例:

#define Square(x) x*x

int main(){

int a=5;
printf("%d\n",Square(a+1));


return 0;
}

预处理之后就会被翻译为

int main(){

int a=5;
printf("%d\n",a+1*a+1);

return 0;
}

 在之后编译的词法扫描语法分析阶段就会由于运算优先级的问题而出错。

//可以改为
#define Square(x)  (x)*(x)

也就是宏的参数中的操作符和宏的内容中的操作符可能因为优先级的问题而导致运算顺序不达预期,尽量给参数都带上括号来有效解决 。

更进一步:

#define Double(x)  x+x

//一个求参数双倍的宏

int main(){
 int x=3;
printf("%d",3*Double(3));

return 0;
}

又出现了优先级的问题。

所以我们建议不仅每个参数要上括号,整体表达式也加上括号,不要吝啬你的括号

#define Double(x)  ((x)+(x))
3.使用宏时不合适参数的副作用

   如,求两数最大值的宏:

此时确实能正常工作,但如果我们更换max的参数:

int m=MAX(a++,b++);

 预处理替换后就变成

a与b的值就产生了我们不希望的变化。

4.宏替换的规则

   在程序中扩展#define定义符号和宏时,需要涉及⼏个步骤。

1. 在调⽤宏时,⾸先对参数进⾏检查,看看是否包含任何由#define定义的符号。如果是,它们⾸先被替换。(检测并替换宏中宏)
2. 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。                                  如上图,printf中左边的字符串M不会被替换,其余所有M都会被替换
5.宏与函数

是不是感觉宏与函数功能很像?

一般宏会用来进行简单的运算,原因有二:

1. ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐ 函数在程序的规模和速度⽅⾯更胜⼀筹。
比如上文的max功能的宏,如果用函数来实现,调用函数就有很多与计算无关的调用,返回等指令,而宏的汇编语句只有一句话。
2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之 。
    还是比如上文的max,这个宏可以适⽤于整形、⻓整型、浮点型等可以⽤于 > 来⽐较的类型。宏是类型⽆关的。而一个函数只能比一个类型。
    对于一个简单的运算,宏2ms,函数3ms,那么宏就有很高的效率(比函数高百分之三十三),而对于1s的宏与1.01s的函数,宏的快速就微不足道了。
宏同时也有劣势:
1. 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
2. 宏是没法调试的,而函数可以调试(调试是在编译之后,编译之后宏都已经被展开)
3. 宏由于类型⽆关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
6.两个奇葩的操作符# 和 ##
      #运算符将宏的⼀个参数转换为字符串字⾯量(会带上双引号)。它仅允许出现在带参数的宏的替换列表中。
#运算符所执⾏的操作可以理解为”字符串化“。
#define PRINT(n) printf("the value of "  #n  "is %d", n);

int main(){

int a=5;
PRINTF(a);

return 0;
}

因为宏的替换不会搜索字符串中的内容,所以如果写成

#define PRINT(n) printf("the value of  n  is %d", n); 双引号中的n就不会被替换
而#a就会被理解成"a",  从而被理解为第二个字符串而打印出来了。(c语言允许多个字符串贴在一起进行打印,如:

printf("hello" "world" "\n");

)

换句话说就是把宏的真实参数名称替换成字符串。

##被称为记号粘合。

    比如我们任然想写一个与比较大小有关的函数,希望他能比较多种类型

int int_max(int x, int y)
{
return x>y?x:y;
}


float float_max(float x, float y)
{
return x>yx:y;
}

//还有很多类型...........

过于繁琐,

    如果使用宏来作为函数的模版:

#define GENERIC_max(type)  \
type  type_max(type x,type y); \
{\
       return (x>y?x:y);\
}

       我们就可以通过GENERIC_max(int)   GENERIC_max(float)     GENERIC_max(double)等形式得到不同的函数,但是预处理(生成.i文件)之后并没有完全变成我们想要的样子:

并且在之后的编译过程中还会由于这样的语法错误而报错(type是宏参数),语法就是这么规定的。
#define GENERIC_max(type)  \
type  type##_max(type x,type y); \
{\
       return (x>y?x:y);\
}

使用##连接两边的符号形成同一个符号,就能生成

int int_max(int x,int y){return (x>y?x:y);}     正确的使用了语法,得到了期望的函数名称。

    ##可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。 ## 被称为记号粘合,这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
 

7.命名 与函数的不同

   ⼀般来讲函数的宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
一般的,我们平时的⼀个习惯是:
把宏名全部⼤写(上文的代码多有不规范)
函数名不要全部⼤写
 (也有例外,比如·包含在<stddef.h>中的offsetof就是全小写,用于计算结构体成员相对于起始位置的偏移量)

这是命名的“潜规则”,这样使用能更加规范化自己的代码。

8.#undef

   这条指令⽤于移除⼀个宏定义。

#define NAME 5

#undef NAME

NAME此时就是未定义的。

9.命令行定义

   在linux中实现。

10.条件编译

在大型代码中常见。

顾名思义,满足条件参与编译,不满足条件不参与编译。

在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃,就使用条件编译。

         比如在一段代码中出现了一个错误,你写了一些调试语句来找bug,使用完之后又不希望删除(下次还可能遇到相同的问题,需要相同的调试代码),留着又碍事,就使用#ifdef和#endif来进行条件编译。

#include<stdio.h>
#define __DEBUG__

int main()
{

   int i=0;
int arr=[0];
for(int i=0;i<10;i++){

*(arr+i)=i;

    #ifdef __DEBUG__
printf("%d",arr[i]);
#endif

}

return 0;

}

        预处理阶段,如果满足条件就留下并被编译,不满足条件就直接干掉。

常用的预处理指令:

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

总的来说,写法与c语言的基本语法很相似

注意,#if   #ifdef等后面的语句不需要加分号。

但是写成如下形式是一个经典的错误

#define M 4
int main()
{
   int a=4;
#if M==a
//调试代码
#endif

return 0;
}

因为这个的判断在预处理阶段,预处理阶段没法判断a。(所以上面强调了是常量表达式,不要使用变量)

11.头文件的包含

本地文件的包含

#include "filename.h"

       先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在
标准位置(如vs的默认路径就是C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include)
查找头⽂件。
        如果找不到就提⽰编译错误。

#include <stdlib.h>

这样的<>直接去标准位置查找,不会在项目路径下查找

        那么是不是说明,我们直接用双引号包含所有的头文件就行了呢??

当然不是的,首先,对于标准库里的文件,先在项目路径下找降低了效率(对于成千上百万行的大项目来说,这样的小细节会影响整体的效率),其次,这样也不便于区分标准库与自己写的头文件。

12.嵌套包含

          一个头文件h被包含进头文件a和头文件b,我们的一个项目既需要a也需要b,那么重复编译h

就会降低整体的效率,浪费时间和空间 我们有以下两种解决方法:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

 或者

#pragma once
//头文件内容
b.编译

   生成汇编代码文件:词法分析 (扫描)    语法分析(产生语法树)  语义分析(包括声明和类型的匹配,并且会报错),生成后缀为.s

c.汇编

   按照机器指令的对照表,将编译时汇编代码翻译成二进制,生成后缀为.o(linux)

2.2链接

 链接解决一个项目中多个文件和模块相互调用的过程

  链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。

我们已经知道,每个源⽂件都是单独经过编译器处理⽣成对应的⽬标⽂件。
假设一个项目中我们有test.c和add.c两个源文件组成。
test.c 经过编译器处理⽣成 test.o
add.c 经过编译器处理⽣成 add.o
假设我们在 test.c 的⽂件中使⽤了 add.c ⽂件中的 Add 函数和 g_val 变量。
       我们在 test.c ⽂件中每⼀次使⽤ Add 函数和 g_val 的时候必须确切的知道 Add 和 g_val 的地
址,但是由于每个⽂件是单独编译的,在编译器编译 test.c 的时候并不知道 Add 函数和 g_val
变量的地址,所以暂时把调⽤ Add 的指令的⽬标地址和 g_val 的地址搁置。等待最后链接的时候由 链接器根据引⽤的符号 Add 在其他模块中查找 Add 函数的地址,然后将 test.c 中所有引⽤到
Add 的指令重新修正,让他们的⽬标地址为真正的 Add 函数的地址,对于全局变量 g_val 也是类
似的⽅法来修正地址。

具体我们只以下例进行学习:

编译的过程中还有一步叫符号汇总,就是在生成的add.c和test.c中抽取出我们自定义的函数符号:

然后会在汇编的过程中形成符号表:将抽取出的函数名称和其对应的地址进行匹配

地址的合并:由于extern的函数并不是真实创造一个函数,所以在单独进行编译时不能从右侧的.c的文件中找到add函数在哪里,故为无效地址0x000。合并后只使用各函数的有效地址,达到修正无效地址的目的。

可以说,没有链接器,多个.c后缀的文件就不能很好的在同一个项目下工作。

但是如果注释左边的add,就失去了有效地址,找不到add时编译器就会报错(如上图左下角)。

同时,也不是必须用extern才能在源文件a中使用源文件b中声明的函数,如以下情况:

当我在一个工程中有这么三个文件,

我在test.c里面使用SLInit函数,在SeqList.h中声明了各个函数

 

但是在SeqList.c中中实现的各个函数的具体功能。

那么此时我就不再需要extern,根据以上编译和预处理中头文件的知识,预处理步骤中头文件中的内容都会被复制进入test.c,所以一系列以下函数声明

 

都进入了test.c

编译时不会报错,而是在最后链接的过程中(如上文)再更正 各个函数的真实地址和数据,得意使用。

3.运行环境 

  1.   任何一个程序都需要先载入内存,所以所有程序都占用内存,这也是为什么一台机器同时多开过多程序会卡的原因。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序 的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成。

2. 程序的执⾏便开始。接着便调⽤main函数。

3. 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执⾏过程 ⼀直保留他们的值。
4. 终⽌程序。正常终⽌main函数,内存被回收,数据传向服务器或者以文件的形式存储在本地。当然,也有可能是意外终⽌。

更多推荐