Advanced C Programming

Autumn 2016 :: ECE 264 :: Purdue University

This is for Fall 2016 (8 years ago)

Code quality standards

In this class, you will learn to write good code. That takes a different kind of skill and effort than just making a small homework assignment work. However, good code is easier to read, debug, and maintain. It also helps you avoid creating bugs in the first place.

The rules in this document are adapted from a variety of sources. While some might disagree on a few small points, most of these reflect prevailing views on C code style.

Summary

1. global constants all caps with underscores
2. comments always explain complex code; never state the obvious (e.g., // main function)
3. struct names camel case with first letter capitalized (e.g., PersonNode)
4. internal helper functions name starts with underscore (e.g., _sort_helper(…)); do not declare .h file
5. curly braces for if/for/… always (i.e., never omit, even for single-statement blocks)
6. indent body of function/if/for/… always
7. multiple statements on one line never
8. dead code never
9. useless variables never
10. vim modeline always; must match the tab settings you have been using
11. constants declare with const not #define (usually)
12. variable names descriptive (usually)
13. NULL vs. 0 vs. '\0' NULL for addresses, 0 for numeric values, '\0' for characters
14. declarations initialize in declaration (usually)
15. for loop counter scope declare and intialize in the for statement (usually)
16. boolean values use true/false from stdbool.h; never use 1/0 for true/false
17. array initializers use where possible/appropriate (usually)
18. struct initializers use where possible/appropriate (usually)
19. data type sizes use sizeof; never make assumptions
20. printf use only for "official" output
21. variableNames vs. variable_name pick one of these two and be consistent
22. curly braces for if blocks be consistent; use one of the two styles (see below)
23. curly braces for functions be consistent; use one of the two styles (see below)
24. indent type be consistent; tabs or spaces; any tab size; declare tab size in vim modeline
25. spacing between functions be consistent; one or two spaces between functions
26. sizeof argument must be a variable name when used in an initialization or malloc(…).
27. global variables constants only (unless explicitly allowed)
28. casting operator rarely—and never with malloc(…)
29. character literals use the character literal form (e.g., 'A') not integer
30. assert use only to check intermediate values for conditions that should never happen

In case of any inconsistency, the descriptions below take precedent over this table.

Required

Global constants

Rule:  Use all caps with words separated by underscores
Reason:  This makes code more readable by making it clear which variables might change.
Good: 
const char* s = COURSE_NAME;

Comments

Rule:  Comment code that is complex, or where the purpose of a block of code would be unclear at first glance. Do not include comments for things that are extremely obvious.
Note: This is consistent with Google's C++ Style Guide under Implementation Comments.

Struct names

Rule:  Use camel case with the first letter capitalized
Reason:  This makes types look very different from instances of those types.
Good: 
struct ListNode{ … };
Good: 
struct Point{ … };
Note: This is consistent with Google's C++ Style Guide under Type Names.

Internal helper functions or global constants

Rule:  Begin with an underscore.
Reason:  This warns readers of your code (including you) that the meaning may be hard to understand. It also lets them know that it may change, so it's best not to refer to it from elsewhere.
Good: 
void _do_something_mysterious() { }
Good: 
const int _MAX_BUFFER_SIZE = 128;
Note:  "Internal" means things that should not be called from outside the current file or context. This rule is more important for larger projects, and is pointless for projects that have only a single file. However, real projects tend to grow, so it is a good habit to build on.

Braces for if, for, while, switch, do, …

Rule:  Always required. (Do not use inside each case of a switch, but use for the switch itself.)
Reason:  Even though C will let you omit the braces, if the body is just one line, it
Good: 
if(a < 1) { foo(); }
Bad: 
if(a < 1) foo();
Note: Apple had a major security bug due to someone omitting the braces (article).

indent body of function/if/for/while/do/switch

Rule:  Body of a function definition or if/for/while/do/switch statement must be indented, relative to the lines that contain the enclosing curly braces.
Exception:  In rare cases, you might want a loop or function with an empty body. In those cases, you may have both braces on the same line as the beginning of the loop. Also, note that this rule does not apply to struct or array initializers.
Good: 
for(int i=0; i<5; i++) { printf("i == %d\n", i); }
Good: 
for(int i=0; i<5; i++) { printf("i == %d\n", i); }
Good:  (or at least acceptable)
if(a == 0) { foo(); } else { bar(); }
Bad: 
for(int i=0; i<5; i++) { printf("i == %d\n", i); }
Bad: 
for(int i=0; i<5; i++) { { printf("i == %d\n", i); }
Bad: 
for(int i=0; i<5; i++) { { printf("i == %d\n", i); }
Bad: 
for(int i=0; i<5; i++) { printf("i == %d\n", i); }
Bad: 
for(int i=0; i<5; i++) { printf("i == %d\n", i); }
Good: (ugly, but marginally acceptable, at least in ECE 264)
for(; s != '\0'; s++) { }

Multiple statements on one line

Rule:  Not allowed
Reason:  It makes code unnecessarily hard to read.
Good #1: 
int a = 1; int b = 2;
Good #2: 
int a = 1; char c = 'c';
Good #3:  (ugly, but marginally acceptable, at least in ECE 264)
for(; *s != '\0'; s++) {}
Bad #1: 
int a = 1; char c = 'c';
Bad #2: 
for(int i=0; i < 5; i++) { printf("i == %d"); }

Dead code

Rule:  Not allowed
Reason:  This makes code harder to debug by concealing potential problems.
Bad: 
if(1 == 0) { doSomething(); // will never get here!!! }
Bad: 
int main() { return EXIT_SUCCESS; } void _foo() { // not called by main(…) or anything else // ... }
Good: 
int main() { _foo(); return EXIT_SUCCESS; } void _foo() { // ... }
Note:  "Dead code" is code that is never called.

Useless variables

Rule:  Not allowed
Reason:  This makes code harder to debug by concealing potential problems.
Bad: 
int foo(int a) { int b = 1; // never used!!! return a * 10; }

Useless assignments

Rule:  Not allowed
Reason:  This makes code harder to debug by concealing potential problems.
Bad: 
int foo() { int a = 0; a = a + 10; // no effect!!! a = 20; return a * 10; }
Note:  Useless assignments are assignments that cannot, under any circumstances, have any effect on the output.
Exception:  This does not apply variables that are initialized to 0 or NULL (or similar).

Vim modeline:

Rule:  Required
Reason:  This ensures that when course staff in ECE 264 view your code, they will have the same indent type that you had. It's also a good habit when working on projects where others might be using Vim (outside of this class).
Good:  (for those who prefer tabs)
/* vim: set tabstop=4 shiftwidth=4 fileencoding=utf-8 noexpandtab: */
Good:  (for those who prefer spaces)
/* vim: set tabstop=4 shiftwidth=4 fileencoding=utf-8 expandtab: */

Constants

Rule:  Declare with const, not #define
Reason:  const enforces types. #define just does a simple text substitution.
Good: 
const int NUM_ASSIGNMENTS = 15;
Bad: 
#define NUM_ASSIGNMENTS 15
Exception:  It's okay to use #define for constants if you have a very specific reason. For ECE 264, you must document the reason in a comment.
Note: This is consistent with Google's C++ Style Guide under Use of const.

Variable names

Rule:  Must describe contents
Reason:  This makes code more readable, especially when you come back to it later or show it to someone who didn't read it (e.g., when asking for help).
Exception:  For array indices or other variables that are used only in a small portion of the code where the meaning is completely obvious at first glance, it is okay to use one or two letters (e.g., i for an index, s or a string, ch for a string, n for some other number). Also, short names are okay for small examples to illustrate programming concepts where brevity is a goal in itself.
Note: This is consistent with Google's C++ Style Guide under General Naming Rules.

NULL vs. 0 vs. '\0'

Rule:  Use NULL for addresses (i.e., null pointers), 0 for numeric values, and '\0' for characters (e.g., string terminator).
Reason:  This makes code more readable and avoids bugs.
Note:  Internally, NULL, 0, and '\0' are all stored as 0.
Reference: This is also part of Google's C++ Style Guide under 0 and nullptr/NULL.

Declarations

Rule:  Declarations initialize
Good: 
int i = 0;
Bad: 
int i; i = 0;
Exception:  In rare occassions, there may be no meaningful value to initialize a variable to. Don't initialize to a meaningless value just to comply with this rule, since that could prevent Valgrind from warning you if you try to access an uninitialized value.
Note: This is consistent with Google's C++ Style Guide under Local variables.

for loop counter scope

Rule:  Declare and initialize for loop counter in the for statement.
Good: 
for(int i = 0; i < 5; i++) { printf("%d\n", i); }
Bad #1: 
int i = 1; for(; i < 5; i++) { printf("%d\n", i); }
Bad #2: 
int i; for(i = 0; i < 5; i++) { printf("%d\n", i); }
Exception:  In rare occassions, you may need to use the counter in code below the for loop. In that case, declare and initialize outside the for loop (like Bad #1 above) and explain why in a comment.

Boolean values

Rule:  Do not use 1 and 0 for true and false. Instead, use the true and false values and the bool type from the stdbool.h library. We are using the C99 version of the C language standard in this class, which includes stdbool.
Note:  We are using the C99 version of the C language standard in this class, which includes stdbool.
Good: 
bool is_positive = (a > 0);
Bad: 
int is_positive = (a > 0);
Reason:  The constants true and false make your code more readable because express what you mean. Since stdbool.h is part of the standard library, they will be readable by others and interoperable with other code.

Array initializers

Rule:  Use where possible (and appropriate)
Good: 
int numbers[] = {1, 2, 3};
Bad: 
int numbers[3]; numbers[0] = 1; numbers[1] = 2; numbers[2] = 3;
Reason:  Array initializers are clearer to read, and avoid certain types of bugs (e.g., numbers[3] = 4 for an array of size 3.).
Note: This is consistent with Google's C++ Style Guide under Local variables.

Struct initializers

Rule:  Use where possible (and appropriate)
Good: 
struct Point point1 = {.x = 1, .y = 2};
Bad #1: 
struct Point point1; point1.x = 1; point1.y = 2;
Bad #2: 
struct Point point1 = {1, 2};
Reason:  Struct initializers are clearer to read. They also prevent mistakes where programmers sometimes forget to initialize one field, or perhaps edit the code later and inadvertently delete part of the initialization. Bad #2 is actually fine in earlier versions of c (e.g. c89), but the new syntax shown as Good prevents errors where you get the order wrong.

Data type sizes

Rule:  Do not make assumptions about the size of any data type. Use the sizeof(…) operator instead.
Good: 
char* s = malloc(n * sizeof(*s));
Bad: 
int* array = malloc(n * 4);

sizeof argument

Rule:  Argument must be a variable name when used in an initialization or malloc(..)
Good: 
struct Node* new_node = malloc(sizeof(*new_node));
Bad: 
struct Node* new_node = malloc(sizeof(struct Node));
Good: 
int* array = malloc(n * sizeof(*array));
Bad: 
int* array = malloc(n * sizeof(int));
Reason:  This reduces duplication of information. If you change the type of the variable later, this will automatically adjust.
Note: This is consistent with Google's C++ Style Guide under sizeof.

printf

Rule:  Use printf only for "official" output. For debugging, use #define to create a log macro that you can turn off with a flag to the compiler (i.e., conditional compilation).
Good: 
#ifdef DBG_LOG #define dbg_log(...) printf(__VA_ARGS__) #else #define dbg_log #endif int main() { dbg_log("Starting...") }
Bad: 
int main() { printf("Starting...") }

assert

Rule:  Use assert only to catch bugs in your code. Do not use assert to catch anything that the user could control, external issues with the system, or anything else outside of your code.

Global variables

Rule:  Do not use global variables, except for constants, unless explicitly allowed for an assignment.
Bad: 
#include <stdio.h> int g = 0; int main(int argc, char* argv[]) { g += 1; … }
Good: 
#include <stdio.h> const int NUM_FEET = 2; int main(int argc, char* argv[]) { … }

Character literals

Rule:  Use the character literal (e.g., 'A') and not the integer literal (e.g., 65) if the data represents a character.
Bad: 
char digit = 48 + n % 10;
Good: 
char digit = '0' + n % 10;

Casting operator

Rule:  Do not use the casting operator with malloc(…) or in any other context, unless it is absolutely necessary for a reason that you completely understand well. Do not add a casting operator "just in case," or to silence warnings that you don't understand.
Bad: 
char* s = (char*)malloc(10 * sizeof(*s));
Good: 
char* s = malloc(10 * sizeof(*s));

Your choice - pick one and be consistent within a given project

Multi-word variable and function names

Option A: 
int list_idx = 0;
Option B:  lowercase camel case
int listIdx = 0;

Braces for if blocks

Option A: 
if(a == 0) { }
Option B: 
if(a == 0) { }

Braces for functions

Option A: 
int foo() { }
Option B: 
int foo() { }

Indent type

Option A:  tabs
Option B:  spaces
Note:  This must be reflected in your vim modeline (see above). Also, you are welcome to choose a different tab size (i.e., instead of 4).
Reason:  If you mix tabs and spaces, your file layout may be hard to read for others with a different tabstop setting.

Spacing

Option A:  one space between functions
Option B:  two spaces between functions

Updates

9/14/2016: Added: No global variables.

Updates

9/14/2016: Added: No global variables.

10/6/2016: Clarified about declarations initialize; added assert to summary table at the top.