>> >> >> Reference << << << <<<<<<Ref>>>>>>
C Program Structure
Modified: 2025-12-31 | Author:ljf12825

一个最小的C程序结构

Hello World实例

// hello.c 
#include <stdio.h>

int main()
{
    /*my first C program */
    printf("Hello, World! \n");

    return 0;
}

命令行参数

执行程序时,可以从命令行传值给C程序,它让程序在启动时就能获得配置信息。这些值被成为命令行参数,它们对程序很重要,特别是当想从外部控制程序,而不是在代码内进行硬编码时,显得尤为重要,是编写命令行工具的基础

是什么

当这样运行程序时

./myprogram -v --output=result.txt input1.dat input2.dat 

-v, --output=result.txt, input1.dat, input2.dat就是命令行参数。它们通过操作系统传递给程序的main函数

为什么main能有参数

程序启动是程序加载器(loader)调用——在Linux是ld-linux-x86-64,它把命令行参数从栈上传给main

main函数的标准形式

C程序的main函数有多种标准形式来接收参数

// 形式一:双参数(C标准形式)
int main(int argc, char *argv[]) {
    // argc:参数计数(argument count)
    // argv:参数向量/值(argument vector)
    // 返回值:通常0表示成功,非0表示错误
}

// 形式二:简略版(C标准形式,不关心参数)
int main(void) {
    // 不接受任何命令行参数
}

// 形式三:三参数(POSIX标准)
int main(int argc, char *argv[], char *envp[]) {
    // envp:表示环境变量列表(environment pointer) 
    // 这在POSIX, GNU/Linux, 系统编程中是合法且使用广泛的,只是它不是C标准指定的形式
    // 但C标准也允许实现定义(implementation-defined)的额外参数,而Linux就扩展允许环境变量指针
}

// 形式四:二级指针版(本质同形式一)
int main(int argc, char **argv, char **envp) {
    // char **argv和char *argv[]本质完全一样,只是语法不同
    // 系统编程中用char **argv更多,因为它强调指向指针的指针,更贴合内存模型
}

参数详解与访问

  1. argcargv的含义
#include <stdio.h> 

int main(int argc, char *argv[]) {
    printf("程序名:%s\n", argv[0]); // argv[0]总是程序自己的名字
    printf("参数总数(含程序名):%d\n", argc);

    // 遍历所有参数
    for (int i = 0; i < argc; i++) {
         printf("argv[%d] = %s\n", i, argv[i]);
    }

    return 0;
}

编译后运行

$ ./example hello world 123
程序名: ./example
参数总数 (含程序名): 4
argv[0] = ./example  # 第0个参数总是程序路径
argv[1] = hello       # 第一个实际参数
argv[2] = world
argv[3] = 123
  1. argv的内存布局 抽象来说,argv实际上是一个字符串指针数组,最后一个元素是NULL
// argv 的实际结构
char *argv[] = {
    "./example", // argv[0]
    "hello", // argv[1]
    "world", // argv[2]
    "123", //argv[3]
    NULL // argv[argc] 总是NULL
};

这意味着可以这样遍历

// 另一种遍历方式(利用 argv[argc] == NULL 的特性
for(int i = 0; argv[i] != NULL; i++) {
    printf("参数 %d: %s\n", i, argv[i]);
}

但实际上Linux的内存布局大概是

argc 
argv[0]
argv[1]
...
argv[argc-1]
NULL
envp[0]
envp[1]
...
NULL
auxv # 辅助向量,ABI视角的额外参数
...

命令行参数和环境变量是紧挨着的

  1. envp的结构 与argv类似,envp是一个以NULL结束的字符串数组
envp[0] = "PATH=/usr/bin"
envp[1] = "HOME=/home/user"
...
envp[n] = NULL

访问示例

#include <stdio.h> 

int main(int argc, char *argv[], char *envp[])
{
    for (char **p = envp; *p != NULL; p++) {
        printf("%s\n", *p);
    }
    return 0;
}

envp很少被提及,因为C标准库已经提供了更规范的接口

#include <stdlib.h> 
char *getenv(const char *name);

不同系统对envp的支持不完全一致,为了可移植性,通常只说前两个参数,但在Linux下,envp完全合法

  1. 实际ABI中的main通常是四个参数 Linux程序入口不是main, 而是_start
_start -> __libc_start_main -> main 

__libc_start_main实际调用main的原型是(glibc中定义)

int main(int argc, char **argv, char **envp);

但此外,加载器还会传递

只是这些不是传给main, 而是留在用户栈顶由glibc解析

auxv的定义(来自<elf.h>

typedef struct {
    uint32_t a_type;
    uint64_t a_un;
} Elf64_auxv_t

它包含系统级关键信息

编译和执行

将上述代码保存为hello.c,在hello.c所在目录下执行:

GCC的基本用法

假设有源文件hello.c

GCC链路的五个阶段

实际上,GCC的链路分为五个阶段,编译占前四个阶段

# 1. 预处理:展开头文件和宏
gcc -E hello.c -o hello.i 

# 2. 编译:将预处理后的代码编译为汇编代码
gcc -S hello.i -o hello.s 

# 3. 汇编:将汇编代码转换为机器码(目标文件)
gcc -c hello.s -o hello.o 

# 4. 链接:将目标文件和库文件链接为可执行文件
gcc hello.o -o hello 

# 5. 加载:Loader运行时工作,加载不属于编译阶段,但实际执行前一定会发生

ccgcc

有时,可以看到这样的命令

$ cc main.c
  1. cc是系统默认C编译器的抽象层
    • 在绝大多数类Unix系统中,cc不是具体编译器,而是一个符号连接或统一入口
    • 执行cc,系统会根据发行版、环境、安装情况,将它指向某个真正的编译器

cc是接口,不是实现,保持兼容性,让Makefile等构建工具可以使用通用的cc命令

  1. gcc是GNU Compiler Collection 中的C编译器前端
    • gcc明确是GNU编译器
    • 功能更全,选项更多,版本特性更固定、可控

gcc是一个具体的武器

多文件项目编译

# 直接编译多个源文件
gcc main.c utils.c helper.c -o myapp

# 或者先分别编译,再链接(适合大型项目)
gcc -c main.c 
gcc -c utils.c 
gcc -c helper.c 
gcc main.o utils.o helper.o -o myapp 

单纯gcc main.c utils.c -o app没问题,但构建性能差,每次都是全量编译
专业做法

main.c -> main.o (只编译改过的文件)
utils.c -> util.o 
helper.c -> helper.o 

然后

gcc main.o utils.o helper.o -o app 

这就是增量构建的原理,提高效率

使用外部库

比如,源码用到了<math.h>

// math_example.c 
#include <stdio.h> 
#include <math.h> 

int main()
{
    double x = 4.0;
    printf("sqrt(%f) = %f\n", x, sqrt(x));
    return 0;
}

编译时需要链接数学库

gcc math_example.c -o math_example -lm

包含自定义头文件

# 项目结构
# myproject/
#   ├── src/main.c
#   ├── src/utils.c
#   ├── include/utils.h
#   └── lib/

# 编译时指定头文件路径
gcc src/main.c src/utils.c -I./include -o myapp

# 如果使用了动态库
gcc src/main.c src/utils.c -I./include -L./lib -lmylib -o myapp
C文件本质结构

C文件天然分成三类

  1. 声明(头文件)
    • 函数声明
    • 结构体声明
    • 常量定义
  2. 实现(源文件)
    • 函数定义
    • 局部变量
    • 静态内部实现(static)
  3. 接口(API) 对外暴露的可链接符,链接器最终会根据符号表决定是否能成功链接

完整项目结构示例

创建项目目录结构

myproject/
├── src/
│   ├── main.c
│   ├── math_utils.c
│   └── io_utils.c
├── include/
│   ├── math_utils.h
│   └── io_utils.h
├── build/
└── Makefile

编译运行

# 进入项目目录
cd myproject

# 创建build目录
mkdir -p build 

# 编译
gcc src/*.c -I./include -o build/myapp 

# 运行
./build/myapp