C Review โ
This is a quick start guide for C programming geared for students who have been exposed to C but have done the majority of their coursework in Java. In the CS curriculum at Boise State University you are exposed to C in CS253 and ECE230/330 which are both prerequisites to this class. The majority of the other required classes use Java as the preferred language. With this fact in mind students may not have had as many opportunities to hone their C skills to the same sharpness as their Java skills. The following sections highlight some important differences between C and Java, and give rough analogies to help bridge the gap between the two.
When working with C it is very important to distinguish between the declaration of a construct (such as a variable, struct, function, etc.) and its definition. A declaration announces the properties of a variable (primarily its type); a definition also causes storage to be set aside. We will visit the topic of declaration and definition throughout this document.
This document is by no means an exhaustive reference for the C language. Its purpose is to help refresh your previous experience with C and point out common pitfalls that plague students. Several very thorough treatises of the C language are listed in the References section below for those who want to dig deeper.
Types, Operators, and Expressions โ
The C language provides only a few basic type specifiers: char, int, float and double. In addition to the basic types there are type modifiers: signed, unsigned, short, and long. You use the basic types and modifiers together like unsigned int foo = 0; which declares a new variable foo of type int that is unsigned.
The sizes of types such as int and long depend on the compiler and platform (long is 8 bytes on x86-64 Linux but 4 bytes on 64-bit Windows). This is in stark contrast to Java where sizes are always the same regardless of what hardware you are running on.
In C automatic type conversions can be surprising when coming from the warm embrace of Java. When an operator, like addition (+), encounters operands of different types, they are converted to a common type according to a small number of rules. In general, automatic conversions widen a narrower operand into a wider one, such as int to double. Watch out for two cases. Converting a large int to float can lose precision (any value above 2^24), and when an operator mixes a signed and an unsigned operand of the same size, the signed one is converted to unsigned, so -1 < 1u is false. Unfortunately, expressions that might lose information, like assigning a longer integer type to a shorter, or a floating-point type to an integer, are not illegal and the compiler in question may only issue a warning instead of an error.
Control flow โ
Control flow in C is similar to Java with some of the same gotchas (like switch statements falling through without an explicit break). You have If-Else, Else-If, Switch, loops (while, for, do/while), break, continue and the infinitely-abusable goto statement.
While the goto statement may have gotten a bad rap it is actually quite handy when used with discipline. To quote the Linux kernel docs the rationale for using the goto statement is:
- unconditional statements are easier to understand and follow
- nesting is reduced
- errors by not updating individual exit points when making modifications are prevented
- saves the compiler work to optimize redundant code away ๐
Functions and Program Structure โ
Java is an OOP language and uses methods while C is a procedural language and uses functions. The purpose of both is to break large computing tasks into smaller ones. While pre-ANSI C (known as K&R C) had very strange function declarations, functions in ANSI C (C89 and greater) have a similar look and feel to Java.
One of the biggest differences between functions in C and methods in Java (besides the OOP differences) is C has function prototypes. Function prototypes allow you to forward declare a function's signature so you can call a function before it is defined.
The classic example is where two functions each call each other. Without prototypes the compiler would not have seen the function bar when called in foo and would not know if the function is being called correctly. Moving the function bar to be defined before foo would not solve the issue because bar is calling foo! Thus, the solution in C is to forward declare both functions before either is used as illustrated below.
/* declares functions foo and bar*/
void foo(void);
void bar(void);
/*defines the function foo*/
void foo(void)
{
if (expr){
bar();
}
}
/*defines the function bar*/
void bar(void)
{
if (expr){
foo();
}
}Header Files โ
Java has no notion of a "header file" in the same way as the C language does. However, we can make rough comparisons between a C header file (ending in a .h) and a Java interface. If the header file contains declarations and definitions it is similar to a Java Abstract class. Keep in mind that these examples are very rough and if you squint too hard the analogy will fall apart. In C, your implementation should be in a .c file which can be roughly compared to a Java concrete class.
In Java if you want to use a class that is defined in another package (file) you need to use the import directive. In C the equivalent statement would be #include <file>. However be aware that #include is actually part of the C pre-processor.
Static Variables โ
The static keyword in Java indicates that the member in question belongs to the class and not the object itself. In C the static declaration, applied to an external variable or function, limits the scope of that object to the rest of the source file being compiled. So in C using the static keyword on a function is akin to using the private keyword in Java.
In C you can also use the static declaration on internal variables (variables declared inside a function). Internal static variables are local to a particular function just as automatic variables are, but unlike automatics, they remain in existence across function calls. This means that using the static keyword on internal variables provides private, permanent storage within a single function. Be aware however that internal static variables are NOT thread safe and should be treated with the same care as global variables in a multi-threaded environment.
/* foo is private to this source file */
static int foo;
/* bar is private to this source file */
static void bar(void){ ... }
static void foobar(void)
{
/* var is private to foobar and will retain its value across func calls */
static int var;
}Initialization โ
In the absence of explicit initialization, external and static variables are guaranteed to be initialized to zero; automatic and register variables have undefined (i.e. garbage) initial values.
Preprocessor โ
In C the preprocessor is the first step in the compilation process. The preprocessor is a simple text substitution mechanism.
The C preprocessor includes such items as:
- #include "foo.h" - copies the contents of "foo.h" into the including file (foo.c)
- #define name replacement text - subsequent occurrences of the token name will be replaced by the replacement text
- #if, #elif, #else, and #endif - similar to an if statement in C
- #ifndef symbol - evaluates to true if symbol is not defined
Input and Output โ
Input and output facilities are not part of the C language itself. The C standard library contains a set of functions that provides input and output. The C standard library in turn relies on the underlying operating system to do all the heavy lifting.
Pointers and Arrays โ
Pointers are generally the hardest thing to grasp in the C language. To quote K&R "A pointer is a variable that contains the address of a variable." While the definition seems recursive you can think of a pointer as actually two things, an address and a value. Pointers are similar to Java reference variables. Pointers and arrays are closely related with many similar rules and properties.
Given a pointer you can get the value it points to with the unary operator * which is called the indirection or dereferencing operator.
Here is an example from the K&R book regarding pointers:
int x = 1, y = 2, z[10];
int *ip; /* ip is a pointer to int */
ip = &x; /* ip now points to x */
y = *ip; /* y is now 1 */
*ip = 0; /* x is now 0 */
ip = &z[0]; /* ip now points to z[0] */For the most part pointers and arrays have a strong relationship. Any operation that can be achieved by array subscripting can also be done with pointers. If we define int a[10]; and int *pa we can make the following statements:
- A pointer is a variable, so
pa = aandpa++are legal. But an array name is not a variable, so constructions likea = paanda++are illegal. - When an array name is passed to a function, what is passed is the location of the initial element. What this means is arrays decay to pointers when passed as a function argument.
Address Arithmetic โ
Given the fact that a pointer is just a variable you can perform operations on it just like other types. However, the results may not be what you are expecting. When adding 1 to a pointer int *pa with the increment operator ++ we are moving the pointer sizeof(int) bytes. The effect is the pointer now points to the next int in memory. For example if we are on a machine where an int is 4 bytes (32 bits) adding 1 to a pointer of the type (int *) would add 4 bytes to the address. This property is very useful when implementing the memory subsystem of an OS.
Strings โ
There is no explicit string type in C like there is in Java. In C a string is just an array of chars that are terminated with a null character ('\0'). Strings are typically represented by a pointer (typed to char *) that points to the first character in the string. In C the char type represents a character from the ASCII table. A very common mistake for beginner C programmers is to forget about the null character when dealing with strings. When allocating memory you must always add 1 to the size of a string to account for the null terminator. For example the string foo requires storage of size 4 bytes, 3 for the letters f, o, and o and 1 for the '\0'.
Structures โ
/* Define a new struct type called foo */
struct foo{
int a;
int b;
};
/* Declare a new variable named bar of type struct foo */
struct foo bar;In C an object is represented by a struct. You can think of a struct as a Java object with everything declared public! You access a structure's members using the dot operator (.) just like in Java. However, if you are dealing with a pointer to a struct you need to use the arrow operator (->).
struct point {
int x;
int y;
};
struct point foo;
struct point *fooptr = &foo;
/* Access x and y with struct var */
foo.x;
foo.y;
/* Access x and y with struct pointer */
fooptr->x;
fooptr->y;Compilation process โ
In C the compilation process is slightly more complex than Java and has multiple stages. It is important to understand the stages that your code goes through during the process so you can differentiate between compilation problems and linking problems. When compiling your code you should always enable as many warnings as possible and should treat warnings as errors.
Building code โ
If your program consists of one .c file and one .h file then it is possible to build everything by hand. However, things get very complex once you start to add more files into the process, so another tool must be used to drive everything. There are hundreds of options available to build a C code base. For this class we will use GNU make. While make is not a perfect system it is installed almost everywhere and is good enough for our purposes.
The C (un)standard โ
In the brilliant paper A Few Billion Lines of Code Later: Using Static Analysis to Find Bugs in the Real World the authors write about the challenges of parsing programming languages against a standard. While the C language has been standardized by ISO, compiler vendors often diverge from the standard, either on purpose (gnu89), because the standard is vague or undefined, or because of bugs in the compiler itself.
The C language does not exist; neither does Java, C++, and C#. While a language may exist as an abstract idea, and even have a pile of paper (a standard) purporting to define it, a standard is not a compiler. What language do people write code in? The character strings accepted by their compiler. Further, they equate compilation with certification. A file their compiler does not reject has been certified as "C code" no matter how blatantly illegal its contents may be to a language scholar.
The Linux kernel is written in gnu11 (it moved from gnu89 to gnu11 in Linux 5.18), which looks like C11 but leverages GCC-specific features. This dialect contains many extensions to the language (the GNU extensions), and many of them are used within the kernel as a matter of course.
References โ
- The C programming language 2nd edition
- Brian W. Kernighan and Dennis M. Ritchie. ISBN: 0-13-110362-8
- Using the GNU Compiler Collection
- clang documentation
- A Few Billion Lines of Code Later: Using Static Analysis to Find Bugs in the Real World
- Al Bessey et al., Communications of the ACM, February 2010, Vol. 53 No. 2, Pages 66-75
- OSTEP ch 14
- Intelยฎ 64 and IA-32 Architectures Software Developer's Manual
- GNU make
- Clang command line documentation