To get on this list, a bug has to be able to cause at least half a
day of futile head scratching, and has to be aggravated by the poor
design of the "C" language. In the interests of equal time, and
to see how the world has progressed in the 20-odd years since "C"
escaped from its spawning ground, see my Top
10
Ways
to
be Screwed by the Java programming language, and for
more general ways to waste a lot of time due to bad software, try my Adventures in Hell page.
A better language would allow fallible programmers to be more productive. Infallible programmers, of the type unix' and "C" designers anticipated, need read no further. In fairness, I have to admit that the writers of compilers have improved on the situation in recent years, by detecting and warning about potentially bad code in many cases.
a=b; /* this is a bug
c=d; /* c=d will never happen */
if(a=b) c; /* a always equals b, but c will be executed if b!=0 */Depending on your viewpoint, the bug in the language is that the assignment operator is too easy to confuse with the equality operator; or maybe the bug is that C doesn't much care what constitutes a boolean expression: (a=b) is not a boolean expression! (but C doesn't care).
Closely related to this lack of rigor in booleans, consider this
construction:
if( 0 < a < 5) c; /* this "boolean" is always true! */Always true because (0<a) generates either 0 or 1 depending on if (0<a), then compares the result to 5, which is always true, of course. C doesn't really have boolean expressions, it only pretends to.
#define assign(a,b) a=(char)bbecomes
assign(x,y>>8)
struct foo { BOOL a};now, F1. and F2 disagree about the fundamental attributes of structure "foo". If they talk to each other, You Lose!
file F1.c contains
#define BOOL char
#include "foo.h"
file F2.c contains
#define BOOL int
#include "foo.h"
int foo (a)Generally speaking, C compilers, and C runtimes either can't or don't tell you there is anything wrong. What actually happens depends on the particular C compiler and what trash happened to be left lying around wherever the caller is going to look for the returned value. Depending on how unlucky you are, the program may even appear to work for a while.
{ if (a) return(1); } /* buggy, because sometimes no value is returned */
Now, imagine the havoc that can ensue if "foo" was thought to
return a pointer!
struct eeh_typeDepending on which C compiler, and which "endian" flavor of machine you are on, this might actually be implemented as
{
uint16 size: 10; /* 10 bits */
uint16 code: 6; /* 6 bits */
};
<10-bits><6-bits>or as
<6-bits><10-bits>
Also, again depending on the C compiler, machine architecture, and various mysterious preference settings,
the items might be aligned to the nearest 8, 16, 32, or 64 bits.
So what matters? If you are trying to match bits with a real world file,
everything!
Need another way to lose big? How about this:
Rect foo = {0,1,2,3}; // assign numbers to the first four slots
You may think you know what those four slots are, but there's at least an
even chance you'll have to discover the hard way if the structure ever
changes.
foo(pointer->member, pointer = &buffer[0]);Works with gcc (and other compilers I used until I tried acc) and does not with acc. The reason is that gcc evaluates function arguments from left to right, while acc evaluates arguments from right to left.
K&R and ANSI/ISO C specifications do not define the order of evaluation for function arguments. It can be left-to-right, right-to-left or anything else and is "unspecified". Thus any code which relies on this order of evaluation is doomed to be non portable, even across compilers on the same platform.
This isn't an entirely non controversial point of view. Read the supplementary dialog
on the subject.
if( ... )which, when adding debugging statements, becomes
foo();
else
bar();
if( ... )There is a large class of similar errors, involving misplaced semicolons and brackets.
foo(); /* the importance of this semicolon can't be overstated */
else
printf( "Calling bar()" ); /* oops! the else stops here */
bar(); /* oops! bar is always executed */
CALLIT(functionName,(arg1,arg2,arg3));CALLIT did more than just call the function. I didn't want to do the extra stuff so I removed the macro invocation, yielding:
functionName,(arg1,arg2,arg3);Oops. This does not call the function. It's a comma expression that:
switch (a) {Still not convinced? Try this one (suggested by Mark Scarbrough )
int var = 1; /* This initialization typically does not happen. */
/* The compiler doesn't complain, but it sure screws things up! */
case A: ...
case B: ...
}
#define DEVICE_COUNT 4
uint8 *szDevNames[DEVICE_COUNT] = {
"SelectSet 5000",
"SelectSet 7000"}; /* table has two entries of junk */
char *f() {
char result[80];
sprintf(result,"anything will do");
return(result); /* Oops! result
is allocated on the stack. */
}
int g()
{
char *p;
p = f();
printf("f() returns: %s\n",p);
}
The "wonderful" thing about this bug is that it sometimes seems to be a
correct program; As long as nothing has reused the particular piece of
stack occupied by result.
Even within a single expression, even with only strictly manifest side effects, C doesn't define the order of the side effects. Therefore, depending on your compiler, I/++I might be either 0 or 1. Try this:
#include <stdio .h>Prints either "Foo got 2", or "Bar got 2"
int foo(int n) {printf("Foo got %d\n", n); return(0);}
int bar(int n) {printf("Bar got %d\n", n); return(0);}
int main(int argc, char *argv[])
{
int m = 0;
int (*(fun_array[3]))();
int i = 1;
int ii = i/++i;
printf("\ni/++i = %d, ",ii);
fun_array[1] = foo; fun_array[2] = bar;
(fun_array[++m])(++m);
}
Prints either i/++i = 1 or i/++i=0;
Actually, this bug is so well known, it didn't even make the list! That doesn't make it less deadly when it strikes. Consider the simplest case:
void foo(a)and in truth, modern compilers will usually flag an error as blatant as the above. However, you just have to be a little more clever to outsmart the compiler. Consider:
{ int b;
if(b) {/* bug! b is not initialized! */ }
}
void foo(int a)
{ BYTE *B;
if(a) B=Malloc(a);
if(B) { /* BUG! B may or may not be initialized */ *b=a; }
}
The compile-time environment of a typical compilation is cluttered with hundreds (or thousands!) of things that you typically have little or no awareness of. These things sometimes have dangerously common names, leading to accidents that can be virtually impossible to spot.
#include <stdio.h>
#define BUFFSIZE 2048
long
foo[BUFSIZ];
//note
spelling
of
BUFSIZ != BUFFSIZE
This compiles without error, but will fail in predictably awful
and mysterious ways, because BUFSIZ is a symbol defined by
stdio.h. A typo/braino like this can be virtually impossible to
find if the distance between the the #define and the error is greater
than in this trivial example.
This is so obvious it didn't even make the list for the first 5 years, but C's arrays and associated memory management are completely, utterly unsafe, and even obvious cases of error are not detected.
int thisIsNuts[4]; int i;
for ( i = 0; i < 10; ++i )
{
thisIsNuts[ i ] = 0; /*
Isn't it great ? I can use elements 1-10 of a 4 element array,
and
no one cares */
}
Of course, there are infinitely many ways to do things like this
in C.
In C, numbers beginning with a zero are evaluated in base 8.
If there are no 8's or 9's in the numbers, then there will be no
complaints from the compiler, only screams from the programmer when he
finally discovers the nature of the problem.
int numbers[] = {
001, // line up numbers for
typographical clarity, lose big time
010,
//
8
not 10
014
};
//
12, not 14
Not convinced ? Try atoi("000010");
{ int a=1,b=2;
char buf[10];
scanf("%d %d",a,b); // don't you mean &a,&b? Prepare to blow!
sprintf(buf,"this is the result: %d %d"); // putting at least 20 characters in a 10 character buffer
// and fetching a couple random vars from the stack.
}
Back to my home page |