• 程序环境与预处理笔记


    🎭🎭前言

    本篇文章学自比特鹏哥,若构成侵权,请及时联系作者,本人一定及时处理
    如果您阅读了本文,并觉得本文不错,您的赞👍将是我最大的奖励🙏🙏
    本文部分案例使用的时c

    一 🧩🧩程序的环境分类

    • 翻译环境,源代码–>可执行的机械指令
    • 执行环境,用于实际执行代码

    二 🪢🪢编译和链接

    2.1 →→执行过程详解

    在这里插入图片描述

    • 组成一个程序的每个源文件通过编译过程分别转化成目标代码
    • 每个目标文件由链接器捆绑在一起,形成一个单一完整的可执行程序
      在这里插入图片描述
    • 各个阶段完成的具体任务
    阶段详解
    预处理( xxx.c–>xxx.i )1. 完成了头文件的包含#include 2. #define定义的符号和宏的定义 3. 删除注释 文本操作
    编译(xxx.i–>xxx.s)把c语言代码转化成汇编代码 1.语法分析 2. 词法分析 3. 语义分析 4. 符号总结
    汇编(xxx.s–>xxx.o)***把汇编代码转化为机器指令(二进制指令)1. 生成符号表 ***
    连接(xxx.o–>xxx.out)多个目标文件和链接库进行连接

    2.2 🎡🎡运行环境

    • 程序执行的过程
      • 1 程序必须载入内存中,【有操作系统的情况下】操作系统自动完成;独立的环境中,程序的载入必须由手工完成
      • 2 程序的执行便开始,接着调用main函数
      • 3 开始执行程序代码,这个时候程序使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程中,一直保留他们的值。
      • 4 终止程序。正常终止main函数;或者意外终止。

    三 🎞️🎞️预处理详解

    3.1 🔣🔣预定义符号

    符号分类详解
    FILE显示正在进行编译的源文件
    LINE当前文件的行号
    DATE文件被编译的日期
    TIME文件按被编译的时间
    STDC如果编译器遵循ANSI C ,其值为1,否则为定义

    ANSI C是由美国国家标准协会(ANSI)及国际标准化组织(ISO)推出的关于C语言的标准。ANSI C 主要标准化了现存的实现, 同时增加了一些来自 C++ 的内容 (主要是函数原型) 并支持多国字符集 (包括备受争议的三字符序列)。 ANSI C 标准同时规定了 C 运行期库例程的标准。

    • 举例
    printf("file:%s line:%d\n",__FILE__,__LINE__);
    
    • 1

    3.2✔️✔️define

    • 语法
    #define NAME stuff
    
    • 1
    • 栗子
    //建议不加分号
    #define MIN 10 //定义字面量的值
    #define reg register//为关键字,创建一个别名
    #define do_forever for(;;)//用形象的符号来代替一种实现
    #define CASE break;case //在写case时自动把break写上
    //如果定义的stuff 太长,可以分行书写,但除了最后一行外,其他每行的后面都加上一个反斜杠(续行符)
    #define DEBUG_PRINT printf("file:%s \t line:%d\t \
    							date:%s \t time:%s\n",\
    							__FILE__,__LINE__,     \
    							__DATE__,__TIME__)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    #include
    #define CASE break;case //在写case时自动把break写上
    int main() {
    	int option = 1;
    	switch (option) {
    		case 1:
    			printf("hhh");
    		CASE 2:
    		CASE 3 :
    			break;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.2.1 #define定义宏

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

    • 宏的声明方式
    #define name(name-list) stuff
    
    • 1
    • 其中的parament-1ist是一个由逗号隔开的符号表,它们可能出现在stuff
    • 注意:
      • 参数列表的左括号必须与name紧邻
      • 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
    #include
    using namespace std;
    #define SQUARE(X) X*X
    int main() {
    	cout << SQUARE(3 + 1)<<endl;//3+1*3+1 =7
    	cout << 10*SQUARE(3 + 1)<<endl;//10*3+1*3+1 =34
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    提示:用于对数值表达式进行求值的宏定义都应该加上方括号,避免在使用宏时中的操作符或临近操作符之间不可预料的相互作用

    3.2.2 #define替换规则

    • 在程序中扩展#define定义符号和宏时,需要涉及几个步骤
        1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
        1. 替换文本随后被插入到程序中原来文本的位置。对干宏,参数名被他们的值替换
        1. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程
    • 注意:
        1. 宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
        1. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
    #include
    using namespace std;
    #define M 100
    #define MAX(X,Y) ((X)>(Y)?(X):(Y))
    int main() {
    	int max = MAX(360, M);
    	cout << max << endl;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.2.3 #和##

    • 把参数插入到字符串中
    #include
    using namespace std;
    #define PRINT(X) printf("the value of "#X" is %d\n",X);
    int main() {
    	int a = 10;
    	PRINT(a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    //把参数插入到字符串中
    #include
    using namespace std;
    #define PRINT(X,FORMAT) printf("the value of "#X" is " FORMAT,X);
    int main() {
    	int a = 10;
    	PRINT(a,"%d");
    	return 0;
    }
    
    //拼接字符
    #include
    using namespace std;
    #define CAT(X,Y,Z) X##Y##Z
    int main() {
    	int class12 = 100;
    	cout << CAT(class,1,2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    3.2.4 🌠🌠带副作用的宏参数

    • 当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用, 那么在使用宏的时候有可能出现危险。
    • 副作用:表达式求值的时候出现永久性效果
    int a=10;
    int b=a+1;//b=11,a=10 无副作用
    int b=++a;//b=11,a=11 有副作用
    
    • 1
    • 2
    • 3
    #include
    using namespace std;
    #define MAX(X,Y) ((X)>(Y)?(X):(Y))
    int main() {
    	int a = 1,b=2;
    	int m = MAX(a++,b++);
    	/*
    		过程详解
    		int m=(a++)>(b++)?(a++):(b++)
    		后置递增:先比较后递增
    		1<2 返回b++ 即3
    		然后a、b完成递增 即a=1+1=2,b=3+1=4
    	*/
    	cout << m<<endl;//3
    	cout << a << endl;//2
    	cout << b << endl;//4
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.2.5 宏🆚🆚函数

    • 宏通常被应用于执行简单的运算,比较两个数中较大的一个
    #define MAX(X,Y) ((X)>(Y)?(X):(Y))
    
    • 1
    • 宏的优势【使用宏进行简单计算而不是用函数的原因】

      • 宏比函数在程序的规模和速度方面更胜一筹,用函数和从函数返回的代码可能比实际执行计算所需的时间更多
      • 宏是类型无关的。函数的参数必须为特定的类型。而宏可以适用于整形、长整型、浮点型登等等
    • 宏的劣势

      • 每次使用宏的时候,一份宏定义的代码将插入到程序中。如果宏比较长,肯能增加程序的长度
      • 宏是没法调试的
      • 宏由于类型无关,不够严谨
      • 宏可能会带来运算符优先级的问题,导致容易出错
    • 宏有时候可以做到函数做不到的事情:宏的参数可以出现类型

    #include
    using namespace std;
    #define MALLOC(num,type) (type*)malloc(num*sizeof(type))
    int main() {
    	int* p = MALLOC(10, int);
    	if (p == NULL) {
    		return -1;
    	}
    	for (int i = 0; i < 10; i++) {
    		*(p + i) = i;
    	}
    	for (int i = 0; i < 10; i++) {
    		cout << *(p + i)<<" ";
    	}
    	free(p);
    	p = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    属性#define定义宏函数
    代码长度每次使用宏,宏代码会插入到程序中。较长的宏,会是程序的长度大幅度增长函数代码只出现在一个地方;每此时用这个函数时,都调用那个地方的同一份代码
    执行速度更快存在函数的调用和返回的额外开销,较慢
    操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
    带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次,结果更容易控制。
    递归宏是不能递归的函数可以递归

    命名约定
    宏的名称全部大写
    函数名不要全部大写,推荐驼峰命名法

    3.3 ⛳⛳#undefine

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

    3.4 📊📊条件编译

    • 在编译一个程序的时候,我们如果将一个语句(一组语句)编译或者放弃是很方便的
    • 比如:调试的代码,删除可惜,保留碍事,可以选择性的编译
    #include 
    #define __DEBUG__
    
    • 1
    • 2

    在这里插入图片描述

    • 常见的条件编译命令
    //1 单个的条件编译
    #if 常量表达式
    	//...
    #endif
    
    //2多个分支的条件编译
    #if 常量表达式
    	//...
    #elif 常量表达式
    	//...
    #else
    	//...
    #endif
    
    //3 判断是否被定义
    #if define(symbol)
    #ifdef 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    #include
    using namespace std;
    int main() {
    #if 1==2
    	cout << "1==2";
    #elif 2==3
    	cout << "2==3";
    #else
    	cout << "else";
    #endif
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    #include
    using namespace std;
    #define TEST 0
    int main() {
    #ifdef TEST
    	cout << "test\n";
    #endif 
    
    #if defined(TEST)
    	cout << "test\n";
    #endif 
    
    #ifndef WHERE
    	cout << "where\n";
    #endif // !WHERE
    
    #if !defined(WHERE)
    	cout << "where\n";
    #endif // !WHERE
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3.5 📦📦文件包含

    • #include可以使领域给文件被编译
    • 替换方式:
      • 预处理器先删除这条指令,并用包含文件的内容替换
      • 一个源文件被包含10次,那么实际就被编译10次

    3.5.1 头文件被包含的方式

    包含类型书写类型查找策略
    本地文件包含#include "filename"现在源文件所在的目录下查找,如果未找到,编译器就像查找库函数头文件一样在准确位置查找头文件;找不到,提示错误
    库文件包含#include直接去库目录下查找

    3.5.2 避免出现同一个头文件重复编译

    • 方法一:条件编译
    #ifdef __自定义名称【推荐头文件名称大写——H】__
    #define __自定义名称【推荐头文件名称大写——H】__
    /*
    	头文件内容
    */
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 方法二:#pragma once
    #pragma once
    /*
    	头文件内容
    */
    
    • 1
    • 2
    • 3
    • 4
    • #pragma once是一个比较常用的C/C++预处理指令,只要在头文件的最开始加入这条预处理指令,就能够保证头文件只被编译一次。

    请添加图片描述

    • 悄悄的你读过,请带走一片智慧的云霞,只留下一个点赞!
      请添加图片描述
  • 相关阅读:
    通过业务加强对访问修饰符 public,private,protected,以及不写(默认)时的区别的应用与理解【Java基础题】
    C语言K&R圣经笔记 2.7类型转换
    python基于django的高校奖学金管理系统
    语言基础 - 1
    Ubuntu上使用SSH连接到CentOS系统
    【记录】Truenas scale|Truenas 的 SSH 服务连不上 VScode,终端能连上
    Java IDEA java.lang.IllegalStateException: Failed to introspect Class报错原因和解决办法
    力扣练习——48 找到小镇的法官
    RabbitMQ 集群 - 普通集群、镜像集群、仲裁队列
    P1876 开灯[洛谷]
  • 原文地址:https://blog.csdn.net/yang2330648064/article/details/126532223