头文件、条件编译、编译工具、make工具的使用

一、防止 头文件 重复包含

在 C/C++ 编程中,为了防止同一个头文件被多次包含(例如,多个源文件都包含了同一个头文件,或者头文件之间相互包含),我们通常使用预处理指令来保护头文件。

作用:避免因重复包含导致的宏、类型、函数等重复定义错误。

标准写法:

#ifndef __HEADER_NAME_H__ #define __HEADER_NAME_H__ // 头文件的实际内容(函数声明、宏定义、结构体定义等) #endif

解释:

  • #ifndef __HEADER_NAME_H__:检查是否未定义宏__HEADER_NAME_H__
  • #define __HEADER_NAME_H__:如果未定义,则定义该宏。
  • #endif:结束条件编译。
  • 当该头文件第一次被包含时,宏未定义,因此会定义宏并包含头文件内容。当它再次被包含时,宏已定义,#ifndef条件为假,整个头文件内容将被预处理器跳过。

示例:

假设我们有如下项目文件:

fun.c 文件

#include "head.h" void add(int a, struct student b) { printf("hello %d\n", a + b.id); }

main.c 文件

#include "head.h" #include "student.h" int main(int argc, char const *argv[]) { struct student stu = {1, 16}; add(2, stu); return 0; }

head.h 文件

#ifndef __HEAD_H__ #define __HEAD_H__ #include <stdio.h> #include "student.h" void add(int a, struct student b); #endif

student.h 文件

#ifndef __STUDENT_H__ #define __STUDENT_H__ struct student { int id; int age; }; #endif

在这个例子中,main.c同时包含了head.hstudent.h,而head.h又包含了student.h。如果没有头文件保护,struct student将被重复定义。通过使用#ifndef保护,确保了student.h的内容只被包含一次。


二、条件编译

条件编译允许程序员根据特定的条件(通常是宏是否定义或其值)来决定哪些代码参与编译。这在跨平台开发、调试和功能开关中非常有用。

2.1 根据宏是否定义

语法:

#ifdef 宏名 /* 如果宏已定义,则编译此部分代码 (code1) */ #else /* 如果宏未定义,则编译此部分代码 (code2) */ #endif

执行情况:如果宏名已定义,则编译code1,否则编译code2

示例:

#include <stdio.h> // #define DEBUG // 注释掉此行,则宏未定义 int main(int argc, char const *argv[]) { #ifdef DEBUG printf("DEBUG 模式已开启。\n"); #else printf("DEBUG 模式未开启。\n"); #endif return 0; }

2.2 根据宏的值

语法:

#define 宏名 值 #if 宏名 /* 如果宏的值非零(为真),则编译此部分代码 (code1) */ #else /* 如果宏的值为零(为假),则编译此部分代码 (code2) */ #endif

示例:

#include <stdio.h> #define VERSION 2 int main(int argc, char const *argv[]) { #if VERSION == 1 printf("运行版本 1 的代码。\n"); #elif VERSION == 2 printf("运行版本 2 的代码。\n"); #else printf("运行未知版本的代码。\n"); #endif return 0; }

三、编译工具

3.1 gcc 编译流程

GCC (GNU Compiler Collection) 是 Linux 下最常用的 C/C++ 编译器。它将源代码转换为可执行文件通常需要四个步骤:

  1. 预处理 (Preprocessing)
    • 作用:处理所有以#开头的预处理指令,如展开头文件、替换宏定义、条件编译等。此阶段不进行语法检查。
    • 命令:gcc -E source.c -o source.i
      • -E:只进行预处理。
      • -o source.i:将预处理后的代码输出到source.i文件。
  2. 编译 (Compilation)
    • 作用:检查语法和词法错误,将预处理后的.i文件转换为汇编语言文件.s
    • 命令:gcc -S source.i -o source.s
      • -S:只进行编译,生成汇编代码。
  3. 汇编 (Assembly)
    • 作用:将汇编代码.s文件转换为机器码,生成目标文件.o(在 Windows 下为.obj)。此文件尚不可执行。
    • 命令:gcc -c source.s -o source.o
      • -c:只进行编译和汇编,不进行链接。
  4. 链接 (Linking)
    • 作用:将一个或多个目标文件与所需的库文件(如 C 标准库)链接在一起,生成最终的可执行文件。
    • 命令:gcc source.o -o executable

常用快捷命令:

  • 一步生成可执行文件:
    • gcc source.c:生成默认的可执行文件a.out
    • gcc source.c -o my_program:生成名为my_program的可执行文件。
  • 分步编译(常用于 Makefile):
    1. gcc -c source.c -o source.o:仅编译,生成目标文件。
    2. gcc source.o -o my_program:链接目标文件,生成可执行文件。

3.2 gdb 调试

GDB (GNU Debugger) 是 Linux 下的命令行调试工具,用于查找程序中的错误。

1. 编译带调试信息的程序

使用-g选项编译,以便 GDB 能获取源代码信息。

gcc -g source.c -o program

2. 启动 GDB

gdb ./program

3. 常用 GDB 命令

命令缩写功能
runr运行程序。
listl列出源代码(默认显示当前行附近的10行)。
breakb设置断点。例如:b 10(在第10行设断点),b main(在main函数入口设断点)。
info breakpointsinfo b查看所有断点信息。
deleted删除断点。例如:d 1(删除编号为1的断点)。
printp打印变量的值。例如:p variable_name
steps单步执行,会进入函数内部
nextn单步执行,不会进入函数内部(跳过函数调用)。
continuec继续运行,直到下一个断点或程序结束。
quitq退出 GDB。
helph查看命令帮助。

4. 调试示例

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

调试步骤:

  1. gcc -g test.c -o test
  2. gdb ./test
  3. (gdb) b main(在 main 函数入口设置断点)
  4. (gdb) r(运行程序,停在断点处)
  5. (gdb) n(单步执行,不进入 fun 函数)
  6. (gdb) s(单步执行,进入 fun 函数)
  7. (gdb) p i(查看变量 i 的值)
  8. (gdb) c(继续运行到结束)
  9. (gdb) q(退出)

补充说明:

  • 段错误 (Segmentation Fault):GDB 有时能直接指出导致段错误的行号,有时则不能,需要结合backtrace (bt)等命令分析。
  • 进入系统函数:使用s单步调试时,可能会进入如printf等系统库函数。若想退出,可以使用finish命令执行完当前函数并返回,或使用until <行号>运行到指定行。
  • 断点行为:当程序停在断点处时(例如断点在第4行),显示的是第4行的代码,但该行代码尚未执行

四、make 工具

Make 是一个工程管理器(或“自动编译管理器”),它可以根据文件时间戳自动发现更新过的文件,从而减少不必要的编译工作量,提高编译效率。它通过读取Makefile(或makefile)文件来执行大量的编译命令。

注意:Makefilemakefile是 make 工具读取的唯一配置文件

4.1 Makefile 基本格式

一个基本的 Makefile 规则格式如下:

目标: 依赖 [TAB]命令

注意:命令前的[TAB]必须的,不能用空格代替。

  • 目标 (Target):通常是要生成的文件名(如main.o,program)。
  • 依赖 (Prerequisites):生成目标所需要的文件列表。
  • 命令 (Recipe):生成目标所需要执行的 shell 命令。

编写思路(逆推法):从最终的可执行文件开始,思考它依赖哪些目标文件(.o),而这些目标文件又由哪些源文件(.c)编译而来。

示例:编译一个简单的test.c文件。

test.c

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

Makefile

test: test.o # 目标test依赖于test.o gcc test.o -o test # 链接命令 test.o: test.c # 目标test.o依赖于test.c gcc -c test.c -o test.o # 编译命令 .PHONY: clean # 声明clean为“伪目标” clean: rm -f *.o test # 删除所有.o文件和可执行文件test

执行:

  • makemake test:默认执行第一个目标(test),即编译链接。
  • make clean:执行名为clean的目标,即清理文件。

伪目标 (Phony Target):clean这样的目标,并不是为了生成一个名为“clean”的文件,而是为了执行一系列命令(如清理)。使用.PHONY声明可以避免与同名文件冲突。

4.2 用 make 管理多个文件

当项目有多个源文件时,Makefile 能清晰地管理依赖关系。

项目文件:

  • main.c(主程序)
  • fun

4.3 Makefile 变量

在 Makefile 中,变量可以简化规则,提高可维护性。

4.3.1 自定义变量

变量名通常用大写表示,取值使用$(变量名)

  • =递归赋值:如果赋值过程中引用的变量后续发生变化,当前变量的值也会随之改变。
  • :=直接赋值:变量的值在定义时确定,不会受后续其他变量变化的影响。
  • +=追加赋值:在变量原有值的基础上追加新值。
  • ?=条件赋值:如果变量之前未定义,则赋值;如果已定义,则保持原值不变。
# = 递归赋值 VAR = hello SUM = $(VAR) world # 最终 SUM 的值会随 VAR 变化 VAR = 12345 # 修改 VAR 后,SUM 也会变成 "12345 world" := 直接赋值 VAR := 12345 SUM := $(VAR) world # SUM 为 "12345 world",后续 VAR 变化不影响 SUM += 追加赋值 VAR += aa # 如果 VAR 原为 "hello",则变为 "hello aa" ?= 条件赋值 NUM ?= QQ # 如果 NUM 未定义,则赋值为 "QQ";否则保持不变 all: echo $(VAR) echo $(SUM) echo $(NUM)

4.3.2 预定义变量

Make 提供了一些预定义变量,有默认值,也可以被覆盖。

  • RM:文件删除程序的名称,默认值为rm -f
  • CC:C 编译器的名称,默认值为cc(通常链接到 gcc)。
  • CFLAGS:C 编译器的选项,无默认值,常用于传递编译参数(如-c -g -Wall)。
  • OBJS:生成的目标文件列表,需要用户自定义。
CC = gcc CFLAGS = -c -g -Wall OBJS = main.o fun.o fin: $(OBJS) $(CC) $(OBJS) -o fin main.o: main.c $(CC) $(CFLAGS) main.c -o main.o fun.o: fun.c $(CC) $(CFLAGS) fun.c -o fun.o .PHONY: clean clean: $(RM) *.o fin

4.3.3 自动变量

自动变量在规则的命令部分使用,其值会根据目标和依赖自动变化。

  • $<:第一个依赖文件的名称。
  • $^:所有的依赖文件列表,以空格分隔。
  • $@:目标文件的完整名称。
CC = gcc CFLAGS = -c -g -Wall OBJS = main.o fun.o fin: $(OBJS) $(CC) $^ -o $@ # $^ 展开为 main.o fun.o, $@ 为 fin main.o: main.c $(CC) $(CFLAGS) $< -o $@ # $< 为 main.c, $@ 为 main.o fun.o: fun.c $(CC) $(CFLAGS) $< -o $@ # $< 为 fun.c, $@ 为 fun.o .PHONY: clean clean: $(RM) *.o fin

简化版 Makefile 写法(使用模式规则):

CC = gcc CFLAGS = -c -g -Wall OBJS = main.o fun.o fin: $(OBJS) $(CC) $^ -o $@ %.o: %.c # 模式规则:所有 .o 文件依赖于同名的 .c 文件 $(CC) $(CFLAGS) $< -o $@ .PHONY: clean clean: $(RM) *.o fin

4.4 make 指令

除了基本的make命令,还有一些有用的选项:

  • make -s:静默模式,隐藏执行的命令(只显示结果)。
  • make -C dir:进入指定的dir目录,然后在该目录下执行make命令。