『C语言进阶』程序环境和预处理

news2024/12/24 2:15:42

在这里插入图片描述

🔥博客主页 小羊失眠啦.
🔖系列专栏 C语言LinuxCpolar
❤️感谢大家点赞👍收藏⭐评论✍️


在这里插入图片描述

文章目录

  • 一、程序的翻译环境和执行环境
  • 二、详解编译+链接
    • 2.1 编译环境
    • 2.2 编译过程
    • 2.3 运行环境
  • 三、预处理详解
    • 3.1 预定义符号
    • 3.2 #define
      • 3.2.1 #define 定义标识符
      • 3.2.2 #define 定义宏
      • 3.2.3 #define 替换规则
      • 3.2.4 #和##
      • 3.2.5 带副作用的宏参数
      • 3.2.6 宏和函数对比
      • 3.2.7 命名约定
    • 3.3 #undef
    • 3.4 命令行定义
    • 3.5 条件编译
    • 3.6 文件包含
      • 3.6.1 头文件被包含的方式
      • 3.6.2 嵌套文件包含

一、程序的翻译环境和执行环境

在ANSIC的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。

说明:计算机能够执行的是二进制指令。但是我们写出的C语言代码是文本信息,计算机不能直接理解,所以通过翻译环境,将C语言代码转化成二进制的指令(可执行程序),在通过执行环境来执行二进制指令。


二、详解编译+链接

2.1 编译环境

在这里插入图片描述

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

在这里插入图片描述

2.2 编译过程

预编译(预处理)—— 编译 —— 汇编

预处理之后产生的结果都放在test.i文件中,编译完成之后产生的结果保存在test.s中,汇编完成之后产生的结果保存在test.o或者是test.obj(在linux中是.o;Windows中是.obj)

预编译:(1)进行头文件的展开(2)删除注释(3)#define定义的符号替换(例如:#define Max 100,在预编译结束之后,Max会被替换为100,并把#define这一行给删掉)
总而言之,就是进行一些文本操作。

编译(把C语言代码转换成汇编代码): (1)语法分析(2)词法分析(3)语义分析(4)符号汇总(全局符号:例如:函数名、main等)

汇编(把汇编代码转换成二进制的指令):(1)形成符号表(全局符号给一个地址,所有全局符号+地址形成符号表)

目标文件(.o)加上链接库通过链接器 链接成为可执行程序(.exe))

Linux中.o目标文件以及可执行文件的文件格式是elf

链接(编译之后生成.o文件,加上链接库通过链接器进行链接):(1)合并段表(2)符号表(不同文件的符号表)的合并以及重定义

链接的时候,多个目标文件进行链接的时候会通过符号表,查看来自外部的符号是否真实存在

2.3 运行环境

程序执行的过程:

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

三、预处理详解

3.1 预定义符号

__ FILE __ //进行编译的源文件
__ LINE __ //文件当前的行号
__ DATE __ //文件被编译的日期
__ TIME __ //文件被编译的时间
__ STDC __ //如果编译器遵循ANSI C,其值为1,否则未定义

#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%s\n", __LINE__);
	printf("%s\n", __DATE__);
	printf("%s\n", __TIME__);
	return 0;
}

运行结果:

E:\VS CodeBase\c_lanuange\231027\231027\test.c
6
Oct 27 2023
10:53:35

记录杂志:

#include<stdio.h>
int main()
{
	int i = 0;
	FILE* pf = fopen("log.txt", "w");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	for (i = 0; i < 10; i++)
	{
		fprintf(pf, "%s %s %s %d\n", __DATE__, __TIME__, __FILE__, __LINE__);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

3.2 #define

3.2.1 #define 定义标识符

语法形式:
. # define name stuff

#define MAX 1000
#define reg register //为register这个关键字创建一个简短的名字
#define do_forever for(;;)//用更形象的符号来替换一种实现
#define CASE break;case//在写case语句时候自动把break写上

例子:

#define M 100
#define STR "abc"
 
int main()
{
	printf("%d\n", M);
	printf("%s\n", STR);
	return 0;
}

运行结果:

100
abc

注意:
在define定义标识符的时候,在最后不要加 ;,否则会出现错误

3.2.2 #define 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

在这里插入图片描述

宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意:
参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

例如:

#include<stdio.h>
#define SQUARE(x) x*x
//这也是个替换,用后面的内容替换前面的宏

int main()
{
	int a = 5;
	int n = SQUARE(a + 1);
	printf("%d\n", n);
	return 0;
}

运行结果:

11

解析: 替换之后是aa+1,也就是5+15+1
如果想让将结果为36,正确的应该是#define SQUARE(x) ((x)*(x)

注意:
在写宏时,我们要把每个参数用括号扩起来,同时宏整体也要扩起来 。

所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中 的操作符或邻近操作符之间不可预料的相互作用。

3.2.3 #define 替换规则

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

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

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

3.2.4 #和##

使用 # ,把一个宏参数变成对应的字符串

#include<stdio.h>
#define PRINT(n,format) printf("the value of "#n" is "format"\n",n);

int main()
{
	int a = 3;
	PRINT(a, "%d");

	int b = 5;
	PRINT(b, "%d");

	float c = 10.0;
	PRINT(c, "%f");
}

运行结果:

the value of a is 3
the value of b is 5
the value of c is 10.000000

使用 ##,把位于它两边的符号合成一个符号

#define CAT(x,y) (x##y)
 
int main()
{
	int a110 = 2023;
	printf("%d\n", CAT(a, 110));
}

运行结果:

2023

注意:字符串是有自动连接的特点

.## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符

3.2.5 带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现的永久性效果。

#include <stdio.h>
 
int main()
{
	int a = 2;
	int b = 0;
	b = ++a;//a = 3; b = 3//这个就有副作用
	a = 2;
	b = a + 1;//a = 2; b = 3 这个没有副作用
	return 0;
}
#include <stdio.h>
#define MAX(x, y) ((x) > (y)? (x): (y))
int main()
{
	int a = 3;
	int b = 5;
	int m = 0;
	m = MAX(a++, b++);//((a++) > (b++) ? (a++) : (b++))后置++,先使用后++ 3>5 a=4 b=6  m=5    b=7
	printf("%d\n", m);
	printf("a = %d, b = %d", a, b);
	return 0;
}

运行结果:

6
a = 4, b = 7

3.2.6 宏和函数对比

宏通常被应用于执行简单的运算。

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于>来比较的类型。 宏是类型无关的。
    缺点: 1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序 的长度。2. 宏是没法调试的。(宏是替换)(把代码插入到相应的位置)3.宏由于类型无关,也就不够严谨4.宏可能会带来运算符优先级的问题,导致程容易出现错误。
#include <stdio.h>
#define MALLOC(num,type) (type*)malloc(num*sizeof(type))
int main()
{
	int* p1 = (int*)malloc(10 * sizeof(int));
	int* p2 = MALLOC(10, int);
//p1 p2效果一样
	return 0;
}
属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中,除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一点
操作符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。
带有副作用的参数参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。函数参数只在传参的时候求值一次结果更容易控制。
参数类型宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的。
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

3.2.7 命名约定

习惯:把宏名全部大写 函数名不要全部大写

3.3 #undef

这条指令用于移除一个宏定义。

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

#include <stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))
 
int main()
{
#undef MAX
	int m = 0;
	m = MAX(2, 3);//此时,这个代码无法用MAX这个宏
	printf("%d\n", m);
	return 0;
}

3.4 命令行定义

许多 C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 编译时进行赋值,代码中并没有进行赋值
例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。(假 定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大写,我们需要一个数组能够大写。)

3.5 条件编译

条件编译指令:在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃编译
比如:调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。

#include <stdio.h>
int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
#if 0
//1的时候就可以运行,不能是变量
		printf("%d\n", i);//此时,这个代码就不运行了
#endif
//这是预处理命令
		//其他
	}
	return 0;
}

常见的条件编译指令:

  1. #if 常量表达式
    //…
    #endif
    // 常量表达式由预处理器求值。
  2. 多个分支的条件编译
    #if 常量表达式
    //…
    #elif 常量表达式
    //…
    #elif 常量表达式
    //…
    #else
    //…
    #endif
  3. 判断是否被定义
    (1)定义
    #if defined(symbol)
    //…
    #endif
    例如:#define M 100;symbol就是指M
    (2)定义
    #ifdef symbol
    //…
    #endif
    (3)没有定义
    #if !defined(symbol)
    //…
    #endif
    (4)没有定义
    #ifndef symbol
    //…
    #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

3.6 文件包含

#include 指令可以使另外一个文件被编译。
预处理器先删除这条指令,并用包含文件的内容替换。 如果一个源文件被包含10 次,那就实际被编译 10 次。

3.6.1 头文件被包含的方式

(1)本地文件包含 : #include “filename”
先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标
准位置查找头文件。
(2)库文件包含 : #include <filename.h>
查找头文件直接去标准路径下去查找
库文件也可以用""的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了

3.6.2 嵌套文件包含

当头文件重复包含,在预处理阶段就会重复保存文件中的内容,这样大大降低了工作效率。为了解决头文件重复包含的问题,我们可以使用条件编译。

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

**解读:**如果没有定义TEST_H,就执行下面的语句,第一次调用,一定没有定义TEST_H,所以执行下面的语句,定义TEST_H,头文件中的内容参与编译;当第二次在调用时,已经定义了 TEST_H,下面的代码不参与编译。

#pragma once

本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,小羊一定认真修改,写出更好的文章~~

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1144551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows找不到文件xxxxx.exe。请确认文件名是否正确后,再试一次

问题现象&#xff1a; Win11系统&#xff0c;每次重启后报如下错误&#xff0c;Windows找不到文件xxxxx.exe。请确认文件名是否正确后&#xff0c;再试一次 w10升级w11后出现 问题原因&#xff1a; xxx文件丢失&#xff0c;不知道是归属于谁的&#xff0c;怀疑是升级给弄丢…

A星算法(A* A Star algorithm)原理以及代码实例,超详细,超简单,大白话谁都能看懂

本文以这篇博主的文章为基础【精选】A*算法&#xff08;超级详细讲解&#xff0c;附有举例的详细手写步骤&#xff09;-CSDN博客 这篇文章的博主做了一个UI界面&#xff0c;但我感觉&#xff0c;这样对新手关注算法和代码本身反而不利&#xff0c;会被界面的代码所干扰。所以笔…

Win11 安装wsl遇到的问题解决

Win11 安装wsl遇到的问题解决 Win11 安装wsl遇到的问题解决WslRegisterDistribution failed:0x8007019eWslRegisterDistribution failed:0x800701bcUbuntu换源WSL通过网络访问Windows Win11 安装wsl遇到的问题解决 WslRegisterDistribution failed:0x8007019e 参考Link WslR…

关于 @Transactional 注解的类中使用 this 调用问题

在一个类中打断点的时候报了一个异常&#xff0c;这个异常&#xff0c;但是一直找不到&#xff0c;直到我在类中调用另外一个方法的时候&#xff0c;看到该方法里面用了 this 调用了 mybatis-plus 的 Api 去操作数据库&#xff0c;而最外层的方法却添加了 Transactional(rollba…

【python debug】python常见编译问题解决方法_1

1. ValueError: could not convert string to float: ‘File’ 问题原因&#xff1a;这里的报错是要把’File’这个转成float失败&#xff0c;非数字字符转换为float失败解决方法&#xff1a;然后去检查哪里出现了这个’File’&#xff0c;data load读入多个文件的数据&#x…

C++-类与对象(上)

一、 auto关键字 1.自动识别数据类型 2.auto的初始化 3.auto简化for循环 nullptr的使用 二、类与对象 1.c中类的定义 2.c语言与c的比较 3.类的访问限定符以及封装 3.1访问限定符 3.2封装 3.3类的作用域 3.4类的声明与定义分离 &#x1f5e1;CSDN主页&#xff1a;d1ff1cult.&…

转变命运!揭秘反转链表的神奇算法!

目录 使用虚拟头节点来辅助实现链表反转直接操作链表实现反转使用递归来实现链表反转 链表是计算机科学中常用的数据结构之一&#xff0c;它由一系列节点构成&#xff0c;每个节点包含一个值和指向下一个节点的指针。链表的灵活性使其在许多场景下被广泛应用&#xff0c;但其中…

S32K144芯片焊接完成后使用S32DS初次下载无法下载解决方法

一、问题现象如下&#xff0c;S32DS Debug下报错 二、原因&#xff0c;原厂芯片出厂时的FLASH Memory的安全机制是激活的&#xff0c;仿真器是可以连上&#xff0c;但是没法读取Flash Memory的内容 三、解决方法 参考图示&#xff0c;解锁后即可正常Debug

2023mathorcup大数据竞赛选题建议及思路

大家好呀&#xff0c;昨天6点2023年第四届MathorCup高校数学建模挑战赛——大数据竞赛开赛&#xff0c;在这里给大家带来初步的选题建议及思路。 注意&#xff0c;本文章只是比较简略的图文讲解&#xff0c;更加详细完整的视频讲解请移步&#xff1a; 2023mathorcup大数据数学…

【Opencv4快速入门】轮廓检测findContours

7.2 轮廓检测findContours 7.2.1 轮廓查找findContours7.2.2 轮廓绘制drawContours图像轮廓是指图像中对象的边界,是图像目标的外部特征,这个特征对于图像分析、目标识别和理解更深层次的含义具有重要的作用。 7.2.1 轮廓查找findContours 图像的轮廓补单能够提供物体的边缘,…

Crypto(6)攻防世界-babyrsa

参考文章&#xff1a; [攻防世界adworld] Crypto - babyrsa - 知乎 (zhihu.com) 涉及到的RSA知识点&#xff1a;

systrace/perfetto如何看surfaceflinger的vsync信号方法-android framework实战车载手机系统开发

背景&#xff1a; hi&#xff0c;粉丝朋友们&#xff1a; 大家好&#xff01;近期分享了surfaceflinger相关的一些blog&#xff0c;有同学就对相关的一些内容产生了一些疑问。 比如&#xff1a;vsync查看问题&#xff0c;即怎么才可以说是vsync到来了。 比如perfetto中surfac…

保姆级教学安装Linux操作系统,以及Linux的语法入门

&#x1f3c5;我是默&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;在这里&#xff0c;我要推荐给大家我的专栏《Linux》。&#x1f3af;&#x1f3af; &#x1f680;无论你是编程小白&#xff0c;还是有一定基础的程序员&#xff0c;这个专…

【性能测试】初识 Jmeter 中的 BeanShell

初识 Jmeter 中的 BeanShell 1.简介1.1 应用场景1.2 BeanShell 类型 2.常用内置变量2.1 log 日志模块2.2 vars 模块2.3 props 模块2.4 prev 模块 3.常见应用场景3.1 Java 文件处理3.2 导入外部 jar 包 BeanShell 是一个小型嵌入式 Java 源代码解释器&#xff0c;完全兼容 Java …

大数据采集技术与预处理学习一:大数据概念、数据预处理、网络数据采集

目录 大数据概念&#xff1a; 1.数据采集过程中会采集哪些类型的数据&#xff1f; 2.非结构化数据采集的特点是什么&#xff1f; 3.请阐述传统的数据采集与大数据采集的区别&#xff1f; ​​​​​​​ ​​​​​​​4.大数据采集的数据源有哪些&#xff1f;针对不同的数…

二叉树的前序、中序和后序非递归

目录 一、前序 二、中序 三、后序 一、前序 力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0)…

C++ 类和对象 (查漏补缺)

Inline 内联函数 内联函数是为了替代宏函数而出来的。 下面用宏实现一个ADD宏函数&#xff1a; 为什么这个ADD宏函数要这么写&#xff0c;首先我们来看&#xff0c;假设这样写&#xff1a; #define ADD(x,y)(xy) 会有什么问题呢&#xff1f; 宏函数是直接替换了&#xf…

FileInputStream文件字节输入流

一.概念 以内存为基准&#xff0c;把磁盘文件中的数据以字节形式读入内存中 二.构造器 public FileInputStream(File file) public FileInputStream(String pathname) 这两个都是创建字节输入流管道与源文件接通 三.方法 public int read() :每次读取一个字节返回&#xff0c;如…

设计模式:原型模式(C#、JAVA、JavaScript、C++、Python、Go、PHP)

上一篇《访问者模式》 下一篇《享元模式》 简介&#xff1a; 原型模式&#xff0c;它是一种创建型设计模式&#xff0c;它允许通过复制原型对象来创建新的对象&#xff0c;而无需知道创建的细节。其工作原…