1076 字
5 分钟
C++ 基础:从源码到可执行文件(编译/链接/类型/调试)

这篇笔记主要在整理“写下第一行 C++ 代码后,它到底经历了什么”和一些容易混淆的基础概念。

从源码到可执行文件:发生了什么#

main.cpp
#include <iostream>
int main()
{
std::cout << "Hello World\n";
std::cin.get();
}

关键点:

  • #include <iostream> 属于预处理指令:预处理阶段会把头文件内容“展开”到当前翻译单元里。
  • operator<< 是重载运算符;你可以把 std::cout << "Hello" 当成对某个函数的调用来理解。
  • main 到达函数末尾时会隐式 return 0;(所以不写 return 0; 也能正常结束)。

编译流程可以粗略理解为:

main.cpp
|
| 预处理 (preprocess) : 处理 #include/#define,得到展开后的源码
v
翻译单元 (translation unit)
|
| 编译 (compile) : 语法/语义分析 + 优化,生成汇编(概念上)
v
汇编 (assemble) : 生成目标文件 .obj/.o(含符号表)
|
| 链接 (link) : 解析符号 + 合并多个 obj + 链接库 -> exe
v
app.exe
TIP

在 Visual Studio 里 Ctrl + F7 只会编译当前 .cpp(生成 .obj),不会执行最后的链接步骤;“生成/生成解决方案”才会把多个 .obj 链接成 .exe

声明 vs 定义:链接器在做什么#

  • 声明(declaration):让编译器知道“有这么个符号”。
  • 定义(definition):提供实体本体(函数体、对象存储等)。
  • 链接器(linker):把所有目标文件/库里的符号合在一起,解决“引用但未在本翻译单元定义”的符号。

一个典型的组织方式:在头文件写声明,在 .cpp 写定义。

Log.h
#pragma once
void Log(const char* message);
Log.cpp
#include <iostream>
#include "Log.h"
void Log(const char* message)
{
std::cout << message << "\n";
}
main.cpp
#include "Log.h"
int main()
{
Log("Hello");
}

如果你只写了声明却没有把对应定义链接进来,通常会在链接阶段看到类似 “unresolved external symbol” 的错误。

基础类型:别背结论,先记住哪些是“实现定义”#

不同平台/编译器的基础类型大小可能不同:

  • Windows/MSVC 常见是 LLP64:int 32-bit,long 32-bit,long long 64-bit,指针 64-bit。
  • Linux/macOS 64-bit 常见是 LP64:long 64-bit,long long 64-bit,指针 64-bit。

想确定就用 sizeof 打印:

#include <iostream>
int main() {
std::cout << "sizeof(bool)=" << sizeof(bool) << "\n";
std::cout << "sizeof(char)=" << sizeof(char) << "\n";
std::cout << "sizeof(short)=" << sizeof(short) << "\n";
std::cout << "sizeof(int)=" << sizeof(int) << "\n";
std::cout << "sizeof(long)=" << sizeof(long) << "\n";
std::cout << "sizeof(long long)=" << sizeof(long long) << "\n";
std::cout << "sizeof(void*)=" << sizeof(void*) << "\n";
}

补充几个容易踩坑的点:

  • char 的大小固定是 1 字节,但 char 是否有符号是实现定义的。
  • 无符号整数溢出是按模回绕;有符号溢出是未定义行为(UB)。

字符与数字:它们只是“解释方式”不同#

char a = 50;
char b = 'A';
short c = 'A';
// 观察输出时,注意区分按字符输出还是按数字输出
// (例如把 char 转成 int 再输出)

直觉可以这样记:内存里只有比特;类型只是你选择如何解释这些比特。

浮点数:float/double 与字面量后缀#

  • float 常见是 4 字节,double 常见是 8 字节,但仍然以 sizeof 为准。
  • 1.0 默认是 double,写 1.0f 才是 float

image.png

image.png

函数:调用开销与可读性#

int Multiply(int a, int b)
{
return a * b;
}

函数调用确实涉及参数传递、返回地址、栈帧等概念,但现代编译器经常会做内联和优化调用路径。

NOTE

经验法则:先写清晰的函数边界;除非性能分析(profile)证明这里是瓶颈,否则不要为了“省一次调用”过早优化。

头文件:重复包含与 include guard#

#pragma once 是最常见的写法:

#pragma once

传统写法是 include guard:

#ifndef _LOG_H
#define _LOG_H
void InitLog();
void Log(const char* message);
#endif

#include <...>#include "..." 的差异主要在搜索路径顺序:通常前者用于系统/第三方头,后者用于项目内头文件。

Visual Studio 调试:断点、单步、内存视图#

  • F9:断点;把表达式拆成多行更利于单步调试。
  • 单步:步入(进函数)、步过(不进函数)、步出(跳出函数)。

image.png

三个常用窗口:自动窗口、局部变量窗口、监视窗口。

内存视图:

image.png

  • 可以输入 &a 获取变量地址。
  • 大多数 x86/x64 机器是小端序(little-endian):多字节整数在内存视图里看起来像“反着读”。
C++ 基础:从源码到可执行文件(编译/链接/类型/调试)
https://fuwari.vercel.app/posts/c/
作者
某不知名的根号三
发布于
2026-01-12
许可协议
CC BY-NC-SA 4.0