- Learn C The Hard Way
- Preface
- Introduction: The Cartesian Dream Of C
- Exercise 0: The Setup
- Exercise 1: Dust Off That Compiler
- Exercise 2: Make Is Your Python Now
- Exercise 3: Formatted Printing
- Exercise 4: Introducing Valgrind
- Exercise 5: The Structure Of A C Program
- Exercise 6: Types Of Variables
- Exercise 7: More Variables, Some Math
- Exercise 8: Sizes And Arrays
- Exercise 9: Arrays And Strings
- Exercise 10: Arrays Of Strings, Looping
- Exercise 11: While-Loop And Boolean Expressions
- Exercise 12: If, Else-If, Else
- Exercise 13: Switch Statement
- Exercise 14: Writing And Using Functions
- Exercise 15: Pointers Dreaded Pointers
- Exercise 16: Structs And Pointers To Them
- Exercise 17: Heap And Stack Memory Allocation
- Exercise 18: Pointers To Functions
- Exercise 19: A Simple Object System
- Exercise 20: Zed's Awesome Debug Macros
- Exercise 21: Advanced Data Types And Flow Control
- Exercise 22: The Stack, Scope, And Globals
- Exercise 23: Meet Duff's Device
- Exercise 24: Input, Output, Files
- Exercise 25: Variable Argument Functions
- Exercise 26: Write A First Real Program
- Exercise 27: Creative And Defensive Programming
- Exercise 28: Intermediate Makefiles
- Exercise 29: Libraries And Linking
- Exercise 30: Automated Testing
- Exercise 31: Debugging Code
- Exercise 32: Double Linked Lists
- Exercise 33: Linked List Algorithms
- Exercise 34: Dynamic Array
- Exercise 35: Sorting And Searching
- Exercise 36: Safer Strings
- Exercise 37: Hashmaps
- Exercise 38: Hashmap Algorithms
- Exercise 39: String Algorithms
- Exercise 40: Binary Search Trees
- Exercise 41: Using Cachegrind And Callgrind For Performance Tuning
- Exercise 42: Stacks and Queues
- Exercise 43: A Simple Statistics Engine
- Exercise 44: Ring Buffer
- Exercise 45: A Simple TCP/IP Client
- Exercise 46: Ternary Search Tree
- Exercise 47: A Fast URL Router
- Exercise 48: A Tiny Virtual Machine Part 1
- Exercise 48: A Tiny Virtual Machine Part 2
- Exercise 50: A Tiny Virtual Machine Part 3
- Exercise 51: A Tiny Virtual Machine Part 4
- Exercise 52: A Tiny Virtual Machine Part 5
- Next Steps
- Deconstructing K & RC Is Dead
Exercise 28: Intermediate Makefiles
In the next three Exercises you'll create a skeleton project directory to use in building your C programs later. This skeleton directory will be used in the rest of the book, and in this exercise I'll cover just the Makefile
so you can understand it.
The purpose of this structure is to make it easy to build medium sized programs without having to resort to configure tools. If done right you can get very far with just GNU make and some small shell scripts.
The Basic Project Structure
The first thing to do is make a c-skeleton
directory and then put a set of basic files and directories in it that many projects have. Here's my starter:
$ mkdir c-skeleton
$ cd c-skeleton/
$ touch LICENSE README.md Makefile
$ mkdir bin src tests
$ cp dbg.h src/ # this is from Ex20
$ ls -l
total 8
-rw-r--r-- 1 zedshaw staff 0 Mar 31 16:38 LICENSE
-rw-r--r-- 1 zedshaw staff 1168 Apr 1 17:00 Makefile
-rw-r--r-- 1 zedshaw staff 0 Mar 31 16:38 README.md
drwxr-xr-x 2 zedshaw staff 68 Mar 31 16:38 bin
drwxr-xr-x 2 zedshaw staff 68 Apr 1 10:07 build
drwxr-xr-x 3 zedshaw staff 102 Apr 3 16:28 src
drwxr-xr-x 2 zedshaw staff 68 Mar 31 16:38 tests
$ ls -l src
total 8
-rw-r--r-- 1 zedshaw staff 982 Apr 3 16:28 dbg.h
$
At the end you see me do an ls -l
so you can see the final results.
Here's what each of these does:
LICENSE
If you release the source of your projects you'll want to include a license. If you don't though, the code is copyright by you and nobody has rights to it by default.
README.md
Basic instructions for using your project go here. It ends in .md
so that it will be interpreted as markdown.
Makefile
The main build file for the project.
bin/
Where programs that users can run go. This is usually empty and the Makefile will create it if it's not there.
build/
Where libraries and other build artifacts go. Also empty and the Makefile will create it if it's not there.
src/
Where the source code goes, usually .c
and .h
files.
tests/
Where automated tests go.
src/dbg.h
I copied the dbg.h
from Exercise 20 into src/
for later.
I'll now break down each of the components of this skeleton project so you can understand how it works.
Makefile
The first thing I'll cover is the Makefile because from that you can understand how everything else works. The Makefile in this exercise is much more detailed than ones you've used so far, so I'm going to break it down after you type it in:
CFLAGS=-g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG $(OPTFLAGS)
LIBS=-ldl $(OPTLIBS)
PREFIX?=/usr/local
SOURCES=$(wildcard src/**/*.c src/*.c)
OBJECTS=$(patsubst %.c,%.o,$(SOURCES))
TEST_SRC=$(wildcard tests/*_tests.c)
TESTS=$(patsubst %.c,%,$(TEST_SRC))
TARGET=build/libYOUR_LIBRARY.a
SO_TARGET=$(patsubst %.a,%.so,$(TARGET))
## The Target Build
all: $(TARGET) $(SO_TARGET) tests
dev: CFLAGS=-g -Wall -Isrc -Wall -Wextra $(OPTFLAGS)
dev: all
$(TARGET): CFLAGS += -fPIC
$(TARGET): build $(OBJECTS)
ar rcs $@ $(OBJECTS)
ranlib $@
$(SO_TARGET): $(TARGET) $(OBJECTS)
$(CC) -shared -o $@ $(OBJECTS)
build:
@mkdir -p build
@mkdir -p bin
## The Unit Tests
.PHONY: tests
tests: CFLAGS += $(TARGET)
tests: $(TESTS)
sh ./tests/runtests.sh
valgrind:
VALGRIND="valgrind --log-file=/tmp/valgrind-%p.log" $(MAKE)
## The Cleaner
clean:
rm -rf build $(OBJECTS) $(TESTS)
rm -f tests/tests.log
find . -name "*.gc*" -exec rm {} \;
rm -rf `find . -name "*.dSYM" -print`
## The Install
install: all
install -d $(DESTDIR)/$(PREFIX)/lib/
install $(TARGET) $(DESTDIR)/$(PREFIX)/lib/
## The Checker
BADFUNCS='[^_.>a-zA-Z0-9](str(n?cpy|n?cat|xfrm|n?dup|str|pbrk|tok|_)|stpn?cpy|a?sn?printf|byte_)'
check:
@echo Files with potentially dangerous functions.
@egrep $(BADFUNCS) $(SOURCES) || true
Remember that you need to indent the Makefile consistently with tab characters. Your editor should know that and do the right thing, but if it doesn't then get a different text editor. No programmer should use an editor that fails at something so simple.
The Header
This makefile is designed to build a library we'll be working on later and to do so reliably on almost any platform by using special features of GNU make
. I'll break down each part in sections, starting with the header.
Makefile:1
These are the usual CFLAGS
that you set in all of your projects, but with a few others that may be needed to build libraries. You may need to adjust these for different platforms . Notice the OPTFLAGS
variable at the end which lets people augment the build options as needed.
Makefile:2
Options used when linking a library, and allows someone else to augment the linking options using the OPTLIBS
variable.
Makefile:3
Setting an optional variable called PREFIX
that will only have this value if the person running the Makefile didn't already give a PREFIX
setting. That's what the ?=
does.
Makefile:5
This fancy line of awesome dynamically creates the SOURCES
variable by doing a wildcard
search for all *.c
files in the src/
directory. You have to give both src/**/*.c
and src/*.c
so that GNU make will include the files in src
and also the ones below it.
Makefile:6
Once you have the list of source files, you can then use the patsubst
to take the SOURCES
list of *.c
files and make a new list of all the object files. You do this by telling patsubst
to change all %.c
extensions to %.o
and then those are assigned to OBJECTS
.
Makefile:8
Using the wildcard
again to find all the test source files for the unit tests. These are separate from the library's source files.
Makefile:9
Then using the same patsubst
trick to dynamically get all the TEST
targets. In this case I'm stripping away the .c
extension so that a full program will be made with the same name. Previously I had replaced the .c
with {.o} so an object file is created.
Makefile:11
Finally, we say the ultimate target is build/libYOUR_LIBRARY.a
, which you will change to be whatever library you are actually trying to build.
This completes the top of the Makefile, but I should explain what I mean by "lets people augment the build". When you run make you can do this:
## WARNING! Just a demonstration, won't really work right now.
## this installs the library into /tmp
$ make PREFIX=/tmp install
## this tells it to add pthreads
$ make OPTFLAGS=-pthread
If you pass in options that match the same kind of variables you have in your Makefile
, then those will show up in your build. You can then use this to change how the Makefile
runs. The first one alters the PREFIX
so that it installs into /tmp
instead. The second one sets OPTFLAGS
so that the -pthread
option is present.
The Target Build
Continuing with the breakdown of the Makefile
I have actually building the object files and targets:
Makefile:14
Remember that the first target is what make
will run by default when no target is given. In this case it's called all:
and it gives $(TARGET) tests
as the targets to build. Look up at the TARGET
variable and you see that's the library, so all:
will first build the library. The tests
target is then further down in the Makefile
and builds the unit tests.
Makefile:16
Another target for making "developer builds" that introduces a technique for changing options for just one target. If I do a "dev build" I want the CFLAGS
to include options like -Wextra
that are useful for finding bugs. If you place them on the target line as options like this, then give another line that says the original target (in this case all
) then it will change the options you set. I use this for setting different flags on different platforms that need it.
Makefile:19
Builds the TARGET
library, whatever that is, and also uses the same trick from line 15 of giving a target with just options changes to alter them for this run. In this case I'm adding -fPIC
just for the library build using the +=
syntax to add it on.
Makefile:20
Now the real target where I say first make the build
directory, then compile all the OBJECTS
.
Makefile:21
Runs the ar
command which actually makes the TARGET
. The syntax $@ $(OBJECTS)
is a way of saying, "put the target for this Makefile source here and all the OBJECTS after that". In this case the $@
maps back to the $(TARGET)
on line 19, which maps to build/libYOUR_LIBRARY.a
. It seems like a lot to keep track of this indirection, and it can be, but once you get it working this means you just change TARGET
at the top and build a whole new library.
Makefile:22
Finally, to make the library you run ranlib
on the TARGET
and it's built.
Makefile:24-24
This just makes the build/
or bin/
directories if they don't exist. This is then referenced from line 19 when it gives the build
target to make sure the build/
directory is made.
You now have all the stuff you need to build the software, so we'll create a way to build and run unit tests to do automated testing.
The Unit Tests
C is different from other languages because it's easier to create one tiny little program for each thing you're testing. Some testing frameworks try to emulate the module concept other languages have and do dynamic loading, but this doesn't work well in C. It's also unnecessary because you can just make a single program that's run for each test instead.
I'll cover this part of the Makefile, and then later you'll see the contents of the tests/
directory that make it actually work.
Makefile:29
If you have a target that's not "real", but there is a directory or file with that name, then you need to tag the target with .PHONY:
so make
will ignore the file and always run.
Makefile:30
I use the same trick for modifying the CFLAGS
variable to add the TARGET
to the build so that each of the test programs will be linked with the TARGET
library. In this case it will add build/libYOUR_LIBRARY.a
to the linking.
Makefile:31
Then I have the actual tests:
target which depends on all the programs listed in the TESTS
variable we created in the header. This one line actually says, "Make, use what you know about building programs and the current CFLAGS settings to build each program in TESTS
."
Makefile:32
Finally, when all of the TESTS
are built, there's a simple shell script I'll create later that knows how to run them all and report their output. This line actually runs it so you can see the test results.
Makefile:34-35
In order to be able to dynamically rerun the tests with Valgrind there's a valgrind:
target that sets the right variable and runs itself again. This puts the valgrind logs into /tmp/valgrind-*.log
so you can go look and see what might be going on. The tests/runtests.sh
then knows to run the test programs under Valgrind when it sees this VALGRIND
variable.
For the unit testing to work you'll need to create a little shell script that knows how to run the programs. Go ahead and create this tests/runtests.sh
script:
echo "Running unit tests:"
for i in tests/*_tests
do
if test -f $i
then
if $VALGRIND ./$i 2>> tests/tests.log
then
echo $i PASS
else
echo "ERROR in test $i: here's tests/tests.log"
echo "------"
tail tests/tests.log
exit 1
fi
fi
done
echo ""
I'll be using this later when I cover how unit tests work.
The Cleaner
I now have fully working unit tests, so next up is making things clean when I need to reset everything.
Makefile:38
The clean:
target starts things off whenever we need to clean up the project.
Makefile:39-42
This cleans out most of the junk that various compilers and tools leave behind. It also gets rid of the build/
directory and uses a trick at the end to cleanly erase the weird *.dSYM
directories Apple's XCode leaves behind for debugging purposes.
If you run into junk that you need to clean out, simply augment the list of things being deleted in this target.
The Install
After that I'll need a way to install the project, and for a Makefile
that's building a library I just need to put something in the common PREFIX
directory, which is usually /usr/local/lib
.
Makefile:45
This makes install:
depend on the all:
target so that when you run make install
it will be sure to build everything.
Makefile:46
I then use the program install
to create the target lib
directory if it doesn't exist. In this case I'm trying to make the install as flexible as possible by using two variables that are conventions for installers. DESTDIR
is handed to make by installers that do their builds in secure or odd locations to build packages. PREFIX
is used when people want the project to be installed in someplace other than /usr/local
.
Makefile:47
After that I'm just using install
to actually install the library where it needs to go.
The purpose of the install
program is to make sure things have the right permissions set. When you run make install
you usually have to do it as the root user, so the typical build process is make && sudo make install
.
The Checker
The very last part of this Makefile
is a bonus that I include in my C projects to help me dig out any attempts to use the "bad" functions in C. Namely the string functions and other "unprotected buffer" functions.
Makefile:50
Sets a variable which is a big regex looking for bad functions like strcpy
.
Makefile:51
The check:
target so you can run a check whenever you need.
Makefile:52
Just a way to print a message, but doing @echo
tells make
to not print the command, just its output.
Makefile:53
Run the egrep
command on the source files looking for any bad patterns. The || true
at the end is a way to prevent make
from thinking that egrep
not finding things is a failure.
When you run this it will have the odd effect that you'll get an error when there is nothing bad going on.
What You Should See
I have two more exercises to go before I'm done building the project skeleton directory, but here's me testing out the features of the Makefile
.
$ make clean
rm -rf build
rm -f tests/tests.log
find . -name "*.gc*" -exec rm {} \;
rm -rf `find . -name "*.dSYM" -print`
$ make check
Files with potentially dangerous functions.
^Cmake: *** [check] Interrupt: 2
$ make
ar rcs build/libYOUR_LIBRARY.a
ar: no archive members specified
usage: ar -d [-TLsv] archive file ...
ar -m [-TLsv] archive file ...
ar -m [-abiTLsv] position archive file ...
ar -p [-TLsv] archive [file ...]
ar -q [-cTLsv] archive file ...
ar -r [-cuTLsv] archive file ...
ar -r [-abciuTLsv] position archive file ...
ar -t [-TLsv] archive [file ...]
ar -x [-ouTLsv] archive [file ...]
make: *** [build/libYOUR_LIBRARY.a] Error 1
$ make valgrind
VALGRIND="valgrind --log-file=/tmp/valgrind-%p.log" make
ar rcs build/libYOUR_LIBRARY.a
ar: no archive members specified
usage: ar -d [-TLsv] archive file ...
ar -m [-TLsv] archive file ...
ar -m [-abiTLsv] position archive file ...
ar -p [-TLsv] archive [file ...]
ar -q [-cTLsv] archive file ...
ar -r [-cuTLsv] archive file ...
ar -r [-abciuTLsv] position archive file ...
ar -t [-TLsv] archive [file ...]
ar -x [-ouTLsv] archive [file ...]
make[1]: *** [build/libYOUR_LIBRARY.a] Error 1
make: *** [valgrind] Error 2
$
When I run the clean:
target that works, but because I don't have any source files in the src/
directory none of the other commands really work. I'll finish that up in the next exercises.
Extra Credit
- Try to get the
Makefile
to actually work by putting a source and header file insrc/
and making the library. You shouldn't need amain
function in the source file. - Research what functions the
check:
target is looking for in theBADFUNCS
regular expression it's using. - If you don't do automated unit testing, then go read about it so you're prepared later.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论