1. 概述
写c++程序的基本工作流程是你有一些C++的源文件,然后将这些源文件给到编译器,编译器将其转变成二进制的东西,二进制的东西可能是某种库,或者是可执行的程序(.exe)。本篇以创建项目结合案例的方式讲述。
- 头文件(.h)
- 源文件(.cpp)
- 头文件预处理
- 编译器编译源文件生成目标文件
- 链接器链接生成的目标文件
- 生成可执行程序文件(.exe)或库
2. 工程创建
下面以创建工程为例
1. 创建一个helloworld工程
以vs2015为例,打开vs,新建项目工程
选择空项目,并给项目起一个名称,我这里起的是HelloWorld
勾选为解决方案创建目录
创建好项目,项目的解决方案资源管理器
树结构
可以进入到项目所在文件浏览器,观察生成的文件
vs解决方案资源管理器中有一个按钮,显示所有文件
按钮
点击该按钮后,便可在项目根目录下创建src的目录
在src目录项创建一个main.cpp的源文件
创建好Main.cpp源文件,编写主入口程序(main函数)
2. main.cpp文件解析
-
#include <iostream> 预处理语句
#
符号之后的语句,都是预处理语句编译器收到源文件后,一旦扫描到这些语句,就优先处理这些预处理语句。这也是为什么叫预处理了,因为他在实际编译发生之前就被处理了。
-
include
,表示需要找到一个文件,在这个例子中,需要找到叫iostream的文件,然后将该文件中的所有内容拷贝到现在的文件内。这些所包含的文件通常被称为“头文件”。这里之所以要包含iostream这个头文件,是因为我们在main函数中需要一个被调用的函数的声明:std::cout
(可以让我们在终端上打印东西) -
main函数 程序入口
main函数是程序的入口,当我们运行程序时,计算机就从这个函数开始执行代码,当程序还在运行,计算机会逐行执行我们的代码。当然,程序也可以中断或者改变执行的顺序。他们是控制语句或者是函数调用,但最主要的还是一行一行的执行。因此,我们的程序首先执行的是
std::cout << "hello world!" << std::endl;
这句,然后是std::cin.get();
这句。运行完main中的所有东西,我们的程序结束了。这里需要注意的是,我们在main函数中并没有返回int值,虽有函数声明中有需要返回int值 但是main函数比较特殊。如果我们没有指定返回值,它会默认返回0。 这种情况只对main函数适用
-
<<
一种重载运算符这其实是一种重载运算符,可以把它理解成一个函数。将字符串
hello world!
推送到cout流中,然后打印到终端,然后推送一个行结束符std::endl
,endl告诉终端跳到下一行。 -
std::cin.get();
等待我们按下enter键,在前往下一句代码之前等待。这个时候程序暂停执行,直到我们按下回车键后,程序继续运行下一行。这里已经没有下一行了,所以程序返回0,意味着代码执行完了。
到现在已经写完main.cpp源代码文件,我们怎么把它转换成可运行的二进制文件。
3. 预处理阶段
#include <iostream>
这是预处理语句,编译器先处理这些语句。在这个例子中,编译器会将iostream
文件中的内容全部包含进来,也就是拷贝粘贴内容到代码文件中。包含进来后,就可以使用std::cout
和std::cin
这些函数了。
4. 编译阶段
当预处理语句处理完了之后,我们的文件将被编译,这个阶段,编译器将所有c++代码转化为实际机器代码,这里有些非常重要的设置决定我们怎么转化代码。
1. vs一些配置简单介绍
这里来介绍下vs界面的一些配置
1. 解决方案配置
和解决方案平台
可以看到vs中有两个重要的下拉菜单,分别是解决方案配置
和解决方案平台
,默认是debug
和x86(win32)
,这里特别说明一下,x86和win32是一个意思。
点击解决方案配置
下拉选项,可以看到Debug
和Release
2个选项。所有vs项目都默认有这两个选项。
点击解决方案平台
下拉选项,可以看到x64
和x86
两个选项。这些配置只是默认的。
配置:只是构建项目的时候的一系列规则而已。
解决方案平台:是指你编译的代码的目标平台
x86:意思是指目标平台是windows 32位,也就是说会生成32位的windows应用程序,
其他复杂的项目的目标平台也不相同,你可能在下拉菜单中看到Android平台,如果你想构建、部署、调试android,如果你想改变到android目标平台,解决方案配置这里需要设置一系列针对目标平台的规则。
2. 项目属性配置
接下来看下解决方案配置规则,我们可以在工程中更改在解决方案资源管理器中选中项目,右键选择属性
点击
可以看到项目的属性配置,这里定义的规则用来构建解决方案配置及解决方案平台。
首先需要注意的是配置
和平台
,要设置成你实际想要的配置和目标平台
你在设置release模式的话,对现在你用的debug模式没有任何影响,如果你忘记了这一点,你会奇怪为什么改了设置没起到一点作用。
这里调整到debug
模式,平台这里是win32
,
win32 等同于 x86 ,因为某些原因搞了不同的名字,但他们是一样的东西。
- 常规属性
包括目标平台版本(SDK版本),输出目录,中间目录等等。
项目默认值
中比较重要的一个配置
配置类型`英文对应Configuration type:应用程序(.exe) 英文对应 application(.exe)。
编译器会生成二进制文件,如果我们希望生成可执行的二进制程序,就是设置为应用程序(.exe)
如果想做一个生成库文件的项目,可以修改这里配置,选择下拉选项中的其他的选项。
- c/c++编译器方面的设置
这里有很多重要的设置项,比如include目录设置,优化(optimization)设置,代码生成设置,预处理定义等等一些配置。
这些规则控制我们的文件如何被编译,你可以看到debug和release的区别。如进入优化配置
页面,可以对比如下
这就是为什么默认的debug模式会更慢的原因,因为对比release模式很多优化都被关掉了,但是关掉优化的好处是让我们可以调试代码。
项目中每一个.cpp文件都会被编译,但头文件(.h)不会被编译,仅仅是cpp文件。
原因是之前说过的,头文件的内容在预处理是包含进来了。
cpp文件被编译的时候,包含进来的文件一起被编译了。
因此我们有了一堆将要被编译的cpp文件,分别被编译器编译,
每一个cpp文件都被编译成了一个object file(目标文件)。
如果使用的是vs,生成的目标文件后缀是`.obj`,
当我们有了这些cpp编译后生成的独立的obj文件后,我们需要把这些obj文件合并成一个执行文件,
实现这个就需要用到链接器(link)
- 可以看到vs中链接器的配置
基本上,链接就是将所有的obj文件,黏合到一起,把所有的obj文件合并成一个.exe文件
2. 编译文件
回到例子
首先,我们要编译这个文件
单独编译Main.cpp这个文件,可以使用快捷键ctrl+F7
或选中文件鼠标右键选择编译。
单文件编译(也叫生成)方式:
1. ctrl + F7快捷键方式
2. 选中文件,鼠标右键点击编译
3. 工具栏中找到单文件编译按钮,点击编译
可以看到输出窗口显示main.cpp编译成功了
如果不想使用ctrl+F7
或选中文件鼠标右键选择编译的方式,可以在工具栏中找到如下图标,点击也是编译单个文件。
如果你的vs工具栏中没有这个图标,可参考工具栏添加移除功能按钮。
如果我们搞点语法错误,例如,忘记写一个;
单独编译文件,看到输出信息将输出报错。
ps 如果你的vs没有错误列表(erro list)展示,可以依次按下ctrl \ e
三个按键或在菜单栏
中找到视图
选项,点击错误列表
vs有很多不同的方法显示错误信息,一种是错误列表(error list)
,另一种是输出(output)
。
错误列表经常会缺失一些信息,所以不能绝对依赖错误列表,错误列表基本上就是分析输出(output)
窗口,然后找到error这个单词,并抓取这部分信息展示出来,然后放到错误列表(error list)
中,所以错误列表
只能用来做参考,这只是个大概,如果需要细节,需要所有的出错信息,那就看输出
窗口。
从输出窗口中,我们可以看到有一处语法错误,main.cpp文件的第10行缺失;
在}
的前面,我们双击这一行错误信息。它会跳转到你的代码出错的位置。
我们修复这个问题,加上缺失的;
,重新编译ctrl + F7
现在我们已经编译了一个文件,当我们单独编译的时候,链接还没有发生,很明显我们单独编译一个文件,不会进行链接,然我们看看编译器编译后都生成了什么
选中解决方案中的项目右键打开项目所在目录
默认情况下,vs会输出构建文件到debug文件夹
进入debug文件夹
我们会看到Main.obj文件,这是我们的编译器生成的目标文件
对于项目中的每一个c++文件,编译器编译都会生成一个obj文件。
5. 链接阶段
回到vs
选择生成项目,这次就不是编译单个文件了,而是构建整个项目
如果你的输出窗口展示的信息量较少,想要展示更多内容,可参考输出窗口信息级别设置。
实际上你会看到生成了.exe文件,我们回到项目所在文件目录,在你的解决方案HelloWorld.sln
目录下的debug
目录
进入debug
目录,可以看到我们生成的.exe文件
双击.exe文件,可以运行它并打印结果。
如果是多个c++文件
我们来看个简单的例子
我们对main函数做下改造,不在main函数中使用std::cout
,用自己logging函数来包裹cout函数,因此,我们建立一个函数
这里的const char*
表示一个包含字符串的类型
重新运行
可以看到可以运行,现在将这个Log函数放到另外一个文件中,我们不希望一个main文件包含所有东西,希望把我们的代码分到很多文件当中,这样可以保证代码干净整洁。
我们新建一个Log.cpp文件
将main.cpp文件中的Log函数剪切到Log.cpp文件中,需要加上iostream
头文件
这时,我们生成Log.cpp单文件,可以看到会有一个报错
cout不是std的成员
,这是编译器告诉我们他不知道cout是什么东西,原因是我们没有声明它,在c++中,任何符号都需要声明,cout在main.cpp中已经声明了,声明它的文件显然就是iostream
,因此,我们要将iostream
添加到Log.cpp文件的最上面。这样的话,我们就可以得到cout函数声明,让我们再次编译Log.cpp单文件,可以看到编译通过了。
回到main.cpp文件,编译main.cpp单文件,会看到找不到Log标识符。
所以,我们将一个函数移动到另一个文件,然后我们对每个文件进行单独的编译,main.cpp文件并不知道还有个叫Log的函数,因为不认识它,所以报错,我们可以通过调用声明来修复此错误。申明就像宣布有个叫log的函数是存在的,这就像是一个承诺,告诉编译器,这里有一个叫Log的函数,编译器只需要相信我们就好了,这对于编译器是个好事情,因为编译器并不关心这个函数是在哪定义的
这里有两个名词声明
和定义
,
声明:这个符号、这个函数是存在的,可以不写出函数体。
定义:这个函数到底是什么,是函数的函数体。
回到main.cpp,声明和实际的定义很相似。声明没有函数体
现在进行编译,可以看到编译通过了
所以,编译器是如何知道log函数会在另一文件中的呢,答案是我们做了声明告诉了编译器,那么它是怎么实际运行到正确的代码,这里就需要链接了,当我们构建整个工程时,不是单个文件,而是项目生成,直接F7或在解决方案的项目中右键生成,我们的所有文件都会被编译,链接器会找到正确的log函数的定义在哪里,将函数定义导入到log函数中,让我们在main.cpp中调用,如果我们找不到定义,将会出现链接错误。
接下来,我们看例子 我们将Log.cpp文件中Log函数改下名字改成Logr保存如下
回到main.cpp文件,我们单独编译main.cpp文件,ctrl+F7
,可以看到编译通过了,因为我们声明了Log函数,编译器相信了,这是单文件编译,还没有到链接阶段。
然而我们构建整个项目,点击项目,右键生成
可以看到出现了链接错误
这个链接错误看上去有些吓人,因为有一些我们自定义函数的额外信息,例如,根据惯例会显示调用函数的系统内部命名。
其实也就是告诉我们,这个无法解析的外部函数log,他的返回值和参数是什么,相对于main.cpp文件来说。无法解析的外部符号是指,链接器无法解析这个符号。链接器的工作就是连接函数,但它找不到log函数连接到哪了,因为我们并没有一个叫做Log的函数定义,一个拥有函数体的Log函数。因此我们修复这个问题,要提供一个Log函数的定义,也就是需要为Log函数写函数体。Log函数的函数体不一定要在Log.cpp文件里,也可以在main.cpp文件里,也可以在其他地方。我们修复回来,提供Log函数定义。
然后再次构建项目,项目生成,可以看到编译通过了。
回到文件浏览器中,进入到项目里面生成的Debug目录,看到里面的目录会有2个.obj文件,因为编译器会为每一个源文件生成一个obj文件,链接器会将他们合并成一个.exe文件。在这个例子中,我们有Log的定义,在Log.obj文件里面。我们的main函数在main.obj里面,链接器从Log.obj文件中,拿出Log函数的定义,放入二进制文件中,也就是我们的helloworld.exe文件,包含Log和main的函数定义。
至此,这就是c++如何工作的最基本的东西。