赞
踩
不像python等动态语言,C、C++是静态语言,需要编译之后,然后才能执行,C++的编译器有很多,比如常见的MSVC、g++(gcc)、等,本文介绍g++,关于g++的详细介绍,本文不再介绍,可以查阅相关文档。
gcc原本是基于Linux平台的,但是在windows平台上提供了一个同样的实现,叫做MingW
即(Minimalist GNU for Windows)。当前是MingW-w64取代了之前的MingW-w32,支持64位的操作系统。可以在官网下载安装,
但是有一个问题是,安装的过程中需要联网,即边下载边安装,这样很容易失败,所以一般是先下载离线文件,然后直接配置即可。
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
比如我有一个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,即连接目标代码,生成可执行程序
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指令可以重新设定这两个变量的值,其语法格式为
其中第二个参数文件名是可省略的,并且其指定的行号在实际的下一行语句才会发生作用。如下所示:
- 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指令的参数的形式如下:
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、链接过程
当链接器进行链接的时候,首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定义表,对其中记录的地址进行重定向(加上一个偏移量,即该编译单元在可执行文件上的起始地址)。然后遍历所有目标文件的未解决符号表,并且在所有的导出符号表里查找匹配的符号,并在未解决符号表中所记录的位置上填写实现地址。最后把所有的目标文件的内容写在各自的位置上,再作一些另的工作,就生成一个可执行文件。
记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
注意:
在编译过程中头文件不参与编译,预编译时进行各种替换以后,头文件就完成了其光荣使命,不再具有任何作用
最后以一张图来结束本次内容:
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)加载速度不一样
由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。再加上局部性原理,牺牲的性能并不多。
1、C#的编译过程
即:C#源代码->编译成程序集,IL语言
2、运行过程,程序集(中间语言IL)->本地代码运行,让CPU进行识别,为了便于读者能更清楚的理解,请参考下图:
注意:C#编译之后生成的 .exe文件和我们平时电脑里面的exe是不一样的,后者是可以直接执行的,而前者只是一个“程序集”,
C#中的 .dll 和 .exe都是一个程序集,存储的是中间代码,后缀只是表示它的类型,比如只是用.exe表示这个程序集是可以执行的,这一定要区分清楚。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。