CSC161 2011S Imperative Problem Solving

Notes on Exam 2: Intermediate C

Preliminaries

I still saw some problems with what I might call general issues of code cleanliness. Make sure to comment code well. Make sure to choose good variable names. Look at your printout to make sure that it's readable.

Beyond the basic formatting issues, there are some important stylistic issues to consider. One important one is that you should strive to avoid duplicated code. If you find that you're writing code with cut-paste-change, see if you can write a helper procedure to factor out common code.

I would also like to see you spend more time organizing your files. In particular, Makefiles make my life much easier when I'm testing your code. (Makefiles with default targets make my life even easier.)

Problems

Problem 01: What Does It Do?

Topics: Code reading, Commenting, Formatting

Hannah and Harvey Hacker don't particularly like all the extra stuff they think I make them do when writing code. In particular, they don't see the point to clear variable names, standard formatting, or comments. They say Our goal is to write code that works efficiently. Why should we care about the rest?

Recently, they sent me the following code. As you may note, they've been a bit aggressive in avoiding my recommendations.


void s (int x[], int y, int l[], int k, int r[], int q) { int i; for
(i = 0; i < k; i++) l[i] = x[i]; for (i = k; i < y; i++) r[i-k] = x[i];
} void m (int x[], int y, int l[], int k, int r[], int q) { int a = 0;
int b = 0; int c = 0; while ((a < k) && (b < q)) x[c++] = (l[a] < r[b])
? l[a++] : r[b++]; while (a < k) x[c++] = l[a++]; while (b < q) x[c++] =
r[b++]; } int h (int x[], int y) { int k = y/2; int q = y - k; int l[k];
int r[q]; if (y <= 1) return y; s (x, y, l, k, r, q); h (l, k); h (r,
q); m (x, y, l, k, r, q); return k + q; }


They've even provided a header file (even though they don't use it).


#ifndef __HHH_H__
#define __HHH_H__

int h(int x[], int y);

#endif // __HHH_H__


Rewrite the code so that it meets my standards. That is, reformat it, choose good names for variables and functions, use mnemonic constants when appropriate, and add comments to help the reader.

You may find the following command-line tester helpful.


/**
 * hhh-ui - a simple UI for the strange code produced by H & H H.
 */

// +---------+--------------------------------------------------------
// | Headers |
// +---------+

#include <stdio.h>
#include <stdlib.h>
#include "hhh.h"


// +---------+--------------------------------------------------------
// | Helpers |
// +---------+

void
print_ints (int nums[], int size)
{
  int i;        // Index variable

  // Special case
  if (size == 0)
    {
      printf ("{}");
      return;
    }

  printf ("{%d", nums[0]);
  for (i = 1; i < size; i++)
    printf (",%d", nums[i]);
  printf ("}");
} // print_ints


// +------+-----------------------------------------------------------
// | Main |
// +------+

int 
main (int argc, char *argv[])
{
  int i;                // Everyone's favorite index
  int size = argc-1;    // The size of the number array
  int nums[size];       // A bunch of numbers
  int result;           // The result of h.

  // Convert arguments to numbers
  for (i = 0; i < size; i++)
    nums[i] = atoi (argv[i+1]);

  // Print preliminary information
  printf ("Original array: ");
  print_ints (nums, size);
  printf ("\n");
  printf ("Size:           %d\n", size);

  // Do the work
  result = h (nums, size);
 
  // Print the results
  printf ("Result:         %d\n", result);
  printf ("Final array:    ");
  print_ints (nums, size);
  printf ("\n");

  // And we're done
  return 0;
} // main



A Solution

Some quick experimentation suggests that h is a sorting algorithm. Hence, h should be renamed to something like sort or, preferably, the name of the sorting routine that it implements.

What sorting routine does it implement? The first helper function, s, copies parts of the array into two other arrays. After splitting, h recurses on those two subarrays. Finally, it calls m to combine those two subarrays. The first helper might then be called split and the second helper might be called merge. As you should recall from your study of sorting in CSC 151, this routine is mergesort.

So, here's the commented code.


/**
 * mergesort.c - An implementation of the legendary merge sort routine.
 */

/**
 * Procedure:
 *   split
 * Parameters:
 *   nums, an array of integers
 *   size, an integer.
 *   left, an array of integers
 *   lsize, an integer
 *   right, an array of integers
 *   rsize, an integer
 * Purpose:
 *   Split the first size elements nums into two arrays, left and right.
 * Produces:
 *   [Nothing; Called for the side effects.]
 * Preconditions:
 *   nums contains at least size elements.
 *   left contains at least lsize elements.
 *   right contains at least rsize elements.
 *   size = lsize + rsize.
 * Postconditions:
 *   nums is unmodified
 *   nums[0..size-1] is a permutation of the combination of
 *     left[0..lsize-1] and right[0..rsize-1]
 */
void
split (int nums[], int size, int left[], int lsize, int right[], int rsize)
{
  int i;
  // Copy elements into the left half
  for (i = 0; i < lsize; i++)
    left[i] = nums[i];
  // Copy elements into the right half
  for (i = lsize; i < size; i++)
    right[i-lsize] = nums[i];
} // split

/**
 * Procedure:
 *   merge
 * Parameters:
 *   nums, an array of integers
 *   size, an integer.
 *   left, an array of integers
 *   lsize, an integer
 *   right, an array of integers
 *   rsize, an integer
 * Purpose:
 *   Merge two sorted arrays into a larger sorted array.
 * Produces:
 *   [Nothing; Called for the side effects.]
 * Preconditions:
 *   nums contains at least size elements.
 *   left contains at least lsize elements.
 *   right contains at least rsize elements.
 *   size = lsize + rsize.
 *   The elements in left are in order.  That is, left[i] <= left[i+1]
 *     for all reasonable i.
 *   The elements in right are in order.  That is, right[i] <= right[i+1]
 *     for all reasonable i.
 * Postconditions:
 *   left is unmodified
 *   right is unmodified
 *   nums[0..size-1] is a permutation of the combination of
 *     left[0..lsize-1] and right[0..rsize-1]
 *   The elements in nums are in order.  That is, nums[i] <= nums[i+1]
 *     for all reasonable i.
 */
void
merge (int nums[], int size, int left[], int lsize, int right[], int rsize)
{
  int lp = 0;	// Pointer into the left array
  int rp = 0;	// Pointer into the right array
  int np = 0;	// Pointer into main array

  // While elements remain in both subarrays
  while ((lp < lsize) && (rp < rsize))
    // Copy the smallest remaining element into nums
    nums[np++] = (left[lp] < right[rp]) ? left[lp++] : right[rp++];

  // Copy all remaining elements from left into nums
  while (lp < lsize)
    nums[np++] = left[lp++];

  // Copy all remaining elements from right into nums
  while (rp < rsize)
    nums[np++] = right[rp++];
} // merge

/**
 * Procedure:
 *   merge_sort
 * Parameters:
 *   nums, an array of integers
 *   size, an integer.
 * Purpose:
 *   Sort nums using the merge-sort algorithm.
 * Produces:
 *   result, the size of the array.  (A pointless value)
 * Preconditions:
 *   nums contains at least size elements.
 * Postconditions:
 *   nums is a permutation of the original array.
 *   The elements in nums are in order.  That is, nums[i] <= nums[i+1]
 *     for all reasonable i.
 */
int
merge_sort (int nums[], int size)
{
  int lsize = size/2;		// The size of the left half
  int rsize = size - lsize;	// The size of the right half
  int left[lsize];		// The left half
  int right[rsize];		// The right half

  // Arrays with 0 or 1 elements are already sorted.
  if (size <= 1)
    return size;

  // Copy elements into the two subarrays
  split (nums, size, left, lsize, right, rsize);

  // Sort the two subarrays
  merge_sort (left, lsize);
  merge_sort (right, rsize);

  // And join them back together
  merge (nums, size, left, lsize, right, rsize);

  // We're done
  return size;
} // merge_sort


Notes

Please make sure that comments precede the code that they comment on. (The one exception is variables, which we comment to the right.) It should be possible for someone to understand the main ideas of your algorithm by scanning down through the comments. (Ideally, that's also how you write your code: First you write your six P's and unit tests, then you put comments in the body to explain the structure of the algorithm, then you write the code.)

In general, you should use slash-slash comments within your code and slash-star comments before procedures. Slash-slash comments encourage you to write less (which is good; internal comments should be short). Also, if you write most of your internal comments using slash-slash comments, you can comment out a chunk of code with slash-star comments.

While many of you figured out what the code did, you had difficulty choosing good names. Since the outer routine (h) was the one that was called, it should have been named sort. Many of you named the merge routine sort, which is inaccurate.

Problem 02: Testing the Testers

Topics: Testing, Binary search, Sorting

Bif and Bitsie Binsearch have been studying binary search and the ways in which we might use it. They note that

In order for binary search to work, the array must be in order from smallest to largest.

Being good students, Bif and Bitsie then write a function to check this condition.

/**
 * Procedure:
 *   is_in_order
 * Parameters:
 *   values[], an array of integers
 *   size, an integer
 * Purpose:
 *   Determine if the first size values in values are in order.
 * Produces:
 *   in_order, an integer that represents a Boolean
 * Preconditions:
 *   0 <= size <= length of values
 * Postconditions:
 *   If values[i] <= values[i+1] for all reasonable i, then
 *     in_order is true (1).
 *   Otherwise, in_order is false (0).
 */
int
is_in_order (int values[], int size)
{
  int i;
  for (i = 1; i < size; i++)
    if (values[i] >= values[i+1])
      return 0;
  return 1;
} // is_in_order

Their colleagues, Terry and Terry Tester suggest that while this function will make some things easier, if is_in_order is not implemented correctly, then it's pointless to have it, and it won't ensure that binary search can be applied. (Of course, you would rarely want to spend the effort checking if the array is in order, since that's much more expensive than binary search, but, hey, it's still an important function.)

How do we determine if there are errors? We write unit tests. What do we do if the unit tests fail? We try to figure out why they've failed.

a. Write unit tests for is_in_order.

b. Correct any errors that your unit tests revealed.

You should turn in two files: A file with the revised version of is_in_order and a file with the unit tests.

A Solution

We'd like to think about a variety of things when testing: Different sized arrays, different kinds of values, etc. Since we have access to the source code, we may have to less testing than we might otherwise (e.g., because we can tell from inspection that the sign of numbers seems unimportant).

We also know that some forms of systematic testing are useful. For example, we might test every permutation of some size array. (Code to automatically generate permutations would be good, but I'm too lazy to write that.)

So, here's my testing code.


/**
 * iio-utest: A simple set of unit tests for is_in_order.
 */

// +---------+--------------------------------------------------------
// | Headers |
// +---------+

#include "is-in-order.h"
#include <stdio.h>
#include <limits.h>


// +---------+--------------------------------------------------------
// | Globals |
// +---------+

int tests = 0;		// How many tests we've conducted.
int errors = 0;		// How many errors wev've encountered


// +---------+--------------------------------------------------------
// | Helpers |
// +---------+

/**
 * Procedure:
 *   print_array
 * Parameters:
 *   a, an array of integers
 *   size, an integer
 * Purpose:
 *   Print out the first size elements of the array in human-readable 
 *   form (more or less).  Does not print a final carriage return.
 * Produces:
 *   [Nothing; Called for the side effect]
 * Preconditions:
 *   a has at least size elements.
 * Postconditions:
 *   a has not been modified.
 */
void
print_array (int a[], int size)
{
  int i;
  if (size == 0)
    {
      printf ("{}");
      return;
    }
  printf ("{%d", a[0]);
  for (i = 1; i < size; i++)
    printf (",%d", a[i]);
  printf ("}");
} // print_array

/**
 * Procedure:
 *   test_iio
 * Parameters:
 *   a, an array
 *   size, an integer
 *   expected, an integer
 * Purpose:
 *   One test of the is_in_order procedure.  Calls is_in_order on the
 *   array and compares the result to expected, updating information for
 *   testing.
 * Produces:
 *   [Nothing; Called for the side effect]
 * Preconditions:
 *   See preconditions for is_in_order.
 * Postconditions:
 *   a is unmodified.
 *   Global variables tests and errors may be updated.
 */
void
test_iio (int a[], int size, int expected)
{
  // Increment the number of tests
  ++tests;

  // Run the procedure
  int result = is_in_order (a, size);

  // If we get an unexpected result
  if (result != expected)
    {
      // Tell the user and update our count of errors
      ++errors;
      printf ("ERROR in is_in_order (");
      print_array (a, size);
      printf (", %d) EXPECTED: %d, RECEIVED %d\n", size, expected, result);
    } // if (result != expected)
} // test


// +------+-----------------------------------------------------------
// | Main |
// +------+

int
main ()
{
  // Size 0 arrays
  int a0a[0];
  test_iio (a0a, 0, 1);

  // Size 1 arrays
  int a1a[1] = { 1 };
  test_iio (a1a, 1, 1);
  int a1b[1] = { 2 };
  test_iio (a1b, 1, 1);
  int a1c[1] = { -1 };
  test_iio (a1c, 1, 1);

  // Size 2 arrays
  int a2a[2] = { 0, 1 };
  test_iio (a2a, 2, 1);
  int a2b[2] = { 1, 0 };
  test_iio (a2b, 2, 0);
  int a2c[2] = { 0, 0 };
  test_iio (a2c, 2, 1);

  // Size 3 arrays
  int a3a[3] = { 1, 2, 3 };
  test_iio (a3a, 3, 1);
  int a3b[3] = { 2, 1, 3 };
  test_iio (a3b, 3, 0);
  int a3c[3] = { 1, 3, 2 };
  test_iio (a3c, 3, 0);
  int a3d[3] = { 3, 1, 2 };
  test_iio (a3d, 3, 0);
  int a3e[3] = { 2, 3, 1 };
  test_iio (a3e, 3, 0);
  int a3f[3] = { 3, 2, 1 };
  test_iio (a3f, 3, 0);

  // A few interesting bigger arrays
  int a4a[4] = { -3, -2, -1, 0 };	        // Negative numbers
  test_iio (a4a, 4, 1);
  int a4b[4] = { 0, 0, 0, 0 };		        // All duplicates
  test_iio (a4b, 4, 1);
  int a5a[5] = { INT_MIN, INT_MIN/2, 0, INT_MAX/2, INT_MAX };
  test_iio (a5a, 5, 1);
  int a8a[8] = { 0, 0, 1, 1, 1, 2, 3, 3 };	// Some duplicates
  test_iio (a8a, 8, 1);
  int a8b[8] = { 0, 0, 1, 2, 1, 2, 3, 3 };	// Some duplicates, mid error
  test_iio (a8b, 8, 0);
  int a8c[8] = { 5, 0, 1, 1, 1, 2, 3, 3 };	// Some duplicates, left error
  test_iio (a8c, 8, 0);
  int a8d[8] = { 0, 0, 1, 1, 1, 2, 3, 0 };	// Some duplicates, rt error
  test_iio (a8d, 8, 0);

  // Print the results
  printf ("%d tests conducted.\n", tests);
  printf ("%d errors encountered.\n", errors);

  // And we're done
  return errors;
} // main


Here's the output

ERROR in is_in_order ({0,1}, 2) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({1,2,3}, 3) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({0,0,0,0}, 4) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({-2147483648,-1073741824,0,1073741823,2147483647}, 5) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({0,0,1,1,1,2,3,3}, 8) EXPECTED: 1, RECEIVED 0
20 tests conducted.
5 errors encountered.

So, our code is broken. We note that some of the instances with duplicate values failed. Why? Let's turn to the original. Well, it says

   if (values[i] >= values[i+1])
      return 0;

That means if a value is equal to the next value, we fail. That's easy enough to fix. What happens?

ERROR in is_in_order ({0,1}, 2) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({1,0}, 2) EXPECTED: 0, RECEIVED 1
ERROR in is_in_order ({1,2,3}, 3) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({0,0,0,0}, 4) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({-2147483648,-1073741824,0,1073741823,2147483647}, 5) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({0,0,1,1,1,2,3,3}, 8) EXPECTED: 1, RECEIVED 0
20 tests conducted.
6 errors encountered.

Yes, it's always comforting when we try to fix code and the number of errors increases. So, what else could be wrong? Let's look at the bounds of the loop.

  for (i = 1; i < size; i++)

Why are we starting at 1? Maybe we should start at 0 so that we compare values[0] to values[1]. Do we get anything better?

ERROR in is_in_order ({2}, 1) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({0,1}, 2) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({1,2,3}, 3) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({0,0,0,0}, 4) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({-2147483648,-1073741824,0,1073741823,2147483647}, 5) EXPECTED: 1, RECEIVED 0
ERROR in is_in_order ({0,0,1,1,1,2,3,3}, 8) EXPECTED: 1, RECEIVED 0
20 tests conducted.
6 errors encountered.

Still 6 errors, but some of them (okay, one of them) is different. The errors don't give us much clue, so it's time to look at the code a bit more. We'll start with the first error, an array of size 1.

  for (i = 0; i < size; i++)
    if (values[i] >= values[i+1])
      return 0;

Well, i is 0 and size is 1, so we compare values[0] to values[1]. Whoops! Two things are clearly wrong. First, we shouldn't be doing any comparisons for an array of size 1. Second, we're looking at a non-existant value, values[1]. The upper bound must be wrong. Let's change it to size-1.

20 tests conducted.
0 errors encountered.

Well, everything passed. We should do a more careful analysis to make sure that it's correct, but it's a good start.

Here's the final code.


#include "is-in-order.h"

int
is_in_order (int values[], int size)
{
  int i;
  for (i = 0; i < size-1; i++)
    if (values[i] > values[i+1])
      return 0;
  return 1;
} // is_in_order


Notes

In grading this, I looked at a few major issues. First, I looked to see whether you had found all of the errors. A good test suite is likely to have identified all of them. The goes too far error is a bit harder, so even a good test suite might have trouble with that. Next, I looked to see whether you were creative in your test suite. If you had thorough tests, I was more lenient on the goes too far error. If you had fewer tests, I was less lenient.

A number of you ignored the main idea of unit testing and printed output for every test. As we discussed, wading through pages of "This test passed" makes it much more likely that someone will miss a failed test. You should only print out (a) optional information on failed tests

A few of you put all of your tests in the same file as is_in_order. Remember ... By separating code into different files, we make it easier to reuse code.

Some of the nicer systematic tests I saw:

Problem 03: Floating-Point Exponents

Topics: IEEE floating-point representation, Bitwise operations

Flo and Flint Floater are having trouble understanding IEEE floating point representation and have come to your class mentor for help. In helping them, your mentor has decided that it would be helpful to extract the key parts of a floating point number (sign, exponent, significand).

Write a program that reads in a 32-bit floating-point number (a float) from the command line and prints out the sign, exponent, and significand.

% ./ieee-exponent 1
For float 1.000000, the sign is +, the exponent is 0, and the significand is 1.000000.
% ./ieee-exponent 2
For float 2.000000, the sign is +, the exponent is 1, and the significand is 1.000000.
% ./ieee-exponent 0.5
For float 0.500000, the sign is +, the exponent is -1, and the significand is 1.00000.

As you solve this problem, I would like you to use the following strategy.

To convert from float to int while preserving the bits, you can use the following procedure. (No, you need not understand how the procedure works.)

int
f2i (float f)
{
  return *((int *) &f);
} // f2i

A Solution

My solution is a bit more involved than most, in part because I tried to keep it as explicit as possible.

bit-utils.h


#ifndef __BIT_UTILS_H__
#define __BIT_UTILS_H__

/**
 * f2i - Convert a float to an integer with the same bit pattern.
 */
int f2i (float f);

/**
 * i2f - Convert an integer to a float with the same bit pattern.
 */
float i2f (int i);

/**
 * ith_bit (n,i) - Get the ith bit of n.
 * Pre: 0 <= i < 32
 */
int ith_bit (int n,  int i);

/**
 * zero_bit (n,i) - Zero the ith bit of n, returning the result.
 * Pre: 0 <= i < 32
 */
int zero_bit (int n, int i);

#endif // __BIT_UTILS_H__



bit-utils.c


/**
 * bit-utils.c - A variety of utilities for working with bitwise values.
 */

// +---------+--------------------------------------------------------
// | Headers |
// +---------+

#include "bit-utils.h"


// +--------------------+---------------------------------------------
// | Exported Functions |
// +--------------------+

int
f2i (float f)
{
  return *((int *) &f);
} // f2i

float
i2f (int i)
{
  return *((float *) &i);
} // i2f

int
ith_bit (int n, int i)
{
  return (n >> i) & 1;
} // ith_bit

int
zero_bit(n, i)
{
  // Our mask has 1's everywhere besides at the 0 position.
  int mask = ~(1 << i);
  return n & mask;
} // zero_bit


ieee.h


#ifndef __IEEE_H__
#define __IEEE_H__

/**
 * ieee.h - Lots of fun utilities for working with IEEE floating-point
 *   representation.
 */

/**
 * Get the sign from the bit representation of a float
 */
int ieee32_sign (float f);

/**
 * Get the exponent from the bit representation of a float.
 */
int ieee32_exponent (float f);

/**
 * Get the value of the significand, given a particular value for the hidden
 * bit.
 */
float ieee32_significand (float f, int hidden);

/**
 * Describe a floating-point number.
 */
void describe_float (float f);


#endif // __IEEE_H__


ieee.c


/**
 * ieee.c
 *   A library of functions for exploring IEEE floating point numbers.
 */

// +---------+--------------------------------------------------------
// | Headers |
// +---------+

#include <stdio.h>
#include <math.h>       // For pow

#include "ieee.h"       
#include "bit-utils.h"	


// +---------+--------------------------------------------------------
// | Helpers |
// +---------+

/**
 * Get the bits for the significand.
 */
int
ieee32_significand_bits (float f)
{
  return f2i (f) & 0x7FFFFF;	// That's 0000000 01111111 11111111 11111111
} // ieee32_significand_bits


// +--------------------+---------------------------------------------
// | Exported Functions |
// +--------------------+

int
ieee32_sign (float f)
{
  return ith_bit (f2i (f), 31);
} // ieee32_sign

int
ieee32_exponent (float f)
{
  int exponent;
  exponent = f2i (f);
  // Drop the left bit
  exponent = zero_bit (exponent, 31);
  // Shift bits over so that they're in the convenient place
  exponent = exponent >> 23;
  // Subtract the bias
  exponent = exponent - 127;
  // And we're done
  return exponent;
} // ieee32_exponent

float
ieee32_significand (float f, int hidden)
{
  int bits = ieee32_significand_bits (f); // Bits of significand
  float result = (float) hidden;        // Result we're computing
  int i;                                // The bit we're looking at
  float value = 0.5;                    // The value of that bit.

  for (i = 22; i >= 0; i--)
    {
      // If the bit is set, add its value
      if (ith_bit (bits, i))
        result += value;
      // Move to the next value
      value /= 2.0;
    } // for

  // And we're done
  return result;
} // ieee32_significand

float
ieee32_significand_hack (float f, int hidden)
{
  int bits = ieee32_significand_bits (f);
  if (hidden) 
    bits = bits | 1 << 23;
  return bits / pow (2, 23);
} // ieee32_significand_hack

void
describe_float (float f)
{
  int sign;             // The sign of the number
  int exponent;         // The exponent of the number
  float significand;    // The significand
  int small;            // Special case: Really small numbers

  // Print it
  printf ("Exploring floating point number %f:\n", f);

  // Get the sign
  sign = ieee32_sign (f);

  // Get the exponent
  exponent = ieee32_exponent (f);

  // Handle special values
  if (exponent == 128)
    {
      // Infinities
      if (ieee32_significand_bits (f) == 0)
        {
          if (sign)
            printf ("  Negative infinity\n");
          else
            printf ("  Positive infinity\n");
        } // infinite
      else
        printf ("Not a number\n");
      return;
    } // special values

  // Determine if it's small
  small = exponent == -127;

  // Get the significand
  significand = ieee32_significand (f, !small);

  // Special case: Small numbers
  if (exponent == -127)
    exponent = -126;

  printf ("  Sign: %s\n", sign ? "-" : "+");
  printf ("  Exponent: %d\n", exponent);
  printf ("  Significand: %f\n", significand);
  printf ("  Significand2: %f\n", ieee32_significand_hack (f, !small));
} // describe_float


ieee-info.c


/**
 * ieee-info num0 num1 num2 ...
 *   Given floating point numbers, print out information on their parts.
 */

// +---------+--------------------------------------------------------
// | Headers |
// +---------+

#include <stdio.h>	// For printf
#include <stdlib.h>	// For atof

#include "ieee.h"       // For all the fun IEEE utilities


// +------+-----------------------------------------------------------
// | Main |
// +------+

int
main (int argc, char *argv[])
{
  int i;	        // Still my favorite counter variable
  float f;	        // The float we're currently playing with

  // Describe each number provided on the command line
  for (i = 1; i < argc; i++)
    describe_float ((float) atof (argv[i]));

  // And we're done
  return 0;
} // main


ieee-examples.c


/**
 * ieee-examples
 *   Print out some examples of IEEE representation
 */

// +---------+--------------------------------------------------------
// | Headers |
// +---------+

#include <stdio.h>	// For printf
#include <stdlib.h>	// For atof

#include "ieee.h"       // For all the fun IEEE utilities
#include "bit-utils.h"	// For itof


// +------+-----------------------------------------------------------
// | Main |
// +------+

int
main (int argc, char *argv[])
{
  float f;
  int i;

  describe_float (1.0);
  describe_float (1.0/0.0);
 
  // A bunch of numbers
  f = 7.0;
  for (i = 0; i < 14; i++)
    { 
      describe_float (f);
      describe_float (-f);
      f *= 1115.0;
    }
  f = .0003;
  for (i = 0; i < 15; i++)
   {
     describe_float (f);
     describe_float (-f);
     f /= 1231;
   }

  // And we're done
  return 0;
} // main


Notes

Many of you did not handle the special cases. I did not penalize you for failing to handle them, but I did not the lack.

Many of you wrote much shorter code, taking advantage of C's ability to immediately parse the bits as an integer.

A few of you wrote extra code to compare your computed result to the input number. That's a nice idea.

A few of you put all the bits into an array for ease of access. If you can get them from the integer to put into the array, you might as well use the code you used to get them from the integer in the first place.

A number of you masked off the significand with some number like 8388607. That number is not very readable. On the other hand, 0x7fffff is quite readable for the last 23 bits are 1.

Problem 04: atoi, Revisited

Topics: Strings, Integers, Library functions

Pam and Pat Parser need to use the atoi function to solve some homework assignments. Since they feel their understanding of atoi is incomplete, they've written the following program.


/**
 * atoi args
 *   Applies atoi to each of its parameters and prints out the results.
 */

#include <stdio.h>
#include <stdlib.h>

int
main (int argc, char *argv[])
{
  int i;
  for (i = 1; i < argc; i++)
    printf ("'%s' => %d\n", argv[i], atoi (argv[i]));
  return 0;
} // main



When they run it, they get some output that doesn't make sense.

$ ./atoi 23 033 0x44 hello 111111111111
'23' => 23
'033' => 33
'0x44' => 0
'hello' => 0
'111111111111' => 2147483647

As this output suggests, atoi does not seem to work well with either octal or hexadecimal numbers. It also does not do well with integer overflow. And it gives no error message if there's an error.

Write a new version of atoi that resolves these problems. In particular, you should support octal and hexadecimal numbers. If an error occurs (e.g., overflow, invalid input) you should return INT_MIN and set the shared variable atoi_error.

Here's a revised version of the Parser's program that takes advantage of this new interface.


/**
 * atoi args
 *   Applies atoi to each of its parameters and prints out the results.
 */

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

extern char *atoi_error;

int
main (int argc, char *argv[])
{
  int i;
  int result;
  for (i = 1; i < argc; i++)
    {
      result = atoi (argv[i]);
      if (result == INT_MIN)
        printf ("Could not parse '%s' because '%s'\n", argv[i], atoi_error);
      else
       printf ("'%s' => %d\n", argv[i], result);
    } // for
  return 0;
} // main



And here's some simulated output.

$ ./atoi 23 033 0x44 hello 111111111111
'23' => 23
'033' => 27
'0x44' => 68
Could not parse 'hello' because 'not a number'
Could not parse '111111111111' because 'integer overflow'

A Solution


/**
 * atoi.c
 *   An improved atoi library.
 */

// +---------+--------------------------------------------------------
// | Headers |
// +---------+

#include <limits.h>        // For INT_MIN


// +---------+--------------------------------------------------------
// | Globals |
// +---------+

char *atoi_error = "";


// +---------+--------------------------------------------------------
// | Helpers |
// +---------+

/**
 * Determine whether a character is a base-10 digit.
 */
static int
is_digit_10 (char ch)
{
  switch (ch)
    {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      return 1;
    default:
      return 0;
    } // switch
} // is_digit_10

/**
 * Determine whether a character is a base 8 digit.
 */
static int
is_digit_8 (char ch)
{
  switch (ch)
    {
    case '8':
    case '9':
      return 0;
    default:
      return is_digit_10 (ch);
    }
} // is_digit_8 

/** 
 * Determine whether a character is a base-16 digit.
 */
static int
is_digit_16 (char ch)
{
  switch (ch)
    {
    case 'a':
    case 'b':
    case 'c':
    case 'd':
    case 'e':
    case 'f':
    case 'A':
    case 'B':
    case 'C':
    case 'D':
    case 'E':
    case 'F':
      return 1;
    default:
      return is_digit_10 (ch);
    }
} // is_digit_16

/**
 * Determine the value of a base-16 digit, without the evil K&R hacks.
 */
static int
digit_value_16 (char ch)
{
  switch (ch)
    {
    case '0': return 0;
    case '1': return 1;
    case '2': return 2;
    case '3': return 3;
    case '4': return 4;
    case '5': return 5;
    case '6': return 6;
    case '7': return 7;
    case '8': return 8;
    case '9': return 9;
    case 'a': return 10;
    case 'A': return 10;
    case 'b': return 11;
    case 'B': return 11;
    case 'c': return 12;
    case 'C': return 12;
    case 'd': return 13;
    case 'D': return 13;
    case 'e': return 14;
    case 'E': return 14;
    case 'f': return 15;
    case 'F': return 15;
    default: return -1;
    } // switch
} // digit_value_16

/**
 * Base 10 atoi.  Based on code from K&R.
 */
static int
atoi_10 (char *a)
{
  int result = 0;       // The computed result
  int sign = 1;         // The sign of the result
  int i=0;              // Counter variable for position
  int digit;		// The value of the current digit

  // Special case: Signed
  if (a[0] == '+')
    i = 1;
  else if (a[0] == '-')
    {
      i = 1;
      sign = -1;
    }

  // Special case: No digits
  if (a[i] == '\0')
    {
      atoi_error = "missing digits in base-10 number";
      return INT_MIN;
    }

  // Process remaining digits
  while (a[i] != '\0')
    {
      if (! is_digit_10 (a[i]))
        {
          atoi_error = "invalid digit in base-10 number";
          return INT_MIN;
        }
      digit = digit_value_16 (a[i]);
      if (result > (INT_MAX - digit) / 10)
        {
          atoi_error = "number too large/small";
          return INT_MIN;
        }
      result = result * 10 + digit;
      i++;
    } // while

  // Incorporate the sign and we're done.
  return result * sign;
} // atoi_10

/**
 * Base 8 atoi.  
 * Pre: a[0] = '0'
 */
static int
atoi_8 (char *a)
{
  int i = 1;            // Position in the string
  int result = 0;       // The computed result
  int digit;		// The value of the current digit

  // Process remaining digits
  while (a[i] != '\0')
    {
      if (! is_digit_8 (a[i]))
        {
          atoi_error = "invalid digit in base-8 number";
          return INT_MIN;
        }
      digit = digit_value_16 (a[i]);
      if (result > (INT_MAX - digit) / 8)
        {
          atoi_error = "number too large/small";
          return INT_MIN;
        }
      result = result * 8 + digit;
      i++;
    } // while

  // We're done
  return result;
} // atoi_8

/**
 * Base 16 atoi.  
 * Pre: a[0] = '0';
 *      a[1] = 'x' or a[1] = 'X'
 */
static int
atoi_16 (char *a)
{
  int i = 2;            // Position in the string
  int result = 0;       // The computed result
  int digit;		// The value of the current digit

  // Special case: No digits
  if (a[i] == '\0')
    {
      atoi_error = "missing digits in base-16 number";
      return INT_MIN;
    }

  // Process remaining digits
  while (a[i] != '\0')
    {
      if (! is_digit_16 (a[i]))
        {
          atoi_error = "invalid digit in base-16 number";
          return INT_MIN;
        }
      digit = digit_value_16 (a[i]);
      if (result > (INT_MAX - digit) / 16)
        {
          atoi_error = "number too large/small";
          return INT_MIN;
        }
      result = result * 16 + digit;
      i++;
    } // while

  // We're done
  return result;
} // atoi_16


// +--------------------+---------------------------------------------
// | Exported Functions |
// +--------------------+

int
atoi (char *a)
{
  // Special case: Empty string
  if (a[0] == '\0')
    {
      atoi_error = "empty string";
      return INT_MIN;
    }

  // Special cases: Begins with 0
  if (a[0] == '0')
    {
      // If it's just "0" ...
      if (a[1] == '\0')
        return 0;
      // If it's hex
      else if ((a[1] == 'x') || (a[1] == 'X'))
        return atoi_16 (a);
      //  Otherwise, it should be octal
      else
        return atoi_8 (a);
    } // begins with 0

  // Normal case
  return atoi_10 (a);
} // atoi 


Notes

A few of you discovered strtol. Unfortunately, strtol does not report errors when errors are appropriate.

There were a number of issues with failure to report all errors. I expected you to handle:

 

History

Sunday, 3 April 2011 [Samuel A. Rebelsky]

  • Wrote the majority of these notes.

Monday, 4 April 2011 [Samuel A. Rebelsky]

 

Disclaimer: I usually create these pages on the fly, which means that I rarely proofread them and they may contain bad grammar and incorrect details. It also means that I tend to update them regularly (see the history for more details). Feel free to contact me with any suggestions for changes.

This document was generated by Siteweaver on Mon Apr 4 10:46:44 2011.
The source to the document was last modified on Mon Apr 4 10:46:43 2011.
This document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CSC161/2011S/Exams/notes.02.html.

Samuel A. Rebelsky, rebelsky@grinnell.edu