1076 字
5 分钟
C++ 基础:从源码到可执行文件(编译/链接/类型/调试)
这篇笔记主要在整理“写下第一行 C++ 代码后,它到底经历了什么”和一些容易混淆的基础概念。
从源码到可执行文件:发生了什么
#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 vapp.exeTIP在 Visual Studio 里
Ctrl + F7只会编译当前.cpp(生成.obj),不会执行最后的链接步骤;“生成/生成解决方案”才会把多个.obj链接成.exe。
声明 vs 定义:链接器在做什么
- 声明(declaration):让编译器知道“有这么个符号”。
- 定义(definition):提供实体本体(函数体、对象存储等)。
- 链接器(linker):把所有目标文件/库里的符号合在一起,解决“引用但未在本翻译单元定义”的符号。
一个典型的组织方式:在头文件写声明,在 .cpp 写定义。
#pragma once
void Log(const char* message);#include <iostream>#include "Log.h"
void Log(const char* message){ std::cout << message << "\n";}#include "Log.h"
int main(){ Log("Hello");}如果你只写了声明却没有把对应定义链接进来,通常会在链接阶段看到类似 “unresolved external symbol” 的错误。
基础类型:别背结论,先记住哪些是“实现定义”
不同平台/编译器的基础类型大小可能不同:
- Windows/MSVC 常见是 LLP64:
int32-bit,long32-bit,long long64-bit,指针 64-bit。 - Linux/macOS 64-bit 常见是 LP64:
long64-bit,long long64-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。
函数:调用开销与可读性
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:断点;把表达式拆成多行更利于单步调试。- 单步:步入(进函数)、步过(不进函数)、步出(跳出函数)。
三个常用窗口:自动窗口、局部变量窗口、监视窗口。
内存视图:
- 可以输入
&a获取变量地址。 - 大多数 x86/x64 机器是小端序(little-endian):多字节整数在内存视图里看起来像“反着读”。
C++ 基础:从源码到可执行文件(编译/链接/类型/调试)
https://fuwari.vercel.app/posts/c/