g++安装配置以及C++的四个编译处理过程(对比C#编译执行过程)

一、C++的编译器

不像python等动态语言,C、C++是静态语言,需要编译之后,然后才能执行,C++的编译器有很多,比如常见的MSVC、g++(gcc)、等,本文介绍g++,关于g++的详细介绍,本文不再介绍,可以查阅相关文档。

二、windows平台下g++的安装与配置

gcc原本是基于Linux平台的,但是在windows平台上提供了一个同样的实现,叫做MingW

即(Minimalist GNU for Windows)。当前是MingW-w64取代了之前的MingW-w32,支持64位的操作系统。可以在官网下载安装,

http://www.mingw-w64.org

但是有一个问题是,安装的过程中需要联网,即边下载边安装,这样很容易失败,所以一般是先下载离线文件,然后直接配置即可。

MingW-w64的离线压缩文件为:

https://sourceforge.net/projects/mingw-w64/files/?source=navbar

可以选择下面的第一个进行下载,即MinGW-W64 GCC-8.1.0

如果上面的下载速度很慢,也可以联系我,在我的网盘里面下载。下载下来是一个大约50M的压缩文件,解压该文件到相应的目录之下,比如我将其解压到D:\Program Files (x86)文件夹下,得到一个mingw64文件夹,会得到如下的一系列文件,如下:

解压之后就不需要在进行安装了,直接配置环境变量即可,关于如何配置环境变量,此处就不再赘述,给环境变量添加的路径为:

D:\Program Files (x86)\mingw64\bin

三、使用g++编译C++代码的四大过程

比如我有一个test.cpp的C++文件,代码如下:

#include<iostream>

using namespace std;

int main()
{
	cout << "hello world!" << endl;
    system("pause");
    return 0;
}

1、预编译——宏的替换、注释消除

进入到代码test.cpp所在的文件夹,执行命令:

g++ -E test.cpp > test.i

这时会生成一个后缀为.i的test.i的文件,这个文件里面进行了宏的替换,还有注释的消除等操作。

2、编译阶段——生成汇编语言

g++ -S test.cpp

这个时候,会生成一个后缀为.s的文件,test.s,.s文件表示是汇编文件,用编辑器打开就都是汇编指令。

3、汇编阶段——生成机器代码

g++ -c test.cpp

会生成一个后缀为.o的文件或者是.obj的文件,test.o,.o是gcc生成的目标文件,用编辑器打开就都是二进制机器码。

注意的是:目标文件的后缀为.obj;对于GCC编译器,目标文件的后缀为.o

4、链接——生成可执行程序

g++ test.o -o test

会生成一个后缀为.exe的文件,test.exe,即连接目标代码,生成可执行程序

四、C++代码运行过程的详解

1、预编译过程

C++为什么要进行预编译、预编译过程到底有什么作用呢?主要体现在以下几点:

宏定义,文件包含,条件编译,注释消除,四个大的部分

(1)宏替换

比如#define m 5,那么在该阶段会将程序中的m全部替换成5

当然,我们还可以定义带有参数的宏,如下

#include <iostream>
using namespace std;
 
#define MIN(a,b) (a<b ? a : b)   //带有参数的宏
 
int main ()
{
   int i, j;
   i = 100;
   j = 30;
   cout <<"较小的值为:" << MIN(i, j) << endl;
 
    return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

较小的值为:30

除此之外,

C++ 提供了下表所示的一些预定义宏:

描述
__LINE__这会在程序编译时包含当前行号。
__FILE__这会在程序编译时包含当前文件名。
__DATE__这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
__TIME__这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。

让我们看看上述这些宏的实例:

#include <iostream> 
using namespace std; 
int main () 
{ 
    cout << "Value of __LINE__ : " << __LINE__ << endl; 
    cout << "Value of __FILE__ : " << __FILE__ << endl; 
    cout << "Value of __DATE__ : " << __DATE__ << endl; 
    cout << "Value of __TIME__ : " << __TIME__ << endl; 
    return 0; 
}

当上面的代码被编译和执行时,它会产生下列结果:

Value of __LINE__ : 6
Value of __FILE__ : test.cpp
Value of __DATE__ : Feb 28 2011
Value of __TIME__ : 18:52:48

(2)条件编译

一般情况下,程序的每一行源代码都是要编译的。特殊情况下,只有满足一定条件的程序才需要编译,这就是条件编译。常用的条件编译关键字主要有#if、#ifdef、#ifndef、#else、#elif和#endif,那有什么好处呢?用来有选择地对部分程序源代码进行编译,可以有选择性的进行编译,提高编译效率。

(3)头文件#include的使用

其功能是将被包含的文件中的源代码放进源文件中,从而实现代码重用。简单的来说就是相当于将#include后面的内容替换到当前的文件里面,这样既方便组织代码,又方便代码重用。

#include "animal.h"实际上就是用animal.h文件的内容将其替换。#define 的作用是替换单个符号,而#include的作用是将这个#include这一句话用其include的头文件中的内容进行替换,比如我有下面的一个例子:比如我有一个头文件,叫做MyCode.h,文件内容:

int function(int a);

int function(int a)
{
    return a;
}

在MyCode.h文件中,我们声明了一个函数function,它带有一个整型的形参,返回值也是一个整型值,并且实现了这个函数。
我们再写一个代码文件,叫“MyCode.c”,内容如下:

#include <stdio.h>
#include "MyCode.h"

int main()
{
    int a = 1;
    int b = 0;
    b = function(a);
    printf("在main函数中,b的值是%d\n", b);
    return 0;
}

下面对这个MyCode.c文件进行预编译,与编译之后这个文件实际上变成了如下:

......这里是stdio的替换内容

int function(int a);

int function(int a)   //将MyCode.h里面的内容替换了进来
{
    return a;
}

int main()
{
    int a = 1;
    int b = 0;
    b = function(a);
    printf("在main函数中,b的值是%d\n", b);
    return 0;
}

总结:可见,#include 文件名 ,这条语句的功能是:在调用的时候复制该.h中的内容、粘贴到调用的地方的效果。这就是动态包含。动态包含的作用就是方便代码的组织和维护。
 

(4)line指令
C语言中可以使用__FILE__表示本行语句所在源文件的文件名,使用__LINE__表示本行语句在源文件中的位置信息。#line指令可以重新设定这两个变量的值,其语法格式为

line number[“filename”]

其中第二个参数文件名是可省略的,并且其指定的行号在实际的下一行语句才会发生作用。如下所示:

void test()  
{  
    cout << ”Current File: ” << FILE << endl; //Current File: d:\test.cpp  
    cout << ”Current Line: ” << LINE << endl; //Current Line: 48  
    #line 1000 “wrongfile”  
    cout << ”Current File: ” << FILE << endl; //Current File: d:\wrongfile  
    cout << ”Current Line: ” << LINE << endl; //Current Line: 1001  
}  

(5)#error指令

编译程序时,只要遇到 #error 就会跳出一个编译错误,既然是编译错误,要它干嘛呢?其目的就是保证程序是按照你所设想的那样进行编译的。
下面举个例子:
程序中往往有很多的预处理指令

#ifdef XXX
...
#else

#endif

当程序比较大时,往往有些宏定义是在外部指定的(如makefile),或是在系统头文件中指定的,当你不太确定当前是否定义了 XXX 时,就可以改成如下这样进行编译:

#ifdef XXX
...
#error "XXX has been defined"

#else

#endif

这样,如果编译时出现错误,输出了XXX has been defined,表明宏XXX已经被定义了。

(6)#pragma指令
#pragma指令的参数有很多种形式,每种形式都代表了一种不同的功能,#pragma指令的参数的形式如下:

  • message
  • argsused
  • exit/startup
  • inline
  • once
  • warning
  • code_seg和data_seg
  • resource
  • saveregs
  • hdrstop/hdrfile

1) #pragma message

#pragma message指令使用于提示一些有用的信息,程序编译的过程中,在编译信息窗口输出这些信息。

简单示例如下:

#define ISPC  
#ifdef ISPC  
#pragma message(“Macro ISPC is defined”) //编译输出:Macro ISPC is defined  
#endif  

2)#pragma argsused

#pragma argsused指令仅允许出现在函数定义之间,且仅影响下一个函数,使警告信息被禁止或者无效。

3)#pragma exit/startup

pragma startup指令可以实现设置程序启动之前需要执行的函数;pragma exit指令可以实现设置程序退出之前需要执行的函数。

4)#pragma once

#pragma once指令可以实现仅编译一次该头文件。一般#pragma once放在头文件的最开始。

5)#pragma warning

#pragma warning可以实现设定提示信息的现实与否以及如何显示。简单示例如下:

#pragma warning (disable:4507 34)
功能:不显示4507和34号警告信息
 
#pragma warning (once:4385)
功能:仅显示一次4385号信息
 
#pragma warning (error:164)
功能:将164号警告信息作为一个错误信息显示
 
#pragma warning (push)
功能:保存所有警告信息的现有警告状态
 
#pragma warning (pop)
功能:从栈中弹出最后一个警告信息

2、编译过程

3、汇编过程

4、链接过程

  • 链接:链接程序的主要工作就是将有关的目标文件(库文件、.o文件)彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
  • 具体工作: 
    当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
  • 链接方式
    • 静态链接:函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。
    • 动态链接:函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中

      记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

注意:在编译过程中头文件不参与编译,预编译时进行各种替换以后,头文件就完成了其光荣使命,不再具有任何作用
最后以一张图来结束本次内容:
在这里插入图片描述

五、静态库和动态库

1、什么是库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

库有两种:静态库(.a、.lib)和动态库(.so、.dll)

所谓静态、动态是指链接。回顾一下,将一个程序编译成可执行程序的步骤:

 

2、静态库

之所以成为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:

  • 静态库对函数库的链接是放在编译时期完成的。
  • 程序在运行时与函数库再无瓜葛,移植方便。
  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

Linux下使用ar工具、Windows下visual studio使用lib.exe,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索。

注意: 

静态库是在编译的时候就需要的,比如我需要某一个静态库 abc.lib 里面的某几个函数 func1,func2,func3  ,所以在链接的时候,就会将静态库 abc.lib 中  需要使用的func1、func2、func3的二进制代码“拷贝”到一个执行文件中,组成最终的我们的可执行文件,所以这个可执行文件一般略微偏大。

编译完成之后,就在也不需要静态库了,执行的时候因为已经将静态库的函数打包在了一起,所以完全不再需要静态库的存在。

 

3、动态库

通过上面的介绍发现静态库,容易使用和理解,也达到了代码复用的目的,那为什么还需要动态库呢?

为什么还需要动态库?

为什么需要动态库,其实也是静态库的特点导致。

(1)空间浪费是静态库的一个问题。

 

(2)另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可。

 

动态库特点总结:

  • 动态库把对一些库函数的链接载入推迟到程序运行的时期。
  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)
  • 将一些程序升级变得简单。
  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。

注意:

Window与Linux执行文件格式不同,在创建动态库的时候有一些差异。

在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字

Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。

与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。

 

注意:

我们并没有把动态库  .dll  或者是  .so 中的二进制代码“拷贝”可执行文件中,而是仅仅“拷贝”一些重定位和符号表信息,这些信息可以在程序运行时完成真正的链接过程,所以我们的程序在运行时如果没有上面的动态库时,将无法正常运行。

 

关于如何在Linux和windows下面生成静态链接库和动态链接库以及如何在程序中使用它们,会在后面的文章里面进行叙述。

另外注意:

.a  就是archive,是好多个.o合在一起,用于静态连接 ,即Static mode,和Windows下面的  .lib文件一样,多个.a可以链接            生成一个exe的可执行文件;
.so 是shared object, 用于动态连接的,和windows的dll差不多,使用时才载入。

 

4、静态库与动态库的区别

(1)可执行文件大小不一样

从前面也可以观察到,静态链接的可执行文件要比动态链接的可执行文件要大得多,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息。

(2)占用磁盘大小不一样

如果有多个可执行文件,那么静态库中的同一个函数的代码就会被复制多份,而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大。

(3)扩展性与兼容性不一样

如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署。

(4)依赖不一样

静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。

即便如此,系统中一班存在一些大量公用的库,所以使用动态库并不会有什么问题。

(5)复杂性不一样

相对来讲,动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活。这也不在本文的讨论范围。

(6)加载速度不一样

由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。再加上局部性原理,牺牲的性能并不多。

 

六 补充:C#的编译运行过程

1、C#的编译过程

即:C#源代码->编译成程序集,IL语言

2、运行过程,程序集(中间语言IL)->本地代码运行,让CPU进行识别,为了便于读者能更清楚的理解,请参考下图:

 

 

注意:C#编译之后生成的 .exe文件和我们平时电脑里面的exe是不一样的,后者是可以直接执行的,而前者只是一个“程序集”,

C#中的  .dll 和 .exe都是一个程序集,存储的是中间代码,后缀只是表示它的类型,比如只是用.exe表示这个程序集是可以执行的,这一定要区分清楚。

 

 

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值