Файл: Recommended C Style and Coding Standards (L.W. Cannon, 1990).pdf

ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 15.06.2025

Просмотров: 395

Скачиваний: 2

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

- 13 -

Avoid names that might conflict with various standard library names. Some systems will include more library code than you want. Also, your program may be extended someday.

12. Constants

Numerical constants should not be coded directly. The #define feature of the C preprocessor should be used to give constants meaningful names. Symbolic constants make the code easier to read. Defining the value in one place also makes it easier to administer large programs since the constant value can be changed uniformly by changing only the define. The enumeration data type is a better way to declare variables that take on only a discrete set of values, since additional type checking is often available. At the very least, any directly-coded numerical constant must have a comment explaining the derivation of the value.

Constants should be defined consistently with their use; e.g. use 540.0 for a float instead of 540 with an implicit float cast. There are some cases where the constants 0 and 1 may appear as themselves instead of as defines. For example if a for loop indexes through an array, then

for (i = 0; i < ARYBOUND; i++)

is reasonable while the code

door_t *front_door = opens(door[i], 7); if (front_door = = 0)

error("can’t open %s\n", door[i]);

is not. In the last example front_door is a pointer. When a value is a pointer it should be compared to NULL instead of 0. NULL is available either as part of the standard I/O library’s header file stdio.h or in stdlib.h for newer systems. Even simple values like 1 or 0 are often better expressed using defines like TRUE and FALSE (sometimes YES and NO read better).

Simple character constants should be defined as character literals rather than numbers. Non-text characters are discouraged as non-portable. If non-text characters are necessary, particularly if they are used in strings, they should be written using a escape character of three octal digits rather than one (e.g., ’\007’). Even so, such usage should be considered machine-dependent and treated as such.

13. Macros

Complex expressions can be used as macro parameters, and operator-precedence problems can arise unless all occurrences of parameters have parentheses around them. There is little that can be done about the problems caused by side effects in parameters except to avoid side effects in expressions (a good idea anyway) and, when possible, to write macros that evaluate their parameters exactly once. There are times when it is impossible to write macros that act exactly like functions.

Some macros also exist as functions (e.g., getc and fgetc). The macro should be used in implementing the function so that changes to the macro will be automatically reflected in the function. Care is needed when interchanging macros and functions since function parameters are passed by value, while macro parameters are passed by name substitution. Carefree use of macros requires that they be declared carefully.

Macros should avoid using globals, since the global name may be hidden by a local declaration. Macros that change named parameters (rather than the storage they point at) or may be used as the lefthand side of an assignment should mention this in their comments. Macros that take no parameters but reference variables, are long, or are aliases for function calls should be given an empty parameter list, e.g.,

#define OFF_A( ) (a_global+OFFSET) #define BORK( ) (zork( ))

#define SP3( ) if (b) { int x; av = f (&x); bv += x; }

Macros save function call/return overhead, but when a macro gets long, the effect of the call/return becomes negligible, so a function should be used instead.

Recommended C Coding Standards

Revision: 6.0

25 June 1990


- 14 -

In some cases it is appropriate to make the compiler insure that a macro is terminated with a semi-

colon.

if (x==3) SP3( );

else

BORK( );

If the semicolon is omitted after the call to SP3, then the else will (silently!) become associated with the if in the SP3 macro. With the semicolon, the else doesn’t match any if! The macro SP3 can be written safely as

#define SP3( ) \

do { if (b) { int x; av = f (&x); bv += x; }} while (0)

Writing out the enclosing do-while by hand is awkward and some compilers and tools may complain that there is a constant in the ‘‘while’’ conditional. A macro for declaring statements may make programming easier.

#ifdef lint

static int ZERO;

#else

#

define ZERO 0

#endif

#define STMT( stuff )

do { stuff } while (ZERO)

Declare SP3 with

#define SP3( ) \

STMT( if (b) { int x; av = f (&x); bv += x; } )

Using STMT will help prevent small typos from silently changing programs.

Except for type casts, sizeof, and hacks such as the above, macros should contain keywords only if the entire macro is surrounded by braces.

14. Conditional Compilation.

Conditional compilation is useful for things like machine-dependencies, debugging, and for setting certain options at compile-time. Beware of conditional compilation. Various controls can easily combine in unforeseen ways. If you #ifdef machine dependencies, make sure that when no machine is specified, the result is an error, not a default machine. (Use ‘‘#error’’ and indent it so it works with older compilers.) If you #ifdef optimizations, the default should be the unoptimized code rather than an uncompilable program. Be sure to test the unoptimized code.

Note that the text inside of an #ifdeffed section may be scanned (processed) by the compiler, even if the #ifdef is false. Thus, even if the #ifdeffed part of the file never gets compiled (e.g., #ifdef COMMENT), it cannot be arbitrary text.

Put #ifdefs in header files instead of source files when possible. Use the #ifdefs to define macros that can be used uniformly in the code. For instance, a header file for checking memory allocation might look like (omitting definitions for REALLOC and FREE):

#ifdef DEBUG

extern void *mm_malloc();

#define MALLOC(size) (mm_malloc(size)) #else

extern void *malloc();

#define MALLOC(size) (malloc(size)) #endif

Recommended C Coding Standards

Revision: 6.0

25 June 1990


- 15 -

Conditional compilation should generally be on a feature-by-feature basis. Machine or operating system dependencies should be avoided in most cases.

#ifdef BSD4

long t = time ((long *)NULL); #endif

The preceding code is poor for two reasons: there may be 4BSD systems for which there is a better choice, and there may be non-4BSD systems for which the above is the best code. Instead, use define symbols such as TIME_LONG and TIME_STRUCT and define the appropriate one in a configuration file such as config.h.

15. Debugging

‘‘C Code. C code run. Run, code, run... PLEASE!!!’’ — Barbara Tongue

If you use enums, the first enum constant should have a non-zero value, or the first constant should indicate an error.

enum { STATE_ERR, STATE_START, STATE_NORMAL, STATE_END } state_t; enum { VAL_NEW=1, VAL_NORMAL, VAL_DYING, VAL_DEAD } value_t;

Uninitialized values will then often ‘‘catch themselves’’.

Check for error return values, even from functions that ‘‘can’t’’ fail. Consider that close( ) and fclose( ) can and do fail, even when all prior file operations have succeeded. Write your own functions so that they test for errors and return error values or abort the program in a well-defined way. Include a lot of debugging and error-checking code and leave most of it in the finished product. Check even for ‘‘impossible’’ errors. [8]

Use the assert facility to insist that each function is being passed well-defined values, and that intermediate results are well-formed.

Build in the debug code using as few #ifdefs as possible. For instance, if ‘‘mm_malloc’’ is a debugging memory allocator, then MALLOC will select the appropriate allocator, avoids littering the code with #ifdefs, and makes clear the difference between allocation calls being debugged and extra memory that is allocated only during debugging.

#ifdef DEBUG

#define MALLOC(size) (mm_malloc(size)) #else

#define MALLOC(size) (malloc(size)) #endif

Check bounds even on things that ‘‘can’t’’ overflow. A function that writes on to variable-sized storage should take an argument maxsize that is the size of the destination. If there are times when the size of the destination is unknown, some ‘magic’ value of maxsize should mean ‘‘no bounds checks’’. When bound checks fail, make sure that the function does something useful such as abort or return an error status.

Recommended C Coding Standards

Revision: 6.0

25 June 1990

- 16 -

/*

*INPUT: A null-terminated source string ‘src’ to copy from and

*a ‘dest’ string to copy to. ‘maxsize’ is the size of ‘dest’

*or UINT_MAX if the size is not known. ‘src’ and ‘dest’ must

*both be shorter than UINT_MAX, and ‘src’ must be no longer than

*‘dest’.

*OUTPUT: The address of ‘dest’ or NULL if the copy fails.

*‘dest’ is modified even when the copy fails.

*/

char *

copy (dest, maxsize, src) char *dest, *src; unsigned maxsize;

{

char *dp = dest;

while (maxsize− − > 0)

if ((*dp++ = *src++) = = ’\0’) return (dest);

return (NULL);

}

In all, remember that a program that produces wrong answers twice as fast is infinitely slower. The same is true of programs that crash occasionally or clobber valid data.

16. Portability

‘‘C combines the power of assembler with the portability of assembler.’’

— Anonymous, alluding to Bill Thacker.

The advantages of portable code are well known. This section gives some guidelines for writing portable code. Here, ‘‘portable’’ means that a source file can be compiled and executed on different machines with the only change being the inclusion of possibly different header files and the use of different compiler flags. The header files will contain #defines and typedefs that may vary from machine to machine. In general, a new ‘‘machine’’ is different hardware, a different operating system, a different compiler, or any combination of these. Reference [1] contains useful information on both style and portability. The following is a list of pitfalls to be avoided and recommendations to be considered when designing portable code:

gWrite portable code first, worry about detail optimizations only on machines where they prove necessary. Optimized code is often obscure. Optimizations for one machine may produce worse code on another. Document performance hacks and localize them as much as possible. Documentation should explain how it works and why it was needed (e.g., ‘‘loop executes 6 zillion times’’).

gRecognize that some things are inherently non-portable. Examples are code to deal with particular hardware registers such as the program status word, and code that is designed to support a particular piece of hardware, such as an assembler or I/O driver. Even in these cases there are many routines and data organizations that can be made machine independent.

gOrganize source files so that the machine-independent code and the machine-dependent code are in separate files. Then if the program is to be moved to a new machine, it is a much easier task to determine what needs to be changed. Comment the machine dependence in the headers of the appropriate files.

gAny behavior that is described as ‘‘implementation defined’’ should be treated as a machine (compiler) dependency. Assume that the compiler or hardware does it some completely screwy way.

Recommended C Coding Standards

Revision: 6.0

25 June 1990


- 17 -

gPay attention to word sizes. Objects may be non-intuitive sizes, Pointers are not always the same size as ints, the same size as each other, or freely interconvertible. The following table shows bit sizes for basic types in C for various machines and compilers.

type

pdp11

VAX/11

68000

Cray-2

Unisys

Harris

80386

iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiser es fam ly 1100 H800

char

8

8

8

8

9

8

8

short

16

16

8/16

64(32)

18

24

8/16

int

16

32

16/32

64(32)

36

24

16/32

long

32

32

32

64

36

48

32

char*

16

32

32

64

72

24

16/32/48

int*

16

32

32

64(24)

72

24

16/32/48

int(*)( )

16

32

32

64

576

24

16/32/48

Some machines have more than one possible size for a given type. The size you get can depend both on the compiler and on various compile-time flags. The following table shows ‘‘safe’’ type sizes on the majority of systems. Unsigned numbers are the same bit size as signed numbers.

Type Minimum No Smaller iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii# Bits Than

char

8

short

16

char

int

16

short

long

32

int

float

24

double

38

float

any *

14

char *

15

any *

void *

15

any *

gThe void* type is guaranteed to have enough bits of precision to hold a pointer to any data object. The void(*)( ) type is guaranteed to be able to hold a pointer to any function. Use these types when you need a generic pointer. (Use char* and char(*)( ), respectively, in older compilers). Be sure to cast pointers back to the correct type before using them.

gEven when, say, an int* and a char* are the same size, they may have different formats. For example, the following will fail on some machines that have sizeof(int*) equal to sizeof(char*). The code fails because free expects a char* and gets passed an int*.

int *p = (int *) malloc (sizeof(int)); free (p);

gNote that the size of an object does not guarantee the precision of that object. The Cray-2 may use 64 bits to store an int, but a long cast into an int and back to a long may be truncated to 32 bits.

gThe integer constant zero may be cast to any pointer type. The resulting pointer is called a null pointer for that type, and is different from any other pointer of that type. A null pointer always compares equal to the constant zero. A null pointer might not compare equal with a variable that has the value zero. Null pointers are not always stored with all bits zero. Null pointers for two different types are sometimes different. A null pointer of one type cast in to a pointer of another type will be cast in to the null pointer for that second type.

gOn ANSI compilers, when two pointers of the same type access the same storage, they will compare as equal. When non-zero integer constants are cast to pointer types, they may become identical to other pointers. On non-ANSI compilers, pointers that access the same storage may compare as dif-

ferent. The following two pointers, for instance, may or may not compare equal, and they may or may not access the same storage6.

hhhhhhhhhhhhhhhhhh

6.The code may also fail to compile, fault on pointer creation, fault on pointer comparison, or fault on pointer dereferences.

Recommended C Coding Standards

Revision: 6.0

25 June 1990


- 18 -

((int *) 2 ) ((int *) 3 )

If you need ‘magic’ pointers other than NULL, either allocate some storage or treat the pointer as a machine dependence.

extern int x_int_dummy;

/* in x.c */

#define X_FAIL

(NULL)

#define X_BUSY

(&x_int_dummy)

#define X_FAIL

(NULL)

#define X_BUSY

MD_PTR1

/* MD_PTR1 from "machdep.h" */

gFloating-point numbers have both a precision and a range. These are independent of the size of the object. Thus, overflow (underflow) for a 32-bit floating-point number will happen at different values on different machines. Also, 4.9 times 5.1 will yield two different numbers on two different machines. Differences in rounding and truncation can give surprisingly different answers.

gOn some machines, a double may have less range or precision than a float.

gOn some machines the first half of a double may be a float with similar value. Do not depend on this.

gWatch out for signed characters. On some VAXes, for instance, characters are sign extended when used in expressions, which is not the case on many other machines. Code that assumes signed/unsigned is unportable. For example, array[c] won’t work if c is supposed to be positive and is instead signed and negative. If you must assume signed or unsigned characters, comment them as SIGNED or UNSIGNED. Unsigned behavior can be guaranteed with unsigned char.

gAvoid assuming ASCII. If you must assume, document and localize. Remember that characters may hold (much) more than 8 bits.

gCode that takes advantage of the two’s complement representation of numbers on most machines should not be used. Optimizations that replace arithmetic operations with equivalent shifting operations are particularly suspect. If absolutely necessary, machine-dependent code should be #ifdeffed or operations should be performed by #ifdeffed macros. You should weigh the time savings with the potential for obscure and difficult bugs when your code is moved.

gIn general, if the word size or value range is important, typedef ‘‘sized’’ types. Large programs should have a central header file which supplies typedefs for commonly-used width-sensitive types, to make it easier to change them and to aid in finding width-sensitive code. Unsigned types other than unsigned int are highly compiler-dependent. If a simple loop counter is being used where either 16 or 32 bits will do, then use int, since it will get the most efficient (natural) unit for the current machine.

gData alignment is also important. For instance, on various machines a 4-byte integer may start at any address, start only at an even address, or start only at a multiple-of-four address. Thus, a particular structure may have its elements at different offsets on different machines, even when given elements are the same size on all machines. Indeed, a structure of a 32-bit pointer and an 8-bit character may be 3 sizes on 3 different machines. As a corollary, pointers to objects may not be interchanged freely; saving an integer through a pointer to 4 bytes starting at an odd address will sometimes work, sometimes cause a core dump, and sometimes fail silently (clobbering other data in the process). Pointer-to-character is a particular trouble spot on machines which do not address to the byte. Alignment considerations and loader peculiarities make it very rash to assume that two consecutivelydeclared variables are together in memory, or that a variable of one type is aligned appropriately to be used as another type.

gThe bytes of a word are of increasing significance with increasing address on machines such as the VAX (little-endian) and of decreasing significance with increasing address on other machines such as the 68000 (big-endian). The order of bytes in a word and of words in larger objects (say, a double word) might not be the same. Hence any code that depends on the left-right orientation of bits in an object deserves special scrutiny. Bit fields within structure members will only be portable so long as

Recommended C Coding Standards

Revision: 6.0

25 June 1990