In a makefile, if a target file does not include the header files it depends on, it can cause some problems

⌚Time: 2026-08-25 00:29:05

👨‍💻Author: Jack Ge

In a makefile, if a target file doesn't include the header files it depends on, it can cause some problems, especially for header-only modules.

For example, I wrote a module.

test.h


#ifndef _TEST_CLASS_
#define _TEST_CLASS_

#include <iostream>

class CTest{

public:
    ~CTest(){}
    CTest(){}

public:
    void test_function_a(){
        std::cout<<"test func a printing.\n";
    }

};

#endif

main.cpp

#include "test.h"

int main(){

    CTest ct;
    ct.test_function_a();

    return 0;
}

makefile

CXX = g++
CXXFLAGS = -std=c++11 -Wall

main: main.o
    $(CXX) -o main main.o
main.o: main.cpp
    $(CXX) $(CXXFLAGS) -c main.cpp
clean:
    rm -f main

Output after running make

g++ -std=c++11 -Wall -c main.cpp
g++ -o main main.o

test func a printing.

But what happens if test.h changes?

void test_function_a(){
    std::cout<<"test func a printing: aaa\n";
}

make && run

g++ -o main main.o

test func a printing.

The output didn't change because test.h wasn't added as a dependency of main.o in the makefile. So when test.h was modified, main.o wasn't recompiled. The functions inside it are still the old ones, so the output is still the same.

You just need to add test.h as a dependency.

main.o: main.cpp test.h
    $(CXX) $(CXXFLAGS) -c main.cpp

If there are many source files, like util.cpp, tool.cpp, and main.cpp, and both main.cpp and tool.cpp include test.h, you can write it like this

CXX = g++
CXXFLAGS = -std=c++11 -Wall
TARGET = main
OBJS = main.o tool.o util.o

all: $(TARGET)

$(TARGET): $(OBJS)
    $(CXX) $(CXXFLAGS) -o $@ $^

main.o: main.cpp test.h
    $(CXX) $(CXXFLAGS) -c $< -o $@

tool.o: tool.cpp test.h
    $(CXX) $(CXXFLAGS) -c $< -o $@

util.o: util.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

clean:
    rm -f $(OBJS) $(TARGET)

.PHONY: all clean

If you used automatically generated dependency rules to simplify the makefile

CXX = g++
CXXFLAGS = -std=c++11 -Wall
TARGET = main

SRCS = main.cpp tool.cpp util.cpp
OBJS = $(SRCS:.cpp=.o)

$(TARGET): $(OBJS)
    $(CXX) -o $@ $^

%.o: %.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

clean:
    rm -f $(TARGET) *.o

Some source files need to include test.h, while others don’t. If you make test.h a dependency for all target files, then anytime it changes, all target files would need to be recompiled, which is really inefficient.

%.o: %.cpp test.h
    $(CXX) $(CXXFLAGS) -c $< -o $@

You can write a separate rule for each target file that uses this header, like tool.o and main.o use this header.

%.o: %.cpp
    $(CXX) $(CXXFLAGS) -c $< -o $@

tool.o main.o: test.h

This is equivalent to writing it like this

tool.o: tool.cpp test.h
    $(CXX) $(CXXFLAGS) -c tool.cpp -o tool.o

main.o: main.cpp test.h
    $(CXX) $(CXXFLAGS) -c main.cpp -o main.o

util.o: util.cpp
    $(CXX) $(CXXFLAGS) -c util.cpp -o util.o

Another way is to use automatic dependency generation. Automatic dependency generation means letting the compiler automatically analyze the header files included in the source files and generate dependencies. This way, whenever any header file is modified, all source files that depend on it will be recompiled.

CXX = g++
CXXFLAGS = -std=c++11 -Wall -g
TARGET = myprogram
SOURCES = main.cpp tool.cpp util.cpp
OBJS = $(SOURCES:.cpp=.o)
DEPS = $(OBJS:.o=.d)

all: $(TARGET)

# link
$(TARGET): $(OBJS)
    $(CXX) $(CXXFLAGS) -o $@ $^

# Compile and generate dependency files
%.o: %.cpp
    $(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@

# Includes dependency files
-include $(DEPS)

clean:
    rm -f $(OBJS) $(DEPS) $(TARGET)

.PHONY: all clean

Parameter Explanation

-MMD: Generates dependency files (.d), only including user header files (excluding system header files)

-MD: Generates dependency files, including all header files (including system header files)

-MP: Generates phony targets for each header file to prevent errors when headers are deleted

It's worth noting that you'd better put -include $(DEPS) after the target rules. If you put it before, it could affect the default target and cause problems.


What if you split a header-only module into a header file and a source file and then keep only the declarations in the header while putting the actual implementations in the source file?

test.h


#ifndef _TEST_CLASS_
#define _TEST_CLASS_

#include <iostream>

class CTest{

public:
    ~CTest();
    CTest();

public:
    void test_function_a();

};

#endif

test.cpp


#include "test.h"

CTest::~CTest(){

}
CTest::CTest(){

}
void CTest::test_function_a(){
    std::cout<<"test func a printing.\n";
}

Add the test.o target file as a link item in the makefile

main: main.o test.o tool.o util.o
    $(CXX) -o $@ $^

...

test.o: test.cpp
    $(CXX) $(CXXFLAGS) -c test.cpp -o test.o

This way, every time you update, you're actually updating its source file, and it will automatically recompile and participates in the final linking each time. Other target files that use this header file don't need to be recompiled, this will help you cut down on some repeated compiling.

But if you still haven't added the dependent header files in the target file, you'll run into problems too, like if you add or remove a function in this module.

test.h


#ifndef _TEST_CLASS_
#define _TEST_CLASS_

#include <iostream>

class CTest{

public:
    ~CTest();
    CTest();

public:

    //void test_function_a(); // you deleted this function
    void test_function_b(); // you added this function

};

#endif

test.cpp


#include "test.h"

CTest::~CTest(){

}
CTest::CTest(){

}
void CTest::test_function_b(){
    std::cout<<"test func b printing.\n";
}

At the linking stage, all the target files that depend on test.h are still using the symbols declared in the old version of test.h because they weren't automatically recompiled, so you'll run into missing symbol issues.

Actually, if the symbols in the header file change, you also have to manually modify the source files that use those symbols in the code. This causes the source files that use the header file to be modified, so they will automatically recompile. This way, it looks like this problem won't happen.

But if that's the case, you changed the parameters of a certain function

test.h


#ifndef _TEST_CLASS_
#define _TEST_CLASS_

#include <iostream>

class CTest{

public:
    ~CTest();
    CTest();

public:

    //void test_function_a(const std::string); // original function
    void test_function_a(std::string &); // you modified this function

};

#endif

You passed a const std::string parameter where you're using this function. And you didn't manually adjust it to fit the new parameter. This way, the object file was wont't be recompiled. After modifying the header file, it will cause a 'symbol not found' issue at the final linking stage, because the object file is looking for the original void test_function_a(const std::string); function.

So whether it's just a header-only module or a module with both header and source files, you should add dependency in the target file that uses it.