程序的编译

程序编译的过程

预处理
这是我们将代码写好之后编译器做的第一步操作,所进行的内容有:
①展开头文件,就是将你代码中所包含的全部头文件拷贝到你打的代码中,而正是因为这一步,就会导致待编译的文件急速膨胀,后续编译动作的效率就会受到很大的影响,不过在c++中这一点正在改进,后续会得到有效处理的;
②展开宏,也就是进行宏替换;
③条件编译,就是对代码进行选择性的编译,这个等会会详细讲解的;
④去掉注释,注释是让我们人类能看懂代码干了些什么,但是编译器不需要这一点;

  在 Linux 环境下,我么可以使用命令行通过 gcc 1 来生成预处理后的文件,预处理产生的结果放在 .i后缀的文件中,以 “test.c” 为例,命令行为:(现在不必知道为什么用这个命令行,后面会讲到的)

gcc -E test.c -o test.i
编译

  预处理后,生成的代码就是我们真正需要的代码了,接下来进行第二步操作-编译,在这一步中,编译器会对预处理生成的代码进行词法分析、语法分析、语义分析、中间代码生成、目标代码优化…最终使其变成汇编指令;
  在 Linux 环境下,我么可以使用命令行通过 gcc 来生成编译后的文件,编译产生的结果放在.s后缀的文件中,以 “test.i” 为例,命令行为:

gcc -S test.i -o test.s
汇编

  编译出来的代码将要进行第三步操作-汇编,这一步会将编译生成的代码转化为二进制的机器指令,到这里我们就已经看不懂 “自己写的” 代码了,这是计算机才能看懂的代码指令;
  在 Linux 环境下,我么可以使用命令行通过 gcc 来生成汇编后的文件,汇编产生的结果放在.o后缀的文件中,以 “test.s” 为例,命令行为:

gcc -c test.s -o test.o
链接

  实际开发过程中,我么不可能在一个源文件中就完成一个项目的开发,我们会将一个项目分成好多个源文件,然后由许多人一起来共同开发,所以我们在将这些分散的源文件进行上面的三个步骤之后,就会得到好多个.o文件,而链接的过程就是把这些.o合并到一起;
  另外链接过程中除了用户自己写的编译的.o文件外,还需要链接一些库文件,因为在我们写的代码中的头文件里,库函数只有声明没有定义,而这些库函数的定义是包含在一个动态库/静态库中,这些都是通过连接过程找到的;
  接下来就可以执行文件了,在 Linux 环境下,我么可以使用命令行通过 gcc 来执行,以 “test.o” 为例,命令行为:

gcc test.o -o test

程序执行的过程

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

预处理详解

预定义符号
#include <stdio.h>
int main()
{
    printf("当前的文件:%s\n", __FILE__);//__FILE__表示当前的文件
    printf("当前的行号:%d\n", __LINE__);//__LINE__表示当前的行号
    //这两个在平时我们打印日志时非常有用,可以帮我们定位
    

    printf("文件被编译的日期:%s\n", __DATE__);//打印文件被编译的日期
    printf("文件被编译的时间:%s\n", __TIME__);//打印文件被编译的时间
    //这个可以用来区分程序的版本号,显示程序的具体编译时间


    //printf("是否遵循ANSI C标准:%s\n", __STDC__ != 0 ? "是" : "否");
    //用来检测编译器是否遵守C标准来实现的
    //如果编译器遵循ANSI C标准,它就是个非零值
    //需要注意的是这个在vs中不能使用,因为没有宏定义
    //这也是为什么我们在今后的工作中不使用vs进行编程,而是选择使用Linux的gcc
    return 0;
}

//当前的文件:E:\vs\Project\Project3\test_everything.c
//当前的行号:5
//文件被编译的日期:Nov 30 2020
//文件被编译的时间:13:05:10
#define详解

首要记住的一点就是:宏的本质就是,在预处理阶段进行文本替换。

#define定义标识符
//定义语法:
#define name stuff
#define MAX 1000             //在后续出现的MAX就会被换成1000
#define reg register         //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)   //用更形象的符号来替换一种实现
#define CASE break;case      //在写case语句的时候自动把 break写上。


// 如果宏定义时过长,可以分成几行写,此时除了最后一行外,每行的最后面都加一个反斜杠    “\"——续行符
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
                          date:%s\ttime:%s\n" ,\
                          __FILE__,__LINE__ ,       \
                          __DATE__,__TIME__ ) 
//打印代码的文件、行号、程序编译的时间,定义成宏,方便书写代码
#define定义宏
//定义语法:
#define name( parament-list ) stuff

  其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中作为参数。参数列表的左括号必须与 name 紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分。

#include <stdio.h>
#define ADD(x, y) x + y
#define MUL(x, y) x * y
int main()
{
  int a = ADD(10, 20) * ADD(10,20);
  printf("a = %d\n", a);// 1
  int a2 = MUL(10, 10 + 10);
  printf("a = %d\n", a2);// 2
  return 0;
}

  看看上面的代码,计算结果是多少呢?其中 1 的答案是:230;2 的答案是:110。你们是不是算错了呢?这就是宏定义的一个缺点、难点,下面我们来看看宏定义和函数进行比较的优缺点:

优点:
①能实现一些函数做不到或者难以做到的事情;
②宏的执行效率要略高于函数,因为函数调用需要传参,这个过程会有开销,而宏只是文本替换,很快;
③宏可以一定程度上实现泛型编程,也就是宏没有参数类型检查,同一个变量可以使用不同类型的数据;
④如果宏定义写的函数出错,编译器会明确的将代码出错的行数准确返回,可以第一时间确定出错位置;
缺点:
①正是因为宏进行的只是简单的文本替换,所以运算的优先级不能被保证,表达式的运行结果和预期的结果会有差别,所以在定义宏的时候多加括号,避免出错;
②还是因为宏进行的只是简单的文本替换,所以没有参数类型检查,这会带来好处也会带来很多坏处;
③宏难以进行调试,在打断点的时候编译器会跳过宏,不能一步步运行宏;
④宏的可读性不如函数好,没有那么有条理,而且宏不能进行递归;

  总的来说,宏的弊大于利,一个正经的编程语言不该有宏的概念,现在很多编程语言都没有宏这种东西,c/c++ 仍旧有,但是 c++ 也已经在努力去掉宏,所以,希望大家在写代码的时候能不使用就尽量不用。

#undef

  如果现在要使用一个 NAME 的宏定义,但是他已经被宏定义过其他的意思,那么仍旧要使用 NAME 该怎么办?#undef这条指令用于移除一个宏定义,使用如下:

#undef NAME

#和##

#

  在平时打印一个字符串的时候,我们可以发现不同的字符串之间打印时是可以拼接的,那么我们可以使用宏来实现printf()

#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
  printf("the value is "FORMAT"\n", VALUE);
int main()
{
    int i = 10;
    PRINT("%d", i + 1);
    return 0;
}

//结果:the value is 11

  当我们需要将参数变成字符串的一部分该怎么办呢?其实我们可以使用#来解决,在宏的参数前加#,能把参数变成一个字符串,然后这个字符串就可以在代码中进行文本拼接,举例如下:

#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
  printf("the value of "#VALUE" is "FORMAT"\n", VALUE);
int main()
{
    int i = 10; 
    PRINT("%d", i + 1);
    return 0;
}

//结果:the value of i+1 is 11
##

  ##的作用是进行字符串拼接,它可以把位于它两边的符号合成一个符号,且允许宏定义从分离的文本片段创建标识符,不过需要注意的是:这样的连接必须产生一个合法的标识符(也就是你创建过的变量,##只具有合并功能,并不具有定义功能),否则其结果就是未定义的。举例如下:

#include <stdio.h>
#define ADD_TO_NUM(num , value)  sum##num += value;
int main()
{
    int sum1 = 0;
    //##两边的变量进行拼接,参数传入后,此时sum##num会变为sum1
    ADD_TO_NUM(1, 10);
    //此时给sum1+=10;那么sum1==10;
    printf("%d\n", sum1);
    return 0;
}

//结果:10

条件编译

  条件编译,顾名思义就是满足某些条件才会编译某段代码,这就是条件编译,通常条件编译常常配合#define来使用,下面介绍一些常见的条件编译指令以及用法:

//1.
#if 常量表达式//常量表达式为真执行下面代码,否则不执行
 //...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
 //..
#endif

//2.多个分支的条件编译
#if 常量表达式//某个常量表达式为真,就执行某个分支代码
 //...
#elif 常量表达式
 //...
#else
 //...
#endif

//3.判断是否被定义
#ifdef symbol//如果symbol被宏定义过,那么就执行,否则不执行
 //...
#endif
#ifndef symbol//如果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

文件包含

头文件包含方式
  • 本地包含
#include "filename"
//文件使用""包含

  查找策略:先在源文件所在目录下查找,如何该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。

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

  查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

文件重复包含

  在写一个项目的时候,代码量往往很大、很繁琐,很容易就会造成头文件重复包含的问题,如果一个文件被包含了 10 次,那么就会被展开 10 次,之前说过,展开头文件会很耗费时间,效率不高,那么如何解决重复包含的问题呢?答案是:条件编译;
  具体使用方法是将下面的两串代码二选其一放到头文件的开始位置,就可避免头文件的重复引入;

  • 第一种:
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

  原理是,在第一次条件编译时,没有宏定义__TEST_H__,所以通过条件编译,展开下面的头文件内容,而如果重复包含了头文件,那么第二次进入时__TEST_H__已经被宏定义了,条件编译失败,就不会重复展开下面的内容。
  这种方法其实大家已经用过很多次了,那就是_CRT_SECURE_NO_WARNINGS,当大家包含了#define _CRT_SECURE_NO_WARNINGS之后,使用scanf就不报错了,没有这个宏的定义的时候, VS 就会多编译一些对于scanf等函数安全检查的逻辑,有这个宏定义, 相关的检查代码就不被编译了。
  这段检查的代码在 stdio.h 里头. 所以必须把这个宏定义到 stdio.h 的上方。

  • 第二种:
#pragma once

  写代码的时候更推家大家使用这个,因为上一种写起来很繁琐,而且还要起名字,这样难免会重复命名,这个简单易写,效果还相同。


  1. GCC 是以 GPL 许可证所发行的自由软件,也是 GNU 计划的关键部分。GCC 的初衷是为 GNU 操作系统专门编写一款编译器,现已被大多数类Unix操作系统(如 Linux、BSD、MacOS X等)采纳为标准的编译器,甚至在微软的 Windows 上也可以使用 GCC。GCC 支持多种计算机体系结构芯片,如 x86、ARM、MIPS 等,并已被移植到其他多种硬件平台。
    GCC 原名为 GNU C 语言编译器(GNU C Compiler),只能处理 C 语言。但其很快扩展,变得可处理 C++,后来又扩展为能够支持更多编程语言,如 Fortran、Pascal、Objective -C、Java、Ada、Go 以及各类处理器架构上的汇编语言等,所以改名 GNU编译器套件(GNU Compiler Collection)。
    博客园发表于 2020-11-30 19:40 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值