IC221: Systems Programming (SP15)


Home Policy Calendar

Lecture 07: Double Arrays, Command Line Arguments, and Error Checking

Table of Contents

1 Double Arrays

We continue our discussion of data types in C by looking at double arrays, which is an array of arrays. This will lead directly to command line arguments and environment variables as these are processed as an array of strings, which are arrays.

1.1 Declaring Double Arrays

Like single arrays, we can declare double arrays using the [ ], but with two. We can also do static declarations of values with { }. Here's an example:

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

int main(int argc, char *argv[]){

  int darray[][4] = { {0, 0, 0, 0},
                       {1, 1, 1, 1},
                       {2, 2, 2, 2},
                       {3, 3, 3, 3}};
  int i,j;

  for(i=0;i<4;i++){
    printf("darray[%d] = { ",i);
    for(j=0;j<4;j++){
      printf("%d ",darray[i][j]); //<---
    }
    printf("}\n");
  }


}
aviv@saddleback: demo $ ./int_doublearray 
darray[0] = { 0 0 0 0 }
darray[1] = { 1 1 1 1 }
darray[2] = { 2 2 2 2 }
darray[3] = { 3 3 3 3 }

Each index in the array references another array. Like before we allow C to determine the size of the outer array when declaring statically. However, you must define the size of the inner arrays. This is because of the way the memory is allocated. While the array example above is square in size, the double array can be asymmetric.

1.2 The type of a double array

Let's think a bit more about what a double array really is given our understanding the relationship between pointers and arrays. For a single array, the array variable is a pointer that references the first item in the array. For a double array, the array variable references a reference that references the first item in the first array. Here's a visual of the stack diagram:

                           .---.---.---.---.
            .---.  _.----> | 0 | 0 | 0 | 0 |  <-- darray[0]
darray ---> | --+-'        '---'---'---'---'
            |---|          .---.---.---.---.
            | --+--------> | 1 | 1 | 1 | 1 |  <-- darray[1]
            |---|          '---'---'---'---'
            | --+-._       .---.---.---.---.
            |---|   '----> | 2 | 2 | 2 | 2 |  <-- darray[2]
            | --+-._       '---'---'---'---'
            '---'   '._    .---.---.---.---.
                       '-> | 3 | 3 | 3 | 3 |  <-- darray[3]
                           '---'---'---'---'               

If we follow the arrays, we see that the type of darray is actually a int **. That means, it is a pointer that references a memory address that stores another pointer that references a memory address of an integer. So when we say double array, we are also referring to double pointers.

To demonstrate this further, we can even show the dereferences directly.

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

int main(int argc, char *argv[]){

  int darray[][4] = { {0, 0, 0, 0},
                       {1, 1, 1, 1},
                       {2, 2, 2017, 2},
                       {3, 3, 3, 3}};

  printf("*(*(darray+2)+2) = %d\n", *(*(darray+2)+2));
  printf("     daray[2][2] = %d\n", darray[2][2]);
}
aviv@saddleback: demo $ ./derefernce_doublearray 
*(*(darray+2)+2) = 2017
     daray[2][2] = 2017

As you can see it takes two dereferences to get to the integer value.

1.3 Array of Strings as Double Arrays

Now let us consider another kind of double array, an array of strings. Recall that a C string is just an array of characters, so an array of strings is a double array of characters. One of the tricky parts of string arrays is the typing declaration.

Before we declared arrays using the following notation:

char str1[] = "This is a string";

But now we know that the type of arrays and pointers are really the same, so we can also declare a string this way:

char * str2 = "This is also a string";

Note that there is a difference between these two declarations in how and where C actually stores the string in memory. Consider the output of this program:

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

int main(int argc, char *argv[]){

  char str1[] = "This is a string";
  char * str2 = "This is also a string";

  printf("str1: %s \t\t &str1:%p\n",str1,str1);
  printf("str2: %s \t &str2:%p\n",str2,str2);

}
aviv@saddleback: demo $ ./string_declare 
str1: This is a string 		 &str1:0x7fff4344d090
str2: This is also a string 	 &str2:0x4006b4

While both strings print fine as strings, the memory address of the two strings are very different. One is located in the stack memory region and other is in the data segment. In later lessons we will explore this further, but for the moment, the important takeaway is that we can now refer to strings as char * types.

Given we know that char * is the type of a string, then array of char *'s is an array of strings.

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

int main(int argc, char *argv[]){

  char * strings[]={"Go Navy!",
                     "Beat Army!",
                     "Crash Airforce!",
                     "Destroy the Irish!"};
  int i;

  printf("strings: %p\n",strings);
  for(i=0;i<4;i++){
    printf("strings[%d]: '%s' %p\n",i,strings[i],strings[i]);
  }
}
aviv@saddleback: demo $ ./string_array 
strings: 0x7fff7af68080
strings[0]: 'Go Navy!' 0x400634
strings[1]: 'Beat Army!' 0x40063d
strings[2]: 'Crash Airforce!' 0x400648
strings[3]: 'Destroy the Irish!' 0x400658

Like before we can see that strings is a pointer that references a pointer to a char, but that's just an array of strings or a double array. Another thing you may notice is that the length and size of each of the strings is different. This is because the way the array is declared with char * as the type of the string rather than char [] which changes how the array is stored.

2 Command Line Arguments

Now that you have seen an array of strings, where else does that type appear? In the arguments to the main() function. This is part of the command line arguments and is a very important part of systems programming.

In your previous classes you have only accepted input from the user by reading from standard in using cin. While we will also use standard input, this class will require reading in more input from the user in the form of command line arguments. These will be used as basic settings for the program and are much more efficient than always reading in these settings from standard input.

You may recall that we already did some work with command line arguments. First we discussed the varied command line arguments for the UNIX command line utilities, like ls and cut and find. We also processed some command line arguments from

2.1 Understanding main() arguments

You may have notices that I have been writing main functions slightly differently then you have seen them before.

//
//argument ____.             ._____ argument
// count       |             |       variables 
//             v             v
 int main(int argc, char * argv[]);

The arguments to main correspond to the command line input. The first is the number of such arguments, and the second is a string array of the argument values. Here's an example that illuminates the arguments:

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

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

  for(i=0;i<argc;i++){
    printf("argv[%d] = %s\n",i,argv[i]);
  }
}
aviv@saddleback: demo $ ./print_args arg1 arg2 arg3 x y z adam aviv
argv[0] = ./print_args
argv[1] = arg1
argv[2] = arg2
argv[3] = arg3
argv[4] = x
argv[5] = y
argv[6] = z
argv[7] = adam
argv[8] = aviv

Looking at the program and its output, you can see that there is correspondence to the arguments provided and the index in the array. Its important to note that the name of the program being run is arg[0], which means that all programs have at least one argument, the name of the program. For example:

aviv@saddleback: demo $ ./print_args 
argv[0] = ./print_args

The name of the program is not compiled into the executable. It is instead passed as a true command line argument by the shell, which forks and executes the program. The mechanics of this will become clear later in the semester when we implement our own simplified version of the shell. To demonstrate this now, consider how the arg[0] changes when I change the name of the executable:

aviv@saddleback: demo $ cp print_args newnameofprintargs
aviv@saddleback: demo $ ./newnameofprintargs 
argv[0] = ./newnameofprintargs
aviv@saddleback: demo $ ./newnameofprintargs a b c d e f
argv[0] = ./newnameofprintargs
argv[1] = a
argv[2] = b
argv[3] = c
argv[4] = d
argv[5] = e
argv[6] = f

2.2 NULL Termination in args arrays

Another interesting construction of the argv array is that the array is NULL terminated much like a string is null terminated. The reason for this is so the OS can determine how many arguments are present. Without null termination there is no way to know the end of the array.

You can use this fact when parsing the array by using pointer arithmetic and checking for a NULL reference:

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

int main(int argc, char *argv[]){

  char ** curarg;
  int i;

  for( curarg=argv , i=0 ; //initialize curarg to argv array and i to 0
       *curarg != NULL;    //stop when curarg references NULL
       curarg++, i++){     //increment curarg and i

     printf("argv[%d] = %s\n", i, *curarg);
  }

}
viv@saddleback: demo $ ./print_args_pointer a b c d e
argv[0] = ./print_args_pointer
argv[1] = a
argv[2] = b
argv[3] = c
argv[4] = d
argv[5] = e

Notice that the pointer incrementing over the argv arrays is of type char **. Its a pointer to a string, which is itself an array of chars, so its a pointer to a pointer. (POINTERS ARE MADNESS!)

2.3 Basic Parsing of Command Line Arguments: atoi() and sscanf()

Something that you will often have to do when writing programs is parse command line arguments. The required error checking procedure can be time consuming, but it is also incredibly important for the overall user experience of your program.

Lets consider a simple program that will print a string a user specified number of times. We would like to execute it this way:

run_n_times 5 string

Where string is printed n times. Now, what we know about command line arguments is that they are processed as strings, so both string and 5 are strings. We need to convert "5" into an integer 5.

There are two ways to do this. The first is atoi() which converts a string to a number, but looking at the manual page for atoi() we find that atoi() does not detect errors. For example, this command line arguments will not be detected:

run_n_times notanumber string

Executing, atoi("notanumber") will return 0, so a simple routine like:

int main(int argc, char * argv[]){
  int i;
  int n = atoi(argv[1]); //does not detect errors

  for(i=0;i<n;i++){
    printf("%s\n",argv[2]);
  }
}

will just print nothing and not return the error. While this might be reasonable in some settings, but we might want to detect this error and let the user know.

Instead, we can convert the argv[1] to an integer using scanf(), but we have another problem. We have only seen scanf() in the concept of reading from standard input, but we can also have it read from an arbitrary string. That version of scanf() is called sscanf() and works like such:

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

  if( sscanf(argv[1],"%d", &n) == 0) ){
    fprintf(stderr, "ERROR: require a number\n");
    exit(1); //exit the program
  }

  for(i=0;i<n;i++){
    printf("%s\n",argv[2]);
  }
}

Recall that scanf() returns the number of items that successfully match the format string. So if no items match, then the user did not provide a number to match the %d format. So this program successfully error checks the first argument. But, what about the second argument? What happens when we run with these arguments?

./run_n_times 5

There is no argv[2] provided and worse because the argv array is NULL terminated, argv[2] references NULL. When the printf() derferences argv[2] it wlll cause a segmentation fault. How do we fix this? We also have to error check the number of arguments.

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

  if(argc < 2){
  fprintf(stderr, "ERROR: invalid number of arguments\n");
    exit(1); //exit the program
  }

  if( sscanf(argv[1],"%d", &n) == 0) ){
    fprintf(stderr, "ERROR: require a number\n");
    exit(1); //exit the program
  }

  for(i=0;i<n;i++){
    printf("%s\n",argv[2]);
  }
}

And now we have properly error checked user arguments to this simple program. As you can see, error checking is tedious but incredibly important.