>> >> >> Reference << << << <<<<<<Ref>>>>>>
>> >> >> Indexer << << << <<<<<<Idx>>>>>>
Matched: 0

Tags

    Categories

      Types

        Top Results

          C Program Structure
          M: 2025-12-31 - ljf12825

          一个最小的C程序结构

          Hello World实例

          // hello.c 
          #include <stdio.h>
          
          int main()
          {
              /*my first C program */
              printf("Hello, World! \n");
          
              return 0;
          }
          
          • 预处理指令:#include <stdio.h>,告诉C编译器在实际编译之前要包含的stdio.h文件
          • 主函数:int main(),程序从这里开始执行
          • 注释:/*...*/,注释中的内容会被编译器忽略
          • 函数:printf(...),C中的一个可用函数
          • 返回:return 0,终止main()函数,并返回值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);
          

          但此外,加载器还会传递

          • auxv(辅助向量)
          • 程序堆栈布局信息
          • ELF header信息

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

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

          typedef struct {
              uint32_t a_type;
              uint64_t a_un;
          } Elf64_auxv_t
          

          它包含系统级关键信息

          • 程序入口地址(AT_ENTRY)
          • 页大小(AT_PAGESZ)
          • 运行时动态链接器地址(AT_BASE)
          • 程序头表位置(AT_PHDR)
          • 随机数(AT_RANDOM)
          • CPU特性(AT_HWCAP)

          编译和执行

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

          • gcc hello.c进行编译 如果代码没有错误,命令提示符会跳到下一行,并生成a.out可执行文件
          • ./a.out执行程序 输出Hello, World!

          GCC的基本用法

          假设有源文件hello.c

          • gcc hello.c:编译生成可执行文件a.out(默认名)
          • gcc hello.c -o hello:编译并指定输出文件名hello

          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