Often, programmers use C because they want total control to use whatever trickery they can, to do something unusual such as control hardware or gain maximum efficiency
The C language brings its own problems, and so trickery is also needed to handle those problems
This aside discusses some lesser known C trickery
The advanced debugging options used in this unit:
-fsanitize=undefined -fsanitize=address
are relatively recent and not very well known
Yet they can almost eliminate the old problems with segfaults
sanitize
doesThe sanitize
options to gcc
or clang
adds extra code to your C program to detect undefined behaviour
The extra code slows your program down, so this is only for testing, not for production
For example -fsanitize=bounds
adds array bound checking, to
catch going past the end of arrays
The -fsanitize=undefined
flag bundles a whole lot of options
together, to try to catch almost all undefined behaviour situations
The main problem with undefined behaviour is finding the line in the program that is causing the problem
You have to add more and more print statements to narrow down gradually where the problem is
With gdb
or valgrind
, as well as a learning curve
for using them, you still have to use search techniques to find the problem
With sanitize
, the extra code stops your program the instant
it does something wrong and reports the line number
You need a reasonably recent version of gcc or clang
The sanitize
options began in version 4.9 of gcc,
unfortunately, version 4.8 is common, so clang
is a better bet if
you have it
You also need the ubsan
library for generating error messages,
and you may need to add the -g
option for line number info and
remove the -O2
or similar option
As usual, Windows causes problems - first you need Cygwin or MSYS2
to get gcc
or clang
Then the ubsan
library is probably not available, so add the
-fsanitize-undefined-trap-on-error
option to cause a trap instead
of an error message
Then use gdb
in the simplest way (gdb program
then
run
) to get an error message
Suppose you want to make a structure read-only; here's one way:
struct point { int x, y; }; typedef struct point const point;
Now, any variable of type point
only has read-only access to
the fields
But, by using the type struct point
instead, code can gain write
access
Here's another way:
struct point; typedef struct point point; int x(point p); int y(point p);
struct point { int x, y; }; inline int x(point p) { return p->x; } inline int y(point p) { return p->y; }
Compiling with the -flto
cross-module-optimisation flag makes
x(p)
as efficient as p->x
Suppose you do this:
int n = 24; char *p = malloc(n); ... n = 2 * n; p = realloc(p, n);
Conventional wisdom is to start small (smaller than n=24
gains nothing) in case of lots of empty arrays
And to double, to reduce the cost of copying in realloc
to
constant time per item on average
If you keep doubling the size of an array, you might end up with
previous arrays of sizes 1
, 2
, 4
,
8
, 16
, 32
, which add up when freed up and
coalesced to 63
, at the moment you want a new array of size
64
, so you may end up never reusing old space
So many programmers now do n = n * 3 / 2
to multiply by
1.5
or some other factor below 2
If you double the size of an array, you end up with up to half of it unused
If you know the array won't grow any more, you can use realloc
to reduce the size to the amount that is used, freeing up the
remainder (and it is likely to be a low-cost call, because realloc
is unlikely to move the array)
You can look out for special cases in your programs
Take scanning, for example, where a source text is divided up into a (flexible) array of tokens
The special case is the tokens are created in one go, with no other allocations going on at the same time
You can increase the size of the array by a fixed amount
(n = n + b
) because most realloc
calls will extend the
array, not copy it, so will be low-cost (try it!)
To make flexible arrays convenient, you need to encapsulate them:
struct list; typedef struct list list;
struct list { token *tokens; int capacity, length; };
The user makes function calls on a list
object, which never
moves, whereas the tokens
array is moved by realloc
calls
A disadvantage of a flexible array is that you can't let the user have a pointer into it, because the data moves
You should look for special cases where you can allocate objects in fixed positions, but give the illusion of an array or linked list
Take tokens again for example: after creating them, you only ever need to iterate through them, not index them at random, so you can give the illusion of a linked list (doubly linked if you want) as follows
The header looks something like this:
struct list; typedef ...; struct token; typedef ...; token *addToken(list ts); token *next(token *t);
Allocate token structures in blocks, reducing the overhead of
malloc
, which is 8 to 16 bytes per object
The next
function is "if at end of block, move to the next
block, else increase the token pointer by one" and you can inline it and use
-flto
The problems with a global variable are:
If you are 100% sure these will never apply to your program, a global variable may be a valid trick
But it is surprising how often a little ingenuity yields a better approach
Languages and libraries often use compressed pointers, because real pointers are now usually 64 bits, which can seem too much
A compressed pointer is a 32-bit integer relative to the base of the heap
Instead of *p
you use base[cp] = *(base + cp)
It seems that the base
pointer needs to be global, so that it
can be accessed from anywhere, but there is a better way
Instead of using offsets relative to the heap base, you can use offsets relative to the object they are stored in
For example, for tree nodes, you can implement:
struct node { int left; ... }; inline node *leftChild(node *n) { return n + n->left; }
See how simple this is - it assumes left
is measured in
sizeof(node)
units, otherwise you have to convert to and from
(char *)
To make relative compressed pointers work, you need to know two things
malloc
increases the size of the heap as necessary for
small allocations, but uses mmap
to allocate somewhere
completely different for large allocations (say >=128k
) so
you need to avoid large allocations in your program
If your program needs more than 4Gb
(more if using larger
units), it may stop working, if a 32-bit integer offset
overflows