C语言中的程序环境与预处理详情

 更新时间:2022年07月08日 11:21:46   作者:烤鸡肉玉米煎饼  
这篇文章主要介绍了C语言中的程序环境与预处理详情,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的朋友可以参考一下

1.程序的翻译环境和执行环境

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

  • 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,也就是从,c文件到.exe文件;
  • 第2种是执行环境,它用于实际执行代码;

翻译环境是由编译器提供的,而执行环境是由操作系统提供的。

如MSVC,DEV C++,Codeblocks这些编译软件都是集成开发环境,也就是集成了编辑,编译,链接和调试等功能。

2.详解编译和链接

2.1程序翻译环境下的编译和链接

从源文件到可执行程序可以分为编译和链接两步,在编译阶段源文件变成了目标文件,在链接阶段目标文件变成了可执行程序。

组成程序的每个源文件通过编译过程分别转化成目标文件;

每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序;

链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且链接器也可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

图解:

2.2深入编译和链接过程

编译本身可以分为预编译(预处理),编译和汇编。

预编译:在预编译阶段会将#include引用的头文件给输入到文件里面,进行#define定义的标识符的替换,以及将注释给删除,因为注释是给程序员看的,不是给电脑看的;

编译:在这个过程中会将C语言代码翻译成汇编代码,编译器会对代码进行词法分析,语法分析,语义分析,符号汇总;

汇编:会把在编译阶段形成的汇编代码翻译成二进制的指令,并将汇总的符号形成一个符号表;

在编译完成之后,就会开始链接,链接过程会合成段表,也就是将目标文件捆绑在一起,以及将符号表合并并进行重定位,最后生成可执行程序。

2.3运行环境

程序执行的过程:

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

3.预处理详解

3.1预定义符号

预定义符号都是语言内置的

__FILE__       //进行编译的源文件

__LINE__      //当前代码的行号

__DATE__    //文件被编译时的日期

__TIME__     //文件被编译时的时间

__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义

预定义符号的使用:

int main()
{
	printf("file:%s\nline:%d\ndata:%s\ntime:%s\n", __FILE__, __LINE__, __DATE__, __TIME__);
	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写上。
// 如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
             date:%s\ttime:%s\n" ,\
             __FILE__,__LINE__ ,    \
             __DATE__,__TIME__ )

在define定义标识符的时候,不要在最后加上;

如下面这种情况,会出现语法错误

#define NUM 100;
int main()
{
	int a = 0;
	if (1)
		a = NUM;
	else
		a = 0;
	return 0;
}

3.2.2#define定义宏

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

宏的声明方式如下:

#define name(parament-list) stuff

其中的parament-list是一个由逗号隔开的符号表,它们可能出现在stuff中

把name(parament-list)这个整体称为宏

注意:

参数列表的左括号必须与name紧贴,如果两者之间存在空格,参数列表就会被解释为stuff的一部分,语法就是这么规定的。

接下来是宏的使用:

比如用宏实现一个数的平方:

#define SQUARE(n) n * n
int main()
{
	SQUARE(6);
	return 0;
}

语句SQUARE(6)就会替换成6 * 6;

解释:宏先是接受一个参数,SQRARE(n)中的n就变成了6,其后宏的内容也就由n * n变成了6 * 6,再将6 * 6替换到程序中使用宏的位置。

但是,这个宏这么写存在一个问题,如下代码:

#define SQUARE(n) n * n
int main()
{
	printf("%d\n", SQUARE(1 + 3));
	return 0;
}

看上去似乎最后的结果是16,然而实际上参数n会被替换成1 + 3,这样最终替换的内容是1 + 3 * 1 + 3,这条表达式最终的结果是7.

所以需要在n的左右两边加上一对括号,如下:

#define SQUARE(n) (n) * (n)

再看另外一个宏定义:

#define DOUBLE(n) (n) + (n)

代码:

#define DOUBLE(n) (n) + (n)
int main()
{
	printf("%d\n", 5 * DOUBLE(3));
	return 0;
}

 看上去最终结果似乎是30,然而替换后语句实际上是

printf("%d\n", 5 * (3) + (3));

所以最终结果是18

所以为了保证获得想要的结果,宏定义表达式两边还需要加上一对括号

#define DOUBLE ((n) + (n))

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

3.2.3#define替换规则

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

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

注意:

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

比如:

#define a 123
int main()
{
	printf("%s", "a");
	return 0;
}

语句printf("%s", "a");中的a并不会被替换成123

3.3.4#和##

如何把参数插入到字符串中?

如下代码:

int main()
{
	printf("abc""def");
	return 0;
}

输出的结果是abcdef

发现字符串是有自动相连的特点的

看下面这个代码:

#define PRINT(FORMAT, VALUE) printf("the value of "#VALUE " is "FORMAT"\n", VALUE)
int main()
{
	int a = 6;
	PRINT("%d", a);
	return 0;
}

最终输出的结果是:

the value of a is 6

所以#VALUE会被预处理器在预处理阶段预处理为"VALUE"

接下来看看##的作用:

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

如下代码:

#define MAXMIN 6
#define MIDDLE MAX##MIN
int main()
{
	printf("%d\n", MIDDLE);
	return 0;
}

注意:连接之后产生的符号必须是已经定义的,否则结果就是非法的。

3.2.5带副作用的宏参数

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

比如:

x + 1;//不带有副作用

x++;  //带有副作用

如下代码可以证明副作用的宏参数带来的问题

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);
	return 0;
}

 在代码预处理之后

z = ( (x++) > (y++) ? (x++) : (y++));

所以最终结果是x=6 y=10 z=9

3.2.6宏和函数对比

宏通常被用于执行简单的运算,比如在两个数中找出较大的一个

#define MAX(a, b) ((a)>(b)?(a):(b))

对于为什么不用函数来完成这个任务,有两个原因:

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

当然宏也是有缺点的:

  • 1.每次使用宏的时候,会将宏定义的代码插入到程序中,除非宏比较短,否则可能大幅度增加程序的长度;
  • 2.宏是没法进行调试的,因为在预处理阶段,宏定义的符号已经发生了替换,此时调试看到的代码和实际上运行时的代码是有所差异的;
  • 3.宏由于类型无关,也就不够严谨了;
  • 4.宏可能会带来运算级优先的问题,导致程序容易出错; 

宏有时候也可以做到函数做不到的事情,比如宏的参数可以出现类型,但是函数不行

如下代码:

#define MALLOC(num, type)\
(type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数

 预处理替换后:

(int *)malloc(10 * sizeof(int));

会节省部分代码。

总的来对比一下宏和函数的区别:

函数和宏的使用语法很相似,所以语言本身没法帮助区分二者,所以平时的命名习惯是:

宏名全部大写

函数名不要全部大写

3.3#undef 

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

语法:

#undef name

使用: 

#define MAX 5
int main()
{
	printf("%d\n", MAX);
#undef MAX
	return 0;
}

3.4命令行定义

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

#include <stdio.h>
int main()
{
  int array [ARRAY_SIZE];
  int i = 0;
  for(i = 0; i< ARRAY_SIZE; i ++)
 {
    array[i] = i;
 }
  for(i = 0; i< ARRAY_SIZE; i ++)
 {
    printf("%d " ,array[i]);
 }
  printf("\n" );
  return 0;
}

3.5条件编译

在编译一个程序的时候如果要将一条语句或者一组语句编译或者放弃掉是很方便的,因为有一个叫条件编译的东西。

对于调试性的代码,删除比较可惜,保留又会碍事,所以可以选择性的去编译。

如下代码:

#define __DEBUG__
int main()
{
	int arr[10] = { 0 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;
#ifdef __DEBUG__
		printf("%d ", arr[i]);//为了观察数组是否被赋值成功
#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 symbo

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指令可以使另一个文件被编译,会让被包含的头文件出现在#include指令的地方

这种替换的方式很简单,预处理器会先删除这条指令,并用包含文件里的内容进行替换,如果这个文  件被包含了10次,那实际上就会被编译10次

3.6.1头文件被包含的方式

本地文件包含:

#include "fliename.h"

查找方法:先在源文件的目录下去查找,如果该头文件未被找到,编译器就会像去查找库函数的头文件一样在标准位置去查找头文件,如果还找不到就提示编译错误。

库文件包含:

#include <filename.h>

查找方法:直接在标准路径下去查找,如果找不到就提示编译错误。

虽然可以对库文件也采用""的包含方式,但是当目录下的文件非常多的时候,这样查找起来的效率就会低一些了,而且也不容易去区分是库文件还是头文件了。

3.6.2嵌套文件包含

如图:

  • common.h和common.c是公共模块
  • test1.h和test1.c使用了公共模块
  • test2.h和test2.c使用了公共模块
  • test.h和test.c使用了test1模块和test2模块

这样最终的程序中就会包含两次common.h了,等于有2份common.h的内容,会造成代码的重复。

对此可以采用条件编译的方式来解决这个问题

在引用每个头文件时在开头写上这么一个内容:

#ifndef __STDIO_H__
#define __STDIO_H__
#include <stdio.h>
#endif

如果没有定义标识符__STDIO_H__就定义__STDIO_H__并且去包含头文件

如果下次还遇到包含头文件的代码,由于__STDUI_H__已经被定义过,所以也就不会进行第二次包含了

或者对于在头文件的开头也可以这么写:

#pragma once
#include <stdio.h>

也可以避免头文件的重复引入

4.其他预处理指令

比如:

#error

#pragma

#pragma

#line

到此这篇关于C语言中的程序环境与预处理详情的文章就介绍到这了,更多相关C语言预处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 基于C语言实现图书管理信息系统设计

    基于C语言实现图书管理信息系统设计

    这篇文章主要为大家详细介绍了基于C语言实现图书管理信息系统设计与实现,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • C语言详细分析宏定义与预处理命令的应用

    C语言详细分析宏定义与预处理命令的应用

    宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的替换。字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现
    2022-07-07
  • C语言实现学生信息管理系统(链表)

    C语言实现学生信息管理系统(链表)

    这篇文章主要为大家详细介绍了C语言实现学生信息管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-06-06
  • Qt编写显示密码强度的控件

    Qt编写显示密码强度的控件

    这篇文章主要为大家详细介绍了Qt编写显示密码强度的控件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-06-06
  • C++ vector的简单实现

    C++ vector的简单实现

    这篇文章主要为大家详细介绍了C++ vector的简单实现,使用数据库,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • c语言中字符串与字符串数组详解

    c语言中字符串与字符串数组详解

    在C语言当中,字符串数组可以使用char a[] [10]; 或者char *a[]; 表示,下面这篇文章主要给大家介绍了关于c语言中字符串与字符串数组的相关资料,需要的朋友可以参考下
    2021-11-11
  • 详解C语言之缓冲区溢出

    详解C语言之缓冲区溢出

    缓冲区是一块连续的计算机内存区域,可保存相同数据类型的多个实例。缓冲区可以是堆栈、堆和静态数据区。在C/C++语言中,通常使用字符数组和malloc/new实现缓冲区。溢出指数据被添加到分配给该缓冲区的内存块之外。缓冲区溢出是最常见的程序缺陷
    2021-06-06
  • C++编程中的格式化输出详解

    C++编程中的格式化输出详解

    这篇文章主要介绍了C++编程中的格式化输出详解,是C++入门学习中的基础知识,需要的朋友可以参考下
    2015-09-09
  • c++获取进程信息列表和进程所调用的dll列表

    c++获取进程信息列表和进程所调用的dll列表

    这篇文章主要介绍了c++获取进程信息列表和进程所调用的dll列表,大家参考使用吧
    2013-11-11
  • C++ 的三种访问权限与三种继承方式

    C++ 的三种访问权限与三种继承方式

    我们知道C++中的类,有三种访问权限(也称作访问控制),它们分别是public、protected、private,C++中继承的方式还有多种。下面通过本文给大家详细介绍,对c++中的访问权限和继承方式感兴趣的朋友一起看看吧
    2016-11-11

最新评论