C++零基础到工程实战(一) -环境搭建与 C++ 程序的生成过程

文章内容

一、环境:装对工作负载,建对项目

1.1 Visual Studio 2022 Community

本文使用 Visual Studio 2022 Community,课程代码以 C++14 为最低标准。新版编译器可以选择旧语言标准,但是旧版编译器未必支持新标准加入的语法和库功能。

安装 Visual Studio 时,需要在工作负载列表中勾选「使用 C++ 的桌面开发」。漏掉这一项,新建项目时就找不到控制台应用模板。安装器在最小集状态下显示 1.25 GB 的空间需求;勾选该工作负载后,总空间需求变为 10.19 GB,其中包含 MSVC v143(VS2022 C++ x64/x86 生成工具)和 Windows 11 SDK 10.0.22621.0。

1.2 新建控制台项目

创建「控制台应用」时,需要指定项目名称、项目位置及解决方案名称,并决定是否勾选「将解决方案和项目放在同一目录中」。项目应避开桌面和含中文的路径。项目名建议全部使用小写:Windows 通常不区分文件名大小写,Linux 和 macOS(若开启大小写敏感设置) 会区分,统一小写可以减少跨平台时的名称差异。

本章采用一个项目对应一个解决方案。勾选「将解决方案和项目放在同一目录中」后,项目路径不再多出一层同名目录。例如,未勾选时可能形成 …\src\ConsoleApplication7\ConsoleApplication7\,勾选后项目可以直接位于 …\src\firstcpp\。

1.3 运行环境

Visual Studio 的控制台模板会生成下面的源文件。主体包含 #include <iostream>、int main() 和一条 std::cout 语句。文件开头的说明以及末尾的运行、调试提示都是模板注释,不参与编译。

cpp
// 代码位置 code/src/firstcpp/firstcpp.cpp(Visual Studio 2022 控制台应用模板生成)// firstcpp.cpp:此文件包含 "main" 函数。程序执行将在此处开始并结束。// #include <iostream> int main(){    std::cout << "Hello World!\n";} // 运行程序:Ctrl + F5 或调试 >“开始执行(不调试)”菜单// 调试程序:F5 或调试 >“开始调试”菜单

按 Ctrl + F5 运行后,控制台打印 Hello World!。随后出现 …\src\firstcpp\x64\Debug\firstcpp.exe(进程 3912)已退出,代码为 0,说明模板代码已经生成 exe,并且进程正常结束。此时工具链、项目配置和程序入口均已就绪。若模板缺失,应回到安装器检查工作负载;若项目能够创建却不能生成,则错误信息会指向后文所述的具体构建阶段。

环境搭建与第一个项目的四个步骤

二、第一个程序的三个组成部分

第一个程序的三个组成部分

2.1 #include <iostream>

以 # 开头的内容属于预处理指令,不是普通的 C++ 语句。预处理发生在编译之前,#include <iostream> 会把 iostream 的内容复制到这一行,形成展开后的翻译单元。
尖括号表示从系统或开发环境提供的库目录中搜索;双引号包含的文件名会先从当前项目查找。

iostream 是 input/output stream,即输入流与输出流。打开该头文件还能看到 #include <yvals_core.h>、#include <istream>、#ifndef _IOSTREAM、#define _IOSTREAM 和 #pragma 等内容。一个看似简短的 include,预处理后会引入更多头文件、宏和编译控制指令;_IOSTREAM 一类宏用于防止头文件被重复包含。

#include 在预处理阶段展开依赖

2.2 int main():程序入口与代码块边界

工程可以有多个 .cpp 文件,程序仍然需要一个确定的起点,main 就是入口函数。int 声明返回值类型,() 是函数参数表,{} 则划定函数体。多个编译单元分别含有 main 时,各自的编译过程可能通过,链接器在合并目标文件时会因入口重复而报错。

2.3 std::cout << "Hello World!\n";

std:: 是命名空间限定,指明 cout 来自标准库命名空间。cout 是 iostream 中声明的控制台输出对象,<< 是流插入运算符,数据方向由右向左:右侧字符串被送入左侧输出流。双引号圈出字符串字面量,\n 是换行转义字符。连续输出时如果不写换行,后续内容会紧接在同一行。

2.4 分号,语句的结束符

C++ 语句的结束不由源码中的换行决定,语句末尾需要英文半角分号 ;。预处理器会删除注释和空行。

三、工程目录里的三类文件

工程目录里的三类文件与生成产物

3.1 源码:.cpp 参与编译,.h 通过 include 引入

每个 .cpp 单独形成一个编译单元,并生成对应的目标文件。.h 通常保存声明和被其他文件引用的内容,它不会作为独立单元编译;某个 .cpp 使用 include 时,头文件内容才会在预处理阶段复制进该编译单元。

3.2 项目配置:.sln、.vcxproj 及附属文件

.sln 是解决方案文件,可以组织多个项目;.vcxproj 是项目文件,记录编译选项、依赖和文件清单。.vcxproj.filters 控制解决方案资源管理器中的目录分组,删除它不影响编译;.vcxproj.user 保存用户级设置,例如调试器工作目录。迁移这个 Visual Studio 工程时需要保留 .cpp、.sln 和 .vcxproj;采用 CMake 的工程则用 CMakeLists.txt 描述构建配置。

3.3 生成产物:.obj、.pdb 与 .exe

以下目录树并列展示源码、配置和生成产物。树中的 x64/Debug 是生成目录,构建期间还会在这里出现日志与增量链接文件。

plain text
# 代码位置 code/src/firstcpp/(Visual Studio 2022 工程目录)code/src/firstcpp/                 # 项目目录(位置避开中文路径)├── .vs/                           # Visual Studio 的本地缓存与用户状态├── firstcpp.sln                   # 解决方案文件:可容纳多个项目├── firstcpp.vcxproj               # 项目文件:编译选项、依赖与文件清单├── firstcpp.vcxproj.filters       # 解决方案资源管理器里的目录分组,删掉不影响编译├── firstcpp.vcxproj.user          # 用户级设置,例如调试器工作目录├── firstcpp.cpp                   # 唯一参与编译的源文件└── x64/Debug/                     # 生成产物    ├── firstcpp.obj               # 编译产物(机器指令形态的目标文件)    ├── firstcpp.tlog/ firstcpp.exe.recipe firstcpp.ilk firstcpp.log vc143.idb    └── firstcpp.pdb               # 调试信息:源码行与可执行文件地址的对应表

firstcpp.cpp 编译后产生 firstcpp.obj,链接器再把目标文件合并成 firstcpp.exe。firstcpp.pdb 保存源码行与可执行文件地址之间的对应关系,供调试器使用。构建期间还会生成 firstcpp.tlog、firstcpp.exe.recipe、firstcpp.ilk、firstcpp.log、vc143.idb 和 vc143.pdb。

Visual Studio 的中间目录相对项目目录,输出目录相对解决方案目录。项目目录与解决方案目录不重合时,obj 和 exe 可能分别位于两个独立的 x64\Debug 目录;两者重合时,这些文件会显示在同一目录层级。查找生成文件不能只认目录名,还要分清当前查看的是项目目录还是解决方案目录。

四、从源代码到可执行文件的五个阶段

从源代码到可执行文件的五个阶段

4.1 预处理:展开依赖文件

预处理器处理 .cpp 及其中的指令,把 #include 指向的文件内容复制进来,同时删除注释和空行,生成展开后的翻译单元。头文件不存在或搜索路径错误,会在这里出现“找不到文件”一类错误。这个阶段处理的是文本,不会生成机器指令;展开后的内容仍要交给编译器检查语法。

4.2 编译与汇编:每个 .cpp 单独处理

编译器对单个翻译单元做语法检查并生成汇编码,汇编器再把汇编码转换成机器指令形态的目标文件。课程流程图用 .s 表示汇编码,这是 GCC、Clang 常用的命名;Windows 下的目标文件扩展名是 .obj,Linux 和 macOS 通常使用 .o;Android、鸿蒙也用 .o,Windows 在这件事上属于例外。

MSVC 的默认表现需要单独区分:cl /c 会直接生成 .obj,不会默认留下可见的汇编文件。需要查看汇编码时可加 /FA,对应输出通常为 .asm;需要查看预处理结果时可加 /P,对应输出为 .i。因此,流程中的“编译后得到汇编码”描述的是逻辑阶段,不等于 Visual Studio 默认会在目录中保留 .s。

每个 .cpp 独立编译,也带来一个影响:两个 .cpp 含有相同定义时,这两个编译单元都可能通过语法检查;链接器合并目标文件时,重复定义才会出现。

4.3 链接:合并目标文件并生成 exe

链接器收集多个 .obj,设置程序入口并接入依赖库,最终生成 .exe。函数已经声明却没有对应实现时,会出现未定义符号;同一符号定义多次会引发重复定义;存在多个 main 则会造成入口冲突。这些都属于链接错误。

4.4 执行:加载并运行程序

操作系统执行 exe 时还需要加载动态库。动态库缺失,以及内存或栈溢出等问题,通常不会在源码转换阶段被发现,而会在程序运行时出现。能够成功生成 exe,只说明构建流程已经完成,不能据此判断运行阶段不会出错。

4.5 把错误与阶段对应起来

阶段 输入 输出 典型错误
预处理 .cpp、头文件与预处理指令 展开后的翻译单元;MSVC /P 可输出 .i 找不到头文件
编译 单个翻译单元 汇编码;GCC/Clang 常用 .s,MSVC /FA 输出 .asm 语法错误
汇编 .s 或 .asm Windows 的 .obj,Linux/macOS 的 .o 语法错误(与编译阶段一并报出)
链接 多个目标文件与依赖库 .exe 未定义符号、重复定义、多个 main
执行 .exe 与运行时动态库 进程及控制台输出 动态库加载失败、内存或栈溢出

include 报错时先检查头文件和搜索路径;语法报错时定位当前 .cpp;多个文件各自编译成功、最终生成失败时,再检查链接器给出的符号名称。exe 已经存在而启动失败,则应把注意力转向动态库、内存和栈等运行条件。错误所在阶段不同,检查对象也随之改变。

五、注释

5.1 单行 // 与多行 /* */ 的适用场景

单行注释从 // 开始,适合说明紧邻的一行代码;多行注释由 /* 和 */ 包围,适合说明一段代码,也可以临时屏蔽一个完整片段。下面的示例保留两种形式,并让注释位于所说明代码的上方。

cpp
// 示例:单行注释与多行注释的两种写法#include <iostream> /*    此代码用于演示多行注释    多行注释也可临时屏蔽一段代码*/int main(){    // 单行注释说明紧邻的输出语句    std::cout << "Hello World!\n";}

这个片段编译运行后同样输出 Hello World!。两种注释都不会进入编译结果,也不会改变控制台内容。

5.2 预处理会删除注释和空行

注释服务于源码阅读。预处理阶段展开 include,并删除注释和空行;编译器随后处理已经展开的翻译单元。
注释内容不会变成机器指令,也不会增加运行时输出。

六、cout

cout 的数据流向与输出缓冲区的刷新时机

6.1 为什么使用 cout 而不是 printf

cout 通过流插入运算符 << 接收不同类型的内容,可以把多段数据连续送入输出流,并由 C++ 类型系统参与检查。C 的 printf 在 C++ 工程中仍可使用,但格式占位符与参数类型不匹配时容易出错。

6.2 using namespace std

命名空间给名称增加前缀,用来避免不同库中的同名对象发生冲突。使用 using namespace std; 后,可以把 std::cout 简写成 cout,但显式的 std::cout 仍然合法。

cpp
// 代码位置 code/src/firstcpp/firstcpp.cpp(注释与 cout 演示)// firstcpp.cpp:此文件包含 "main" 函数。程序执行将在此处开始并结束。// /*    此代码用于测试 cout,尽量简洁*/#include <iostream>// 命名空间输入简化using namespace std; int main(){    // 输出测试 cout 的代码    std::cout << "Hello World!\n";    // 换行并刷新缓冲    cout << "Hello Count" << endl;    // cout 输出多个内容    cout << "字符串一" << 200;}

程序运行后依次显示三行:Hello World!、Hello Count、字符串一200。最后一条语句可以连续插入字符串和整数;由于上一条 endl 已经换行,第三段内容从新行开始。

6.3 \n、endl 与 flush:缓冲区何时刷新

输出通常先进入缓冲区,攒够一批后再交给系统,以减少频繁输出的开销。\n 负责换行,但不主动刷新缓冲区;endl 会换行并刷新;flush 会刷新,但不换行。缓冲区已满或程序结束时,缓存内容也会写到控制台。

七、小结

预处理负责展开依赖并删掉注释,编译和汇编把单个 .cpp 变成目标文件,链接把多个目标文件合成可执行程序,运行时的问题要等到执行阶段才暴露。排错时先看当前阶段的输入和本该出现的产物,再按错误类型定位环节。

觉得不错?给作者点个赞吧

分类:c++

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

C++零基础到工程实战(一) -环境搭建与 C++ 程序的生成过程 · Yinchen 的博客