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

        

目录

1. 编译+链接

1.1 翻译环境

2. 编译

3. 运行环境

2. 预处理详解

2.1 预定义符号

2.2 #define定义标识符

  2.3  #define定义宏

2.4 #define的替换规则

2.5 #和##

2.6 带副作用的宏参数

2.7 宏和函数的对比

   2.8 #undef     

2.9 命令行定义

2.10 条件编译

3. 文件包含

3.1 头文件的包含

3.2 嵌套文件的包含


        在学习程序环境和预处理之前我们先来了解以下程序都有哪些环境呢?第一种是翻译环境,在翻译环境中,源代码被转换为可执行的机器指令。第二种是执行环境,顾名思义就是用于实际执行代码。

1. 编译+链接

1.1 翻译环境

        

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

2. 编译

        

         我们可以在gcc上查看编译期间的每一步都发生了什么。我们创建一个test.c的文件

#include <stdio.h>
int main()
{
int i = 0;
for(i=0; i<10; i++)
{
printf("%d ", i);
}
return 0;
}

        1.预处理 选项 gcc -E test.c -o test.i
        预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。头文件的包含、#define定义符、注释的删除都是在预处理阶段完成的。
        2. 编译 选项 gcc -S test.c
        编译完成之后就停下来,结果保存在test.s中。编译阶段将C语言代码转换成汇编代码。
        3. 汇编 gcc -c test.c
        汇编完成之后就停下来,结果保存在test.o中。该阶段把汇编代码转化成了二进制指令。

        提一个小问题?发现被调用的函数没有定义是在哪个阶段呢?

        答案是:链接。

3. 运行环境

        程序执行的过程分为以下几部分:
        1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
        2. 程序的执行便开始。接着便调用main函数。

        3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
        4. 终止程序。正常终止main函数;也有可能是意外终止。

2. 预处理详解

2.1 预定义符号

        我们首先来看一看预处理中的一些预定义符号,这些符号都是语言内置的。

__FILE__    //进行编译的源文件
__LINE__   //文件当前的行号
__DATE__   //文件被编译的日期
__TIME__   //文件被编译的时间
__STDC__   //如果编译器遵循ANSI C,其值为1,否则未定义

2.2 #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 MAX 1000;
#define MAX 1000

        这块同样给大家举一个例子:当#define MAX 1000后面加上之后, 下面代码中的max = 1000;;,而1000;;这是两条语句,会出现语法错误。因此我们在使用define定义标识符的时候建议不要加上;,这样容易导致问题。

if(condition)
max = MAX;
else
max = 0;

  2.3  #define定义宏

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

#define name( parament-list ) stuff


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

注意:

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

        学了宏的声明,我们来看下面一段代码, 大家觉得会输出什么呢? 是36吗?答案是11。

#include<stdio.h>
#define SQUARE( x ) x * x

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

        那么上述代码为什么会出现这种问题呢?

        这是因为在替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
   printf ("%d\n",a + 1 * a + 1 );此时在宏定义上加上两个括号就能够解决这个问题。

#define SQUARE(x) (x) * (x)

这个时候就能够达到我们预期的结果36.

        我们再来看一段代码,这次博主变聪明了,再宏定义中使用了括号来避免之前的问题,但是下面这段代码的结果到底会是什么呢 ?是100?不不不,输出结果是55.

#define DOUBLE(x) (x) + (x)

int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

        那么上述的代码为什么会和我们想象的不一致呢?在替换之后,程序的输出变成了printf ("%d\n",10 * (5) + (5));而乘法的优先级高于加法,所以结果是55。这个时候在宏定义表达式两边加上一对括号就可以得到我们的预期值——100。

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

2.4 #define的替换规则

        在程序中扩展#define定义符号和宏时,需要涉及以下几个步骤。
        1)在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
        2)替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换。
        3)最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
        1. 宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
        2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

2.5 #和##

         # ,把一个宏参数变成对应的字符串。
        ##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符但是这样的连接必须产生一个合法的标识符。否则其结果是未定义的。

2.6 带副作用的宏参数

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

x+1;//不带副作用
x++;//带有副作用
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

        上面的代码输出值是什么呢?首先我么要能够知道预处理器处理之后的结果是什么?

z = ( (x++) > (y++) ? (x++) : (y++));
        5       8       6       9

        而当y == 9之后再次++y的值就是10,因此上述输出的结果就是,6,10,9.

2.7 宏和函数的对比

        我们直接上表格:

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

        这里给大家提一手命名的约定:把宏名全部大写,函数名不要全部大写。

   2.8 #undef     

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

        这条指令是用来移除一个宏定义的。

2.9 命令行定义

        我们来看下面这段代码:

#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;
}

        在这段代码中ARRAY_SIZE是未定义的,但是大多数C 的编译器都允许在命令行中定义符号 用于启动编译过程。

        编译指令:

gcc -D ARRAY_SIZE=10 programe.c

2.10 条件编译

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

        我们来看看常见的一些条件编译指令:

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

3. 文件包含

         在上文中我们已经了解到#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。这种替换的方式很简单:预处理器会先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那么实际上就被编译了10次.

3.1 头文件的包含

//本地文件包含
#include "filename"

        本地文件包含首先在源文件所在目录下查找,如果未找到该头文件,编译器就像查找库函数头文件一样在标准位置查找头文件。如果依然找不到就提示编译错误。

//库文件包含
#include <filename.h>

        查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。那么对于库文件能否也使用“ ”的形式包含呢,当然是可以的

3.2 嵌套文件的包含

        上图中comm.h和comm.c是公共模块。test1.h和test1.c使用了公共模块。test2.h和test2.c使用了公共模块。test0.h和test0.c使用了test1模块和test2模块。这样最终程序中就会出现两份comm.h的内容.

        针对头文件的重复引用我们使用条件编译来解决!这里有两种方式:

//方式1:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif  //__TEST_H__

//方式2:
#pragma once

注意:如果多个源文件同时用到的全局整数变量的声名和定义如果放在头文件中会被多次引用,相当于同一个头文件被重复拷贝在文件中,就相当于重定义。因此头文件一般放自定义的数据类型以及函数的声明。

        除了文章中所提到的还有一些其他的预处理指令,比如#error, #line等等,都有者自己不同的作用。

        

          

  • 8
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 17
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值