15-213 Intro to Computer Systems: Code Style

Just as important as the functionality of your code is your code's readability to others. Therefore, in 15-213 (and other CS courses you will take), we will be paying close attention to your coding style and taking it into consideration when assigning grades.

Each semester the question of "What do you mean by 'good style'?" comes up. The course staff has created this document to try and answer that question. The most basic requirement is a consistent and logical style that makes the purpose of your code clear to the reader. We expect you to pick something that is readable and makes sense, and then stick to that through an entire project. The key points we will be looking for are:

Good Documentation

Good code should be mostly self-documenting: your variable names and function calls should generally make it clear what you are doing. Comments should not describe what the code does, but why; what the code does should be self-evident. (Assume the reader knows C better than you do when you consider what is self-evident.)

There are several parts of your code that do generally deserve comments:
  • File header: Each file should contain a comment describing the purpose of the file and how it fits in to the larger project. This is also a good place to put your name and email address.
  • Function header: Each function should be prefaced with a comment describing the purpose of the function (in a sentence or two), the function's arguments and return value, any error cases that are relevant to the caller, any pertinent side effects, and any assumptions that the function makes.
  • Large blocks of code: If a block of code is particularly long, a comment at the top can help the reader know what to expect as they're reading it, and let them skip it if it's not relevant.
  • Tricky bits of code: If there's no way to make a bit of code self-evident, then it is acceptable to describe what it does with a comment. In particular, pointer arithmetic is something that often deserves a clarifying comment.

Good Use of Whitespace

Proper use of whitespace can greatly increase the readability of code. Every time you open a block of code (a function, "if" statement, "for" or "while" loop, etc.), you should indent one additional level.

You are free to use your own indent style, but you must be consistent: if you use four spaces as an indent in some places, you should not use a tab elsewhere. We recommend that you not use tabs, and only use spaces for indenting. (If you would like help configuring your editor to indent consistently, please feel free to ask the course staff.)

Line Length

While there are many different standards for line length, we require that your lines be no longer than 80 characters, so we can easily view and print your code. To quickly check that file.c does not exceed 80 characters, run "wc -L file.c" to see its max line length.

Good Variable Names

Variable names should be descriptive of the value stored in them. Local variables whose purpose is self-evident (e.g. loop counters or array indices) can be single letters. Parameters can be one (well-chosen) word. Global variables should probably be two or more words.

Multiple-word variables should be formatted consistently, both within and across variables. For example, "hashtable_array_size" or "hashtableArraySize" are both okay, but "hashtable_arraySize" is not. And if you were to use "hashtable_array_size" in one place, using "hashtableArray" somewhere else would not be okay.

Magic Numbers

Magic numbers are numbers in your code that have more meaning than simply their own values. For example, if you are reading data into a buffer by doing "fgets(stdin, buf, 256)", 256 is a "magic number" because it represents the length of your buffer. On the other hand, if you were counting by even numbers by doing "for (int i = 0; i < MAX; i += 2)", 2 is not a magic number, because it simply means that you are counting by 2s.

You should use #define to clarify the meaning of magic numbers. In the above example, doing "#define BUFLEN 256" and then using the "BUFLEN" constant in both the declaration of "buf" and the call to "fgets".

No "Dead Code"

"Dead code" is code that is not run when your program runs, either under normal or exceptional circumstances. These include "printf" statements you used for debugging purposes but since commented. Your submission should have no "dead code" in it.

Modularity of Code

You should strive to make your code modular. On a low level, this means that you should not needlessly repeat blocks of code if they can be extracted out into a function, and that long functions that perform several tasks should be split into sub-functions when practical. On a high level, this means that code that performs different functions should be separated into different modules; for example, if your code requires a hashtable, the code to manipulate the hashtable should be separate from the code that uses the hashtable, and should be accessed only through a few well-chosen functions.

Failure Conditions/Error Checking

When writing a program, we usually only consider the success case. It is equally, if not more, important to consider the failure cases. Many things can fail in your program: the user's input might not match your expected format, malloc might return NULL, the filename the user gave you might not exist, the user might not have permission to read the file they specified, the disk might fill up, the network host you were talking to might be down... the list goes on. It is important to think about what your program can do to resolve these errors, or how it should present them to the user if it can't resolve them. For example, if malloc fails in a crucial part of your program, you might have no choice but to print a fatal error message and exit. But if the user specifies an invalid file to open in an interactive program, it would be much nicer if you told them the file was invalid and gave them a chance to correct the name.

In particular, network servers (and, as you will learn if you take 15-410, operating systems) need to be particularly robust. No matter what one client (or process) tries to do, your server (or kernel) should never crash. Error handling is more difficult in such cases, as you need to convert what is a "fatal error" from a client's perspective into something that won't actually kill the server process.

Proper Memory and File Handling

If you allocate memory (malloc, calloc), you should free it after use. Your program should not have memory leaks. If you use open a file, you should close it after use. Closing a file is very important, especially with output files. The reason is that output is often buffered.

Consistency

This style guide purposefully leaves many choices up to you (for example, where the curly braces go, whether one-line "if" statements need braces, how far to indent each level). It is important that, whatever choices you make, you remain consistent about them. Nothing is more distracting to someone reading your code than random style changes.