Common Sense C - Advice and Warnings For C and C++ Programmers - (1882419006)
Common Sense C - Advice and Warnings For C and C++ Programmers - (1882419006)
Preface
About the Author
Chapter 1—Introduction
What's the Problem?
"Real Programmers" And C
A Better C
Conquering C
Bibliography
Appendix
Index
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
Table of Contents
Table of Contents
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
Table of Contents
Acknowledgments
Several people played a key role in creating this book. Jennifer
Hamilton pressed the case for C and C++ and stimulated my
analysis of where C's problems lie. Arguing with her over C
facilities and programming style helped me refine my own side
of the debate. Mike Otey provided invaluable technical review.
Trish Faubion helped turn the original rough style into one that
retained its bite, but was much more polished. Katie McCormick
Tipton, Barb Gibbens, and Kathy Blomstrom all helped refine
my writing. And Dave Bernard and Sharon Hamm wielded just
the right mix of encouragement and threat to make the book
actually happen. My sincere thanks to all.
Dedication
Table of Contents
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
Chapter 1
Introduction
array[++top] = item;
array[next++] = item;
++top;
array[top] = item;
array[next] = item;
++next;
A Better C
Many claims have been made for C++, but one thing seems
certain: C++ is a "better" — if more complex — version of C. C+
+ adds some important language features missing in C; for
example, reference parameters, inline functions, and templates to
define generic functions and classes. These features aid clearer
programming and can reduce — but not eliminate — the need
for macros in C++ programs.
Conquering C
To pick the right projects for C or C++, and then use the
language effectively, you have to ignore a lot of conventional
attitudes towards C and C programming practices. Many of these
attitudes and practices are rooted in a time and place 15 years
ago when C was a major step forward for systems programmers.
Today there are good alternatives to C for many applications,
and programming practices have changed considerably. One of
the most important differences between 15 years ago and today
is that businesses are placing much more emphasis on
controlling software development costs than on modest
improvements in performance. Thus, developers trying to
control costs want to avoid language features such as address
pointers and coding practices such as folding a sequence of
distinct operations into a single statement.
Chapter 2
Common Mistakes and How to Avoid Them
if (x = y)
printf("Equal values");
if (x EQ y)
printf("Equal values");
2 && 4
2 & 4
Lazy Logic
Yes, C is a devilishly clever little language. It's quick to write,
too. Suppose you've written a function, get_customer, to return
either an integer customer ID or zero if no customer is input.
Why ywaste time with "verbose" code like
custid = get_customer();
if (custid > 0) {
/* Process the customer */
}
if (custid = get_customer()) {
/* Process the customer */
}
if (custid = get_customer())
You should also use only Boolean variables and functions with
the logical operators && and ||. Following this practice
eliminates problems caused by accidentally using the bitwise
operators & and | in logical expressions.
Understand one thing about C, and all its mysteries are revealed.
C was -- and is -- a language meant as a portable replacement for
machine-dependent assembly languages. Keep this in mind
when you consider the following example.
Suppose you code an array of part numbers and their names and
a few lines to display a list of parts, as shown in Figure 2.1. If
you remember C is for machine-level programming, you won't
be suprised to find there's no part number 11. In C, 011 is not 11;
it's 9! Integer constants that begin with 0 are octal (Get it? The 0
looks like O for Octal.)
struct part {
int part_number;
char description[30];
}
main() {
int i;
There are even more subtle ways octal constants can sneak up on
you. Suppose you want to read an integer part from the standard
input and then output it, using
int part_number;
scanf("%i", part_number);
printf("Number %i", part_number);
int part_number;
scanf("%d", part_number);
printf("Number %d", part_number);
It Hurts So Good
If you're new to C, you may think I'm blowing its problems out
of proportion. You may wonder whether C's flaws significantly
hamper the work-a-day C programmer. The answer is yes, most
C programmers do suffer from C's flaws; but like some
mainframe COBOL programmers and some midrange RPG
programmers, C programmers sometimes take pride in their
ability to overcome the language's deficiencies. And after
enough years chasing errant pointers, many C programmers
become numb to the pain of using a language that can crash the
debugger and freeze their PC.
*****
Chapter 3
Foolproof Statement and Comment Syntax
if (xcnt < 2)
return
date = x[0];
time = x[1];
Brace Yourself
If (xcnt < 2)
return;
date = x[0];
time = x[1];
if (xcnt < 2)
printf("Timestamp array is too small\n");
return;
date = x[0];
time = x[1];
if (xcnt < 2) {
printf("Timestamp array is too small\n");
}
return;
date = x[0];
time = x[1];
if (xcnt < 2) {
return;
}
date = x[0];
time = x[1];
Note that using braces also lets the compiler catch a missing
semicolon, so you get lots of protection by following this simple
rule.
if (xcnt < 2) {
date = x[0];
return (date);
}
This doesn't solve the original problem we looked at, but it does
show how to code return statements so you're less likely to be
tripped up by other problems with complex expressions.
Follow This Advice, or Else
if (xcnt < 2)
if (xcnt != 0) return;
else {
date = x[0];
time = x[1];
}
if (xcnt < 2) {
if (xcnt != 0) {
return;
}
else {
date = x[0];
time = x[1];
}
}
if (xcnt < 2) {
if (xcnt != 0) {
return;
}
}
else {
date = x[0];
time = x[1];
}
#define IF { if (
#define THEN ) {
#define ELSE } else {
#define ELSEIF } else if (
#define ENDIF } }
IF xcnt < 2
THEN IF xcnt != 0
THEN return;
ENDIF
ELSE date = x[0];
time = x[1];
ENDIF
Give Me a Break
switch (color) {
case 1: printf("red\n");
case 2: printf("blue\n");
}
Given this code, when color is 1, both "red" and "blue" are
printed. The proper code is
switch (color) {
case 1: printf("red\n");
break;
case 2: printf("blue\n");
}
Of course, when you add another color, you'd better add another
break after the second case. C's switch is not what is generally
recognized as a "case" multiway conditional control structure;
it's nothing more than a jump table. The compiler evaluates the
switch expression simply to determine the target of a jump (i.e.,
go to) operation into the code that follows. Unless you code a
break, execution will continue sequentially through the code for
cases that follow the case that matches the switch expression
value.
Previous Table of Contents Next
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
if (color == 1) {
printf("red\n");
}
else if (color == 2) {
printf("blue\n");
}
else {
printf("Invalid color\n");
}
Better yet, you can use the EQ macro described in the last
installment and the macros presented above (IF, THEN,
ELSEIF, ELSE, and ENDIF) to code the tests as in Figure 3.1.
This solution has a compact, table-oriented layout and avoids the
hazards of raw C.
strcpy(prv_opcode, op_code);
strcpy(prv_opcode, opcode);
setnull(stk_opcode);
setnull(op_symbol);
setnull(op_suffix);
op_is_ctlop = FALSE;
From C to Shining C
*****
struct part {
int part_number;
char description[30];
};
main() {
int i;
/* Array of part numbers and descriptions
*/
struct part part_table[100] =
{
{011, "Wrench" },
{067, "Screwdriver" },
{137, "Hammer" },
{260, "Pliers" },
/*etc. */
{0, "sentinel" }
};
for (i=0; i<100; i++) /* Print the list
of parts */
{
if (part_table[i].part_number == 0)
break;
printf("%i %s\n", part_table[i].
part_number,
part_table[i].description);
}
}
Chapter 4
Hassle-free Arrays and Strings
Deck: C Arrays can cause disarray, and C Strings can tie you up
in knots
by Paul Conte
Suppose you need to store the number of orders for each month
(1 to 12). A "good" C programmer might declare an array and
reference an array element as in the following code:
int month;
int orders[12];
...
++orders[month - 1];
There, see how simple arrays that start at 0 are! There are at least
a dozen other ways to code this example, but not one of them
overcomes the conflict between natural counting systems, which
begin with 1, and C's arrays, which begin with 0. This
"impedance mismatch" between the natural world and C
increases the likelihood of "off by 1" array and loop errors.
int month;
int orders[12 + 1];
...
++orders[month];
...
for (month = 1; month <= 12; month++) {
printf("Total for month %d is %d\n",
month, orders[month]);
}
int month;
TABLE( orders, int, 12 );
.
.
.
++ orders[ month ];
#define ENDOVER }}
String Symphony
strcpy(b, a);
Figure 4.5 shows one way to guard a string copy, using the
sizeof operator. If the source string (including the '\0' terminator)
fits in the target, the whole string is copied; otherwise, only as
much as will fit is copied, and a null terminator is added. Even
though this technique may truncate some strings, your program
will continue its proper execution flow, rather than take some
wild path caused by overwriting part of the program's
instructions.
int month;
STRING( print_line, 80 );
Rough C Coming
Most of the C pitfalls I've covered in the first three chapters are
fairly easily circumvented by avoiding certain language
constructs and using macros. Strings presented the first example
of inherent C constructs for which there is no simple, universal
solution (other than perhaps moving to C++). In Chapter 5, I'll
take up pointers, a feature of C even more difficult than strings
to handle safely.
*****
* Declare C arrays with one extra element, and don't use the element
with subscript 0.
* Use macros to define tables and loops over them.
* Always guard a string assignment against overwriting the target
variable.
* Create macros and functions to define strings and provide "safe"
string operations.
Chapter 5
Simplified Variable Declarations
by Paul Conte
int x;
main( void ) {
int x;
...
}
Let's say the first x refers to a storage location we'll call S1, and
the second x refers to a storage location we'll call S2. The scope
of the S1 object (storage location) is simply the region (i.e.,
lines) of source code where references to x are references to S1;
likewise, the scope of the S2 object is the region of source code
where references to x are references to S2. In this example, the
scope of S1 (the first x) is everywhere outside the main function,
and the scope of S2 (the second x) is only within the main
function. Obviously, for the program to be clear, these two
regions of the program can't overlap; each reference to x must
refer to just one of the storage locations. Scope is also called
"visibility" because you can "see" an object (e.g., read or change
a storage location) only within its scope. In this chapter, I use
"visibility" for the general concept and "scope" to refer to C's
specific lexical scope attribute.
C's rules for function visibility are simple: If you specify static
storage class, a function is visible throughout the source file in
which it's defined, but not in other source files. With extern or
no storage class specifier, a function is visible throughout the
program (i.e., across all files), and you can call it from
anywhere. These rules lead to my first suggestion: Declare
functions static if you intend to call them only from within the
same source file.
C's rules for variable visibility are far more complex than those
for function visibility. I've listed these rules in a table (Figure
5.1) that shows, for any variable declaration, where that variable
is visible. I've also listed C's scope and linkage attributes and
whether the declaration causes storage to be allocated (in C
terminology, whether it is a definition as well as a declaration).
You can use this guide to help you understand some of the
mysterious changes that can occur when a C variable has
unexpected visibility. You also may need it to follow my next
few examples, but later I'll show you a far more useful table for
C programming.
Figure 5.1
Visibility of C Variables
DECLARED OR REFERENCED INSIDE A BLOCK (function
or nested block):
Scope/
Where Storage Initial Storage Visibility
Linkage
specified class value allocation
Within
same
1. Block
(none) Yes Yes block,
Declared scope,
including
all
in block auto No Yes no nested
linkage blocks,
except
register any nested
block (and
its
static nested
blocks)
with an
identical
identifier
without
extern
2. extern No CASE A: Enclosing scope has
Declared identical, visible
in block identifier — same scope, linkage,
allocation, visibility as the
matching identifier
CASE B: Otherwise, same as if
declared extern outside function
(see 7, below)
3.
extern Yes ILLEGAL
Declared
in block declaration
4. Not CASE A: Enclosing scope has
declared identical, visible identifier
but declared in the same source file
referenced prior to the reference — same as if
in block declared extern in block (see 2,
above)
CASE B: Otherwise, ILLEGAL
declaration
EXTERNAL DECLARATIONS (declared outside any
function):
Scope/
Which Storage Initial Storage Visibility
Linkage
declaration class value allocation
5. First (none) Yes Yes File Rest of file
scope except any
declaration No external block (and
its nested
blocks)
with an
identical
identifier
declared
without
extern, and
blocks it
contains
and Other
files with
an identical
external
linkage
identifier
declared in
them (No
other file
may
allocate
(define) an
identical
external
linkage
identifier.)
6. First static Yes Yes File Rest of file
scope, except any
declaration No internal block (and
linkage its nested
blocks)
with an
identical
identifier
declared
without
extern, and
blocks it
contains
7. First extern Yes Yes File Rest of life
scope, except any
declaration external block (and
linkage its nested
blocks)
with an
identical
identifier
declared
without
extern, and
blocks it
contains
and Other
files with
an identical
external
linkage
identifier
declared in
them (One
other file
must have
an identical
external
linkage
identifier
allocated
(defined) in
it.)
8. First extern Yes Yes Same as if declared
outside function
without extern or static
declaration
(see 5, above)
9. Second Same scope, linkage,
Must have same
or allocation, and
visibility as first
later type and linkage
declaration
declaration as first declaration
C syntax flies in the face of the "say what you mean, mean what
you say" principle of programming. The keyword extern is
simply an abbreviation for "external" — a word you'd expect to
be related to visibility or scope and to mean something like
"outside the current context." And you'd expect the other
relevant keyword, static, to relate to how storage is managed.
Thus, you might read the declaration
extern int x;
int x;
static int x;
Not only does C's syntax lack consistency, but it also confuses
things by using the static storage class keyword to specify
visibility. My theory is that Humpty Dumpty was on the original
C design team. As he told Alice, "When I use a word, it means
just what I choose it to mean — neither more nor less."
static int x;
main( void ) {
extern int x;
...
}
where the use of extern results in internal linkage. I'll spare you
a diversion down the cul-de-sac you enter when you have more
than one external declaration for the same variable (which C
allows). As we pass through the maze, however, notice that it
matters whether you initialize a variable in an external
declaration when you specify extern. (With an initializer, the
declaration allocates storage; without one, it doesn't.) Yet when
you specify static or no storage class, initialization doesn't effect
storage allocation.
UnC-eemly Solutions
The fastest way out of C's maze is to decide where you want to
go — that is, what kinds of visibility you want to use. I've found
three "classes" of visibility adequate to define most variables and
functions:
C-through Macros
SHARE int x;
static int x;
which gives x the desired visibility. Note that you never use
SHARE inside a function because it would just define a local
variable.
Figure 5.2
How to Declare C Variables and Functions
"Visibility" Macros
#define SHARE static
#define EXPORT
#define IMPORT extern
Declaring Variables
Where Storage/visibility
Visibility Storage referenced specifier(s)
Local Automatic In block Define at beginning of
block with no specifier
Local Static In block Define as static at
beginning of block
In nested (No declaration, use
Nonlocal —
block implicit extern)
File Static In function Define as SHARE
before first function in
file
Declare as IMPORT at
beginning of each
function where
referenced
Program Static In function Define as EXPORT
before first function in
file where variable is to
be allocated
Declare as IMPORT
before first function in
file(s) where variable is
used
Declare as IMPORT at
beginning of each
function where
referenced
Declaring Functions
Callable
Visibility specifier
from
Same file
SHARE
(only)
Any file EXPORT
The SHARE macro isn't a silver bullet to slay all of C's visibility
monsters, nor does it boost C's visibility features into a class
with Modula 2's. But it makes a lot more sense to write SHARE
rather than static when you're trying to specify a variable's
visibility.
extern int x;
For export variables you want visible throughout all source files
(i.e., the entire program), use the EXPORT macro to declare
(and define) the variable before the first function definition in
the file "owning" the variable. In most cases, this is the file
containing the main function.
EXPORT int x = 0;
int x = 0;
IMPORT x;
...
void func1( void ) {
IMPORT x;
...
}
C Coding Suggestions
* Declare functions static if you intend to call them only from within
the same source file.
* Use the most restricted visibility possible for variables; avoid
shared variables.
* Put all external declarations before the first function definition in a
file.
* Put functions that must share data and external declarations for
their shared variables in a file by themselves.
* Use EXPORT, SHARE, and IMPORT macros to clarify the
intended visibility of a variable.
Chapter 6
Practical Pointers
int x = 25;
int *y;
y = &x;
printf( "y is %d\n", y );
What is printed is the address stored in y, not the value (i.e., 25)
stored at that address. The correct printf statement is:
* rfee = 100;
* afee = 50;
Figure 6.2 shows the fees function rewritten to use two local
variables in the calculations. The function’s last two statements
assign the calculated values to the locations pointed to by the
pointer parameters. This technique isolates and simplifies
dereferencing and can significantly reduce errors. Figure 6.3
shows how to handle in/out parameters by initializing the local
variables to the dereferenced parameters.
/*
| Calculate fees as base plus adjustment
based on age
| and income
*/
/*
| Return values
*/
* rfee = reg_fee;
* afee = act_fee;
}
* rfee = reg_fee;
* afee = act_fee;
}
Previous Table of Contents Next
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
But when using an array variable, you should stick with array
notation such as a[i] to keep your code’s meaning obvious. An
added benefit in using such notation is that, in some contexts, the
C compiler can catch mistakes in expressions using array names
that it can’t catch with pointers (e.g., C lets you change an
address in a pointer variable, but you can’t change the address
referred to by an array name). And before you let some “old
hand at C” convince you that direct manipulation of pointers is
“so much faster” than subscripting arrays, read “Pulling a `Fast'
One,” page XX. In business applications and most utility
software, you can freely use array subscripts without
performance concerns.
I’ve read the viewpoint that since C array notation is really just
shorthand for pointer operations, you should use pointer notation
because it more “honestly” shows what’s going on. If you’re
trying to dissuade someone from using C, this argument has
merit. C pointer and dereferencing notation certainly looks
stranger than array notation to most programmers and warns
newcomers that C isn’t your ordinary HLL. But in the long run,
array notation expresses high-level data constructs much better
than pointer notation.
Finger Pointing
char * curstr;
char * prvstr;
prvstr = curstr;
But after these statements are executed, both curstr and prvstr
point to “xyz”. The assignment prvstr = curstr copies the address
stored in curstr to prvstr, not the contents of the memory location
curstr points to.
if ( prvstr == NULL ) {
printf( "No memory available\n" );
}
else {
strcpy( prvstr, curstr );
}
*ptr = val;
ptr =
(int *) malloc( sizeof( val ) );
*ptr = val;
if ( ptr NE NULL ) {
*ptr = val;
}
else {
printf( "ptr is NULL\n");
}
/*
| Return month name
*/
char names[10][12] = {
"January", ... "December" };
/*
| Allocate and load storage for string
val
|
| Return pointer or NULL, if error
*/
string_t p;
if ( val == NULL ) {
printf( "Invalid NULL value pointer
\n" );
return NULL;
}
p = (string_t) malloc( strlen( val ) +
1 );
if ( p == NULL ) {
printf( "No memory for %s\n", val );
return NULL;
}
else {
strcpy( p, val );
return p;
}
}
/*
| Return month name
*/
char names[10][12] = {
"January", ... "December" };
return new_string( names[ month - 1 ] );
}
Amnesia
Figure 6.8 shows how leakage can occur. The first malloc
operation allocates a memory block and stores a pointer to it in
ptr. After using this memory to hold a character string, the code
reuses ptr to point to memory containing a different string. This
code will be executed fine, but the memory originally allocated
to hold the first string will remain marked “in use,” even though
it can’t be referenced or deallocated after the second malloc
operation (assuming the pointer value isn’t copied to another
pointer variable).
/*
| ptr MUST be NULL or address of block
allocated
| by calloc, malloc, or realloc functions
|
| The value of allocate is either a valid
pointer
| of the specified ptr_type, or NULL if no
memory
| can be allocated.
*/
#define PTR *
#define contents_of( x ) ( * ( x ) )
#define address_of( x ) ( & ( x ) )
/*
| Return values
*/
int reg_fee;
int act_fee;
int age;
int income;
.
.
.
reg_fee = 100;
act_fee = 50;
.
.
.
fees( address_of( reg_fee ), address_of
( act_fee ), age, income );
It helps to know about C’s traps and pitfalls, and it never hurts to
know a few arcane C rules to amaze your programming friends
and baffle your boss. The next exercise serves both purposes.
Read the following code slowly and carefully. Then before
reading the solution, write down what you think the value of x is
at the end of this sequence of statements.
float x=4.0;
float y=2.0;
float *z;
z=$amp;x;
x=y++/*z;
x=y++ /*z
... */
So the value of x is 2.0, and the compiler digests most of the rest
of the program as a comment. One absent blank (between / and
*) makes a world of difference. This may seem like a contrived
problem, but consider several seemingly simpler, and more
likely, assignment statements:
z = &y;
x = y + *z;
x = ++ *z;
x = *z ++;
The second statement adds the value of y (2.0) and the contents
of the location z points to (also 2.0) and puts the result in x. No
surprises here. The third statement increments the contents of the
location z points to (changing the value from 2.0 to 3.0) and
assigns the new value to x. No surprises here either. But the final
statement, which looks similar to the others, is actually quite
different. This statement assigns 3.0 (the new value of *z) to x
— so far so good — and then increments the address stored in z,
rather than the contents of the location z points to. Because
unary increment operators (++ and —) bind more tightly than
the dereferencing operator (*), you have to use parentheses to
apply post-increments to a dereferenced pointer:
x = (*z) ++;
Of course, if we rewrite the previous expressions using the
contents_of macro,
x = y++/contents_of(z);
x = y + contents_of(z);
x = ++ contents_of(z);
x = contents_of(z) ++;
while(*result++ = *str1++)
while(*result++ = *str1++);
—result
while(*result++ = *str2++);
}
cat(x, a, b);
printf("%s\n", x); /* Prints: Hello
world! */
A Final Twist
When I ran the original benchmarks for this sidebar, I used the
Microsoft C /qc (quick compile) option. Subsequently, I
repeated the tests without the /qc option, and the code in Figure
6.A no longer worked. A call to Microsoft revealed another C
pitfall: There’s no standard order for evaluating the left and right
sides of an assignment expression. With the /qc option, MS-C
follows the intuitive approach and evaluates the right side (i.e.,
(char) toupper( *str )) before the left side (*str++). This
approach produces the expected results. But without /qc, MS-C
evaluates the left side first, incrementing the address in str before
it’s used in the right-hand expression. This causes each
invocation of upper_case to chop the leading, non-null character
from the string, eventually wiping out the string altogether. This
discovery inspired a new guideline for avoiding another C
pitfall: Don’t use ++ or — in assignments. This rule isn’t limited
to expressions involving pointers. The expression
x = (i) + (i++)
Chapter 7:
Macros and Miscellaneous Pitfalls
x = 3;
y = cube( x + 1 );
z = 5 * double( x );
x + 1*x + 1*x + 1
x + ( 1 * x ) + ( 1 * x ) + 1
3 + ( 1 * 3 ) + ( 1 * 3 ) + 1
or 10.
5 * x+x
( 5 * x ) + x
Following this rule, the cube and double macros can be defined
as:
#define cube( x ) ( ( x ) * ( x ) * ( x ) )
#define double( x ) ( ( x ) + ( x ) )
x = 3;
y = double( ++x );
y = ( ( ++x ) + ( ++x ) );
instead of
++i;
next = ary[ i ];
Once is Enough
When you create your own macros, you should try to avoid
evaluating a macro argument more than once, if possible. This
practice reduces the problem of unintended side effects. For
example, an obvious improvement to the double macro
definition is:
#define double( x ) ( 2 * ( x ) )
if ( x < 0 )
if ( traceon ) printf( "%s\n",
"Negative input" );
else
if ( traceon ) printf( "%s\n", "OK
input" );
if ( x < 0 ) {
ptrace( traceon, "Negative input" );
}
else {
ptrace( traceon, "OK input" );
}
But when you’re creating macros, you shouldn’t assume that the
person using the macro will follow similar guidelines.
Correcting this problem isn’t a simple matter of adding braces to
the macro definition because you would then have to not place a
semicolon after ptrace(…) when you used the macro — an
unacceptable exception to normal C syntax. Instead, drawing on
a suggestion by Andrew Koenig, you can restructure the macro
as an expression instead of a statement:
unsigned char c;
c = '\xff';
if ( c != '\xff' ) print( "Impossible!
\n" );
would it seem impossible to print “Impossible!”? Not with some
C compilers. The C standard lets compiler writers decide
whether the default char type means signed char or unsigned
char. The default sign of the char type affects how char values
are converted in mixed-type expressions. If the default is signed,
the compiler will convert the character constant '\xff' to a signed
integer by extending the high-order bit. (Oddly enough, C
defines character constants as int type.) Thus, '\xff' would have a
16-bit integer value of 0xffff. To evaluate c != '\xff', the
compiler will convert the explicitly declared unsigned character
c to the integer value 0x00ff, thus making it unequal to the value
of the character constant '\xff'.
if ( c != (unsigned) '\xff' )
The general rule is: Carefully cast any operation that involves a
char variable and any operand other than another char variable.
char c;
while ( ( c = getchar() ) != EOF ) ...
you’ll wait a long time before the loop ends. Because the value
of c will always be treated as an unsigned integer, it will never
equal -1. With a compiler that uses signed char as the default for
char variables, the loop may end before the last character is read,
since a character with a value that converts to an integer value of
-1 may be read from the input stream.
main(...) {
int arg1;
f( arg1 );
...
}
void f( int parm1 ) {
parm1 = 10;
return;
++ str;
Only the const keyword, used with array notation, specifies that
both the array address and it’s contents must be treated as read-
only within the function.
if ( x < o ) {
printf( "Invalid value.\n" );
exit;
}
c = getchar();
if ( errno != 0 ) {
/* handle error */
}
But this code may report false errors because most of C’s library
functions set the library-defined variable errno to a non-zero
value only when an error occurs.
errno = 0;
fileptr = fopen( ... );
if ( fileptr == NULL ) {
/* An error occurred in fopen()
| Now it's valid to examine errno
*/
if ( errno != 0 ) {
/* handle error */
}
}
The rule for using errno is: Set errno to 0 before a function call,
and use errno only after a function returns a value indicating the
function failed.
New Dimensions
int x[10][10];
y = x[ ++i, ++j ];
does not indicate that two subscripts are used in the reference to
array x. Instead, ++i, ++j is a comma-separated sequence of
expressions, and the expression value is the value of the last sub-
expression in the sequence (i.e., j, after j is incremented). C
doesn’t actually have true multi-dimensional arrays. Recall that
for the most part, C array notation is really a variation on pointer
notation, and that a[i] is equivalent to *(a+i). To get the effect of
a multi-dimensional array in C, you declare “arrays of arrays” (i.
e., two levels of pointer-based addressing). The notation a[i][j]
means *((*(a+i))+j). In the incorrect example above, the value of
x[ ++i, +j&43;# ] is the same as *(x+(++j)), which is an address,
not an integer. In C, always use one pair of [] for each level of
array subscripting.
r = x * y * z;
may be evaluated as
tmp = x * y;
r = tmp * z;
or as
tmp = y * z;
r = x * tmp;
Note that even parentheses will not guarantee the ordering, and
even (x * y) * z may be evaluated as
tmp = y * z;
r = x * tmp;
In many cases, it may not matter what the order of evaluation is,
but if it does, you should use separate statements to specify
order-dependent operations.
C Coding Suggestions
* Put parentheses (or other explicit delimiters) around the macro text
and around each macro argument within the macro text.
* Never pass an expression that has side effects as a macro argument.
* Place simple increment and decrement operations in separate
statements.
* Avoid evaluating an argument more than once, if possible.
* When defining a macro, be sure to consider all the contexts in
which the macro may be used.
* Carefully cast any operation that involves a char variable and any
operand other than another char variable.
* Always use int (not char) variables to store return values from
fgetc, getc, getchar, putc, putchar, and ungetc functions.
* Always declare a function prototype at the beginning of any file in
which you use the function.
* Define a header file with function prototypes for every file that has
global functions that may be referenced in other files.
* Declare formal parameters to functions as const, if they should not
be changed.
* Be sure you’ve coded parentheses after all function invocations.
* Set errno to 0 before a function call, and use errno only after a
function returns a value indicating the function failed.
* In C, always use one pair of [] for each level of array subscripting.
* Use separate statements to specify order-dependent operations.
* Avoid identifiers that differ only in the case (i.e., upper and lower)
of some letters.
* Tread carefully in C; stick to simple and well-understood
techiniques; and avoid “clever” programming.
Previous Table of Contents Next
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
Chapter 8
Working with C++
Right off the bat, C++ simplifies comments and avoids the
danger of the “runaway comments” I described in Chapter 3. If
you use // for comments everywhere but in macro definitions,
you won’t have to worry about where the comment ends — it’s
always at the end of the same source line. And look how clean
comments appear:
One place where you have to use a C++ “trick” instead of const
is when you want to declare a constant within the scope of a
class. The following syntax is illegal because you can’t assign an
initial value to a static class member:
class file_list {
static const int MAX_FILES = 10; //
Illegal!
char * file_name[ MAX_FILES ];
};
class file_list {
enum { MAX_FILES = 10 }; // Legal
char * file_name[ MAX_FILES ];
};
class classX {
static int objX_cnt; // Can't
initialize here!
};
...
int classX::objX_cnt = 0; // Initialize
here.
class classX {
inline int f( void );
};
inline int classX::f( void ) {
...
}
rather than
class classX {
int f( void ) {
...
}
};
C++ templates are another one of its true bright spots. They
provide a way to implement a generic piece of code that can
work on different data types. This eliminates many of the places
in C where a complex macro would be used instead of a
function, so that the code can work with more than one type.
Templates are also much simpler to use than some of the
advanced C++ techniques for writing functions that can handle
multiple types.
Macros have always been one of C’s most flexible tools, and I
pointed out in previous chapters how useful they are for paving
over some of C’s rough spots. The power of macros — and their
pitfalls — motivated some of the best new features of C++.
Where a better C++ alternative exists, use it instead of a macro.
On the other hand, you’ll still find some important uses of
macros in C++; for example, using PTR and contents_of macros
instead of *, as described in Chapter 6. You can benefit from
extending these C macros to cover new C++ features, such as
references. The following code uses a simple REF macro to
produce easily read code:
x + y = 7
Non-Plused
Now let’s turn to the darker side of C++. Suppose, in the spirit
of OOP, you decide to “advance” from standard C programming
techniques to C++ techniques. One of the first ways you might
try this new approach is by putting an object “wrapper” around
calls to system functions. The example I use (based on an
example in C++ Programming Style, by Tom Cargill) assumes
there are system functions to iterate over a list of output files in a
specified output queue. This example uses an OUTQ type that is
simply a system “handle” for an output queue control block
allocated by the system when open_outq is called. The example
also uses the following OUTQ_ENTRY structure, which
describes the layout of a static area filled by a call to the system-
supplied next_outq_entry function:
struct OUTQ_ENTRY {
char ofile_name[NAMESIZE];
// ... Rest of OUTQ_ENTRY fields
};
Figure 8.1 shows a fragment of the OutQ class definition and the
nextname member function. The following code shows how you
might declare two OutQ objects and then attempt to print the
first entry from both, side-by-side:
OutQ q1("PRINTER1");
OutQ q2("PRINTER2");
printf("%s\t%s\n", q1.nextname(),q2.
nextname());
LISTINGA LISTINGB
LISTINGA LISTINGA
OutQ q1("PRINTER1");
printf("%s\t%s\n", q1.nextname(),q1.
nextname());
Instead of printing the first two output files in q1, this code
prints the second output file twice. Although the HoldName
member avoids memory conflicts between separate objects, it
does not avoid memory conflicts between multiple invocations
of the same object’s state-changing member functions. The
solution to this problem is much more complex, requiring either
some form of dynamic memory management by the OutQ class,
or special coding techniques for multiple references to the same
OutQ object. As this example illustrates, object-oriented
programming in C++ is no simple panacea to all your
programming ills. It’s often the case that half-complete class
definitions introduce sneaky traps for the unwary. And creating
robust classes for what appear at first to be simple objects is
often much more difficult than you first imagine.
String a("abc");
String b("");
...
b=a;
String a("abc");
String & b = a; // a and b refer to
// the same string
b=a;
Because both a and b refer to the same object, the release of b’s
memory actually releases a’s memory as well—before the copy
takes place—and the assignment fails. Note that a similar
problem could occur in a plain C (or other language) function
that didn’t guard against modifying an output argument that
might also be an input argument. But common goals of creating
C++ classes are to simplify assignment and expressions for new
object types and to “hide” memory management. Thus, you’ll
encounter more occasions where you have to watch out for
unexpected ways that member functions, including overloaded
operators, can get you in trouble.
The previous examples are by no means the only way you can
get yourself in trouble with C++’s OOP facilities. In fact, I chose
some of the simpler cases to fit within the scope of this book. In
Further Reading, I describe several excellent books that delve
into the topic more deeply. The main point I want to emphasize
is that C++ objects can either simplify or complicate
programming, depending on how thoughtfully their classes are
implemented. C++ objects are supposed to make working with
complex program structures as straightforward as working with
C’s built-in types. Keeping that in mind, I recommend that you
avoid using C++ classes unless they’ve been implemented in a
way that makes them safe and simple to use. A program is
weakened, not strengthened, by using classes that produce
surprising results under some conditions.
C Coding Suggestions
class OutQ {
OUTQ * hOutQ; // Handle for output queue
public:
OutQ(char * outqname) {// Constructor
hOutQ = open_outq(outqname);
}
const char * nextname() {
OUTQ_ENTRY * tmp_qentry = next_outq_entry
(hOutQ);
return (tmp_qentry ? tmp_qentry-
>ofile_name : NULL);
}
};
class OutQ {
OUTQ * hOutQ; // Handle for output queue
char HoldName[NAMESIZE];
public:
OutQ(char * outqname) { // Constructor
hOutQ = open_outq(outqname);
}
const char * nextname() {
OUTQ_ENTRY * tmp_qentry = next_outq_entry
(hOutQ);
if (! tmp_qentry ) return NULL;
strcpy(HoldName, tmp_qentry->ofile_name);
return HoldName;
}
};
class String {
char * strdata;
int strlength;
public:
String(char * cs); // Constructor using
standard
String& operator=(const String& ss); //
Assignment
};
c
har * MakeString(const char * cs) {
char * tmpstr = new char[strlen(cs) + 1];
strcpy(tmpstr, cs);
return tmpstr;
}
String::String(char * cs) {
strdata = cs ? MakeString(cs) : NULL;
strlength = cs ? strlen(cs) : 0;
}
String& String::operator=(const String&
ss) {
delete [] strdata;
strdata = ss.strdata ? MakeString(ss.
strdata) : NULL;
strlength = ss.strdata ? ss.
strlength : 0;
return * this;
}
Chapter 9
Managing C and C++ Development
Using C++ does not lessen the need for discipline. C++ retains
almost all of C’s low-level operations and adds several
additional layers of language features. Although classes,
inheritance, templates, and other C++ language features allow a
programmer to work at a higher level of abstraction, these
features can also increase the complexity of writing programs, if
they’re not used in a well-thought-out and highly consistent
manner.
By now it may sound like the only way to work with C or C++ is
to keep a cane switch handy to whip programmers into
submission. That’s not the case. The “discipline” I’m speaking
of is more akin to the discipline good athletes demonstrate in the
preparation and execution of their sport. Like successful athletes,
top-flight programmers, project leaders, and managers have
learned that although discipline takes effort, it brings satisfaction
as well. Discipline can free developers from the crush of
problems caused by little, unavoidable mistakes and can make
possible the use of more powerful software development
techniques.
Follow each ( and [ with a space, and precede each ) and ] with a
space.
The first approach is risky and would only make sense if it were
combined with some other guidelines, such as always checking a
program with a compiler or “lint” utility that could detect the use
of = where == was expected.
With a good editor, it’s so easy to manipulate text that it’s not a
burden to pay careful attention to programming standards,
especially ones that deal with lexical style. By contrast, “dumb
terminal” and line-oriented editors, or editors without mouse and
window support or an extension language, are poor choices to
encourage programmers to “polish” code.
No Train, No Gain
C and C++ aren’t for amateurs. They are powerful and can be
dangerous. Programmers should have a solid background in
programming methods and some experience writing “throw-
away” programs in C or C++ before attempting their first
mission-critical applications. Don’t learn C by writing a
production program!
The C and C++ world is awash with slick debuggers. But before
you place a debugger high on your list of C/C++ tools, consider
this fact: Every minute spent using the debugger is a waste of
time — nothing is being produced. The place to control errors in
C programs is during production, not by debugging.
Reuse It Or Lose It
Principles Of Reuse
And most of all, it’s a matter of attitude. Doing it the same old
way won’t work for programming in C or managing C
development. A successful C programmer will take extra steps
to guard against language traps or dangerous programming
practices. Likewise, a successful manager will be sure that his or
her programmers follow safe, standardized C programming
practices. With the right attitude and a good set of programming
guidelines, you can have “calm Cs ahead.”
Table of Contents
BIBLIOGRAPHY
Books
Having read other SAA manuals, I was surprised that the SAA C
reference actually shed some light on several C pitfalls. But
don't look to it as your primary source for learning C, or even as
the first place to turn for an "official" reference. In too many
cases, the information is scattered and inconsistent. For example,
"linkage" is discussed sometimes as an attribute of a variable
(which it is) and at other times as a relationship between two
identifiers (which, although it makes more sense for the term
"linkage," isn't the way C defines it).
Table of Contents
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
Table of Contents
Chapter 2
Chapter 3
Chapter 4
* Declare C arrays with one extra element, and don't use the element
with subscript 0.
* Use macros to define tables and loops over them.
* Always guard a string assignment against overwriting the target
variable.
* Create macros and functions to define strings and provide "safe"
string operations.
Chapter 5
* Declare functions static if you intend to call them only from within
the same source file.
* Use the most restricted visibility possible for variables; avoid
shared variables.
* Put all external declarations before the first function definition in a
file.
* Put functions that must share data and external declarations for
their shared variables in a file by themselves.
* Use EXPORT, SHARE, and IMPORT macros to clarifythe
intended visibility of a variable.
Chapter 6
Table of Contents
Common Sense C - Advice & Warnings for C and C++
Programmers
(Publisher: 29th Street Press)
Author(s): Paul Conte
ISBN: 1882419006
Publication Date: 10/01/92
Table of Contents
#if...#endif facilities, 61
* dereferencing operator, 39
omissions of, 39-40
reducing use of, 47
++ and -- operators, 50
array subscripts and, 59
in assignments, 56
side effects of, 59
Ada language, 1, 2
comments and, 18
Addresses, 39-40
argument, 10-11
changing, in pointer variable, 41-42
allocate macro, 46
code for, 47
AND operator, 8
Arguments. See specific types of arguments
Array notation, 41-42, 63
compared to pointer/dereferencing notation, 42
multidimensional arrays and, 65
Arrays, 21
declaring, 22
multidimensional, 65
rules for using, 21-23
starting, 21
See also Array notation
Array subscripts, 54
++/-- operations within, 59
using [] and, 65
Assignments, 7
++/-- operators in, 56
functions, implementing, 76
in if statement, 8
levels of indirection in, 43
of one string variable to another, 24
pointers and, 43
as separate statements, 10, 14
within return statement, 13
Awk language, 84
Braces, 15-16
conditional statements and, 14, 15, 60
in macro definitions, 60-61
C++ language, 1, 4
advantages, 71-73
classes, 73-76, 77
coding suggestions, 78
comments, 69-70
compiler, 85
const variables and, 70
defined, 69
development, 79-88
disadvantages, 73-77
explained, 4
in-line functions, 71
macros and, 72
objects, 74, 77
output parameters and, 2
parenthesis in, 73
standards, 80-84
"streams" I/O package, 72
strings and, 25, 27
templates, 71-72
training in, 84
working with, 69-78
See also C language
C++ Programming Style, 73
Case-sensitivity, 66
cat function, 51-52
code for, 51
defined, 51
successful call of, 52
unsuccessful call of, 52
See also Functions
char variable, 61
coding suggestions, 67
default, type, 61
rule for using, 62
signed, 61
unsigned, 61
See also Variable declarations
C language
alternative to other languages, 1
assembly language and, 2-3
basic problem with, 1-3
condensed coding style and, 3
conquering, 4-5
development, 79-88
machine-level programming and, 10
programming attitude of, 5
programming with, 3
sequence of operations, 3
source/executable libraries, 5
speed and, 54-56
standards, 80-84
training in, 84
uses for, 1
See also C++ language
Classes, 73-76, 77
creating, 74, 80
definitions of, 74
OutQ, 73-75
reusing, 80
string, 75-76
C library functions, 62
errno variable, 64-65
See also Functions
Code analyzers, 85
CodeCheck, 85
Code reuse, 87
Coding
alternative, styles, 83
changing standards and, 83-84
discipline, 80
loops, 21
quick, 14
Coding suggestions, 12
array and string, 27
C++, 78
char variable, 67
macro, 67
pointers, 53
syntax, 19
variable declaration, 37
See also Coding
Comments
C++ and, 69-70
delimiting, 18
macro definitions and, 69-70
runaway, 18
sample code for, 18
syntax for, 17-20
Components, reusable, 87
principles of, 87-88
Constants
const variables and, 70
integer, 70
octal, 10
const variables, 70
See also Variable declarations
C pointers, 1
at application level, 2
See also Pointers
D
Dangling pointers, 44
creating, 44
See also Pointers
Debuggers, 85
Declarations, 29
examples of, 29-30
external, 31-32
between functions, 33-34
making, 35
primary rule for, 33
See also Variable declarations; Visibility
Dereferenced pointer parameters, 40
local variables and, 41
notation, 42
post-increments and, 50
when to use, 40-41
Eiffel language, 12
else clauses, 15-16
errno variable, 64
using, 65
See also Variable declarations
Errors, typo, 13
Export variables, 33
defining, 33
See also Variable declarations
extern keyword, 30, 32
Function arguments, 58
declaring, as const, 63
passing, 62-63
Function parameters, 2
Function prototypes, 62
for C library functions, 62
header files and, 62
Functions
addresses for, 10-11
assignment, implementing, 76
cat, 51-52
C library, 62
errno variable, 64-65
coupled, 34
declaring, 35
static, 30
EXPORT, 35
free(), 72
in-line, 71
malloc(), 72
member, 71
revised, 77
month_name, 46
new_string, 45
outq_entry, 73, 74
parenthesis in, 64
returning two values, 40
scanf, address for, 10-11
SHARE, 35
strcpymax, 58
strupr, 54, 55
system, 73
upper_case, 54-55
visibility classes and, 33
Header files, 62
High-level language (HLL), 40
Identifiers, 66
global, 66
if statements, 7
assignment in, 8
else clause matching and, 15-16
expressions within parenthesis, 9
logical expressions in, 9
non-zero value of, 9
In/out parameters, 41
local variables with, 42
Integers, 61-62
constants, 70
decimal, 10
unsigned, 62
int variable, 62
See also Variable declarations
Linkage, 30
internal, 32
Lint filters, 7-8
Local variables, 33
dereferenced parameters and, 41
with in/out parameters, 42
using, 33
when to use, 40-41
See also Variable declarations
Logical expressions, 9
Loops, 21
errors and, 22
for, 22
while, 51-52
Macro arguments, 59
evaluating, 60
Macros
address_of, 48
using, 49
allocate, 46, 47
and/AND, 8
C++ and, 72
conditional logic and, 60
contents_of, 48
C++ and, 72
using, 49, 50
cpystr, 25
cube, 57-58
defining, 60
definitions of, 57-58
braces and, 60-61
double, 57-58
improving, 60
ELSE, 16
ELSEIF, 16
coding with, 17
ENDIF, 16
EQ, 8
EXPORT, 35-36
GLOBAL, 37
IF, 16
IMPORT, 34, 36
or/OR, 8
OVER_TABLE, 23
parenthesis in, 58
pitfalls of, 57-61
avoiding, 58
pointer-related, 48
PTR, 48
C++ and, 72
using, 49
ptrace, 61
REF, 72
for safe strings, 25
using, 26
SEMIGLOBAL, 37
SHARE, 34, 35
size of operator and, 24-25
standard, 81
STRING, 25
STRING_TABLE, 25
strmaxlen, 25
TABLE, 22
table definition, 22-23
table loop, 23
THEN, 16
malloc operation, 46
Memory allocation, 46
Memory leakage, 46
avoiding, 48
reasons for, 47
Memory management, automated, 75-76
Mistakes, common, 7-12
if statements and, 7-8
month_name function, 46
Nested blocks, 33
new_string function, 45
NULL pointers, 43-44
allocate and, 46
alternatives to, 43-44
checking for, 44
reallocate with, 46
See also Pointers
Object data, 74
Object-oriented programming (OOP), 4
applying, techniques, 75
concepts, 4
features, 69
learning, facilities, 84
Operations
char variable and, 62
decrement, 59
embedding, 59
increment, 59
order-dependent, 65
post increment, 3
sequence of, 3, 65-66
Operators
(-), 76
bitwise, 8
delete, 72
logical, 8, 9
new, 72
OR, 8
overloading, 76
precedence levels, 10
sizeof, 24
OutQ class, 73-74
definition of, 74
revised, 75
See also Classes
Parenthesis
after function invocations, 64
C++ and, 73
if statements and, 9
in macros, 58
in order-dependent operations, 65-66
return statement expressions and, 14
using, 10
Pascal language, 1, 2
as foundation to C, 84
Pointers, 1, 39-56
* operator and, 39
address, 2
at application level, 2
array notation and, 41
in assignment statements, 43
changing addresses in, 41-42
dangling, 44
dereferenced, parameters, 40
handle, 2
initializing, in their definitions, 46-47
macros related to, 48
notation, 42, 63
null, 43-44
ways of implementing, 2
Pointer variables
automatic, 47
static, 47
See also Pointers; Variable declarations
"Pretty printers," 85
Programming editors, 83-84, 85
PVCS Version Manager, 86
R
References, 40, 63
C++, 72
return statements
coding, 15
parenthesis and, 14
Return values
after malloc, 46
negative/zero, 9-10
Reuse, coding, 87
principles of, 87-88
Sample code
array of part numbers, 11
for comments, 18
See also Coding; Coding suggestions
Scope, 30
defined, 30
Semicolon, missing, 13-14
Share variables, 33
defining, 33
using, 33
See also Variable declarations
Signals, 66
Software configuration management (SCM), 86
tools, 86
Source-code checkers, 7-8
Standards, programming, 80-84
approaching, 81
company, 80-81
discipline in, 80
evolution of, 83-84
getting started with, 81-83
indenting example, 81
instituting, 82-83
for lexical style, 80
suggested list of, 81-82
symbol use and, 82-83
Statements
conditional
braces and, 14, 15, 60
curly brackets around, 14
subordinate, adding/deleting, 15-16
syntax for, 13-20
See also specific statements
static keyword, 30, 32
initialization and, 32
Streams, 72
Strings, 24
assignments, 24
classes, 75-76
copy function, 26
guarding, copy, 24
macros for, 25
using, 26
objects, 76
safe, 25
type, 45
variable-length, 24
switch statement, 16-17
rule for using, 17
Values, uninitialized, 11
See also Return values
Variable declarations, 29-37
case-sensitivity and, 66
EXPORT, 36, 37
IMPORT, 36, 37
making, 35
nested blocks and, 33
SHARE, 36, 37
visibility classes and, 33
See also Declarations; Visibility
Visibility, 30
attributes of, 30
classes of, 33
concept of, 30
of C variables, 31
export, 33
function rules, 30
local, 33
share, 33
uses of, 30
variable, 30
See also Declarations
Table of Contents