zoukankan      html  css  js  c++  java
  • Makefile 的用处,解决已包含头文件但还是 undefined reference to

    A.c (main函数)

    #include "B.h"
    
    int main(void) {
          //内容
          return 1;
    }
    

    B.c

    #include "B.h"
    void func_b() {
          //内容
    }
    

    B.h

    #ifndef B_H_
    #define B_H_
    
    //头文件内容
    void func_b();
    #endif
    

    注:B_H_ 是规范的写法,_B_H 不是规范写法,因为c库内置的定义都是下划线开头的,用户定义的头文件不应该以下划线开头

    好了,我们编译一下(Linux 下的可执行文件可以没有后缀名,而 Windows 下的可执行文件需要 exe 后缀,即 A.exe)

    gcc A.c -o A
    

    这时会提示 undefined reference to func_b

    为什么呢,我们看一下编译工具预处理后但还没汇编的源码文件

    gcc -E A.c -o A.i
    

    这时候你可以看到 A.i 里面的内容,只有 A.c 和 B.h 的内容以及其他链接信息,但没有包含 B.c 的内容。
    因为我们的编译命令错了

    正确的编译命令

    gcc A.c B.c -o A
    

    单文件的编译流程:
    hello.c(预处理) -> hello.i(编译) -> hello.s(汇编) -> hello.o(链接) -> hello

    gcc -E    hello.c -o hello.i   //预处理
    gcc -S    hello.i -o hello.S   //编译成汇编码
    gcc -c    hello.s -o hello.o   //汇编成二进制
    gcc       hello.o -o hello     //链接
    

    多文件就要生成多个.o文件,然后用以下命令链接起来

    gcc 1.o 2.o 3.o -o hello
    

    当然如果目录里只放需要的文件,也可以使用

    gcc *.c -o hello
    

    但是文件多了之后,只放需要的文件就几乎不可能了,何况还有子目录呢

    由于文件过多,所以后来人们使用专用的脚本来处理
    其中 Makefile Cmake 等是比较受欢迎的

    下面放个例子
    目录结构

    $ tree 
    .
    ├── bit_bmp.h
    ├── bmp
    │   ├── 000.bmp
    │   ├── 1.bmp
    │   ├── 2.bmp
    │   ├── 3.bmp
    │   ├── 4.bmp
    │   ├── 5.bmp
    │   └── background.bmp
    ├── Makefile
    ├── my_graph (生成的可执行文件)
    ├── my_graph.c
    ├── my_graph.o  (用于连接的Object)
    ├── mylib
    │   ├── graph.c
    │   ├── graph.h
    │   └── graph.o  (用于连接的Object)
    ├── show_bmp2.c
    ├── state
    └── state.c
    
    2 directories, 18 files
    


    效果如 make 后输出的日志一样,当然你复制make后的日志执行也是一样的,但是只打 make 命令就可以完成多爽啊,脚本的魅力就在于此

    语法详解:
    前面的 := 都是变量定义

    解释一下第10行到第17行
    产物名: 原料1名 原料2名
    命令(如果原料更新了则执行此处)

    如 第13行

    graph.o: mylib/graph.c mylib/graph.h
    	$(CC) -c mylib/graph.c -o mylib/graph.o
    

    产物graph.o: 原料mylib/graph.c mylib/graph.h
    如果原料更新了就执行命令 $(CC) -c mylib/graph.c -o mylib/graph.o

    注意:其中原料名必须和其依赖的产物名一致。

    思考:命令和冒号前都要标注产物文件名,那岂不是很多余?

    那我们用上通配符符号:%
    

    $@ 表示目标文件
    $< 表示第1个依赖文件
    $^ 表示所有依赖文件

    好,改写一下我的 Makefile

    Ps. Makefile 会根据文件是否更新而决定是否编译某个文件

    另外

    不要在头文件里定义函数或变量,头文件里应该只有声明
    因为c语言里,可以重复声明,但不能重复定义。

    当头文件多次包含时,就会导致重复定义,这个问题是 #ifndef #endif 条件宏也无法解决的(因为c语言处理头文件包含就是把整个头文件合并到一个文件里,而条件宏只对单个Object文件有效,多个Object文件连接后里就会导致很多重复的地方)

    所以头文件里只应该有声明,而不应该有定义。

    正确做法:
    在 c 文件里,定义全局变量如 int your_value = 0;
    在 h 头文件里,声明外部全局变量 extern int your_value;

    另外,头文件里应该只放对外部有用的东西,或者方便修改的宏。仅对头文件同名的 c 文件有效的声明或宏,应该只放在 c 文件里。

    总结

    当出现如 undefined reference to `album' 时,
    检查如下:

    1. 是否编译了所需要的全部 .c 或.o 文件

    当出现如
    my_graph.o:(.data+0x18): multiple definition of `MUSIC'
    state.o:(.data+0x18): first defined here 时,
    检查如下:

    1. 头文件里是否有定义 (应该只有声明,不应该有定义)

    其他

    在C语言中,数组是不可拷贝的,因此当数组作为参数传递时,会退化成指针,所以sizeof宏得到的是一个指针的大小 [知乎]
    函数内,参与运算的形参是实参的拷贝。而拷贝过程只发生了值传递,既传递了数组的地址,而把大小丢了。实参既数组本身你可以理解为一个地址加一个类型加一个大小。而形参就剩个地址了。这就是数组名作函数入参会退化为指针。[知乎]

  • 相关阅读:
    Atitit. visual studio vs2003 vs2005 vs2008  VS2010 vs2012 vs2015新特性 新功能.doc
    Atitit. C#.net clr 2.0  4.0新特性
    Atitit. C#.net clr 2.0  4.0新特性
    Atitit.通过null 参数 反射  动态反推方法调用
    Atitit.通过null 参数 反射  动态反推方法调用
    Atitit..net clr il指令集 以及指令分类  与指令详细说明
    Atitit..net clr il指令集 以及指令分类  与指令详细说明
    Atitit.变量的定义 获取 储存 物理结构 基本类型简化 隐式转换 类型推导 与底层原理 attilaxDSL
    Atitit.变量的定义 获取 储存 物理结构 基本类型简化 隐式转换 类型推导 与底层原理 attilaxDSL
    Atitit.跨语言反射api 兼容性提升与增强 java c#。Net  php  js
  • 原文地址:https://www.cnblogs.com/yucloud/p/Makefile.html
Copyright © 2011-2022 走看看