Here's a cut and paste from one of my Makefiles:
# This defines subdirectories. I have sources in ./src.
SRCDIR := src
OBJDIR := obj
BINDIR := bin
# Just a bunch of -I commands to the compiler.
# You probably don't need these.
INCLUDES += -I. -I./generated -I/usr/local/include -I/usr/local/include/antlr4-runtime
# This gives options to g++. -O3 is optimization.
# The includes are from above, but you probably could ignore.
# --std=c++17 means this is C++ 17 rather than something older.
CXXFLAGS = -O3 ${INCLUDES} --std=c++17 -g ${AUTO_ARGUMENT}
# You can probably skip this.
LDFLAGS += -L/usr/local/lib
# This defines the command for building. You can probably make
# this much shorter.
COMPILE.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
# This is a link command. You'll see I'm forcing a static link
# against a library, but of course, yo udon't need that.
${BINDIR}/wave: ${OBJECTS}
${CXX} ${OBJECTS} ${LDFLAGS} /usr/local/lib/libantlr4-runtime.a -o ${BINDIR}/wave
# Because sources can be in two different directories, I have
# two generic rules. This rule builds the generated source
# (output from antlr4) into obj/foo.o
${OBJDIR}/%.o : generated/%.cpp
$(COMPILE.cc) $(OUTPUT_OPTION) $<
# Same thing but the code i'm actually writing.
# Make already knows about $(OUTPUT_OPTION) and $< is
# the input.
${OBJDIR}/%.o : src/%.cpp
$(COMPILE.cc) $(OUTPUT_OPTION) $<
I have my source in ./src and some generated files (from antlr4) in ./generated, so I have two rules for building those.
Thus, you make a rule (or in my case, 2 rules) to produce .o files in the ${OBJDIR} directory and then one rule to produce the binary in ${BINDIR}.