I don't understand what is the best way to debug c code. What is your general approach for any compiler to c debug code ?
Eyeball mark 2. The mark 2 is the version that understands that
developer intent and
what the code actually does are two independent things, and whenever they diverge, you are likely to end up with a bug.
I've written so much C, and read/gone through even more C code, that I do not usually need to run the binary in a debugger; I can usually detect the bugs with eyeball mark 2 just fine. For my own code, I do need to sleep between writing the code and going through it with debugging-eyes; otherwise the human visual filtering machinery will intervene making me miss some of the errors.
I do often use both
fprintf(stderr, "describe key variable values\n", key variables) at key position in the code, for example at the beginning of a function I'm working on, and things like Graphviz DOT format output for trees and graphs, that I can then easily visualize using e.g.
dot (from Graphviz).
When I learned to write unit "tests" first (to examine low-level solutions, for example abstract data type implementations – like disjoint sets – and their accessor functions), and then incorporate the implementations one by one into a working program, testing frequently and fixing all bugs and implementing all error checks before going on to the next step, and compiling at each step with warnings enabled, I suddenly became much more productive with much fewer bugs.
As an example, whenever I start writing a new command-line tool, I start by writing a working skeleton that does nothing:
#include <stdlib.h>
int main(int argc, char *argv[])
{
return EXIT_SUCCESS;
}
Then I add command-line parsing, usually using
getopt() (POSIX) or
getopt_long() (GNU extension):
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <locale.h>
#include <getopt.h>
#include <stdio.h>
static void usage(const char *arg0)
{
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);
fprintf(stderr, " %s FILE...\n", arg0);
fprintf(stderr, "\n");
}
int main(int argc, char *argv[])
{
static const struct option longopts[] = {
{ .name = "help", .has_arg = 0, .flag = NULL, .val = 'h' },
{ 0 }
};
const char *arg0 = (argc && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
int opt;
if (!setlocale(LC_ALL, ""))
fprintf(stderr, "Warning: Current locale is not supported.\n");
while ((opt = getopt_long(argc, argv, "h", longopts, NULL)) != -1) {
switch (opt) {
case 'h':
usage(arg0);
return EXIT_SUCCESS;
default: /* '?', and catch-all for unimplemented but specified options */
usage(arg0);
return EXIT_FAILURE;
}
}
/* Require at least one non-option argument */
if (optind >= argc) {
usage(arg0);
return EXIT_FAILURE;
}
/* argv[optind] through argv[argc-1] are the non-option arguments. */
/* Debug output: */
for (int i = optind; i < argc; i++)
fprintf(stderr, "Argument %d (%d): \"%s\".\n", i - optind + 1, i, argv[i]);
return EXIT_SUCCESS;
}
Because I like efficiency (being lazy!), I also tend to apply my standard Makefile skeleton. Let's assume the above is named
example.c, and we want to compile it to
./ex. The needed
Makefile is then
CC := gcc
CFLAGS := -Wall -Wextra -O2
LDFLAGS := -lm
PROGS := ex
.PHONY: all clean
all: $(PROGS)
clean:
rm -f *.o $(PROGS)
%.o: %.c
$(CC) $(CFLAGS) -c $^
ex: example.o
$(CC) $(CFLAGS) $^ $(LDFLAGS) -o $@
although the indent has to be with Tab characters which this forum converts to spaces, so you need to run
sed -e 's|^ *|\t|' -i Makefile to fix the indentation. (You can run it safely at any point, and I recommend doing so after modifying the Makefile). Then, to recompile the program from scratch, you only need to run
make clean all. (Plain
make only rebuilds modified sources; here, only if
example.c has been modified.)
This way, whenever I write additional code, I already know that any compilation error is due to the code I've added since the latest successful build. Before adding more code, I fix those errors. It saves a LOT of time.
If I am not sure yet what data structure I should use, I create a separate minimal test program(s), and implement the data structure there. For trees and graphs, I often generate random data, and output the tree or graph in DOT format, using a safe tree/graph traversal (usually using a flag, "already described" in each node) to avoid getting caught in a traversal loop, and visually examine a few dozen trees/graphs to see it works. I then create the pathological cases (like what happens if the input is the same, in the worst possible order, and so on), and verify my code won't get confused.
When I've found a thing that works, I then transplant the needed code to my main project, and recompile and check. Again, the key is implementing and verifying each part, before advancing to the next (writing any additional code), so that the amount of code one needs to check stays minimal.
I can categorically say that writing a full program without compiling it at any point, is the wrong approach for me at least. It wastes time, because I'm not perfect. If I leverage the compiler and especially its capability to warn about suspicious code, I don't need to worry much about typos – I do rely on
man pages instead of memorizing standard C and POSIX C interfaces –, and concentrate on the more important things like the overall design and algorithms, I am both faster and generate fewer bugs. At this point, I'm pretty sure I've written over half a million lines of C, although only a fraction of that as paid work. (But I have worked on and provided bug-fixing patches to quite a few open source projects, including to the Linux kernel, the GNU standard library, and the GCC compiler suite.)