Have you ever encountered a situation like this:
A .c file includes another .h header file, using a Makefile to build (compile) the application.
The first time you compile and execute, everything works fine!
However, if you modify the .h header file and try to compile again, issues arise:
The expected execution flow is: make detects that the modification time of the .h header file has been updated, and thus recompiles all .c files that include this header file.
But the actual result is: make does not recognize the modification of the .h header file.
What could be the reason? Let’s explore it step by step.
A header file: hello.h
#ifndef _HELLO_
#define _HELLO_
#define NUM 1
#endif
A source file: main.c
#include <stdio.h>
#include "hello.h"
int main(int argc, char *agv[])
{
printf("NUM = %d \n", NUM);
return 0;
}
Makefile file:
OBJS := main.o
TARGET := main
all : $(OBJS)
gcc -o $(TARGET) $(OBJS)
%.o: %.c
gcc $< -c -o $@
Now let’s execute make for the first time to compile:
$ make
gcc main.c -c -o main.o
gcc -o main main.o
Execute it:
$ ./main
NUM = 1
Now we change the NUM in the hello.h file to 2, and the current file modification time is:
$ ll
total 28
-rw-rw-r-- 1 root root 58 Jun 7 20:52 hello.h
-rwxrwxr-x 1 root root 8608 Jun 7 20:51 main*
-rw-rw-r-- 1 root root 122 Jun 7 20:51 main.c
-rw-rw-r-- 1 root root 1528 Jun 7 20:51 main.o
-rw-rw-r-- 1 root root 100 Jun 7 20:51 Makefile
Then we execute the make command again:
$ make
gcc -o main main.o
As you can see: make only executed the linking command in the Makefile (from the target file main.o to the executable file main), and did not execute the gcc main.c -c -o main.o compilation command to recompile the target file.
This indicates that make did not recognize that the hello.h header file had been modified, even though it should have detected it based on the file’s modification time!
Let’s take a look at this rule in the Makefile:
%.o: %.c
gcc $< -c -o $@
The target file main.o only depends on the main.c file and does not depend on the hello.h file.
The execution rule of make is: it will only execute the compilation command if the target file does not exist or if the dependency file is newer than the target file.
Therefore, even though hello.h has been modified, it is not a dependency of the target file main.o.
make finds that main.o already exists in the current directory and is newer than main.c, so it will not recompile main.o.
Thus, even if hello.h has been modified, it will not take effect because make does not consider hello.h as a dependency of main.o!
Note: None of the operations involved executing a clean operation.
Now that we know the reason, it’s easy to fix. We can manually add the header file hello.h to the dependencies, right?!
Modify the last few lines of the Makefile to look like this:
HEADERS := hello.h
%.o: %.c ${HEADERS}
gcc $< -c -o $@
This means adding the .h file to the dependencies of the .o file. This way, after modifying the .h file, when you execute the make command again, it will recompile the .o target file.
You can try this; it will definitely work.
At this point, the problem is solved, but it still feels like a rather crude approach.
Think about it: what if there are many .c and .h files? We can’t manually add them one by one, right?
Modify the Makefile to look like this:
OBJS := main.o
TARGET := main
all : $(OBJS)
gcc -o $(TARGET) $(OBJS)
-include *.d
%.o: %.c
gcc $< -c -MMD -o $@
There are 2 changes:
1. Added the -include *.d directive; 2. Added the -MMD parameter to the gcc compilation command;
Let’s execute it and see. First compilation:
$ ll // Check current files
total 12
-rw-rw-r-- 1 root root 58 Jun 7 21:06 hello.h
-rw-rw-r-- 1 root root 122 Jun 7 20:51 main.c
-rw-rw-r-- 1 root root 119 Jun 7 21:05 Makefile
$
$ make // Compile
gcc main.c -c -MMD -o main.o
gcc -o main main.o
$
$ ll // Check current files again
total 32
-rw-rw-r-- 1 root root 58 Jun 7 21:06 hello.h
-rwxrwxr-x 1 root root 8608 Jun 7 21:06 main*
-rw-rw-r-- 1 root root 122 Jun 7 20:51 main.c
-rw-rw-r-- 1 root root 23 Jun 7 21:06 main.d
-rw-rw-r-- 1 root root 1528 Jun 7 21:06 main.o
-rw-rw-r-- 1 root root 119 Jun 7 21:05 Makefile
$
$ ./main // Execute
NUM = 1
Did you notice: there’s an additional file main.d, and its content is:
main.o: main.c hello.h
This file was generated because of the -MMD parameter in the Makefile, and its content is exactly the dependency information we need for the target file.
Then, in the Makefile, we include this .d file, allowing make to know that: the main.o file depends on both main.c and hello.o.
Now, if we modify the content of hello.h, for example, changing NUM to 10, and compile and execute again:
$ make
gcc main.c -c -MMD -o main.o
gcc -o main main.o
$
$ ./main
NUM = 10
Bingo, the result is correct!
Recommended Reading
[1] C Language Pointers – From Basic Principles to Advanced Techniques, Explained with Diagrams and Code
[2] Step-by-Step Analysis – How to Implement Object-Oriented Programming in C
[3] The Underlying Debugging Principles of gdb Are So Simple
[4] Is Inline Assembly Really Scary? This Article Will Put Your Fears to Rest!
[5] Everyone Says Software Architecture Should Be Layered and Modular, But How Should It Be Done?
[Insert here: WeChat Official Account]
