Skip to content

Day1 - "Hello, World!"

欢迎来到 C 语言的世界!作为编程入门的第一步,我们将通过经典的 “Hello, World!” 程序,系统而深入地了解 C 语言的基本结构和核心概念。这个看似简单的程序实际上是一个完整的微型系统,包含了编译器、标准库、操作系统接口等多个层面的协作。让我们逐层拆解,深入每一个细节,为后续学习打下坚实基础。

下面的教程中一般使用 GCC 作为命令行例子,MSVC 直接编译运行即可,


#include <stdio.h>
int main() {
printf("Hello World!\n");
return 0;
}

注意:虽然这段代码只有短短四行(忽略空行),但它触发了整个 C 语言工具链的运作,从预处理、编译、汇编到链接,最终生成可执行文件。每一行都值得深入剖析。


1. 头文件引入:#include <stdio.h> —— 预处理阶段的桥梁

Section titled “1. 头文件引入:#include <stdio.h> —— 预处理阶段的桥梁”

#include 是 C 预处理器(Preprocessor)指令,它在编译器真正编译源代码之前执行。预处理器不理解 C 语法,它只是文本替换工具。

当你写:

#include <stdio.h>

预处理器会查找 stdio.h 文件的内容,并将其原封不动地插入到当前源文件的该行位置。你可以通过以下命令查看预处理后的结果(GCC):

Terminal window
gcc -E helloworld.c -o helloworld.i

打开 helloworld.i,你会发现成千上万行代码被插入进来——这就是标准库的函数声明、宏定义和类型定义。

C 语言采用“先声明,后使用”的原则。编译器在遇到 printf(...) 时,必须提前知道:

  • printf 是什么?(函数)
  • 它接受什么参数?(const char *format, ...
  • 它返回什么类型?(int,即成功输出的字符数)

这些信息都记录在 stdio.h 中,形式如下(简化版):

int printf(const char *format, ...);

如果没有这个声明,编译器将报错:隐式声明函数 ‘printf’(implicit declaration of function ‘printf’),在严格模式下甚至直接拒绝编译。

1.3 尖括号 <> 与双引号 "" 的区别

Section titled “1.3 尖括号 <> 与双引号 "" 的区别”
形式查找路径适用场景
#include <stdio.h>系统标准库目录(如 /usr/include标准库或第三方库头文件
#include "myheader.h"当前源文件所在目录 → 系统目录项目自定义头文件

查找顺序示例(GCC):

对于 #include "config.h"

  1. 先在当前 .c 文件所在目录查找 config.h
  2. 若未找到,再到 -I 指定的包含路径查找
  3. 最后去系统目录查找

对于 #include <stdio.h>

  1. 直接在系统标准库路径查找(如 /usr/include
  2. 可通过 -I 参数添加额外搜索路径

2. 主函数定义:int main() —— 程序的生命起点

Section titled “2. 主函数定义:int main() —— 程序的生命起点”

当你在命令行输入 ./helloworld(Linux)或 helloworld.exe(Windows),操作系统会加载并执行程序,但不同平台的底层机制略有不同。

  1. 加载可执行文件到内存(由内核的 execve 系统调用完成)
  2. 创建进程和主线程
  3. 设置堆栈、环境变量、命令行参数
  4. 跳转到 _start 函数(由 C 运行时库 crt0.ocrt1.o 提供)
  5. _start 初始化标准库(如设置 stdin/stdout/stderr、调用全局构造函数等)、设置 argc/argv
  6. _start 调用程序员编写的 main()
  7. main() 返回后,_start 调用 exit() 终止程序,触发清理(如调用 atexit 函数、全局析构函数等)

也就是说,main 并不是程序真正的“第一行代码”,而是程序员可见的入口点

  1. 加载器(Windows Loader)将 PE 格式可执行文件(.exe)映射到内存
  2. 创建进程和主线程(由 Windows 内核完成)
  3. 加载器调用入口点函数 —— 默认是 mainCRTStartup(控制台程序)或 WinMainCRTStartup(GUI 程序)
  4. CRT 启动函数(如 mainCRTStartup)执行初始化
    • 初始化 C 运行时库(CRT)
    • 设置 argc/argv(从命令行参数解析)
    • 初始化堆、设置异常处理、初始化全局对象(C++)
    • 设置标准输入/输出句柄
  5. CRT 启动函数调用程序员的 main()(或 wmainWinMainwWinMain
  6. main() 返回后,CRT 启动函数调用 exit(),触发:
    • 调用 atexit 注册函数
    • 调用全局对象析构函数(C++)
    • 关闭 CRT、释放资源
  7. 最终调用 ExitProcess() 退出进程

同样,在 Windows 上,main 也不是程序真正的入口 —— 真正的入口是 CRT 启动函数(如 mainCRTStartup,它由 MSVC 链接器默认链接,程序员通常无需修改。

步骤Linux (GCC)Windows (MSVC)
可执行格式ELFPE (Portable Executable)
真正入口点_startmainCRTStartup / WinMainCRTStartup
初始化工作glibc CRT 初始化MSVC CRT 初始化
程序员入口函数main / main(int,char**)main / wmain / WinMain / wWinMain
退出机制exit()_exit()exit()ExitProcess()
  • 在 MSVC 中,你可以通过链接器选项 /ENTRY:YourFunction 自定义入口点(跳过 CRT),但这样你就必须自己处理堆、标准库、全局构造等,通常不推荐。
  • 在 Linux 中,也可以通过 gcc -nostartfiles 跳过 _start,直接指定入口(如 gcc -e myentry),但同样需自行初始化。

无论平台如何,操作系统 + 运行时库共同协作,为 main 函数准备好“舞台”,程序员只需专注业务逻辑 —— 这正是现代编程环境的优雅之处。

C 标准规定 main 函数只有两种合法形式:

int main(void); // 无参数版本
int main(int argc, char *argv[]); // 带命令行参数版本

注意:在 C 语言中,int main() 是旧式(K&R)函数声明,表示“参数未指定”,调用时传参不会被编译器检查,可能导致未定义行为,不推荐使用。应明确写为 int main(void)。

int main(int argc, char *argv[])
  • argc(argument count):命令行参数个数(包括程序名本身)
  • argv(argument vector):指向参数字符串数组的指针

示例:

Terminal window
./helloworld Alice 25

则:

  • argc = 3
  • argv[0] = "./helloworld"(程序路径)
  • argv[1] = "Alice"
  • argv[2] = "25"
  • argv[3] = NULL(标准保证)

你可以这样打印所有参数:

#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}

好的,这是在原文档基础上增加关于 Windows CMD 和 PowerShell 中返回值的描述。

return 0; 不仅仅是“程序结束”,它是程序执行完毕后,向父进程(通常是操作系统 Shell)传递一个“状态码”(Exit Code)。父进程可以根据这个状态码来判断子进程的执行结果。

  • 约定俗成:
    • 0: 表示程序正常执行,任务成功完成。
    • 非零值 (1~255): 表示程序在执行过程中发生了错误或异常。不同的数值可以用来区分不同的错误类型,方便自动化脚本和调用者进行错误处理。

在类 Unix 系统的 Shell 中,可以通过特殊变量 $? 来获取上一条命令执行后的退出码。

Terminal window
# 编译并运行一个返回 0 的 C 程序
gcc success.c -o success_app
./success_app
# 查看退出码
echo $? # 输出 0
Terminal window
# 编译并运行一个返回 1 的 C 程序
gcc failure.c -o failure_app
./failure_app
# 查看退出码
echo $? # 输出 1

在 Shell 脚本中,可以根据退出码进行条件判断,实现更复杂的逻辑:

Terminal window
./myprogram
if [ $? -eq 0 ]; then
echo "程序成功执行"
else
echo "程序执行失败,错误码为 $?"
fi

在 Windows 的命令提示符 (CMD) 中,退出码存储在 %ERRORLEVEL% 这个动态环境变量中。你可以通过 echo 命令来查看它。

Terminal window
C:\> myprogram.exe
C:\> echo %ERRORLEVEL%
  • 如果 myprogram.exe 返回 0,则会输出 0
  • 如果 myprogram.exe 返回 1(或其他非零值),则会输出 1

在批处理脚本 (.bat) 中,%ERRORLEVEL% 常用于条件判断:

Terminal window
@echo off
myprogram.exe
REM 检查 ERRORLEVEL 是否等于 0
if %ERRORLEVEL% equ 0 (
echo 程序成功执行
) else (
echo 程序执行失败,错误码为 %ERRORLEVEL%
)

注意if errorlevel N 的判断逻辑是 if %ERRORLEVEL% >= N,所以精确判断需要使用 if %ERRORLEVEL% equ N 的语法。

PowerShell 提供了更现代化的处理方式。它使用一个名为 $LASTEXITCODE 的自动变量来存储从外部程序(非 PowerShell cmdlet)返回的退出码。

Terminal window
# 运行可执行文件
.\myprogram.exe
# 查看退出码
Write-Host $LASTEXITCODE

或者直接输入变量名:

Terminal window
$LASTEXITCODE

在 PowerShell 脚本中,你可以这样进行判断:

Terminal window
.\myprogram.exe
if ($LASTEXITCODE -eq 0) {
Write-Host "程序成功执行"
} else {
Write-Host "程序执行失败,错误码为 $LASTEXITCODE"
}

此外,PowerShell 还有一个布尔类型的变量 $?,它表示上一条命令是否成功

  • 如果上一条命令的退出码为 0$?True
  • 如果退出码为非 0$?False

这使得条件判断更加简洁:

Terminal window
.\myprogram.exe
if ($?) {
Write-Host "程序成功执行"
} else {
Write-Host "程序执行失败"
# 如果需要,仍可查看具体的错误码
Write-Host "错误码: $LASTEXITCODE"
}

重要:即使你的程序非常简单,或者你当前不关心它的返回值,也请始终在 main 函数的末尾写上 return 0;。这不仅是 C/C++ 语言规范的要求,更是向代码的调用者(无论是人类用户还是自动化脚本)清晰地传达“任务已成功完成”的信号。这是一个专业程序员应有的素养。


3. 输出语句:printf("Hello World!\n"); —— 格式化输出的核心

Section titled “3. 输出语句:printf("Hello World!\n"); —— 格式化输出的核心”
int printf(const char *format, ...);
  • 返回值:成功输出的字符数(包括换行符),出错时返回负数
  • 第一个参数:格式字符串(format string)
  • 后续参数:可变参数列表(variable arguments),数量和类型由格式字符串决定

格式字符串中的 % 符号是“转换说明符”的起始标记,其后跟格式代码:

printf("姓名:%s,年龄:%d,身高:%.2f\n", name, age, height);
说明符含义示例输入输出
%d有符号十进制整数2525
%u无符号十进制整数4000000000U4000000000
%x小写十六进制255ff
%X大写十六进制255FF
%f浮点数3.141593.141590
%.2f保留两位小数3.141593.14
%c单个字符'A'A
%s字符串"Hello"Hello
%%输出百分号本身%
printf("|%10s|\n", "Hi"); // 右对齐,宽度10 → | Hi|
printf("|%-10s|\n", "Hi"); // 左对齐,宽度10 → |Hi |
printf("|%05d|\n", 42); // 补零,宽度5 → |00042|

转义字符以反斜杠 \ 开头,用于表示不可打印或有特殊含义的字符:

转义序列含义ASCII 值作用
\n换行10 (LF)光标移动到下一行开头
\r回车13 (CR)光标回到当前行开头
\t水平制表符9 (HT)跳到下一个制表位(通常 8 字符)
\\反斜杠92输出一个 \
\"双引号34在字符串中包含 "
\'单引号39在字符常量中使用 '
\0空字符0字符串结束标志
\x41十六进制65输出 ‘A’
\101八进制65输出 ‘A’

注意:在 Windows 系统中,换行通常需要 \r\n(CRLF),但在 C 语言中,printf("\n") 会被运行时库自动转换为 \r\n,无需手动处理。


4. 程序终止:return 0; —— 生命周期的终点

Section titled “4. 程序终止:return 0; —— 生命周期的终点”
  • return 0;:从 main 函数返回,交还控制权给 C 运行时(_start
  • exit(0);:立即终止程序,执行清理操作(如调用 atexit 注册的函数、刷新缓冲区、关闭文件等)

示例:

#include <stdio.h>
#include <stdlib.h>
void cleanup() {
printf("清理资源...\n");
}
int main() {
atexit(cleanup); // 注册退出时调用的函数
printf("程序开始\n");
exit(0); // 会调用 cleanup()
// return 0; // 同样会调用 cleanup()
printf("这行不会执行\n");
}

建议:在 main 中使用 return;在其他函数中如需立即退出,使用 exit()

C99 标准规定:如果 main 函数执行到末尾未遇到 return 语句,编译器自动插入 return 0;

int main() {
printf("Hello\n");
// 编译器自动添加 return 0;
}

虽然合法,但显式写出 return 0; 有以下好处:

  • 提高可读性:明确程序意图
  • 便于调试:在 return 处设断点
  • 兼容旧标准(C89 要求必须显式返回)
  • 与其他函数风格一致

最佳实践:始终显式写出 return 0;


Terminal window
gcc helloworld.c -o helloworld

背后实际执行四个步骤:

Terminal window
gcc -E helloworld.c -o helloworld.i
  • 处理所有 # 开头的指令(#include, #define, #ifdef 等)
  • 展开头文件、宏替换、条件编译
  • 输出 .i 文件(纯 C 代码,无预处理指令)
Terminal window
gcc -S helloworld.i -o helloworld.s
  • 将预处理后的 C 代码编译为汇编代码(.s 文件)
  • 进行语法分析、语义检查、优化
Terminal window
gcc -c helloworld.s -o helloworld.o
  • 将汇编代码转换为机器码(目标文件 .o
  • 生成可重定位的二进制代码
Terminal window
gcc helloworld.o -o helloworld
  • 将目标文件与标准库(如 libc.alibc.so)合并
  • 解析外部符号引用(如 printf 的实际地址)
  • 生成最终可执行文件

完整手动编译流程

Terminal window
gcc -E helloworld.c -o helloworld.i
gcc -S helloworld.i -o helloworld.s
gcc -c helloworld.s -o helloworld.o
gcc helloworld.o -o helloworld

或一步到位:

Terminal window
gcc helloworld.c -o helloworld
选项作用示例
-o指定输出文件名gcc test.c -o myapp
-Wall开启所有警告gcc -Wall test.c
-Wextra额外警告gcc -Wextra test.c
-g生成调试信息gcc -g test.c
-O2二级优化gcc -O2 test.c
-I添加头文件搜索路径gcc -I./include test.c
-L添加库文件搜索路径gcc -L./lib test.c
-l链接指定库(如 math.h 需 -lmgcc test.c -lm

重要:使用 math.h 中的函数(如 sqrt, sin)时,必须链接数学库:

Terminal window
gcc math_program.c -o math_program -lm
Terminal window
cl helloworld.c
  • 默认生成 helloworld.exe
  • 自动链接 C 运行时库
  • 调试版本:cl /Zi helloworld.c

macOS 默认使用 Clang:

Terminal window
clang helloworld.c -o helloworld

行为与 GCC 基本一致。


学习总结 —— 夯实基础,展望未来

Section titled “学习总结 —— 夯实基础,展望未来”

“Hello, World!” 程序虽小,却完整展示了 C 程序的生命周期:

  1. 预处理阶段#include 引入标准库声明
  2. 编译阶段:语法检查、代码生成
  3. 链接阶段:连接库函数实现
  4. 运行阶段
    • 操作系统加载程序
    • C 运行时调用 main
    • 执行 printf 输出内容
    • return 0 报告成功状态

通过这个程序,你已接触:

  • 编译器工作原理:预处理、编译、汇编、链接
  • 标准库机制:头文件声明与库函数实现分离
  • 函数调用约定:参数传递、返回值
  • 格式化输出printf 的强大功能
  • 程序结构:从入口到出口的完整流程

给初学者的深度建议

  1. 动手实践:修改示例代码,观察不同输出
  2. 查看预处理结果:理解 #include 的本质
  3. 调试返回值:在 Shell 中检查 $?
  4. 尝试命令行参数:让程序更灵活
  5. 阅读编译器警告-Wall -Wextra 是你的好朋友

记住:每一个复杂的系统,都是由简单的组件构建而成。今天的 “Hello, World!”,就是明天操作系统、编译器、游戏引擎的起点。保持好奇,持续探索!

名言:“计算机科学中的任何问题都可以通过增加一个间接层来解决。” —— David Wheeler
而今天,你已经理解了第一个间接层:#include <stdio.h> 如何连接你的代码与标准库。