第一个 C/C++ 程序:从 main 函数到编译运行
第一个程序的目标不是打印一句话。 真正目标是看清:源码文件如何被编译成可执行文件,程序入口在哪里,返回值如何交给操作系统,标准库输出如何工作。 把这个过程理解清楚,后面遇到编译错误、链接错误和运行错误时才不会混在一起。
第一个 C 程序
创建 hello.c:
#include <stdio.h>
int main(void) {
printf("Hello, C\n");
return 0;
}
编译运行:
clang -std=c23 -Wall -Wextra -Wpedantic hello.c -o hello-c
./hello-c
输出:
Hello, C
-o hello-c 指定输出文件名。
如果不指定,很多 Unix 编译器会默认输出 a.out。
第一个 C++ 程序
创建 hello.cpp:
#include <iostream>
int main() {
std::cout << "Hello, C++\n";
return 0;
}
编译运行:
clang++ -std=c++23 -Wall -Wextra -Wpedantic hello.cpp -o hello-cpp
./hello-cpp
输出:
Hello, C++
C++ 用 clang++ 或 g++ 编译。
原因是 C++ 程序需要链接 C++ 标准库,C 编译驱动通常不会自动完成这件事。
main 是用户级入口
main 是程序进入用户代码的标准入口。
操作系统实际加载程序后,会先执行运行时启动代码。
启动代码准备参数、运行时和全局初始化,再调用 main。
操作系统加载程序
-> C/C++ runtime startup
-> 初始化全局状态
-> 调用 main
-> 接收返回值
-> 执行退出清理
所以 main 不是进程的第一条机器指令。
但它是你作为程序员通常需要关心的第一入口。
return 0 的意义
main 返回整数状态码。
惯例上 0 表示成功,非零表示失败。
shell 可以读取这个状态码。
./hello-c
echo $?
如果输出 0,说明程序按约定成功退出。
在自动化脚本和 CI 中,退出码决定流程是否继续。
因此返回值不是装饰。
#include 不是导入库
#include <stdio.h> 和 #include <iostream> 引入的是声明。
它们让编译器知道 printf 或 std::cout 的接口。
真正的库链接发生在后续链接阶段。
头文件:告诉编译器名字和类型
库文件:提供名字对应的实现
链接器:把调用和实现连接起来
入门时先记住:缺头文件通常是编译错误。 缺库通常是链接错误。
编译和链接可以分开
单文件命令会把编译和链接一起做完。 你也可以拆开看。
clang -std=c23 -Wall -Wextra -c hello.c -o hello.o
clang hello.o -o hello-c
第一条命令生成目标文件 hello.o。
第二条命令链接成可执行文件。
hello.c -> hello.o -> hello-c
源码 目标文件 可执行文件
这个分解非常重要。
多文件工程就是大量 .o 文件最终被链接到一起。
常见编译错误
少写分号:
int x = 1
编译器可能报告:
error: expected ';' after expression
使用未声明名字:
cout << "hello\n";
如果没有写 std::cout 或 using 声明,编译器会说名字不存在。
error: use of undeclared identifier 'cout'
错误信息要从第一条开始读。 后续错误可能是第一条引发的连锁反应。
常见链接错误
只声明函数,不提供定义:
int add(int a, int b);
int main(void) {
return add(1, 2);
}
编译能过。
链接会失败,因为找不到 add 的实现。
undefined reference to `add'
这说明编译器知道函数接口,但链接器找不到函数体。
常见运行错误
运行错误发生在程序已经生成后。 例如数组越界:
int a[3] = {1, 2, 3};
printf("%d\n", a[10]);
这可能打印奇怪数字,也可能崩溃,也可能暂时看不出问题。 C/C++ 不保证每个越界都立刻报错。 后面会用 sanitizer 把这类问题尽早暴露。
注释怎么写
C/C++ 支持两类注释:
// 单行注释
/*
多行注释
*/
注释不是用来重复代码。 好的注释解释意图、约束和风险。
// length 已经过上游边界检查,这里只做热路径复制。
copy_bytes(dst, src, length);
这种注释比“调用 copy_bytes 函数”更有价值。
格式不是小事
统一格式能减少审查噪音。 初学时可以遵守简单规则:
- 一个语句一行。
{}即使只有一行也写完整。- 缩进保持一致。
- 名字表达含义。
- 不在一行写太多逻辑。
if (count > 0) {
average = sum / count;
}
这比压缩成一行更容易审计错误路径。
最小调试方法
最简单的调试是打印关键状态。
printf("count=%d\n", count);
C++:
std::cout << "count=" << count << '\n';
打印调试适合入门,但不要让生产代码只靠散落打印。 后续工程章节会讲日志、断言、sanitizer 和调试器。
断言用于检查内部不变量
#include <assert.h>
int divide(int a, int b) {
assert(b != 0);
return a / b;
}
断言用于开发期发现“按设计不应该发生”的情况。 它不是用户输入校验。 发布构建可能关闭断言,所以不能依赖断言做权限或安全检查。
工程风险
第一个程序也有工程风险:
- 用错编译器驱动导致 C++ 标准库链接失败。
- 忽略 warning,把潜在错误带入后续章节。
- 把编译错误、链接错误和运行错误混为一谈。
- 用本机特性写出不可移植代码。
- 不记录编译命令,无法复现问题。
小程序阶段养成正确习惯,大工程阶段才不会失控。
小结
第一个程序真正教你的不是输出文本,而是 C/C++ 的基本运行链路。
源文件先编译成目标文件。
目标文件再链接成可执行文件。
程序从 main 进入用户代码。
返回值交给操作系统。
理解这条链路,是学习所有语法和底层机制的入口。