步调环境和预处置处罚

[复制链接]
发表于 2025-11-19 01:29:34 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
本章重点:

  • 步调的翻译环境
  • 步调的实行环境
  • 讲授:C语言步调的编译+链接
  • 预界说符号先容
  • 预处置处罚指令#define
  • 宏和函数的对比
  • 预处置处罚操纵符#和##的先容
  • 下令界说
  • 预处置处罚指令#include
  • 预处置处罚指令#undef
  • 条件编译

1.步调的翻译环境和实行环境

 在ANSIC的任何一种实现中,存在两个差别的环境。
        第一种是翻译环境,在这个环境中源代码被转换为可实行的呆板指令(呆板指令就是二进制指令)。
        第二种是实行环境,它用于现实实行代码。
2.详解编译+链接

1,翻译环境




  • 构成每一个步调的每个源文件通过编译过程分别转换成目的代码(object code)
  • 每个目的文件由链接器(linker)捆绑在一起,形成一个单一而完备的可实行步调
  • 链接器同时也会引入标准C函数库中任何被步调所用到的函数,而且它可以搜索步调员个人的步调库,将其必要的函数也链接到步调中。
2,编译本身也分为几个阶段



检察代码每一步都发生了什么:(Linux操纵体系,gcc步调)
1,预处置处罚 选项gcc test.c - E -o test.i
      预处置处罚完成之后就停下来将产生的结果放到放在test.i文件中
2,编译 选项gcc test.c -S(gcc test.i - S,直接从test.i就是预处置处罚后开始编译)(gcc test.c -S表现从预处置处罚前就是从源文件开始编译)
      编译完成后就停下来,结果生存在test.s中
3,汇编 gcc-c test.c
      汇编完成后就停下来,结果生存在test.o中
编译这个过程的学习可以参考《编译原理》这个课程
和《步调员的自我修养》这本书。
VIM学习资料
简明VIM练级攻略
https://coolshell.cn/articles/5426.html
给步调员的VIM速卡
https://coolshell.cn/articles/5479.html
3,运行环境


  • 步调必须载入内存中。再有操纵体系的环境中:一样寻常这个有操纵体系完成。在独立的环境中,步调的载入必须手工安排,也大概是通过可实行代码置入只读内存来完成。
  • 步调的实行便开始,接着便调用main函数
  • 开始实行步调代码。这个时间步调员将利用一个运行时堆栈(stack)(函数栈帧),存储函数的局部变量和返回地点。步调同时也可以利用静态(static)内存,存储于静态内存中的变量是在步调的整个实行过程不停生存它们的值。
  • 停止步调。正常停止main函数,也大概不测停止。
3.预处置处罚详解

1,预界说符号

   __FILE__        //举行编译的源文件
__LINE__        //文件当前的行号
__DATE__       //文件被编译的日期
__TIME__        //文件被编译的时间
__STDC__       //如果编译器依照ANSI C,其值为1,否则未界说
    (  对于这个gcc编译器为1,VS编译器未界说)
  常用编译器:gcc clang(苹果维护的)
  这些预界说符号都是语言内置的。

(我们写日记的话可以将日期写上去)

2,#define

2.1 #define界说的标识符

   语法:
  #define name stuff 
  1. #define MAX 100
  2. #define reg register   //为register这个关键字创建一个简单的名字
  3. #define do_forever for(;;)    //用更形象的符号来替换一种实现
  4. #define CASE break;case    //写case语句的时候自动把break写上
  5. //如果定义的stuff过长,可以分行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)(续行符转义回车,让回车不是回车)
  6. #define DEBUG_PRINT printf("file:%s\tiline:%d\t\
  7.                                                         date:%s\ttime:%s\n",\
  8.                                                         __FILE__,__LINE__,\
  9.                                                         __DATE__,__TIME__)
复制代码
 #define在预处置处罚时就被更换,在VS编译器下也能看到预处置处罚的结果

会在文件夹中产生.i的文件。 
//可以相识一下国际乱码大赛
2.2#define界说的宏

#define机制包罗一个规定,答应把参数更换到文本中,这种实现通常称为宏(macro)或界说宏(define macro)
下面是宏的阐明方式:
#define name(parament-list)stuff
此中parament-list是一个由逗号隔开的符号表,他们大概出如今stuff中。
注意:
参数列表必须与name紧邻。
如果两者之间有任何空缺的存在,参数列表就会被表明为stuff的一部分
  1. #define SQUARE(X)  X*X
  2. int main()
  3. {
  4.         int r = SQUARE(5);//5*5//25
  5.         //int r = SQUARE(5 + 1); //5+1*5+1//11
  6.         printf("%d\n", r);
  7. }
复制代码
  1. #define DOUBLE(X)  (X)+(X)
  2. int main()
  3. {
  4.         int a = 5;
  5.         printf("%d\n", 10 * DOUBLE(a));//10*5+5=55
  6.         return 0;
  7. }
复制代码
注意宏是完成更换的,不是完成盘算的,所我们在利用的时间通常要在盘算部分,和总的结果部分都加括号。
   比如上述两段代码宏的界说我们都应该写成
  #define SQUARE(X)  ((X)*(X))
  #define DOUBLE(X)  ((X)+(X))
   提示:用于对数值表达式举行求值的宏界说都应该用这种方式加上括号,制止在利用宏的时间由于参数中操纵符或相近操纵符之间不可预料的相互作用。
2.3#define更换规则               

 在步调中扩展#define界说符号和宏时,必要涉及以下几个步调:   
       1.在调用宏时,起首对参数举行查抄,看看是否包罗任何由#define界说的符号。如果是,他们起首被更换。
       2.更换文本随后被插入到步调中原来文本的位置。对于宏,参数名被它们的值所更换。
       3.末了,再次对结果文件举行扫描,看看他们是否包罗任何由#define界说的符号。如果是,就重复上述过程
  1. #define M 100
  2. #define DOUBLE(X)  (X)+(X)
  3. int main()
  4. {
  5.         DOUBLE(M + 2);//DOUBLE(100 + 2);
  6.         //((100 + 2)+ (100 + 2))
  7.         return 0;
  8. }
复制代码
注意:

  • 宏参数和#define界说中可以出现其他#define界说的符号。但是对于宏,不能出现递归。
  • 当预处置处罚器搜索#define界说的符号的时间,字符串常量的内容并不被搜索。 (字符串常量被以为在字符串内部)
  1. ​
  2. #define M 100
  3. #define DOUBLE(X)  (X)+(X)
  4. int main()
  5. {
  6.         "M";
  7.         "DOUBLE(3)";
  8.         return 0;
  9. }
  10. ​
复制代码


  • 2.4#和##
#是将宏参数变成对应字符串。 
##可以把位于他两边的符号归并成一个符号,它答应宏界说从分离的文本片断创建标识符

1,怎样把参数插入到字符串中

 起首我们必要知道字符串是可以分开输入的

 观察一下代码
  1. #include<stdio.h>
  2. int main()
  3. {
  4.         int a = 10;
  5.         print(a);
  6.         printf("the value of a is %d\n", a);
  7.         int b = 20;
  8.         print(b);
  9.         printf("the value of b is %d\n", b);
  10.         return 0;
  11. }
复制代码
我们可以发现输出部分有很多重复的,但是我们写一个函数的话无论怎样计划都不能把参数插入到字符串中,这个时间我们就可以界说一个宏来实现
  1. #define print(N) printf("the value of "#N" is %d\n",N);
  2. int main()
  3. {
  4.         int a = 10;
  5.         print(a);
  6.         //printf("the value of a is %d\n", a);
  7.         int b = 20;
  8.         print(b);
  9.         //printf("the value of b is %d\n", b);
  10.         return 0;
  11. }
复制代码
我们还可以通过宏打印差别范例的数据
  1. #define print(N,FORMAT) printf("the value of "#N" is "#FORMAT "\n",N);
  2. int main()
  3. {
  4.         int a = 10;
  5.         print(a,%d);
  6.         //printf("the value of a is %d\n", a);
  7.         double b = 20.535;
  8.         print(b,%lf);
  9.         //printf("the value of b is %d\n", b);
  10.         return 0;
  11. }
复制代码
##能把两边的符号归并成一个




  • 2.5带副作用的宏参数
 当宏参数再宏的界说中出现高出一次的时间,如果参数带副作用,那么利用这个红的时间就有大概出现伤害,导致不可猜测的结果。副作用就是表达式求职的时间出现永世性的结果
   比方:
x+1;//不带副作用
  x++;//带有副作用,用过之后x的值发生了改变
  如我们是有一个三目操纵符界说MAX
  1. #include<stdio.h>
  2. #define MAX(a,b) ((a)>(b)?(a):(b))
  3. int main()
  4. {
  5.         int a = 5;
  6.         int b = 4;
  7.         int m = (a++, b++);
  8.         //int m=((a++)>(b++)?(a++):(b++))
  9.         printf("m=%d\n",m);
  10.         printf("a=%d,b=%d\n", a, b);
  11.         return 0;
  12. }
复制代码
  分析这段代码:起首后置++先利用后++,5>4,错误举行(a++),这个时间a=6,b=5,,盘算结果返回6,即m=6,(a++)后a=7
  这个是时间就没有待会我们想要的两个数中较大的值
  

  • 2.6宏和函数的对比
像上面代码我们用宏来找两个书中的叫大数
有两个利益
  1.用于调用函数和从函数返回的代码大概比现实实行这个小型盘算工作必要的时间更多,以是宏,在时间上更胜一筹。 (宏之间带入数值盘算,函数必要创建函数栈帧,参数传参,盘算,函数返回)
  2.函数的参数必须声明为特定的范例。以是函数只能在范例符合的表达式上利用。繁殖这个宏可以实用于整形,长整型,浮点型等可以用来比力能用>来比力的任何范例,宏是范例无关的。更具有机动性。
固然,宏和函数比也有劣势和缺点
  1.每次利用宏的时间,一份宏界说的代码将插入到步调中。除非宏比力短,否则大概大幅增长步调的长度。
  2.宏是没法调试的(宏在预处置处罚阶段就完成了数值的更换,但是调试是完成编译和链接,并天生可实行步调之后举行的)
  3.由于范例无关,宏也不敷严谨。
  4.宏大概会带来运算符优先级的题目,导致步调容易堕落。(利用宏的时间肯定要多加括号)
宏有的时间能做到函数做不到的事变。比如:宏的参数可以出现范例,但是函数做不到。
  1. #define MALLOC(num,type) malloc(num*sizeof(type));
  2. int main()
  3. {
  4.     //malloc(40);
  5.     //malloc(10, int);
  6.     int* p= MALLOC(10, int);
  7.     //int *p = malloc(10*sizeof(int));
  8.     return 0;
  9. }
复制代码
宏和函数的对比
属性#define界说宏函数代码长度每次利用时,宏代码都会被插入到步调中。除了非常小的宏外,步调长度会大幅度增长函数代码只出现于一个地方;每次利用这个函数时,都调用那同一个地方的函数。实行速率更快存在函数的调用和返回的额外开销,以是相对慢一些操纵符优先级宏参数的求值,是在全部四周表达式的上下文环境里,除非加上括号,否则邻近操纵符的优先级大概会产生不可预料的结果,以是发起宏在誊写的时间多写些括号函数参数值在函数调用的时间求值一次,他的结果值传给函数,表达式求值的结果更容易猜测。带有副作用的参数参数大概被更换到宏体中多个位置,以是带有副作用的参数求值大概会产生不可预料的结果。函数参数只在传参的时间求值一次,结果更容易控制。参数范例宏的参数与范例无关,只要对参数的操纵是正当的,他就可以实用于任何参数范例。函数的参数是与范例有关的,如果参数范例差别,就必要差别的函数,纵然他的执利用命是雷同的。调试宏是不方便调试的函数时可以逐语句调试的递归宏是不能递归的函数是可以递归的2.7定名约定

一样寻常来讲,函数的宏的利用语法很相似。以是语言本身没法帮我们区分二者。那我么寻常的一个风俗是:把宏名全部大写(MAX),函数名不要全部大写(Max)
但是也有一些宏会用小写:比方offsetof,另有有写getchar也是用宏界说的。
3,#undef

这条指令用于移除一个宏界说。
   #undef NAME
  //如果现存的一个名字必要被重界说,那么它的就名字起首要被移除。
  1. #define M 100
  2. int main()
  3. {
  4.         printf("%d\n", M);
  5. #undef M
  6.         printf("%d\n", M);//识别不出来
  7.         return 0;
  8. }
复制代码
4,下令行界说(Linux体系下gcc编译器下实现)

很多C语言编译器提供了一种本领,答应在下令行中界说符号。用于启动编译过程。
比方:当我们根据同一个源文件要编译出一个步调的差别版本的时间,这个特性有点用处。(假定某个步调中声明确一个某个长度的数组,如果呆板内存有限,我们必要一个很小的数组,但是别的一个呆板,内存大些,我们必要一个数组可以大概大些。)
  1. #include<stdio.h>
  2. int main()
  3. {
  4.         int arr[SZ];
  5.         int i = 0;
  6.         for (i = 0;i < SZ;i++)
  7.         {
  8.                 arr[i] = i;
  9.         }
  10.         for (i = 0;i < SZ;i++)
  11.         {
  12.                 printf("%d ", arr[i]);
  13.         }
  14.         return 0;
  15. }
复制代码
可以在gcc编译器下先界说SZ在运行:gcc test.c -D SZ=10   (回车)./a.out
gcc test.c -D SZ=100   (回车)./a.out
-D是界说,与SZ间的空格无关紧要。
5,条件编译

在编译一个步调的时间我们如果要将一条语句(一组语句)编译大概放弃是很方便的。由于我们有条件编译指令。
比如说:调试性代码,像printf,删除可以,生存又碍事,我们可以选择性的编译
  1. #include<stdio.h>
  2. #define __DEBUG__//不需要的话直接将他注释掉
  3. int main()
  4. {
  5.         int i = 0;
  6.         int arr[10] = { 0 };
  7.         for (i = 0;i < 10;i++)
  8.         {
  9.                 arr[i] = i;
  10. #ifdef __DEBUG__
  11.                 printf("%d ", arr[i]);
  12. #endif //DEBUG
  13.         }
  14. }
复制代码
   1:
  #if 常量表达式
     //……
#endif 
//常量表达式由于处置处罚器求值
  如:
#define __DEBUG__ 1
#if __BEBUG__ //为真实行
   //……
#endif
  
  #if 0//为真实行
//……
#endif
  
  #if 2==3//为真实行
//……
#endif
    2.多个分支的条件编译
#if 常量表达式
//……
#elif 常量表达式
//……
#else 
//……
#endif 
  
  #define M 3
int main()
{
#if M<5
    printf("good\n");
#elif M==5
    printf("nice\n");
#else 
    printf("perfect\n");
#endif
    return 0;
}
    3.判断是否被界说
#if define(symbol)
#ifdef symbol
  #if !define(symbol)
#ifndef symbol
  
  //#define MAX
int main()
{
#if define(MAX)
    printf("max\n");
#endif
    return 0;
}
  
  int main()
{
#ifdef MAX
    printf("max\n");
#endif
    return 0;
}
     4.嵌套指令
#if defined(OS_UNIX)
    #ifdef OPTION1
          unix_version_option1();
    #endif
    #ifdef OPTION2
          unix_version_option2();
    #endif
#elif defined(OS_UNIX)
    #ifdef OPTION2
          unix_version_option1\2();
    #endif
#endif
   在头文件中会有很多如许的嵌套指令
6,文件包罗

6.1头文件包罗

我们已经知道,#include指令可以使别的一个文件被编译。就像它现实出现于#include指令的地方一样。
这种更换方式很简朴:
预处置处罚器先删除这条指令,并用包罗文件的内容更换。
如许源文件被包罗10次,那就是即被编译10次
我们在写本身的头文件的时间有两种方法可以制止头文件被多次包罗
  1. #ifndef __TEST_H__
  2. #define __TEST_H__
  3. int ADD(int x, int y);
  4. #endif
复制代码
  1. #pragma once
  2. int ADD(int x, int y);
复制代码
之前我们在说引用库里的头文件的时间用<>,引用本身的头文件用""
<>和""的区别是查找计谋差别
#include<stdio.h>
<>查找计谋:直接去库目次下查找
#include"test.h"
""查找计谋:
1.先去代码地点的路径下查找。
2.如果上面找不到,再去库目次下查找。
查找库文件也可以用"",但是服从低,最好不消。
6.2嵌套文件包罗


防止头文件被嵌套包罗,我们可以接纳上面两种方式来制止。
在《高质量C/C++编程指南》中附录的测验试卷(很紧张)
就有如许两道题
   1.头文件中的ifndef/define/endif是干什么用的
  2.#include<filename.h>和#include"filename.h"的区别
  4.其他预处置处罚指令 

   #error
  #pragma
  #line
  ……
  #pragma pack()在结构体部分先容
  参考《C语言深度解剖》学习。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表