CSPP 51081
Unix Systems Programming
C Style Sheet

Grading on Function and Design

Your programming assignments will be graded on function and design. Your program must meet the functional specifications given (in part) by the assignment; this establishes the correct functioning of your program. But your program must not only be read by the computer, it must be read by humans as well, so your program must also be well-designed using a clear and consistent style. When you write your program keep in mind the following readers:

The evaluation of your program's function counts 70% of the assignment grade, the evaluation of your programming style counts 30%. This note explains each of these components.

Functionality

A common mistake that people make when trying to design something completely foolproof is to underestimate the ingenuity of fools.
--Douglas Adams

Each program has a functional specification which specifies

The specifications largely govern how the program is expected to behave under normal conditions. But the program may also be used under extreme conditions when things are not normal. This may be due to mistakes on the part of the user of the program or due to system failures. The functional specification given to you may leave you the task of handling the myriad of possible mistakes in an appropriate manner. Your program should be designed with this in mind: try to minimize the mistakes a user could make and make sure that your program handles errors gracefully. You are responsible for your program's behavior under any condition the user may put it.

Your program will be judged on correct functionality as given by the functional specification for the program. You will be expected to submit a typescript file which shows that your program runs according to the functional specifications for the program. This is meant as an authentication by you that your program works as expected. We will verify that your program works as expected; but we will also run your program in ways that are unexpected, to see how your program responds. Here are some general guidelines that will be used in evaluating your program.

  1. You are responsible for your program's behavior under all conditions, not just those given by the functional specification. Try to expect the unexpected. This requires that you think beyond the specification given to you, to what a user might do with your program. This to be an ongoing process. It is hard to predict every use to which your program will be put. Grading is one of the mechanisms to expose incorrect behavior by your program. Every program you submit you will have a chance to correct or redesign.
  2. Your program must meet the expectations established by the specification. The expectations establish what is to be expected under normal conditions, and may include what is to be expected under some of the unusual conditions that a program may be subjected. You will provide a typescript file documenting that you have tested your program under the normal and abnormal conditions provided by the specification and your program meets these expectations. If you are unable to satisfy the expectations then you should state this in the DOC file under the heading of BUGS.
  3. Your program must compile on a machine in the CSPP cluster. You will be informed on which machine your program will be tested. It must compile and run on this machine. Most of the programs for this class will compile and run correctly on any Unix/Linux architecture.
  4. Your program must compile without warnings when compiled with gcc -WALL. Compiler warnings will be taken to be a sign of poorly designed code which is vulnerable to bugs: disfunctional behavior by your program as determined by the functional specification.
  5. Your code will be evaluated on the efficient use of system resources in its design. There are several components here: Develope the good habit of respecting limited system resources.
  6. Document any deviations or design decisions you include over and above the functional specifications given to you. This will ensure you recieve proper credit.
  7. Test every function and system call in your program for error. Do not assume the system is working normally. For example, do not assume that every file you try to open with open actually opens a file. Check for errors on every system call. All errors must be sent to stderr (for file streams FILE *) or STDERR_FILENO (file descriptor 3, for low-level I/O.)
  8. Your program should be robust against human variation. Here are some examples of things where your program should attempt to be flexible:
  9. Be consistent with similar usage already established. This will minimize human error. My favorite example comes from the C library: gets replaces a terminating \n with a null character, fgets leaves the newline alone.
  10. Place sensible bounds on buffers or design your code without bounds. For example, an operating system places a bound on the length of filenames. This is given by the constant NAME_MAX defined in limits.h, and is at least 255 bytes. Use this constant to size a buffer to hold a file name, do not make-up a number on your own. This will help prevent errors when your buffer is too small and waste when it is too large.
  11. Your program should handle errors gracefully. This involves two components

Style

Thirty percent of your grade rests on you coding style. Coding style has three components Keep in mind that certain files are intended to be publicly accessible: your DOC file giving the functional specification, and your header file with publicly accessible functions, constants and variables. Rules of style will be more strictly enforced in these files, especially documentation. You have more flexibility in the style you choose for source files since these files contain many objects which are only locally accessible. You may find it appropriate to relax standards of documentation.

Modularity

Rule of Modularity: Write simple parts connected by clean interfaces.

Modularity is a way to organize programs. A module is a collection of functions (services) available to other parts of the program (the clients.) Each module consists of two parts

Dividing a program into modules consisting of related functions has the following advantages: When designing your program pay careful attention to the modularity of your program

In C modules are implemented by creating header files and source files and enforced by the use of storage classes. The use of distinct header and source files are mainly for the human reader. The header file ends in .h (although this is not necessary) and contains the publicly accessible objects (functions, variables, constants, types) and documentation on the proper use and effects to be expected. So, the header file is an essential part of a module's interface with the human user. The source file ends in .c (this is necessary) and contains the code implementing the public services in the header file as well as services which may only be known and used within the source file.

Dividing a module into header and source file does not enforce the distinction between public services usable by other parts of the program and private services which are to be restricted to the source file in which they are defined. If you define a variable at the top level of a file, that variable can be accessed by any other file; function definitions are, by default, accessible by any other file. For this reason, C uses storage class specifiers for functions and variables to enforce the extent to which these objects can be used. A storage class has three compenents

  1. Duration: the period of time storage is allocated
  2. Scope: the region of C program text over which a declaration is visible. (Scope is limited to some part of the C source file in which the variable occurs.)
  3. Linkage : where the variable is defined. The definition determines the initial value and where this value is stored. (A variable can be linked to a definition in another C file.)
C has several storage class specifiers, but for modularity the two key ones are static and extern. Suppose you define a variable at the top level of your program, int size=10;. Did you intend the value of size to be accessible outside the file? If so it is a public variable, otherwise it is a private variable. Similarly, when you define a function static int inc_size() in a file do you intend the function to be used by other files, in which case it is a public function; or is the function to be used only within the file, in which case it is a private function.

If you intend for the variable size defined at the top level of the file to be accessed only within the file then use the static modifier to ensure it is kept private to the file. For example, the definition static int size=10; at the top level of your file will prevent any other file from referencing the value defined by size. The scope of this variable will still be the entire file, but it cannot be referenced by another file. Similarly, if you intend the function inc_size() to be used only within the file in which it is defined then it should be declared with the static modifier. For example, the prototye static int inc_size(); specifies that the function inc_size() can be used anywhere within the file, but may not be used outside the file. (This function may increment the value size and we may not wish for clients to have this privilege.)

If you intend the variable size to be referenced outside the file or the function inc_size() to be used outside the file, then you do not need to use any modifier. Just define these at the top level of the file. (All functions are top level definitions by default, in C.) To reference size in a different file, you must declare it with the extern modifier: extern size;. To use the function inc_size() in a different file you must include its prototype in the file using the extern modifier: extern int get_inc();. There are two ways of including these declarations: either explicitly writing them into the file before you use them, or by writing them in a header file which is among the list of #includes in the file.

Each service has a contract with its client: it promises to do exactly what it says it will and nothing more. Modular code enforces this contract. Here are some rules of thumb to maintain modular code:

  1. Organize your program into modules of related services (functions, variables, constants and types.)
  2. Make sure your modules are as independent of other modules as possible.
  3. Use well-documented header files specifying the public interface of the module for your human user. This specifies the contract the function is making to a user of the function.
  4. Use the static storage class specifier to enforce privacy in your source file.
  5. Your module should be responsible for releasing all resources it creates after these are no longer needed. For example, close all files which were opened; free all memory obtained by malloc. If you have a function which creates an object consider have a dual function which releases the system resources for that object when it is no longer required.
  6. Design each function to do one thing well. It is better to build a new function to do a new job, rather than complicate an old function with new features.
  7. If you have a global variable which may change over the course of the program, designate one function whose purpose is to modify the variable and keep the variable private to the file. This restricts access to the variable and aids in understanding the program flow. If an outside user must change a global variable provide a public function which does this.
  8. Functions should not make unexpected changes to the process state that persist beyond the return from the function. If a function does change the process state then it should save and restore the previous state. Here are some examples of ways a function can have non-local effects on the process state: If a public function leaves a lasting effect on the program state, then document this in your description of the function in the header file.
  9. The best approach for handling errors encountered in a function is to return a non-standard value (typically, a negative value or NULL for a pointer.) Let the main section of the code determine how errors should be handled. Think carefully how you want functions to respond to errors. If you print error messages, exit the program or set global variables (like errno) you are leaving a lingering effect on the program. Are you certain that this will be appropriate every time the function is to be use?
  10. Functions should release hidden resources used during its execution if these resources are no longer needed. For example, close all files which were opened during the execution of the funtion; free all memory obtained by malloc.

Documentation

Programs are to be read by computers and human's alike. Good documentation should permeate your code. Keep in mind that I will be reading your code for the following purposes

When documenting your code keep in mind that both you and I will be reading your code. I will need to read your DOC file and your header files with the publicly accessible objects, so follow the guidelines put down below for documenting these files and the objects found within them (especially, the public functions in the header file.) Use your own judgement in documenting your source files, or in your README file (if you choose to include one.) It is very important that you provide good documentation in theDOC file and in the header files, since these are the places your reader will look to first. Rules for documenting will be strictly enforced in these places. Documentation of your source files is up to your judgement. Here are some pointers to keep in mind
File Description Each file should have an introduction that gives the name of the file and describes the purpose of the set of functions which make-up the file. In addition it should provide any hints or warnings about the use of the functions in the file.
Function Descripton Each public function should have an introduction that describes the purpose of the function, the meaning of each of its arguments, the meaning of the return value, the possible error conditions, and any side-effects the function produces. Keep your description clear and simple. A local function may have a very specific and narrow purpose, so a one line description may be plenty sufficient. If your public functions are well-documented in the header file, a one line description can be sufficient in the source file.
Local Variables The local variables should have names that describe their purpose. Consider commenting variables when their name does not quite capture their purpose. This is more important with global variables, especially those which are publicly accessible.
Code Paragraphs Each chunk of code in the function should have a brief description to explain where it fits in the flow of the function. Furthermore, explaining a piece of code helps clarify your logic.
Sneaky Lines C invites you to write clever, efficient code. As you get used to the language, you will find yourself writing code that looks a little weird. When you feel that twinge of cleverness, put a comment like:
/* yes, I mean =, it should not be == */

Readability

Readability consists of two components

The key to readability is consistency. By consistently following a set of rules to guide the readability of your code, your reader will anticipate key properties of your code by the layout on the screen or the names chosen. This will aid in reasoning about the program. Poorly written code stands out by the effort of a reader to follow it. Here are some rules of thumb to keep in mind:
  1. Arrange your code to reflect the logical structure of your program. Use separation between the major logical parts of a function definition (initialization code, main code, and termination code). This separation may take the form of extra spacing or short comments introducing the section of code.
  2. Use a consistent indentation to separate code blocks. In C, code blocks are usually grouped by braces.
  3. Choose names that allow your reader to anticipate the use of the object with the name. Functions and global variables are usually given more descriptive names. Local variables (especially those which fulfill a commonly used role) can be given shorter names. Often it is better to be terse, if you are consistent. For example, variables used for counting the iterations of a loop can be given a very short name, such as i; often this makes the code more readable than a more verbose name.
  4. Functions should be simple. You should be able to reason about the function without strain. It is easier to enforce modularity with simple functions and judge correctness of your code. There are several measures of simplicity:

Choose a coding style and follow it consistently. Remember readability aids your reader by allowing the reader to anticipate the purpose of a section of code or of an object named in your program. This makes the code easier to reason about, and minimizes errors in use or bugs in performance.