> 文章列表 > 适合小白学习预处理与程序环境,这篇文章就够了

适合小白学习预处理与程序环境,这篇文章就够了

适合小白学习预处理与程序环境,这篇文章就够了

目录

一. 前言

二. 正文

2.1 “冷知识”:程序环境

2.21 翻译环境——程序从无到有:程序编译 + 链接

2.22 运行环境——程序开跑

2.3  那些鲜为人知:预定义符号

2.4  预处理指令 #define

2. 41 #和## —— 

2.42  宏和函数优劣对比

2.5  #undef:  终结宏

2.6  鸡肋——条件编译(底层对库进行修改使用比较广泛)

2.7  文件包含#include

2. 71 如何解决头文件多重包含?

三. 结语


一. 前言

        本小节跟大家分享预处理与程序环境的相关知识,希望能给大家带来帮助。

二. 正文

2.1 “冷知识”:程序环境

        在 ANSIC 的任何一种实现中,存在两个不同的环境
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代 码。

2.21 翻译环境——程序从无到有:程序编译 + 链接

 如图这是程序由人写的源代码——>机器所能阅读的机器语言的过程图:

  • 组成一个程序的每个源文件通过编译过程分别转换目标代码object code)。
  • 每个目标文件由链接器linker)捆绑在一起,形成一个单一而完整的可执行程序。
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库, 将其需要的函数也链接到程序中。

        

 以上我们对程序编译+链接有了初步的认识,下面是其中的详细步骤,及各步骤的功能,如图:

2.22 运行环境——程序开跑

程序执行的过程
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须 由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用 main 函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈 stack ),存储函数的局部变量和返回地址。程序同时也可以使用静态(static )内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止 main 函数;也有可能是意外终止。

2.3  那些鲜为人知:预定义符号

预定义符号
这些预定义符号都是语言内置的。
比如:
__FILE__       // 进行编译的源文件
__LINE__     // 文件当前的行号
__DATE__     // 文件被编译的日期
__TIME__     // 文件被编译的时间
__STDC__     // 如果编译器遵循 ANSI C ,其值为 1 ,否则未定义
举个栗子:
printf ( "file:%s line:%d\\n" , __FILE__ , __LINE__ );

2.4  预处理指令 #define

这里就是我们最熟悉的#define 了,

用法1:

#define name stuff     (注:这里后面不要加分号“;”,因为在预编译时,stuff会替换程序中所有的name,所以你要的是stuff,不是stuff;)

用法2:

#defifine 定义宏
#defifine 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(defifine macro)。
下面是宏的申明方式:
#define name( parament - list ) stuff 其中的 parament - list 是一个由逗号隔开的符号表,它们可能出现在stuffff中。
注意:
1. 参数列表的左括号 必须 name 紧邻 。 如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分。
比如:
#define SQUARE( x )    x * x  // 正确的写法
2. 替换目标尽量用括号包裹,避免出现运算顺序错误。
#define text(x, y)     x + y          // 高危代码,非常容易发生错误
//  比如:printf("%d", 2 * text(2, 3));   // 输出是:7   而你想要的是10,所以正确的写法应该是
#define text(x, y)     ((x) + (y)) 
//  所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号避免在使用宏时由于参数中的操作符或

邻近操作符之间不可预料的相互作用

注意:
1. 宏参数和 #define 定义中可以出现其他 #define 定义的变量。但是对于宏,不能出现递归
2. 当预处理器搜索 #defifine 定义的符号的时候,字符串常量的内容并不被搜索

2. 41 #和## —— 

思考:如何把参数插入到字符串中?
首先我们看看这样的代码:
char* p = "hello ""bit\\n";
    printf("hello ""bit\\n");
    printf("%s\\n", p);

 从结果上我们会发现都打印了 hello bit 。我们发现字符串是有自动连接的特点的。

   另外一个技巧是: 使用 # 把一个宏参数变成对应的字符串

比如:

第一种:#define text(x)  printf("I " "x" "China\\n");  //参数x 需要是个字符串

第二种:#define text(x)  printf("I "#x"China\\n");    // x同样要是字符串

## 的作用:
   ##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。
#define ADD_TO_SUM(num, value)
sum##num += value;
...
ADD_TO_SUM(5, 10);// 作用是:给 sum5 增加 10.
注: 这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。

2.42  宏和函数优劣对比

性                                              #define 定义宏                                                                函数
代码长度           每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,      每次使 用这个函数时,都调用那个地方的同 一份代码
                         程序的长度会大幅度增长函数代码只出现于一个地方;
执行速度                                             更快                                                                     存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级        宏参数的求值是在所有周围表达式的上下文环境里,                       函数参数只在函数调用的时候求值一次,它的结果值传递
                              除非加上括号,否则邻近操作符的优先级可能会产生                       给函数。表达式的求值结果更容易预测。 
                              不可预料的后果,所以建议宏在书写的时候多些括号。                                                              
带有副作用         参数可能被替换到宏体中的多个位置,所以带有副作用的参                函数参数只在传参的时候求值一次,结果更容易控制。
 的参数                数求值可能会产生不可预料的结果。
参数类型          宏的参数与类型无关,只要对参数的操作是合法的,它就可                  函数的参数是与类型有关的,如果参 数的类型不同,就                                                                                                                                           需要不同的函数, 即使他们执行的任务是不同的。
                                                                                                                                       以使用于任何参数类型。
调 试                  宏是不方便调试的                                                                                           函数是可以逐语句调试的                                                    
递归                   宏是不能递归的                                                                                   函数是可以递归的

2.5  #undef:  终结宏

这条指令用于移除一个宏定义。
#undef NAME            //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

2.6  鸡肋——条件编译(底层对库进行修改使用比较广泛)

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
         调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
如:
#include <stdio.h>
#define max 2
int main()
{
#if max == 1printf("hehe\\n");  // 不编译
#elif max == 2printf("baba\\n");  // 编译
#elseprintf("gungun\\n");  // 不编译
#endif // 1return 0;
}

2.7  文件包含#include

    我们已经知道, #include 指令可以使 另外一个文件 被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10 次,那就实际被编译10 次。(多次包含很浪费资源)
    我们会注意到:
#include  <stdio.h>   
#include "text.h"       //  自建头文件 

有“”  与  <>  两种,其实是两种查找策略:

  •  “”   :先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头 文件。 如果找不到就提示编译错误。
  • <> :   查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。(库文件自带的安装路径)

这样是不是可以说,对于库文件也可以使用 “” 的形式包含?答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

2. 71 如何解决头文件多重包含?

解决办法:

  • 高级的: 头文件开头 加上 #pragma once

 #pragma once

  • 自己写: 在头文件开头
#ifndef _text_h_   // 没定义 返回1;否则返回0,结束编译
#define _text_h_// ....... 各种包含头文件,函数声明
int k(int x, int y);
// .......#endif // !_text_h_

三. 结语

     本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获请留下你的小赞,你的点赞和关注将会成为博主创作的动力。