Module 2: Elements of C


Reserved words and identifiers

C's 37 reserved words:

auto          enum          restrict          unsigned
break         extern        return            void
case          float         short             volatile
char          for           signed            while
const         goto          sizeof            _Bool
continue      if            static            _Complex
default       inline        struct            _Imaginary
do            int           switch
double        long          typedef
else          register      union
Note:

Identifiers in C:

In-Class Exercise 2.1: What kind of a compiler error do you get if you try to use a reserved word as an identifier?


Comments

There are two types of comments in ANSI C99:

Consider this example: (source file)
#include <stdio.h>


/* This is a block comment. Everything between the begin-comment symbol
   and the end-comment symbol is ignored by the compiler. It is conventional 
   to use a separate line for the end-comment symbol.
*/


// This is an in-line comment.

int main (/* A strange place for a block comment */)
{
  printf ("Hello World!\n");  // Another inline comment.
}
Note:


Data types

About C's data types:

Integer types (ANSI C99 additions shown in bold):

Type Typical
storage size
Range Print
specifier
int 2 or 4 bytes -32,768 to 32,767 (2 bytes)
-2,147,483,648 to 2,147,483,647 (4 bytes)
%d
unsigned int 2 or 4 bytes 0 to 65,535 (2 bytes)
0 to 4,294,967,295 (4 bytes)
%u
short 2 bytes -32,768 to 32,767 %d
unsigned short 2 bytes 0 to 65,535 %u
long 4 bytes -2,147,483,648 to 2,147,483,647 %ld
unsigned long 4 bytes 0 to 4,294,967,295 %lu
long long 8 bytes -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 %lld
unsigned long long 8 bytes 0 to 18,446,744,073,709,551,615 %llu

Let's look at an example: (source file)

#include <stdio.h>

int main ()
{
  int numDaysInYear = 365;
  long int numStarsInUniverse = 2000000000L;
  unsigned long long int largestIntegerInC = 18446744073709551615LL;

  printf ("numDaysInYear = %d\n", numDaysInYear);
  printf ("numStarsInUniverse = %ld\n", numStarsInUniverse);
  printf ("largestIntegerInC = %llu\n", largestIntegerInC);
}
Note:

Screen output:

In-Class Exercise 2.2: Consider the program
#include <stdio.h>

int main ()
{
  int numDaysInYear = 365;
  long int numStarsInUniverse = 2000000000L;
  unsigned long long int largestIntegerInC = 18446744073709551615LL;

  printf ("numDaysInYear = %d  numStarsInUniverse = %ld  largestIntegerInC = %llu\n",   
          numDaysInYear, numStarsInUniverse, largestIntegerInC);
}
What happens when you mistakenly use %d for all the integers above?

Floating-point types:

Type Typical
storage size
Range Print
specifier
Approximate
precision
float 4 bytes 1.2 x 10-38 to 3.4 x 1038 %f 6 decimal places
double 8 bytes 2.3 x 10-308 to 1.7 x 10308 %lf 15 decimal places
long double 10 bytes 3.4 x 10-4932 to 1.1 x 104932 %llf 19 decimal places

Consider this example: (source file)

int main ()
{
  // Constant with "F" appended:
  float PI = 3.141F;

  // Constant in exponent format:
  double doublePI = 314.159265E-2;

  // Long double constant:
  long double ldoublePI = 3.14159265358979L;

  // Output in exponent format:
  printf ("float PI = %7.5e\n", PI);

  // Output in decimal format with field width and number of significant digits:
  printf ("double PI = %16.10lf\n", doublePI);

  // Long double's need to be printed as double's
  printf ("long double PI = %15.12lf\n", (double) ldoublePI);
}
Note:
In-Class Exercise 2.3: It's a common error to mistype the format string. Find out what happens when you use %d for a floating point number and %f for a double.

Character types:

Type Typical
storage size
Range Print
specifier
char 1 byte -128 to 127 %c
unsigned char 1 byte 0 to 255 none
signed char 1 byte -128 to 127 none

Consider this example: (source file)

int main ()
{
  char letter = 'a';
  unsigned char letter2 = 'b';
  signed char letter3 = 'c';

  printf ("letter = %c\n", letter);
  printf ("letter2 = %c\n", letter2);
  printf ("letter3 = %c\n", letter3);
}
Note:


Casting

Consider this example: (source file)

int main ()
{
  int i = 5;
  long j = 6;
  double d = 3.141;

  j = i;         // Works fine. Implicit cast from int to long.
  d = i;         // Works fine. Implicit cast from int to double

  i = j;         // May not compile.
  i = d;         // May not compile.

  d = 3.141;
  i = (int) j;   // Compiles. Explicit cast from long to int.
  i = (int) d;   // Compiles. Explicit cast from double to int.

  // Cast's can be used in any expression:
  printf ("The int part of d=%lf is %d\n", d, (int) d);
}
Note:
  • To assign a variable higher in the cast hierarchy to one lower, an explicit cast is required.

  • An explicit cast is the desired type in parentheses placed before the target of the cast:
      i = (int) d;   
        

  • Many compilers don't warn you if you are "down" casting: this is a common source of error.

  • The implicit cast hierarchy is:
    short -> int -> unsigned int -> long -> unsigned long -> long long -> float -> double -> long double

  • Use an explicit cast in any assignment down the hierarchy.


Pointers

What is a pointer?

  • A pointer stores a memory address.

  • A pointer usually has an associated type, e.g., an int pointer vs. a char pointer.

  • Pointers (memory addresses) are themselves numbers and can be added (if it makes sense to do so).
Let's look at an example: (source file)
// Declare some global integers:
int i = 5;
int j = 6;
int sum;

int main ()
{
  // Int pointer declarations:
  int *intPtr;
  int *intPtr2;
  int *intPtr3;

  // First, let's print the address of variable i:
  printf ("Variable i is located at address %lu\n", &i);

  // Now extract the address of variable i into the pointer:
  intPtr = & i;

  // Print.
  printf ("The int at memory location %lu is %d\n", intPtr, *intPtr);

  // Let's do some arithmetic.
  intPtr2 = & j;
  sum = (*intPtr) + (*intPtr2);
  printf ("The sum of %d and %d is %d\n", *intPtr, *intPtr2, sum);

  // Now for something stranger:
  printf ("The integer at location %lu is %d\n", (intPtr+1), *(intPtr+1)); 
  printf ("The integer j=%d is at location %lu\n", j, &j);

  // Let's see what the default initial value of intPtr3 is:
  if (intPtr3 == NULL) {
    printf ("intPtr3 has been initialized to NULL\n");
  }
  else {
    printf ("intPtr3 has been initialized to %lu\n", intPtr3);
  }

}
Note:
  • The (asterisk) operator * is used in pointer declarations and in dereferencing pointers.

  • The (ampersand) operator & is used for extracting memory addresses:
      // Now extract the address of variable i into the pointer:
      intPtr = & i;
        

  • Pointers (memory addresses) can be printed using the %lu format specifier.

  • Dereferenced pointers can be used in expressions:
      sum = (*intPtr) + (*intPtr2);
        

  • Pointers themselves can be used in expressions:
      printf ("The integer at location %lu is %d\n", (intPtr+1), *(intPtr+1)); 
        
    In this case, we examine the next address beyond i, which happens to be where j is stored.

  • Pointers can be compared (equality comparison) to NULL.

  • Curiously, NULL is not a reserved word but a constant.

  • ANSI C99 initializes pointers to NULL, but prior versions of C do not.

  • Important: it's best to assume pointers and variables are not initialized.

Consider an example with char pointers: (source file)

int main ()
{
  int i = 5;
  char *charPtr;  // Pointer declaration
  int j;

  // Make the char pointer point to the start of the integer:
  charPtr = (char*) (& i);

  // Extract the byte, store it in the integer j and print j.
  j = (int) *charPtr;
  printf ("First byte: %d\n", j);

  // Get the next byte and print:
  j = (int) *(charPtr+1);
  printf ("Second byte: %d\n", j);

  // Get the third byte and print:
  j = (int) *(charPtr+2);
  printf ("Third byte: %d\n", j);

  // Get the fourth byte and print:
  j = (int) *(charPtr+3);
  printf ("Fourth byte: %d\n", j);
}
Note:
  • The integer i occupies four bytes, only one of which, the fourth, contains "5".

  • To extract the first byte, make the char pointer charPtr point to the start of the integer and extract the byte.

  • Every increment of the charpointer will point it to the next byte.

  • A char pointer is declared with the asterisk symbol to the left the variable:
      char *charPtr;  // Pointer declaration
        

  • Similarly, a pointer is dereferenced using the asterisk symbol:
      j = *charPtr;   // Retrieve whatever charPtr points to.
        

  • The byte so referenced is cast into an int type for printing (since C has no byte type).

  • The term (charPtr+1) adds "1" to the pointer and thus points to the next memory address. To dereference, we apply the asterisk.

Examining pointers in a debugger:

  • Let's compile the first example with the debug option
      gcc -g -o pointer pointer.c
      
    and run inside the debugger:
      gdb pointer
      

  • Inside the debugger, we will set a breakpoint, step through and print the pointer intPtr and what it points to:
    (gdb) l
    7
    8       int main ()
    9       {
    10        // Int pointer declarations:
    11        int *intPtr;
    12        int *intPtr2;
    13        int *intPtr3;
    14
    15        // First, let's print the address of variable i:
    16        printf ("Variable i is located at address %lu\n", &i);
    (gdb) break 16
    Breakpoint 1 at 0x10720: file pointer.c, line 16.
    (gdb) run
    Starting program: pointer
    Breakpoint 1, main () at pointer.c:16
    16        printf ("Variable i is located at address %lu\n", &i);
    (gdb) p intPtr
    $1 = (int *) 0x0
    (gdb) next
    Variable i is located at address 134000
    19        intPtr = & i;
    (gdb) next
    22        printf ("The int at memory location %lu is %d\n", intPtr, *intPtr);
    (gdb) p intPtr
    $2 = (int *) 0x20b70
    (gdb) p *intPtr
    $3 = 5
    (gdb) 
    
    The l (list) command lists the program.






    The break command creates a breakpoint so you can stop midway through execution.

    The p (print) command prints variables.

    The next command executes the next statement.
In-Class Exercise 2.4: Write a program to use a pointer to a pointer to an int. Fill in the needed assignments below to make the program print "5".
int main ()
{
  int i = 0;
  int *p = NULL;
  int **p2 = NULL;

  // Fill in the assignments here to make the program work:


  // Should print "5";
  printf ("i = %d\n", **p2);
}


Operators

An example with arithmetic operators (source file)

int main ()
{
  double x = 6, y = 5;
  int i = 8, j = 5;

  // Standard: plus, minus, multiple, divide
  printf ("x+y=%lf\n", x+y);     // Prints 11.0
  printf ("x-y=%lf\n", x-y);     // Prints 1.0
  printf ("x*y=%lf\n", x*y);     // Prints 30.0
  printf ("x/y=%lf\n", x/y);     // Prints 1.2

  // Integer divide and remainder:
  printf ("i/j=%d\n", i/j);      // Prints 1
  printf ("i mod j=%d\n", i%j);  // Prints 3

  // Post and pre-increment:
  printf ("i++ = %d\n", i++);    // Prints 8
  printf ("++j = %d\n", ++j);    // Prints 6

  // Assignment shortcut example.
  i += j;
  printf ("i=%d\n", i);          // Prints 15

}

An example with bitwise operators (source file)

int main ()
{
  int a = 9;
  int b = 4;

  printf ("a&b = %d\n", a&b);          // Bitwise AND: Prints 0
  printf ("a|b = %d\n", a|b);          // Bitwise OR: Prints 13 
  printf ("a^b = %d\n", a^b);          // Bitwise EOR: Prints 13

  printf ("~a = %d\n", ~a);            // Complement: Prints -10
  printf ("b << 1 = %d\n", (b << 1));  // Left shift by 1: Prints 8
  printf ("b >> 2 = %d\n", (b >> 2));  // Right shift by 2: Prints 1
}

An example with boolean operators (source file)

int main ()
{
  double x = 5, y = 6, z = 6;
  int result;

  printf ("x < y = %d\n", (x < y));     // Prints 1 (true)
  printf ("x <= y = %d\n", (x <= y));   // Prints 1
  printf ("y > z = %d\n", (y > z));     // Prints 0 (false)
  printf ("y >= z = %d\n", (y >= z));   // Prints 1
  printf ("x == y = %d\n", (x == y));   // Prints 0
  printf ("y == z = %d\n", (y == z));   // Prints 1
  printf ("x != y = %d\n", (x != y));   // Prints 1

  result = (x < y);
  printf ("result=%d\n", result);       // Prints 1

  if (result == 1) {      // Equality comparison.
    printf ("x < y\n");
  }
  else {
    printf ("x >= y\n");
  }
  // Prints "x < y"
}
Note:
  • In C, boolean operators result in 1 (indicating "true") or 0 (indicating "false").

  • These results can be assigned to int-compatible variables.

Boolean types:

  • C originally did not provide any boolean types.

  • There are now four ways of working with boolean's:
    1. Use the original approach of 1 and 0:
      int main ()
      {
        double x = 5, y = 6;
      
        int result = (x < y);
        if (result == 1) { 
          printf ("x < y\n");
        }
        else {
          printf ("x >= y\n");
        }
      }
          

    2. Define your own boolean types using the pre-processor:
      #define boolean int
      #define true 1
      #define false 0
      
      int main ()
      {
        double x = 5, y = 6;
      
        boolean result = (x < y);
      
        if (result == true) { 
          printf ("x < y\n");
        }
        else {
          printf ("x >= y\n");
        }
      }
          

    3. Use a recent addition to the standard C library:
      #include <stdio.h>
      #include <stdbool.h>
      
      int main ()
      {
        double x = 5, y = 6;
      
        bool result = (x < y);
      
        if (result == true) { 
          printf ("x < y\n");
        }
        else {
          printf ("x >= y\n");
        }
      }
          

    4. Use the new C99 _Bool type:
      #include <stdio.h>
      #include <stdbool.h>
      
      int main ()
      {
        double x = 5, y = 6;
      
        _Bool result = (x < y);
      
        if (result == true) { 
          printf ("x < y\n");
        }
        else {
          printf ("x >= y\n");
        }
      }
          

  • We recommend using the stdbool package, as shown in (3) above.


Strings

Strings in C:

  • There is limited support for strings in C.

  • There is no separate string type.

  • Strings are simply a sequence of char's where the last character is the special character \0.

  • String variables are declared as pointers to type char.
Example: (source file)
int main ()
{
  char *hStr = "hello";     // String initialization.
  char *wStr = "world";
  char ch;

  // Note the use of "%s"
  printf ("%s %s\n", hStr, wStr);

  printf ("3rd char in wStr is %c\n", *(wStr+2));     // Prints 'r'

  // Let's get the 6-th char: should be '\0'
  ch = *(wStr+5);
  if (ch == '\0') {
    printf ("char is string terminator\n");
  }
  else {
    printf ("char is not string terminator\n");
  }
}
Note:
  • Although not visible, the character immediately after the last real character in a string is the "null" character \0.

  • Strings are enclosed in double-quotes.

  • String variables are declared as char * variables.

  • Strings can be printed using the %s print-format specifier.


Constants

There are two ways of defining constants in C:

  • The simple and traditional way is to use the preprocessor:
    #define PI 3.14159
    
    int main ()
    {
      double radius = 5.0;
    
      printf ("Area of Circle with radius %lf is %lf\n", radius, PI*radius*radius);
    }
      

  • Another way is to use the const modifier that makes a variable immutable:
    const double PI = 3.14159;
    
    int main ()
    {
      double radius = 5.0;
    
      printf ("Area of Circle with radius %lf is %lf\n", radius, PI*radius*radius);
    }
      
    The latter approach is more general, and can be applied to function parameters as well:
    const double PI = 3.14159;
    
    // "const" declaration makes it illegal for a function to 
    // change the input parameter.
    
    double computeArea (const double radius)
    {
      return PI * radius * radius;
    }
    
    int main ()
    {
      double radius = 5.0;
      printf ("Area of Circle with radius %lf is %lf\n", radius, computeArea(radius) );
    }
      


Control flow

An overview of C's control flow statements via examples: (source file)

int main ()
{
  int i = 1;

  // if-statement:
  if (i == 0) {
    printf ("i is zero\n");
  }


  // if-statement with compound expression:
  if ( (i >= -1) && (i <= 1) ) {
    printf ("-1 <= i <= 1\n");
  }


  // if-else combination
  if (i == 1) {
    printf ("one\n");
  }
  else if (i == 2) {
    printf ("two\n");
  }
  else {
    printf ("larger than two\n");
  }


  // Variation of if-else above:
  if (i == 1) {
    printf ("one\n");
  }
  else {
    if (i == 2) {
      printf ("two\n");
    }
    else {
      printf ("larger than two\n");
    }
  }


  // Equivalent switch statement:
  switch (i) {
    case 1: {
      printf ("one\n");
      break;
    }
    case 2: {
      printf ("two\n");
      break;
    }
    default: {
      printf ("larger than two\n");
    }
  }


  // for-loop example:
  printf ("Numbers 0 through 9: \n");
  for (i=0; i < 10; i++) {
    printf (" %d\n", i);
  }

  
  // while-loop equivalent:
  printf ("Numbers 0 through 9: \n");
  i = 0;
  while (i < 10) {
    printf (" %d\n", i);
    i++;
  }


  // do-while equivalent:
  printf ("Numbers 0 through 9: \n");
  i = 0;
  do {
    printf (" %d\n", i);
    i++;
  } while (i < 10);



  // Example of using "break"
  printf ("Numbers 0 through 9: \n");
  i = 0;
  while (1) {
    if (i == 10) {
      break;
    }
    printf (" %d\n", i);
    i++;
  }
  

  printf ("Odd numbers less than 10:\n");
  i = 0;
  while (1) {
    // If we've reached the limit, break out of the loop.
    if (i == 10) {
      break;
    }

    // If it's an even number, skip to next iteration of loop.
    if (i % 2 == 0) {
      i++;
      continue;
    }

    // Print odd number.
    printf (" %d\n", i);
    i++;
  }

}
Note:
  • Between a for-loop and a while-loop, use a while-loop only if the while-loop is easier to write.

  • Similarly, prefer a while-loop over an equivalent do-while.

  • ANSI C99 allows declaration of the for-loop variable in the for-loop:
        for (int i=0; i < 10; i++) {
          // ...
        }
        
    Versions prior to C99 do not. For compatibility with many textbooks, we will declare variables at the top.


Arrays

There are two ways of creating an array in C:

  • Declare an array with a static size.

  • Declare an array variable without a size, and allocate memory dynamically.
Let's look at an example: (source file)
int main ()
{
  int A[10];          // Statically-sized unidimensional array.
  double B[20][20];   // 2D array.

  int *A2 = NULL;     // Declaration of 1D array variable.
  double **B2 = NULL; // Declaration of 2D array variable.

  int i, j;           // For-loop variables.

  int sum;            // For use in examples.
  double dSum;      


  // Static example. The space is already allocated, so the
  // array can be used immediately.
  for (i=0; i < 10; i++) {
    A[i] = i * 100;                // Fill the array with some numbers.
  }

  sum = 0;
  for (i=0; i < 10; i++) {
    sum += A[i];                   // Compute the sum of array's numbers.
  }
  printf ("sum = %d\n", sum);      // Print sum.


  // Dynamic version of example using "malloc" to allocate space.
  // Note the use of the "sizeof" keyword.
  A2 = (int*) malloc (sizeof(int) * 10);
  for (i=0; i < 10; i++) {
    A2[i] = i * 100;               // Fill the array with some numbers.
  }

  sum = 0;
  for (i=0; i < 10; i++) {
    sum += A2[i];                  // Compute the sum of array's numbers.
  }
  printf ("sum = %d\n", sum);      // Print sum.



  // 2D example with static array.
  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      B[i][j] = i*j;               // Fill the array with some numbers.
    }
  }

  dSum = 0;
  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      dSum += B[i][j];             // Compute the sum of array's numbers.
    }
  }
  printf ("dSum = %lf\n", dSum);   // Print sum.

  
  // Dynamic version of 2D example. Note the two-step allocation.

  // Allocate space for 20 pointers-to-double
  B2 = (double **) malloc (sizeof(double*) * 20);
  for (i=0; i < 20; i++) {
    // For each such pointer, allocate a size-20 double array.
    B2[i] = (double*) malloc (sizeof(double) * 20);
  }

  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      B2[i][j] = i*j;              // Fill the array with some numbers.
    }
  }

  dSum = 0;
  for (i=0; i < 20; i++) {
    for (j=0; j < 20; j++) {
      dSum += B2[i][j];            // Compute the sum of array's numbers.
    }
  }
  printf ("dSum = %lf\n", dSum);   // Print sum.

  // Must free allocated memory when not needed anymore:
  free (A2);
  free (B2);

}
Note:
  • Although ANSI C99 allows declaration of for-loop variables in the for-statement, our example is compatible with earlier versions of C.

  • Dynamic memory allocation is done using malloc and is returned using free.

  • Consider the first example:
      A2 = (int*) malloc (sizeof(int) * 10);
        
    Note:
    • The variable A2 is declared as a pointer to int:
        int *A2 = NULL;     // Declaration of 1D array variable.
              
    • The argument to malloc is the total amount of space (in bytes) needed.
    • We need space for 10 integers.
    • Since we don't know how much space an int takes up, we get the current system's size-of-an-int by using the sizeof operator.
    • Thus, an alternative way of writing the same code is:
             spaceNeededInBytes = sizeof(int) * 10;
             A2 = (int*) malloc (spaceNeededInBytes);
             
    • Finally, note that malloc's return value is declared as void* (i.e., pointer to anything). It must be cast as the desired pointer:
        A2 = (int*) malloc (sizeof(int) * 10);
             

  • Consider the second example:
      B2 = (double **) malloc (sizeof(double*) * 20);
      for (i=0; i < 20; i++) {
        B2[i] = (double*) malloc (sizeof(double) * 20);
      }
        
    Note:
    • The first statement allocates an array of pointers (to double):
        B2 = (double **) malloc (sizeof(double*) * 20);
              
    • We need space for 20 double-pointers.
    • Since we don't know the size of a pointer-to-double, we use sizeof(double*).
    • Note that malloc's return value (a pointer) must be cast to the type of B2.
    • Once we've allocated the array of pointers, each of those pointers must be made to point to an array of double's:
        for (i=0; i < 20; i++) {
          B2[i] = (double*) malloc (sizeof(double) * 20);
        }
              

  • Consider what it means to access B2[4][6]:
    • B2[4] is the 5-th element in the array pointed to by B2.
    • This 5-th element is itself a pointer - to an array of double's.
    • Thus, B2[4][6] is the 7-th element in the array pointed to by B2[4].
In-Class Exercise 2.5: Draw the "memory picture" for arrays B and B2 in the example above, just before the free() function calls are executed. That is, draw a picture with sample memory addresses that shows how these arrays are located in memory.
In-Class Exercise 2.6: Create a 2D array to store and print Pascal's triangle. Sample output:
    1 1 
   1 2 1 
  1 3 3 1 
 1 4 6 4 1