文章内容
- 1.1 Visual Studio 2022 Community
- 1.2 新建控制台项目
- 1.3 运行环境
- 2.1 #include
- 2.2 int main():程序入口与代码块边界
- 2.3 std::cout << "Hello World!\n";
- 2.4 分号,语句的结束符
- 3.1 源码:.cpp 参与编译,.h 通过 include 引入
- 3.2 项目配置:.sln、.vcxproj 及附属文件
- 3.3 生成产物:.obj、.pdb 与 .exe
- 4.1 预处理:展开依赖文件
- 4.2 编译与汇编:每个 .cpp 单独处理
- 4.3 链接:合并目标文件并生成 exe
- 4.4 执行:加载并运行程序
- 4.5 把错误与阶段对应起来
- 5.1 单行 // 与多行 / / 的适用场景
- 5.2 预处理会删除注释和空行
- 6.1 为什么使用 cout 而不是 printf
- 6.2 using namespace std
- 6.3 \n、endl 与 flush:缓冲区何时刷新
一、环境:装对工作负载,建对项目
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 语句。文件开头的说明以及末尾的运行、调试提示都是模板注释,不参与编译。
// 代码位置 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 一类宏用于防止头文件被重复包含。

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 是生成目录,构建期间还会在这里出现日志与增量链接文件。
# 代码位置 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 单行 // 与多行 /* */ 的适用场景
单行注释从 // 开始,适合说明紧邻的一行代码;多行注释由 /* 和 */ 包围,适合说明一段代码,也可以临时屏蔽一个完整片段。下面的示例保留两种形式,并让注释位于所说明代码的上方。
// 示例:单行注释与多行注释的两种写法#include <iostream> /* 此代码用于演示多行注释 多行注释也可临时屏蔽一段代码*/int main(){ // 单行注释说明紧邻的输出语句 std::cout << "Hello World!\n";}这个片段编译运行后同样输出 Hello World!。两种注释都不会进入编译结果,也不会改变控制台内容。
5.2 预处理会删除注释和空行
注释服务于源码阅读。预处理阶段展开 include,并删除注释和空行;编译器随后处理已经展开的翻译单元。
注释内容不会变成机器指令,也不会增加运行时输出。
六、cout

6.1 为什么使用 cout 而不是 printf
cout 通过流插入运算符 << 接收不同类型的内容,可以把多段数据连续送入输出流,并由 C++ 类型系统参与检查。C 的 printf 在 C++ 工程中仍可使用,但格式占位符与参数类型不匹配时容易出错。
6.2 using namespace std
命名空间给名称增加前缀,用来避免不同库中的同名对象发生冲突。使用 using namespace std; 后,可以把 std::cout 简写成 cout,但显式的 std::cout 仍然合法。
// 代码位置 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 变成目标文件,链接把多个目标文件合成可执行程序,运行时的问题要等到执行阶段才暴露。排错时先看当前阶段的输入和本该出现的产物,再按错误类型定位环节。