目录
像VS这样的IDE帮我们包办了很多的事情,同时把很多的配置参数有序、图形化地展现给我们。但VSCODE这样的轻量级IDE省去了很多这样的东西,这给了程序员巨大的灵活性,让程序员可以自定义这些被包办的事情(当然有时候又非常繁琐)。同时VSCODE通过另一种方式来简化流程,即各种插件,当然插件也经常需要进行配置。所以VSCODE+MSVC配置的实质是让我们从基本的命令开始来构建我们的程序,例如各样的编译指令。
在Windows的C/C++编程中,很多人选择gnu工具链(gcc make cmake),而微软自己也有一套MSVC工具链(cl nmake cmake msbuild)。相比之下,后者的资料少一些(但微软的MSVC官方文档非常齐全,还有中文支持),且通常是直接使用VS IDE的,对于编译指令细节讲得比较少。故本文将从实践出发,通过几个简单的例子来快速入门MSVC工具链。
EXE: HelloWorld
首先从简单的exe程序开始。这部分主要跟着微软官方文档来,写的不清楚的地方可以查看原官方文档,文档内容主要涵盖启动方法、生成运行、Debug和插件设置:
Configure Visual Studio Code for Microsoft C++https://code.visualstudio.com/docs/cpp/config-msvc
设置运行环境
因为msvc编译器的运行需要先设置好一系列的环境变量才能运行,所以直接从vscode的命令行窗口是不能运行msvc的。
要运行,有两个方法:一是自己把所有需要的环境变量设置好,二是用msvc提供的设置脚本自动设置。第二个方法更方便,所以这里用第二个。
装完vs后可以看到开始菜单里有几个命令行(下图),点击可以见到。跟直接运行cmd不同的是,这些命令行会在启动的时候运行msvc提供的脚本自动设置cl需要的环境变量,因此在这些命令行环境中运行的程序都能获得这些环境变量,再运行cl已经可以成功。下图中运行的是x64的命令行,因此cl编译出来的程序是64位的。
查看这些命令行的属性,以x64的为例,是下面的命令。解析:1、在cmd中运行 echo %comspec% 可以打印出这个变量的内容:C:\WINDOWS\system32\cmd.exe,所以翻译一下命令的开头是 cmd /k 。2、/k 选项是指让命令行执行完后面的命令后不关闭,设置为不关闭的原因是我们还要继续在这个命令行环境中开发程序。3、后面双引号括起来的命令是cmd启动时要先执行的东西,可以发现指向的是一个bat脚本,脚本里就是配置环境变量的一些操作。
%comspec% /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"
根据解析,我们可以自定义我们想要的环境启动器,比如在项目文件夹中新建一个bat脚本文件作为启动器,在里面写入下面的指令。这里我们想要的是命令行最后停留的目录是我们的项目文件目录,以后启动的时候我们就不用再手动定位项目了。/k 后跟的是用 & 拼接的两条指令:一条是运行vs环境配置脚本;后一条是定位到我们的项目文件夹,即cd %projectdir%,我把项目文件夹放在了桌面上。后面还可以继续加命令,注意:后面的所有命令都要被最外层的双引号括起来。
cmd /k " "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat" & cd "C:\Users\lenovo\Desktop\myproj" "
运行我们的环境启动器,运行"code ." 启动VSCODE,并打开项目文件夹。
编写&运行
首先需要VSCODE里装好官方提供的C/C++插件,这方便我们一开始自动生成配置文件,后面还会介绍怎么自己写配置文件。
首先新建一个helloworld子文件夹,里面新建源代码文件:helloworld.cpp,写入简单的代码。
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<string> msg {"Hello", "C++", "World", "from", "VS Code", "and the C++ extension!"};
for (const string& word : msg)
{
cout << word << " ";
}
cout << endl;
return 0;
}
接下来点右上角的三角运行箭头,选择运行程序(ps 这个三角箭头是C/C++插件提供给我们的),然后上面会弹出来让我们选择编译器,可以发现插件已经识别出msvc了,我们就选择第一个cl.exe。
如果没有在配好的环境中启动VSCODE,那么这时插件会提示必须在从命令行启动VSCODE,那我们是没有办法直接通过插件提供的设置进行编译的。 解决的方法有,就在官方文档的最后,我们放到后面讲。
编译运行成功,我们发现项目中多了一个.vscode文件夹,这里放了一个tasks.json。tasks.json就是存放本项目需要用到的所有生成指令的配置文件;每一中生成过程都是一个task,比如用来生成简单exe的是一个task,生成DLL的是一个task,生成LIB的也是一个task。这里插件自动生成的task是用来生成exe的task,具体解析看前面的官方文档。这里解析关键的:label指的是该task的名称,方便辨认;command和args组成task的核心编译指令:cl.exe [各种编译参数] 源码文件,看到里面用到了一些环境变量,主要是路径名和文件名。在下面的命令行中可以看到替换成环境变量具体内容后的helloworld的生成指令。对照这条具体命令大概就可以理解tasks.json里command和args的设置方法了。说说参数:cl是msvc的编译器,/Zi 参数跟生成debug版本有关,/EHsc跟异常处理模型有关,/Fe指定生成后程序的文件名,最后面跟的是源代码文件。
模仿这个,我们在VSCODE里新建一个命令行,运行自己的编译指令。ps 这条省略了Zi参数,生成的是Release版,要Debug的话还是要用Zi参数。编译成功后命令行运行一下,这样我们就知道了插件的原理:在tasks.json中设置编译指令,生成时运行对应的编译指令。
cl /EHsc helloworld.cpp
接着我们进行调试和插件设置,不是重点,这里就直接参看官方文档内容吧,步骤解说很详细,这里只讲下.vscode中的配置文件:launch.json是项目调试的配置,c_cpp_properties.json是插件在本项目中的配置,再加上tasks.json和settings.json等,这些配置文件都只在本项目中生效,如果其他项目想要用同样的配置,可以直接把.vscode文件夹整个复制到需要的项目中,免去重配的麻烦。当然可以调全局配置,全局配置的话适用于每个项目,但如果有的只在本项目中才会用到,那也不需要配置成全局的,免得出现混乱。
免设置运行环境的方法
不先设置环境,msvc无法正常运行,那么直接打开的VSCODE里怎么用msvc呢?很简单,我们在每次生成时都先在命令行中运行下设置环境脚本、再运行生成命令就行了。两步写在一行里是这样的,其实就是用 & 拼起来。当然,这时候插件是用不了了。ps 执行过一次环境配置脚本后,这一个命令行其实已经可以用msvc了,以后直接运行后面的生成命令就行。
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat" & cl /EHsc helloworld.cpp
前面我们解析过tasks.json的原理就是配置生成指令而已,所以上面的合成命令可以写道tasks.json里,具体写法在官方文档最后,看一下就大概明白是什么意思了。
LIB: 加法函数 Add
接下来两个部分的核心其实就是写msvc编译指令而已,本文在命令行直接运行,也可以写新的task,模仿exe的写法编就行。我们同时演练一下宏指令,学习一下LIB和DLL的写法。参考两篇不错的文章:第一篇介绍extern关键字和一些C/C++混合编写问题;第二篇介绍宏指令的细节。
extern “C“ 用法详细说明_小学徒666的博客-CSDN博客_extern “c”https://blog.csdn.net/weixin_40593838/article/details/122474117C/C++预处理器,宏定义,宏函数浅析_乔以亦的博客-CSDN博客_c/c++预处理宏
https://blog.csdn.net/qq_33865609/article/details/121243887 使用VS构建LIB的过程在官方文档中,步骤详细,实在搞不懂的话可以用VS顺着文档走一遍再回来看。演练:创建并使用静态库 (C++) | Microsoft Learn
https://learn.microsoft.com/zh-cn/cpp/build/walkthrough-creating-and-using-a-static-library-cpp?view=msvc-170
C语言LIB
新建子项目文件夹addlib,为了显得项目更有序,我们新建专门放源代码文件的src文件夹。先新建头文件add.h
#ifndef ADD_H
#define ADD_H
#ifdef cplusplus
#define EX_C_CPP extern "C"
#else
#define EX_C_CPP extern
#endif
EX_C_CPP int add(int a,int b);
#endif
解析一下几个宏指令:
- 前两行+最后一行是库头文件的写法惯例,意思是如果没有定义过ADD_H,那我就定义ADD_H,并声明我的库函数,否则不进行任何导入操作。为什么这么写呢?主要是为了库的使用者。因为如果库的使用者还用了另一个同名的库(同名的话可能是功能相似的库),为了避免这两个同名库的代码出现冲突,那么这个库就不导入了。ADD_H可以随便定义,这个标识只是为了告诉其他库“我已经被导入了”的信息,至于这个信息各个库怎么处理(或者不处理)是各自的事。
- 中间部分:如果定义了cplusplus,那就使用extern "C",否则使用extern。因为库函数要被其他程序使用,所以需要extern关键字让这个库函数能被库使用者看见,不然只有库内部才能用这个函数。extern "C"是告诉编译器使用类C语言的连接规则。如果c++程序中使用该库的函数,应该使用 extern "C",不然将无法使用,然而对于c程序来中,其本来就使用C语言的连接规则,且C语言中没有这种写法,故使用extern即可。
- 最后部分:声明库函数。库是一个C语言库,故编译库时不定义cplusplus,该条声明为 extern int add(int a,int b)。如果c++程序需要使用本库,在编译时应定义cplusplus,则在include的时候,头文件的声明为 extern "C" int add(int a,int b)。具体原因参见前面讲extern "C"的博客。
接着在add.c中实现加法函数
#include "add.h"
int add(int a,int b) {
return a+b;
}
编译:命令行/task
编译可以在命令行手动编译,或写成task,方便其他LIB的编译需求。VSCODE下方新建terminal,进入addlib,运行编译命令
cl /c src/add.c
lib add.obj
第一行 /c 参数告诉编译器不要链接,因此在addlib下只生成一个add.obj文件;接着lib add.obj用add.obj生成add.lib。在addlib下编译的好处是编译出的文件都在src外,与源码文件分离开来。也可以将这个编译流程写成task:type 为 shell 说明该task在命令行中执行;command+args 就是上面那两条指令合并起来;options里的cwd是shell的目录,fileDirname指的是源码目录,故要加上\\..指向上一级的子项目目录,这样才能达到编译文件与源码文件分离的目的。
{
"type": "shell",
"label": "C: cl.exe build LIB",
"command": "cl.exe",
"args": [
"/c",
"${fileDirname}\\${fileBasenameNoExtension}.c",
"&","lib","${fileDirname}\\..\\${fileBasenameNoExtension}.obj"
],
"options": {
"cwd": "${fileDirname}\\.."
},
"problemMatcher": [
"$msCompile"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Release Version LIB Generation in separate directory"
}
]
测试Add.lib
我们创建一个新的子项目atest用于测试,在下面新建src源码目录。为了验证extern "C",我们的atest设置为C++程序,新建源码文件atest.cpp。
#include "add.h"
#include <iostream>
int main() {
std::cout<<add(2,3)<<std::endl;
return 0;
}
在子项目目录中执行编译指令,这次参数有点多,分为两段:/link前面和后面。
- 前面的/I 是增加include头文件的目录,这里指向addlib的源码目录;/Dcplusplus 相当于在atest.cpp最前面加入一行#define cplusplus,那么在后面的#include "add.h"中就会在 add 函数的声明前加上 extern "C",不加这个参数或者这个define的话会报错,可以试一下。
- /link 告诉cl 把后面的部分交给链接器,应该链接所有的LIB,这里是add.lib,然后用/libpath增加lib搜索目录,这里是addlib项目目录。
编译成功!atest目录有一个atest.exe和atest.obj,运行atest.exe可以看到输出为5。
cl /EHsc src/atest.cpp /Dcplusplus /I ../addlib/src/ /link add.lib /libpath:../addlib
DLL: 乘法函数 Mul
这里,我们实践一遍DLL的编写,用cl编译DLL并测试。我们还会看看带预编译头文件的项目怎么编译。下面的官方文档是用VS编写DLL并测试。如果下面阅读遇到困难,走一遍VS的流程帮助理解。附带两篇是关于预编译头文件的官方文档。
演练:创建和使用自己的动态链接库 (C++) | Microsoft Learnhttps://learn.microsoft.com/zh-cn/cpp/build/walkthrough-creating-and-using-a-dynamic-link-library-cpp?view=msvc-170/Y (precompiled headers) | Microsoft Learn
https://learn.microsoft.com/en-us/cpp/build/reference/y-precompiled-headers?view=msvc-170
nullhttps://learn.microsoft.com/zh-cn/cpp/build/creating-precompiled-header-files?view=msvc-170
C语言DLL
老样子,新建一个muldll的子项目,下建src源码目录。源码目录新建mul.h
#ifndef MUL_H
#define MUL_H
#ifdef cplusplus
#define EX_C_CPP extern "C"
#else
#define EX_C_CPP extern
#endif
#ifdef export
#define DLL_EI dllexport
#else
#define DLL_EI dllimport
#endif
EX_C_CPP __declspec(DLL_EI) int mul(int a,int b);
#endif
这次我们在cplusplus一段后面还加了export一段。编写DLL时,除了用extern、还应该用__declspec(dllexport)修饰待导出到DLL的函数,对于DLL使用者时,应该用dllimport来修饰待从DLL导入的函数(ps 不写也行,原理参看下面博客)。对此,我们把dllimport设置为默认选项,方便使用者使用,当我们编译生成DLL时就应该#define export了。
__declspec(dllimport)加和不加的区别_iihacker_cat的博客-CSDN博客_dllimport不写https://blog.csdn.net/xiaohua_de/article/details/78243025 另外,还有def文件导出的方法,这里略去。参看下面的博客。
两种方式实现DLL导出函数之dllexport与def文件导出 - DllExport - 代码 - 我的学习圈 - WRITE-BUG (writebug.com)https://www.writebug.com/git/Gentleman/DllExport 新建mull.c,实现乘法函数
#include "mul.h"
int mul(int a,int b){
return a*b;
}
编译DLL
命令行输入编译指令。/Dexport 设置为导出,/LD告诉链接器生成为DLL文件。可以看到,生成了一个mul.dll和一个mul.lib,注意这个LIB跟上一部分的LIB功能上不一样,这个LIB仅用来辅助使用DLL,主要的内容是保存在DLL里,而上一部分的LIB包含库的所有内容。
cl /Dexport src/mul.c /LD
测试Mul.dll
新建mtest子项目文件夹,在下面新建src源码文件夹。这里我们实践一下带预编译文件的项目的编译生成方法。预编译头文件主要用于节省编译生成的时间,因为预编译头文件里的东西在没有修改的情况下只会编译一次,而不是每次项目编译时都会编译。
生成预编译文件
新建预编译头文件pch.h,在里面放入项目要导入的各种库。注意,放进预编译头文件的库最好基本不修改,因为每次修改都会让cl把所有东西编译一遍,无法节省时间。一般放的都是不会修改的标准库。
#ifndef PCH_H
#define PCH_H
#include <iostream>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <vector>
#include <algorithm>
#include <Windows.h>
#endif
接着新建预编译的源代码文件pch.cpp
#include "pch.h"
生成预编译文件,在子项目目录执行如下命令。尽管pch.h里没有cplusplus等段,但是预编译文件的编译参数必须跟使用该预编译文件的程序的编译参数相同(编译一致性,参见上面官方文档),因此要上/EHsc /D参数;/Yc告诉cl生成预编译文件,预编译头文件名为pch.h,生成的文件默认名为pch.pch,因为不需要链接,所以加上/c。
我们看到生成了pch.pch和pch.obj两个文件。要使用预编译文件的话只需要这两个文件即可,与源代码文件夹里的pch.h和pch.cpp都没有关系了。
cl /EHsc /Dcplusplus /c /Ycpch.h src/pch.cpp
使用预编译文件
新建测试程序mtest.cpp
#include "mul.h"
#include "pch.h"
using namespace std;
int main() {
cout<<2*3<<endl;
return 0;
}
编译mtest,忽略讲过的/Dcplusplus、/EHsc、/I、/link。注意/Yu参数,它告诉编译器使用指定的预编译文件,尽管写的是pch.h,但其实指的是pch.pch,跟pch.h无关;/Fe指定exe的名字为hello;注意尽管我们使用的是mul.dll,但生成时还是要使用mul.lib来辅助DLL的使用;在/link后,添加pch.obj以告诉链接器要把pch.obj也链接在一起,这是使用msvc必要的,参见上面的博客。
cl /EHsc /Dcplusplus /I ../muldll/src /Yupch.h /Fe:hello src/mtest.cpp mul.lib /link /libpath:../muldll/ pch.obj
可以看到,我们生成了hello.exe,运行成功输出6。
CMAKE
msvc工具链也提供了对应的CMAKE来作为项目管理的工具。在VS创建项目时也可以选择使用CMAKE来管理。我们在VSCODE中使用msvc提供的CMAKE。
安装CMAKE
打开Visual Studio Installer,找到VS版本后点击修改;找到C++的桌面开发,勾选里面的"用于Windows的C++ CMake工具"并安装。这样我们获得了msvc的CMAKE。实际上也可以使用CMAKE官方网站的版本,需要一些配置即可,这里忽略。
安装完后,我们可以在VSCODE的命令行中输入cmake,发现已经可以运行了。注意,msvc的CMAKE还是要在配置好的开发环境中才能运行。
使用CMAKE
新建hellocmake的子项目文件夹,下面新建源代码目录src,接着是hellocmake.cpp
#include <iostream>
int main() {
std::cout<<"hello cmake"<<std::endl;
return 0;
}
接着新建CMakeLists.txt,作为CMAKE的编译配置文件。第一行设置项目名,第二行设置生成的程序名和需要使用的源代码文件。
PROJECT(cmaketest)
ADD_EXECUTABLE(hellocmake hellocmake.cpp)
回到子项目目录。运行命令产生生成目录,-S指定源代码文件夹;-B指定生成目录所在位置。可以看到生成目录中是一些配置文件,包括编译器的选择等,每一个项目在第一次使用CMAKE生成时都需要运行这条命令产生生成目录,有了这个目录后就不用运行这条命令了。cmake --help可以看到手动选择编译器的参数。
cmake -S src -B build
接下来,开始编译生成。--build指定生成目录的位置,可以注意到已经不需要我们指定源代码目录了,因为这些信息都在生成目录的文件里;--config 是一些选项,这里指定为生成Release版本,默认情况下是Debug版本;-j 是并行生成的并行数量,可用于缩短大项目生成需要的时间。
cmake --build build --config Release -j 6
生成成功!运行build\Release文件夹中的hellocmake.exe,成功运行!注意:也可以使用VSCODE的CMAKE插件来生成,插件提供了方便的配置操作,比如查看和选择可用的编译器等。具体过程本文略去,可以参看其他的博客。