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.