C语言程序环境和预处理

在这里插入图片描述

系列文章目录



前言

预处理是C语言的一个重要功能,它由预处理程序负责完成。当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define)、文件包含(#include)、条件编译(#ifdef)等。合理使用预处理功能编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。


让我们一起来学习一下C语言中的程序环境和预处理吧!
在这里插入图片描述

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

重点:在ANSI C的任何一种实现中,存在两个不同的环境。

1.程序的翻译环境

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
在这里插入图片描述
1.组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
2.每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
3.链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程 序员个人的程序库,将其需要的函数也链接到程序中。

其中编译和链接也分为几个步骤:
在这里插入图片描述
其中分为更细的话:
在这里插入图片描述

2.程序的执行环境

程序的执行过程:

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

二、预处理详解

1.预定义符号

在这里插入图片描述
代码如下:

int main()  //预处理(定义)符号
{
    
    
int i = 0;
for (i = 0; i < 10; i++)
{
    
    
printf("file:%s line=%d data=%s time=%s  i=%d\n", __FILE__,__LINE__ ,__DATE__,__TIME__,i);
}
return 0;
}
__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__FUNCTIO__ // 文件被执行的函数
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

2.define的两种用法

1.define定义标识符

代码如下:

#define MAX 100
#define STR "bit education"
#define reg register
#define print printf("bit education\n")
int main() //预处理指令
{
    
    
reg int age;
printf("%d\n", MAX);
printf("%s\n", STR);
print;
return 0;
}
2.define定义宏

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

//#define SQUARE(X) X*X
#define SQUARE(X) (X)*(X)
//定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。所以使用宏时候不要省括号
#define DOUBLE(X) (X)+(X)
int main()
{
    
    
	printf("%d\n", SQUARE(3));
	printf("%d\n", 3 * 3);
	printf("%d\n", SQUARE(3 + 2)); //11
	printf("%d\n", 3 + 2 * 3 + 2);
	//printf("%d\n", SQUARE(3 + 2)); //25
	printf("%d\n", 2 * DOUBLE(5));
	printf("%d\n", 2 * 5 + 5);
	return 0;
}
#define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
1. 宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

3.#和##的用法

1.#可以把参数插入到字符串中
在这里插入图片描述
代码如下:

#include<stdio.h>
#define print(format,data)  printf("the value of "#data" is "format"\n",data)
 //"data" == #data把参数插入到字符串中
int main()
{
    
    
	float f = 4.5f;
	print("%f",f);
	printf("the value of f is %f\n", f);

	int a = 10;
	print("%d", a);
	printf("the value of a is %d\n", a);

	int b = 20;
	print("%d", b);
	printf("the value of b is %d\n", b);
	return 0;
}

2.##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符,这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。
在这里插入图片描述
代码如下:

#define CAT(X,Y) X##Y
int main()
{
    
    
	int Class100 = 1000;
	printf("%d\n", CAT(Class, 100));
	return 0;
}

4.带有副作用的宏参数

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

#define _CRT_SECURE_NO_WARNINGS   1
#include<stdio.h>
#define MAX(X,Y) ((X)>(Y)?(X):(Y))  //(((a++)>(b++)?(a++):(b++)))
int main()
{
    
    
	int a = 10;
	int b = 20;
	int m = 5;
	int n1 = m+1;  
	printf("m=%d,n1=%d\n", m, n1);
	int n2 = ++m;                //此时m为带有副作用,因为m的值也在变
	printf("m=%d,n1=%d\n", m, n2);
	int ret = MAX(a++, b++);
	printf("a=%d,b=%d\n", a, b);
	printf("ret=%d\n", ret);
	return 0;
}

重点:宏和函数的对比

  1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序
    的规模和速度方面更胜一筹。
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可
    以适用于整形、长整型、浮点型等可以用于来比较的类型。宏是类型无关的。

当然和宏相比函数也有劣势的地方:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

5.undef和命令行的作用

1.undef移除宏

代码如下:

#include<stdio.h>
#define MAX 100
int main()
{
    
    
	printf("%d\n", MAX);
    #undef MAX
	printf("%d\n", MAX); //此时MAX报错
	return 0;
}
2.命令行的作用

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

gcc - D sz = 10 
int main()
{
    
    
	int arr[sz] = {
    
     0 };
	int i = 0;
	for (i = 0; i<sz; i++)
	{
    
    
		arr[i] = i;
	}
	for (i = 0; i<sz; i++)
	{
    
    
		printf("%d\n", arr[i]);
	}
	return 0;
}

指令图:
在这里插入图片描述

6.条件编译

条件编译:满足某种条件时就参与编译,不满足条件时就不参与编译
在这里插入图片描述
代码如下:

#define __DEBUG__ 1
#define PRINT 1
int main()
{
    
    
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d ", i);
#ifdef  __DEBUG__
		printf("bit education\n");
#endif
	}

#ifdef PRINT    //正面
	printf("哈哈\n");
#endif
#ifndef PRINT  //反面
	printf("哈哈啊\n");
#endif

#if defined PRINT    //正面
	printf("bit\n");
#endif
#if !defined PRINT  //反面
	printf("bit啊\n");
#endif

#if 5-3
	printf("bit NB\n");
#elif 5-5
	printf("bit CA\n");
#elif
	printf("呵呵\n");
#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

7.offsetof的模拟实现和文件包含

offsetof该宏用于求结构体中一个成员在该结构体中的偏移量。

1.用宏实现offsetof

在这里插入图片描述

代码如下:

#define _CRT_SECURE_NO_WARNINGS   1
#include<stdio.h>
#include"标头.h" //头文件的包含->相当于进行了函数的声明
#include<stddef.h>
#define MY_OFFSET0F(struct_name,mem_name) (int)&(((struct_name*)0)->mem_name)
struct stu
{
    
    
	char c;
	int i;
	double d;
};
int main()
{
    
    
	int a = 10;
	int b = 20;
	int sum = add(a, b);
	printf("%d\n", sum);
	printf("%d\n", offsetof(struct stu, c));
	printf("%d\n", offsetof(struct stu, i));
	printf("%d\n", offsetof(struct stu, d));	
	printf("%d\n", MY_OFFSET0F(struct stu, c));
	printf("%d\n", &(((struct stu*)0)->c));
	printf("%d\n", MY_OFFSET0F(struct stu, i));
	printf("%d\n", MY_OFFSET0F(struct stu, d));
	return 0;
}

在这里插入图片描述

2.文件包含的关键

#include<stdio.h>应用于c语言提供的库函数的头文件,如果使用<>,查找是直接去库目录下寻找。
#include"标头.h" 应用于自定义的头文件,如果使用"",查找去当前工程的目录下查找,如果找不到,再去库目录下查找。
当前目录 : D : \visua stdio 2013代码\预处理符号、预处理指令详解6文件包含\预处理符号、预处理指令详解6文件包含。

如果程序中的函数要使用同一个公共模块用条件编译来解决每个头文件的开头写:
#ifndef TEST_H
#define TEST_H
头文件的内容
#endif //TEST_H
或者:
#pragma once
就可以避免头文件的重复引入。


总结

以上就是今天要讲的内容,本文简单介绍了C语言中程序环境和预处理的相关问题,如果上述有任何问题,请懂哥指教,不过没关系,主要是自己能坚持,更希望有一起学习的同学可以帮我指正,但是如果可以请温柔一点跟我讲,爱与和平是永远的主题,爱各位了。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44918090/article/details/115074926
今日推荐