IC221: Systems Programming (SP18)


Home Policy Calendar Units Assignments Resources

Unit 2: C Programming

Table of Contents

1 C Programming and Unix

In this course, all of our programming will be in C, and that's because C is a low level language, much closer to the hardware and Operating System then say C++ or Java. In fact, almost all modern operating systems are written in C including Unix, Linux, Mac OSX, and even Windows.

The C programming language, itself, was developed for the purpose of writing the original Unix operating system. It shouldn't be that surprising then, that if you want to learn how to write programs that interact with the Unix system directly, then those programs must be written in C. And, in so doing, the act of learning to program in C will illuminate key parts of the Unix system.

In many ways, you can view C as the lingua franc of programming; it's the language that every competent programmer should be able to program, even a little bit in. The syntax and programming constructs of C are present in all modern programming languages, and the concepts behind managing memory, data structures, and the like underpin modern programming. Humorously, while nearly all programmers know how to program in C, most try to avoid doing so because the same power that C provides as a "low level" language is what makes it finicky and difficult to deal with.

2 C Programming Preliminaries

First: YOU ALREADY KNOW C! That's because you've been programming C in your previous class since C is a subset of the C++ language. Not all of the programs you wrote are valid C programs, but the structure and syntax are the same. If you were to look at a C program, you'd probably understand it to some extent, but there are a few things that C++ has that C does not; however most are the same. For example:

  • Conditionals: Use if and else with the same syntax
  • Loops: Use while and for loops with the same syntax
  • Basic Types: Use int, float, double, char
  • Variable Declaration: Still must declare your variables and types
  • Functions: function declaration is the same
  • Arrays and Pointers: Memory aligned sequences of data and references to that data

The big differences between C and C++ is:

  • No namespace: C doesn't have a notion of namespace, everything is loaded into the same namespace.
  • No objects or advanced types: C does not have advanced types built in, this includes string. Instead, strings are null terminated arrays of char's. But you can create more advanced data types like structs, but they also have slightly different properties.
  • No function overloading: Even functions with different type declarations, that is, take different types of input and return different types, cannot share the same name. Only the last declaration will be used.
  • All functions are pass-by-value: You cannot declare a function to take a reference, e.g., void foo(int &a). Instead, you must pass a pointer value.
  • Different Structures: Structures in C use a different syntax and are interpreted differently.
  • Variable Scoping: Variable deceleration is tightly scoped to code blocks, and you must declare variables prior to the block to use them. For example, for(int i, ....) is not allowed in C. Instead, you must declare i prior to the start of the for loop.

While clearly, the two programming languages, C++ and C, are different, they are actually more alike then different. In fact, C is a subset of C++, which means that any program you write in C is also a C++ program. There are often situations, when programming in C++ is not your best choice for completing the task while using C libraries are. This is particularly relevant whenever you need to accomplish system related tasks, such as manipulating the file system or creating new processes. However, most programs you write in C++ are not C programs.

2.1 Hello World

When learning any programming language, you always start with the "Hello World" program. The way in which your program "speaks" says a lot about the syntax and structure of programming in that language. Below is the "Hello World" program for C++ and C, for comparison.

/*hellowrold.cpp*/
#include <iostream>

using namespace std;

// Hello World in C++
int main(int argc, char * argv[]){
  cout << "Hello World" << endl;
}
/*helloworld.c*/
#include <stdio.h>

// Hello World in C
int main(int argc, char * argv[]){
  printf("Hello World\n");
}

To begin, each of the programs has a #include, which is a compiler directive to include a library with the program. Both of the include statements ask the compiler to include the I/O library. While in C++ this was the iostream library, in C, the standard I/O library is stdio.h. The .h refers to a header file, which is also a C program that library or auxiliary information is generally stored.

2.2 Compiling a C program

The compilation process for C is very similar to that of C++, but we use a C compiler. The standard C compiler on Unix system is gcc, the gnu C compiler. For example, to compile helloworld.c, we do the following.

#> gcc helloworld.c

Which will produce an executable file a.out, which we can run

#> ./a.out
Hello World

If we want to specify the name of the output file, you use the -o option.

#> gcc helloworld.c -o helloworld
#> ./helloworld
Hello World

There are more advanced compilation techniques that we will cover in lab, such as including multiple files, compiling to object files, and using pre-compiler directores.

2.3 Includes

The process of including libraries in your program looks very similar to that of C++, and uses the include statement. Note, all C libraries end in .h, unlike C++. Here are some common libraries you will probably want to include in your C program:

  • stdlib.h : The C standard library, contains many useful utilities, and is generally included in all programs.
  • stdio.h : The standard I/O library, contains utilities for reading and writing from files and file streams, and is generally included in all programs.
  • unistd.h : The Unix standard library, contains utilities for interacting with the unix system, such as system calls
  • sys/types.h : System types library, contains the definitions for the base types and structures of the unix system.
  • string.h : String library, contains utilities for handling C strings.
  • ctype.h : Character library, contains utilities for handing char conversions
  • math.h : Math library, contains basic math utility functions.

When you put a #include <header.h> in your program, the compiler will search for that header in its header search path. The most common location is in /usr/include. However, if you place your filename to include in quotes:

#include "header.h"

The compiler will look in the local directory for the file and not the search path. This will become important when we start to develop larger programs.

2.4 Control Flow

The same control flow you find in C++ is present in C. This includes if/else statements.

if( condition1 ){
  //do something if condition1 is true
}else if (condition2){
  //do something if condition1 is false and condition2 is true
}else{
  //do this if both condition1 and condition2 is true
}

While loops:

while( condition ){
  //run this until the condition is not true
}

And, for loops:

//run init at the start
for ( init; condition; iteration){
  //run until condition is false preforming iteration on each loop.
}

In previous versions of C, you were not able to declare new variables within the for loop. However, as of last year, with the latest standard, rejoice! You can now do the following without error:

for(int i=0; i < 10; i++){
  printf("%d\n",i); //i scoped within this loop
}

The declaration of the variable i exists within the scoping of the loop. Referring to i outside of the loop is an error, and if you declared a different i outside the loop, actions within the loop would not affect the outer declaration. For example:

int i=3;
for(int i=0;i<100;i++){
   printf("%d\n",i); //prints 0 -> 99
}
printf("%d\n",i); //prints 3 --- different i, different scope!

Essentially, it works the same as C++. However, as old habits die hard, throughout these notes, the old C style may be used, but know that the new standard applies here.

2.5 True and False

C does not have a boolean type, that is, a basic type that explicitly defines true and false. Instead, true and false are defined for each type where 0 or NULL is always false and everything else is true. All basic types can be used as a condition on its own. For example, this is a common form of writing an infinite loop:

while(1){
  //loop forever!
}

3 Format Input and Output

3.1 printf() and scanf()

The way output is performed in C++ is also quite different than that of C. In C++ you use the << and >> to direct items from cin or towards cout using iostreams. This convention is not possible in C, and instead, format printing and reading is used. Let's look at another example to further the comparison.

/*enternumber.cpp*/
#include <iostream>

using namespace std;

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

  cout << "Enter a number" << endl;
  cin >> num;

  cout << "You entered " << num << endl;
}
/*enternumber.c*/
#include <stdio.h>

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

  printf("Enter a number\n");
  scanf("%d", &num); //use &num to store 
                     //at the address of num

  printf("You entered %d\n", num);
}

The two programs above both ask the user to provide a number, and then print out that number. In C++, this should be fairly familiar. You use iostreams and direct the prompts to cout and direct input from cin to the integer num. C++ is smart enough to understand that if you are directing an integer to output or from input, then clearly, you are expecting a number. C is not capable of making those assumptions.

In C, we use a concept of format printing and format scanning to do basic I/O. The format tells C what kind of input or output to expect. In the above program enternumber.c, the scanf asks for a %d, which is the special format for a number, and similar, the printf has a %d format to indicate that num should be printed as a number.

There are other format options. For example, you can use %f to request a float.

/* getpi.c */

#include <stdio.h>
int main(int argc, char * argv[]){

  float pi;
  printf("Enter pi:\n");
  scanf("%f", &pi);

  printf("Mmmm, pi: %f\n", pi);
}

And you can use the format to change the number of decimals to print. %0.2f says print a float with only 2 trailing decimals. You can also include multiple formats, and the order of the formats match the additional arguments

int a=10,b=12;
float f=3.14;
printf("An int:%d a float:%f and another int:%d", a, f, b);
//              |          |                  |   |  |  |
//              |          |                  `---|--|--'
//              |          `----------------------|--'    
//              `---------------------------------'

There are a number of different formats available, and you can read the manual pages for printf and scanf to get more detail.

man 3 printf
man 3 scanf

You have to use the 3 in the manual command because there exists other forms of these functions, namely for Bash programming, and you need to look in section 3 of the manual for C standard library manuals.

For this class, we will use the following format characters frequently:

  • %d : format integer
  • %u : format an unsigned integer
  • %f : format float/double
  • %x : format hexadecimal
  • %s : format string
  • %c : format a char
  • %l : format a long
  • %lu: format an unsigned long
  • %% : print a % symbol

3.2 The FILE * and opening files

The last part of the standard C library we haven't explore is reading/writing from files. Although, you've done this already in the form of the standard files, e.g., standard input, output, and error, we have demonstrated how to open, read, write, and close other files that may exist on the file system.

All the file stream functions and types are defined in the header file stdio.h, so you have to include that. In later lessons, we will look into using, the system call API to do all of our I/O.

Open files in the standard C library are referred to as file streams, and have the type:

FILE * stream;

and we open a file using fopen() which has the following prototype:

FILE * fopen(const char *path, const char *mode);

The first argument path is a string storing the file system path of the file to open, and mode describes the settings of the file. For example:

FILE * stream = fopen("gonavy.txt", "w");

will open a file in the current directory called "gonavy.txt" with write mode. We'll discuss the modes more shortly.

File streams, as pointers, are actually dynamically allocated, and they must be deallocated or closed. The function that closes a file stream is fclose()

fclose( stream );

3.3 File Modes

The mode of the file describes how to open and use the file. For example, you can open a file for reading, writing, append mode. You can start reading/writing from the start or end of the file. You can truncate the file when opening removing all the previous data. From the man page, here are the options:

The argument mode points to a string beginning with one of the following sequences  (possibly  followed
by additional characters, as described below):

r      Open text file for reading.  The stream is positioned at the beginning of the file.

r+     Open for reading and writing.  The stream is positioned at the beginning of the file.

w      Truncate  file  to zero length or create text file for writing.  The stream is positioned at the
       beginning of the file.

w+     Open for reading and writing.  The file is created if it does not exist, otherwise it  is  trun‐
       cated.  The stream is positioned at the beginning of the file.

a      Open  for  appending  (writing  at end of file).  The file is created if it does not exist.  The
       stream is positioned at the end of the file.

a+     Open for reading and appending (writing at end of file).  The file is created  if  it  does  not
       exist.   The  initial  file  position for reading is at the beginning of the file, but output is
       always appended to the end of the file.

One key thing to notice from the modes is that any mode string with a "+" is for both reading and writing, but using "r" vs. "w" has different consequences for if the file already exists. With "r" a file will never be truncated if it exists, which means its contents will be deleted, but "w" will always truncate if it exits. However, "r" mode will not create the file if it doesn't exist while "w" will. Finally, append mode with "a" is a special case of "w" that doesn't truncate with all writes occurring at the end of the file.

As you can also see in the man page description, the file stream is described as having a "position" which refers to where within the file we read/write from. When you read a byte, you move the position forward in the file. In later lessons, we will look into how to manipulate the stream more directly. For now

3.4 File Errors

Looking at the man page for the file, we can check for errors when opening files:

RETURN VALUE
       Upon successful completion fopen(), fdopen() and freopen() return a FILE pointer.  Otherwise,  NULL  is
       returned and errno is set to indicate the error.

So we can check for NULL for errors, for example:

if ( (stream = fopen("DOESNOTEXIST.txt", "r")) == NULL){
   fprintf("ERROR ... \n");
}

An error could occur, like above, for the file not existing, but it could also be because you have insufficient permissions. Consider, a file that you do not have read permission on, but you open the file with "r" mode, then that would be an error condition. Similarly, if you try and open a file for writing without having write permission, that will cause an error.

Additionally, you can get errors when you try and read/write from a file that you opened with the wrong mode. This will cause the read/write function (discussed below) to fail. In those cases, you should check the return values of those functions.

While I do not show error checking below, it is important that your code does error checking.

3.5 Format input/output from File Streams

Just as you worked with the standard file streams, stdin, stdout, and stderr, we can do format input and output with file streams that you open with fopen().

3.5.1 Format Output with fprintf()

Let's start where we always start with understanding a new input/output system, hello world!

/*hello_fopen.c*/
#include <stdio.h>
#include <stdlib.h>

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

  FILE * stream = fopen("helloworld.txt", "w");

  fprintf(stream, "Hello World!\n");

  fclose(stream);
}
aviv@saddleback: demo $ ./hello_fopen 
aviv@saddleback: demo $ cat helloworld.txt
Hello World!
aviv@saddleback: demo $ ./hello_fopen 
aviv@saddleback: demo $ cat helloworld.txt
Hello World!

The program opens a new stream with the write mode at the path "helloworld.txt" and prints to the stream, "Hello World!\n". When we execute the program, the file helloworld.txt is created if it doesn't exist, and if it does it is truncated. After printing to it, we can read it with cat, and we see that in fact "Hello World!" is in the file. If we run the program again, we still have "Hello World!" in the file, just one, and that's because the second time we run the program, the file exists, so it is truncated. The previous "Hello World!" is removed and we write "Hello World!".

However if we wanted to open the file in a different mode, say append, we get a different result:

/*hello_append.c*/
#include <stdio.h>
#include <stdlib.h>

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

  FILE * stream = fopen("helloworld.txt", "a");//<--

  fprintf(stream, "Hello World!\n");

  fclose(stream);
}
aviv@saddleback: demo $ ./hello_append 
aviv@saddleback: demo $ ./hello_append 
aviv@saddleback: demo $ ./hello_append 
aviv@saddleback: demo $ cat helloworld.txt 
Hello World!
Hello World!
Hello World!
Hello World!

The original "Hello World!" remains, and the additional "Hello World!"'s are append to the end of the file. Printing "Hello World!" does not require a format, but fprintf() can format just like printf(), but to a file stream. For example, consider this simple program that prints information about the command line arguments.

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

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

  int i;

  FILE * stream = fopen("cmd.txt", "w");

  for(i=0;i<argc;i++){
    fprintf(stream, "argv[%d] = %s\n", i, argv[i]);
  }

  fclose(stream);
  return 0;
}
aviv@saddleback: demo $ ./command_info 
aviv@saddleback: demo $ cat cmd.txt
argv[0] = ./command_info
aviv@saddleback: demo $ ./command_info a b c d e f
aviv@saddleback: demo $ cat cmd.txt
argv[0] = ./command_info
argv[1] = a
argv[2] = b
argv[3] = c
argv[4] = d
argv[5] = e
argv[6] = f

3.5.2 Format Input with fscanf()

Just as we can format print to files, we can format read, or scan, from a file. fscanf() is just like scanf(), except it takes a file stream as the first argument. For example, consider a data file with following entries:

aviv@saddleback: demo $ cat file.dat
Aviv Adam 10 20 50 3.141592 yes
Pepin Joni 15 21 53 2.781 no

We can write a format to read this data in with fscanf():

int main(int argc, char * argv){

  FILE * stream = fopen("file.dat", "r");

  char fname[1024],lname[1024],yesno[4];
  int a,b,c;
  float f;

  while ( fscanf(stream,
                 "%s %s %d %d %d %f %s",
                 fname, lname, &a, &b, &c, &f, yesno) != EOF){

    printf("First Name: %s\n",fname);
    printf( "Last Name: %s\n",lname);
    printf("         a: %d\n",a);
    printf("         b: %d\n",b);
    printf("         b: %d\n",c);
    printf("         f: %f\n",f);
    printf("     yesno: %s\n", yesno);
    printf("\n");
  }

  fclose(stream);
  return 0;
}

And when we run it, we see that we scan each line at a time:

aviv@saddleback: demo $ ./scan_file 
First Name: Aviv
Last Name: Adam
         a: 10
         b: 20
         b: 50
         f: 3.141592
     yesno: yes

First Name: Pepin
Last Name: Joni
         a: 15
         b: 21
         b: 53
         f: 2.781000
     yesno: no

One thing you should notice from the scanning loop is that we compare to EOF, which is special value for "End of File." The end of the file is encoded in such a way that you can compare against it. When scanning and you reach end of the file, EOF is returned, which can be detected and used to break the loop.

Another item to note is that scanning with fscanf() is the same as that with scanf(), and is white space driven to separate different values to scan. Also, "%s" reads a word, as separated by white space, and does not read the whole line.

3.6 Printing to stdout and stderr

By default, printf() prints to stdout, but you can alternative write to any file stream. To do so, you use the fprintf() function, which acts just like printf(), except you explicitly state which file stream you wish to print. Similarly, there is a fscanf() function for format reading from files other than stdin.

printf("Hello World\n"); //prints implicitly to standard out
fprintf(stdout, "Hello World\n"); //print explicitly to standard out
fprintf(stderr, "ERROR: World coming to an endline!\n"); //print to standard error

The standard file descriptors are available in C via their shorthand, and you can refer to their file descriptor numbers where appropriate:

  • stdin : 0 : standard input
  • stdout : 1 : standard output
  • stderr : 2 : standard error

4 Basic Data Types

In the last lesson we review the basic types of C. For reference, they appear below:

  • int : integer number : 4-bytes
  • short : integer number : 2-bytes
  • long : integer number : 8-bytes
  • char : character : 1-byte
  • float : floating point number : 4-bytes
  • double : floating point number : 8-bytes
  • void * : pointers : 8-bytes on (64 bit machines)

These types and the operations over them are sufficient for most programming; however, we will need more to accomplish the needed tasks. In particular, there are three aspects of these types that require further exploration:

  1. Advanced Structured Types: Create new types and formatted by combining basic types.
  2. Pointers: Working with references to data
  3. Arrays: Organizing data into linear structures.

4.1 Booleans

C does not have a built in boolean type, like C++ does. Instead, anything that is equivalent to numeric 0 is false and everything that is not 0 is true. For example, this is an infinite loop. As 1 will always be true.

while(1){
  //loop forever
}

In C many different elements you may not expect can have a zero value. For example, NULL is equivlanet to zero; it's the pointer value that is 0. The character \0 is also 0. Be on the look out for these implicit 0's.

4.2 Advanced Types: struct

An incredibly useful tool in programming is to be able to create advanced types built upon basic types. Consider managing a pair of integers. In practice, you could declare two integer variables and manage each separately, like so:

int left;
int right;
left = 1;
right = 2;

But that is cumbersome and you always have to remember that the variable left is paired with the variable right, and what happens when you need to have two pairs or three. It just is not manageable.

Instead, what we can do is declare a new type that is a structure containing two integers.

struct pair{    //declaring a new pair type 
  int left;     //that containing two integers
  int right;
};

struct pair p1;       //declare two variables of that type
struct pair p2;

p1.left = 10;    //assign values to the pair types
p1.right = 20;

p2.left = 0;
p2.right = 5;

The first part is to declare the new structure type by using the keyword struct and specify the basic types that are members of the structure. Next, we can declare variables of that type using the type name, struct pair. With those variables, we can then refer to the member values, left and right, using the . operator.

One question to consider: How is the data for the structure laid out in memory? Another way to ask is: How many bytes does it take to store the structure? In this example, the structure contains two integers, so it is 8 bytes in size. In memory, it would be represented by two integers that are adjacent in memory space.

struct pair
.--------------------.       
|.--------..--------.|
||<- 4B ->||<- 4B ->||     
||  left  ||  right ||
|'________''________'|
'--------------------'
 <----- 8 bytes ----->

Using the . and the correct name either refers to the first or second four bytes, or the left or right integer within the pair. When we print its size, that is exactly what we get.

printf("%lu\n", sizeof(struct pair));

While the pair struct is a simple example, throughout the semester we will see many advanced structure types that combine a large amount of information. These structures are used to represent various states of the computer and convey a lot of information in a compact form.

4.3 Defining new types with typedef

While structure data is ever present in the system, it is often hidden by declare new type names. The way to introduce a new type name or type definition is using the typedef macro. Here is an example for the pair structure type we declared above.

typedef struct{    //declaring a new structure
  int left;        //that containing two integers
  int right;
} pair_t;          //the type name for the structure is pair_t

pair_t p1;       //declare two variables of that type
pair_t p2;

p1.left = 10;    //assign values to the pair types
p1.right = 20;

p2.left = 0;
p2.right = 5;

This time we declare the same type, a pair of two integers, but we gave that structure type a distinct name, a pair_t. When declaring something of this type, we do not need to specify that it is a structure, instead, we call it what it is, a pair_t. The compiler is going to recognize the new type and ensure that it has the properties of the structure.

The suffix _t is typically used to specify that this type is not a basic type and defined. This is a convention of C, not a rule, but it can help guide you through the moray of types you will see in this class.

5 Pointers and Arrays

5.1 Pointers

In C, pointers play a larger role than in C++. Recall that a pointer is a data type whose value is a memory address. A pointer must be declared based on what type it references; for example, int * are pointers to integers and char * are pointers to chars. Here are some basic operations associated with pointers.

  • int * p : pointer declaration
  • *p : pointer dereference, follow the pointer to the value
  • &a : Address of the variable a
  • p = &a : pointer assignment, p now references a
  • *p = 20 : assignment via a dereference, follow the pointer and assign a the value.

Individually, each of these operations can be difficult to understand. Following a stack diagram, where variables and values are modeled. For the purposes of this class, we will draw stack diagrams like this:

+----------+-------+
| variable | value |
+----------+-------+

If we have a pointer variable, then we'll do this:

+----------+-------+
| pointer  |  .------->
+----------+-------+

This will indicate that the value of the pointer is a memory address that references some other memory.

To codify this concept further, let's follow a running example of the following program:

int a = 10, b;
int *p =  &a;
a = 20; 
b = *p; 
*p = 30;
p = &b;

Let us walk through it step by step:

(1) Initially, a has the value 10, b has not been assigned to, and p references the value of a.

int a = 10, b;
int *p =  &a; // <-- (1)
a = 20; 
b = *p; 
*p = 30;
p = &b;
+---+----+  
| a | 10 |<-.
+---+----+  |
| b |    |  |   arrow for pointer indicates 
+---+----+  |   a reference
| p |  .----+
+---+----+

(2) Assigning to a changes a's value, and now p also references that value

int a = 10, b;
int *p =  &a; 
a = 20; // <--  (2)
b = *p; 
*p = 30;
p = &b;
+---+----+  
| a | 20 |<-.
+---+----+  |
| b |    |  |
+---+----+  |
| p |  .----+
+---+----+

(3) p is dereferenced with *, and the value that p referenced is assigned to b

int a = 10, b;
int *p =  &a; 
a = 20; 
b = *p; // <-- (3)
*p = 30;
p = &b;
+---+----+  
| a | 20 |<-.
+---+----+  |
| b | 20 |  |   *p means to follow pointer 
+---+----+  |   to get value
| p |  .----+
+---+----+

(4) Assigning to *p stores the value that memory p references, changing a's value

int a = 10, b;
int *p =  &a; 
a = 20; 
b = *p; 
*p = 30; // <-- (4)
p = &b;
+---+----+  
| a | 30 |<-.
+---+----+  |
| b | 20 |  |   assigning *p follows pointer 
+---+----+  |   to store value
| p |  .----+
+---+----+

#+ENDHTML (5) Assigning to p requires an address, now p references the memory address of b

int a = 10, b;
int *p =  &a; 
a = 20; 
b = *p; 
*p = 30; 
p = &b; // <-- (5)
+---+----+  
| a | 30 |
+---+----+  
| b | 20 |<-.
+---+----+  | 
| p |  .----+
+---+----+

5.2 Pointers to structures

Just like for other types, we can create pointers to structured memory. Consider for example:

typdef struct{
  int left;
  int right;
} pair_t;

pair_t pair;
pair.left = 1;
pair.right = 2;

pair_t * p = &pair;

This should be familiar to you as we can treat pair_t just like other data types, except we know that it is actually composed of two integers. However, now that p references a pair_t how do we deference it such that we get to member data? Here is one way.

printf("pair: (%d,%d)\n", (*p).left, (*p).right);

Looking closely, you see we first use the * operator to deference the pointer, and then the . operator to refer to a member of the structure. That is a lot of work and because C requires us to frequently access members of structures via a pointer reference. To alleviate that, C has a shortcut operation, the arrow or ->, which dereferences and then does member reference for pointers to structures. Here is how that looks:

printf("pair: (%d,%d)\n", p->left, p->right);

p->right = 2017;
p->left  = 1845;

5.3 Array Types

The last type are array types which provides a way for the program to declare an arbitrary amount of the same type in continuous memory. Here is a simple example with an array of integers:

int array[10]; //declare an array of 10 integers
int i;

//assign to the array  
for(i=0;i<10;i++){
  array[i] = 2*i; //index times 2
 }

//reference the array
for(i=0;i<10;i++){
  printf("%d:%d\n", i,array[i]); 
 }
aviv@saddleback: demo $ ./array-example
0:0
1:2
2:4
3:6
4:8
5:10
6:12
7:14
8:16
9:18

We declare an array using the [ ] following the variable name. We use the term index to refer to an element of an array. Above, the array array is of size 10, which means that we can use indexes 0 through 9 (computer scientist start counting at 0). To index the array, for both retrieval and assignment, we use the [ ] operators as well.

5.4 Arrays and Pointers

Now, it is time to blow your mind. It turns out that in C arrays and pointers are the same thing. Seriously. Well, not exactly the same, but basically the same.

Let me demonstrate. First consider how and what happens when you assign a pointer to an array.

/*pointer-array.c*/
#include <stdio.h>
#include <stdlib.h>

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

  int array[10];
  int i;
  int * p = array; //p points to array

  //assign to the array  
  for(i=0;i<10;i++){
    array[i] = 2*i; //index times 2
  }

  //derefernce p and assign 2017
  *p = 2017;

  //print the array
  for(i=0;i<10;i++){
    printf("%d:%d\n", i,array[i]); 
  }

}
aviv@saddleback: demo $ ./pointer-array 
0:2017
1:2
2:4
3:6
4:8
5:10
6:12
7:14
8:16
9:18

Notice that at index 0 the value is now 2017. Also notice that when you assigned the pointer value, we did not take the address of the array. That means p is really referencing the address of the first item in the array and for that matter, so is array!

It gets crazier because we can also use the [ ] operators with pointers. Consider this small change to the program:

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

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

  int array[10];
  int i;
  int * p = array; //p points to array

  //assign to the array  
  for(i=0;i<10;i++){
    array[i] = 2*i; //index times 2
  }

  //index p at 5  and assign 2017
  p[5] = 2017;   //<---------------!!

  //print the array
  for(i=0;i<10;i++){
    printf("%d:%d\n", i,array[i]); 
  }

}
aviv@saddleback: demo $ ./pointer-array-index 
0:0
1:2
2:4
3:6
4:8
5:2017 //<---------!!!
6:12
7:14
8:16
9:18

In this case we indexed the pointer at 5 and assigned to it the value 2017, which resulted in that value appearing in the output. What is the implication of this? We know that p is a pointer and we know to assign to the value referenced by a pointer it requires a dereference, so the [ ] must be a dereference operation. And it is. In fact we can translate the [ ] operation like so:

p[5] = *(p+5)

What the [ ] operation does is increments the pointer by the index and then deference. As a stack diagram, we can visualize this like so:

.-------+------.
| array |   .--+--.
|-------+------|  |
|       |    0 |<-'<-.
|-------+------|     |
|       |    2 |     |
|-------+------|     |
|       |    4 |     |
|-------+------|     |
|       |    6 |     |
|-------+------|     |
|       |    8 |     |
|-------+------|     |
|       | 2017 |<----+----- p+5, array+5
|-------+------|     |
|       |   12 |     |
|-------+------|     |
|       |   14 |     |
|-------+------|     |
|       |   16 |     |
|-------+------|     |
|       |   18 |     |
|-------+------|     |
| p     |   .--+-----'
'-------+------'

This is called pointer arithmetic, which is a bit complicated, but we'll return to it later when discussing strings. The important take away is that there is a close relationship between pointers and arrays. And now you also know why arrays are indexed starting at 0 — it is because of pointer arithmetic. The first item in the array is the same as just dereferencing the pointer to the array, thus occurring at index 0.

Before I described that relationship as the same, but they are not exactly the same. Where they differ is that pointers can be reassigned like any variable, but arrays cannot. They are constants. For example, this is not allowed:

int a[10];
int b[10];
int *p;

p = a; // ok

b = p; // not ok!

Array pointers are constant, we cannot reassign to them. The reason is obvious when you think: if we could reassign the array pointer, then how would reclaim that memory? The answer is you could not. It would be lost.

In the next lessons, we will continue to look at arrays and pointers, but in the context of strings, which are simply arrays of characters with the property that they are null terminated.

5.5 Close connection between pointers and arrays

Recall that an array is a contiguos region of memory that stores a sequence of the same data items We declare arrays statically using the [ ] symbols and a size, and you can also reference and assign to an array using the [ ] symbol.

int array[10]; 
int i; 

for(i=0;i < 10; i++){
 array[i] = i*2; 
}

Additionally, arrays and pointers are closely linked, and, in fact, an array variable is a special type of pointer whose value cannot change. When you declare an array:

int array[10];

You are asking C to do two things. First, this is a request to allocate 10 integers of memory, contiguously, or 40 bytes. The second part is to assign the address of that memory allocation to the variable array and make it constant so that the value that array references cannot change.

Essentially, array is a pointer to the contiguous memory. We can then access the individual integers in that memory region using the [ ] operator. But, we also know that this operation is equivalent to a deference.

          .---.
array --> |   |  array[0] == *(array+0) 
          +---+
          |   |  array[1] == *(array+1) 
          +---+
          |   |  array[2] == *(array+2)
          +---+
          :   :  etc.
          '   '

When you index into an array, you are effectively following the pointer plus the index. That is, the operation of array[i] says to following the pointer referenced by the variable array, move i steps further, and then return the value found at that memory location. The concept of pairing arrays and pointers in this style is called pointer arithmetic and is an incredibly powerful tool of C programming and used a lot with C strings.

6 C Strings

A string in C is simply an array of char objects that is null terminated. Here's a typical C string declaration:

char str[] = "Hello!"

A couple things to note about the declaration:

  • First that we declare str like an array, but we do not provide it a size.
  • Second, we assign to str a quoted string.
  • Finally, while we know that strings are NULL terminated, there is no explicit NULL termination.

We will tackle each of these in turn below.

6.1 Advanced Array Declarations

While the declaration looks acquired at first without the array size, this actually means that the size will be determined automatically by the assignment. All arrays can be declared in this static way; here is an example for an integer array:

int array[] = {1, 2, 3};

In that example, the array values are denoted using the { } and comma separated within. The length of the array is clearly 3, but the compiler can determine that by inspecting the static declaration, so it is often omitted. However, that does not mean you cannot provide a size, for example

int array[10] = {1, 2, 3};

is also perfectly fine but has a different semantic meaning. The first declaration (without a size) says allocate only enough memory to store the statically declared array. The second declaration (with the size) says to allocate enough memory to store size items of the data type and initialize as many possible to this array.

You can see this actually happening in this simple program:

/*array_deceleration.c*/
#include <stdio.h>
#include <stdlib.h>

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

  int a[] = {1,2,3};
  int b[10] = {1,2,3};
  int i;

  printf("sizeof(a):%d sizeof(b):%d\n", 
         (int) sizeof(a),
         (int) sizeof(b)
         );

  printf("\n");
  for(i=0;i<3;i++){
    printf("a[%d]: %d\n", i,a[i]);
  }    

  printf("\n");
  for(i=0;i<10;i++){
    printf("b[%d]: %d\n", i,b[i]);
  }    
}
aviv@saddleback: demo $ ./array_decleration
sizeof(a):12 sizeof(b):40
a[0]: 1
a[1]: 2
a[2]: 3

b[0]: 1
b[1]: 2
b[2]: 3
b[3]: 0
b[4]: 0
b[5]: 0
b[6]: 0
b[7]: 0
b[8]: 0
b[9]: 0

As you can see, both decelerations work, but the allocation sizes are different. Array b is allocated to store 10 integers with a size of 40 bytes, while array a only allocated enough to store the static declaration. Also note that the allocation implicitly filled in 0 for non statically declared array elements in b, which is behavior you'd expect.

6.2 The quoted string declaration

Now that you have a broader sense of how arrays are declared, let's adapt this to strings. The first thing we can try and declare is a string, that is an array of char's, using the declaration like we had above.

char a[]   = {'G','o',' ','N','a','v','y','!'};
char b[10] = {'G','o',' ','N','a','v','y','!'};

Just as before we are declaring an array of the given type which is char. We also use the static declaration for arrays. At this point we should feel pretty good — we have a string, but not really. Let's look at an example using this declaration:

/*string_declerations.c*/
#include <stdio.h>
#include <stdlib.h>

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

  char a[]   = {'G','o',' ','N','a','v','y','!'};
  char b[10] = {'G','o',' ','N','a','v','y','!'};
  int i;

  printf("sizeof(a):%d sizeof(b):%d\n", 
         (int) sizeof(a),
         (int) sizeof(b)
         );


  printf("\n");
  for(i=0;i<8;i++){
    //print char and ASCII value
    printf("a[%d]: %c (%d)\n", i,a[i],a[i]);
  }    


  printf("\n");
  for(i=0;i<10;i++){
    //print char and ASCII value
    printf("b[%d]: %c (%d) \n", i,b[i],b[i]);
  }    

  printf("\n");
  printf("a: %s\n",a); //format print the string
  printf("b: %s\n",b); //format print the string

}
aviv@saddleback: demo $ ./string_declerations 
sizeof(a):8 sizeof(b):10

a[0]: G (71)
a[1]: o (111)
a[2]:   (32)
a[3]: N (78)
a[4]: a (97)
a[5]: v (118)
a[6]: y (121)
a[7]: ! (33)

b[0]: G (71) 
b[1]: o (111) 
b[2]:   (32) 
b[3]: N (78) 
b[4]: a (97) 
b[5]: v (118) 
b[6]: y (121) 
b[7]: ! (33) 
b[8]:  (0) 
b[9]:  (0) 

a: Go Navy!?@
b: Go Navy!

First observations is the sizeof the arrays match our expectations. A char is 1 byte in size and the arrays are allocated to match either the implicit size (7) or the explicit size (10). We can also print the arrays iteratively, and the ASCII values are inset to provide a reference. However, when we try and format print the string using the %s format, something strange happens for a that does not happen for b.

The problem is that a is not NULL terminated, that is, the last char numeric value in the string is not 0. NULL termination is very important for determining the length of the string. Without this special marker, the printf() function is unable to determine when the string ends, so it prints extra characters that are not really part of the string.

We can change the declaration of a to explicitly NULL terminate like so:

char a[]   = {'G','o',' ','N','a','v','y','!', '\0'};

The escape sequence '\0' is equivalent to NULL, and now we have a legal string. But, I think we can all agree this is a really annoying way to do string declarations using array formats because all strings should be NULL terminated anyway. Thus, the double quoted string shorthand is used.

char a[]   = "Go Navy!";

The quoted string is the same as statically declaring an array with an implicit NULL termination, and it is ever so much more convenient to use. You can also more explicitly declare the size, as in the below example, which declares the array of the size, but also will NULL terminate.

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

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

  char a[]   = "Go Navy!";
  char b[10] = "Go Navy!";
  int i;

  printf("sizeof(a):%d sizeof(b):%d\n", 
         (int) sizeof(a),
         (int) sizeof(b)
         );


  printf("\n");
  for(i=0;i<9;i++){
    //print char and ASCII value
    printf("a[%d]: %c (%d)\n", i,a[i],a[i]);
  }    


  printf("\n");
  for(i=0;i<10;i++){
    //print char and ASCII value
    printf("b[%d]: %c (%d) \n", i,b[i],b[i]);
  }    

  printf("\n");
  printf("a: %s\n",a); //format print the string
  printf("b: %s\n",b); //format print the string

}
aviv@saddleback: demo $ ./string_quoted 
sizeof(a):9 sizeof(b):10

a[0]: G (71)
a[1]: o (111)
a[2]:   (32)
a[3]: N (78)
a[4]: a (97)
a[5]: v (118)
a[6]: y (121)
a[7]: ! (33)
a[8]:  (0)

b[0]: G (71) 
b[1]: o (111) 
b[2]:   (32) 
b[3]: N (78) 
b[4]: a (97) 
b[5]: v (118) 
b[6]: y (121) 
b[7]: ! (33) 
b[8]:  (0) 
b[9]:  (0) 

a: Go Navy!
b: Go Navy!

You may now be wondering what happens if you do something silly like this,

char a[3] = "Go Navy!";

where you declare the string to be of size 3 but assign a string requiring much more memory? Well … why don't you try writing a small program to finding out what happen, which you will do in homework.

6.3 String format input, output, overflows, and NULL deference's:

While strings are not basic types, like numbers, they do have a special place in a lot of operations because we use them so commonly. One such place is in formats.

You already saw above that %s is the format character to process a string, and it is also the format character used to scan a string. We can see how this all works using this simple example:

/*format_string*/
#include <stdio.h>
#include <stdlib.h>

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

  char name[20];

  printf("What is your name?\n");
  scanf("%s",name);

  printf("\n");
  printf("Hello %s!\n",name); 

}

There are two formats. The first will ask the user for their name, and read the response using a scanf(). Looking more closely, when you provide name as the second argument to scanf(), you are saying: "Read in a string and write it to the memory referenced by name." Later, we can then print name using a %s in a printf(). Here is a sample execution:

aviv@saddleback: demo $ ./format_string 
What is your name?
Adam

Hello Adam!

That works great. Let's try some other input:

aviv@saddleback: demo $ ./format_string 
What is your name?
Adam Aviv

Hello Adam!

Hmm. That didn't work like expected. Instead of reading in the whole input "Adam Aviv" it only read a single word, "Adam". This has to do with the functionality of scanf() that "%s" does not refer to an entire line but just an individual whitespace separated string.

The other thing to notice is that the string name is of a fixed size, 20 bytes. What happens if I provide input that is longer … much longer.

aviv@saddleback: demo $ ./format_string 
What is your name?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdam

Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdam!
 *** stack smashing detected ***: ./format_string terminated
Aborted (core dumped)

That was interesting. The execution identified that you overflowed the string, that is tried to write more than 20 bytes. This caused a check to go off, and the program to crash. Generally, a segmentation fault occurs when you try to read or write invalid memory, i.e., outside the allowable memory segments.

We can go even further with this example and come up with a name sooooooo long that the program crashes in a different way:

What is your name?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
Segmentation fault (core dumped)

In this case, we got a segmentation fault. The scanf() wrote so far out of bounds of the length of the array that it wrote memory it was not allowed to do so. This caused the segmentation fault.

Another way you can get a segmentation fault is by dereferencing NULL, that is, you have a pointer value that equals NULL and you try to follow the pointer to memory that does not exist.

/*null_print.c*/
#include <stdio.h>
#include <stdlib.h>

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

  printf("This is a bad idea ...\n");
  printf("%s\n",(char *) NULL);
}
aviv@saddleback: demo $ ./null_print
This is a bad idea ...
Segmentation fault (core dumped)

This example is relatively silly as I purposely dereference NULL by trying to treat it as a string. While you might not do it so blatantly, you will do something like this at some point. It is a mistake we all make as programmers, and it is a particularly annoying mistake that is inevitable when you program with pointers and strings. It can be frustrating, but we will also go over many ways to debug such errors throughout the semester.

7 Sting Library Functions

Working with strings is not as straight forward as it is in C++ because they are not basic types, but rather arrays of characters. Truth be told, in C++ they are also arrays of characters; however, C++ provides a special library that overloads the basic operations so you can treat C++ strings like basic types. Unfortunately, such conveniences are not possible in C.

As a result, certain programming paradigms that would seem obvious to do in C do not do as you would expect them to do. Here's an example:

/*string_badcmp.c*/
#include <stdio.h>
#include <stdlib.h>

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

  char str[20];

  printf("Enter 'Navy' for a secret message:\n");

  scanf("%s",str);

  if( str == "Navy"){
    printf("Go Navy! Beat Army!\n");
  }else{
    printf("No secret for you.\n");
  }


}

And if we run this program and enter in the appropriate string, we do not get the result we expect.

aviv@saddleback: demo $ ./string_badcmp 
Enter 'Navy' for a secret message:
Navy
No secret for you.

What happened? If we look at the if statement expression:

if( str == "Navy" )

Our intuition is that this will compare the string str and "Navy" based on the values in the string, that is, is str "Navy" ? But that is not what this is doing because remember a string is an array of characters and an array is a pointer to memory and so the equality is check to see if the str and "Navy" are stored in the same place in memory and has nothing to do with the actual strings.

To see that this is case, consider this small program which also does not do what is expected:

/*string_badequals.c*/
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]){
  char s1[]="Navy";
  char s2[]="Navy";
  if(s1 == s2){
    printf("Go Navy!\n");
  }else{
    printf("Beat Army!\n");
  }
 printf("\n");
  printf("s1: %p \n", s1);
  printf("s2: %p \n",s2);
}
aviv@saddleback: demo $ ./string_badequals 
Beat Army!

s1: 0x7fffe43994f0 
s2: 0x7fffe4399500

Looking closely, although both s1 and s2 reference the same string values they are not the same string in memory and have two different addresses. (The %p formats a memory address in hexadecimal.)

The right way to compare to strings is to compare each character, but that is a lot of extra code and something we don't want to write every time. Fortunately, its been implemented for us along with a number of other useful functions in the string library.

7.1 The string library string.h

To see all the goodness in the string library, start by typing man string in your linux terminal. Up will come the manual page for all the functions in the string library:

STRING(3)                              Linux Programmer's Manual                             STRING(3)

NAME
       stpcpy,  strcasecmp,  strcat, strchr, strcmp, strcoll, strcpy, strcspn, strdup, strfry, strlen,
       strncat, strncmp, strncpy, strncasecmp,  strpbrk,  strrchr,  strsep,  strspn,  strstr,  strtok,
       strxfrm, index, rindex - string operations

SYNOPSIS
       #include <strings.h>

       int strcasecmp(const char *s1, const char *s2);

       int strncasecmp(const char *s1, const char *s2, size_t n);

       char *index(const char *s, int c);

       char *rindex(const char *s, int c);

       #include <string.h>

       char *stpcpy(char *dest, const char *src);

       char *strcat(char *dest, const char *src);

       char *strchr(const char *s, int c);

       int strcmp(const char *s1, const char *s2);

       int strcoll(const char *s1, const char *s2);

       char *strcpy(char *dest, const char *src);

       size_t strcspn(const char *s, const char *reject);

       char *strdup(const char *s);

       char *strfry(char *string);

       size_t strlen(const char *s);

       ...

To use the string library, the only thing you need to do is include string.h in the header declarations. You can further explore different functions string library within their own manual pages. The two most relevant to our discussion will be strcmp() and strlen(). However, I encourage you to explore some of the others, for example strfry() will randomize the string to create an anagram – how useful!

7.2 String Comparison

To solve our string comparison delimina, we will use the strcmp() function from the string library. Here is the revelant man page:

STRCMP(3)                              Linux Programmer's Manual                             STRCMP(3)

NAME
       strcmp, strncmp - compare two strings

SYNOPSIS
       #include <string.h>

       int strcmp(const char *s1, const char *s2);

       int strncmp(const char *s1, const char *s2, size_t n);

DESCRIPTION
       The  strcmp()  function  compares  the two strings s1 and s2.  It returns an integer less than,
       equal to, or greater than zero if s1 is found, respectively, to be less than, to match,  or  be
       greater than s2.

       The  strncmp()  function  is similar, except it compares the only first (at most) n bytes of s1
       and s2.

RETURN VALUE
       The strcmp() and strncmp() functions return an integer less than, equal  to,  or  greater  than
       zero if s1 (or the first n bytes thereof) is found, respectively, to be less than, to match, or
       be greater than s2.

It comes in two varieties. One with a maximum length specified and one that relies on null termination. Both return the same values. If the two strings are equal, then the value is 0, if the first string string is greater (larger alphabetically) than it returns 1, and if the first string is less than (smaller alphabetically) then it returns -1.

Plugging in strcmp() into our secrete message program, we get the desired results.

/*string_strncp.c*/
#include <stdio.h>
#include <stdlib.h>

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

  char str[20];

  printf("Enter 'Navy' for a secret message:\n");

  scanf("%s",str);

  if( strcmp(str,"Navy") == 0 ) {
    printf("Go Navy! Beat Army!\n");
  }else{
    printf("No secret for you.\n");
  }

}
aviv@saddleback: demo $ ./string_strcmp 
Enter 'Navy' for a secret message:
Navy
Go Navy! Beat Army!

7.3 String Length vs String Size

Another really important string library function is strlen() which returns the length of the string. It is important to differentiate the length of the string from the size of the string.

  • string length: how many characters, not including the null character, are in the string
  • sizeof : how many bytes required to store the string.

One of the most common mistakes when working with C strings is to consider the sizeof the string and not the length of the string, which are clearly two different values. Here is a small program that can demonstrate how this can go wrong quickly:

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

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

  char str[]="Hello!";
  char * s = str;

  printf("strlen(str):%d sizeof(str):%d sizeof(s):%d\n",
         (int) strlen(str),  //the length of the str
         (int) sizeof(str),  //the memory size of the str
         (int) sizeof(s)    //the memory size of a pointer
         );
}
aviv@saddleback: demo $ ./string_length 
strlen(str):6 sizeof(str):7 sizeof(s):8

Note that when using strlen() we get the length of the string "Hello!" which has 6 letters. The size of the string str is how much memory is used to store it, which is 7, if you include the null terminated. However, things get bad when you have a pointer to that string s. Calling sizeof() on s returns how much memory needed to store s which is a pointer and thus is 8-bytes in size. That has nothing to do with the length of the string or the size of the string. This is why when working with strings always make sure to use the right length not the size.

8 Pointer Arithmetic and Strings

As noted many times now, strings are arrays, and as such, you can work with them as arrays using indexing with [ ]; however, often when programmers work with strings, they use pointer arithmetic. For example, here is a routine to print a string to stdout:

void my_puts(char * str){

  while(*str){
    putchar(*str);
    str++;
  }

}

This function my_puts() takes a string and will write the string, char-by-char to stdout using the putchar() function. What might seem a little odd here is the use of the while loop, so lets unpack that:

while(*str)

What does this mean? First notice that str is declared as a char * which is a pointer to a character. We also know that pointers and arrays are the same, so we can say that str is a string that references the first character in the string's array. Next the *str operation is a dereference, which says to follow the pointer and retrieve the value that it references. In this case that would be a character value. Finally, the fact that this operation occurs in the expression part means that we are testing the value that the pointer references for not be false, which is the same as asking if it is not zero or not NULL.

So, the while(*str) says to continue looping as long the pointer str does not reference NULL. The pointer value of str does change in the loop and is incremented, str++, for each interaction after the call to putchar().

Now putting it all together, you can see that this routine will iterate through a string using a pointer until the NULL terminator is reached. Phew. While this might seem like a backwards way of doing this, it is actually a rather common and straight foreword programming practice with strings and pointers in general.

8.1 Pointer Arithmetic and Types

Something that you might have noticed is that we have been using pointer arithmetic for different types in the same way. That is, consider the two arrays below, one an array of integers and one a string:

int a[]  = {0,1,2,3,4,5,6,7};
char str = "Hello!";

Both arrays are the same length, 7, but they are different sizes. Integers are 4-bytes, so to store 7 integers requires 4*7=24 bytes. But characters are 1 byte in size, so to store 7 characters requires just 7 bytes. In memory the two arrays may look something like this:

       <------------------------ 24 bytes ---------------------------->
       .---------------.----------------.--- - - - ---.----------------.
a   -> |             0 |              1 |             |              7 |
       '---------------'----------------'--- - - - ---'----------------'

       .---.---.---.---.---.---.
str -> | H | e | l | l | o | \0|
       '---'---'---'---'---'---'
       <-------  7 bytes ------> 

Now consider what happens when we use pointer arithmetic on these arrays to dereference the third index:

/*pointer_math.c*/
#include <stdio.h>
#include <stdlib.h>

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

  int a[] = {0,1,2,3,4,5,6,7};
  char str[] = "Hello!";

  printf("a[3]:%d str[3]:%c\n", *(a+3),*(str+3));

}
aviv@saddleback: demo $ ./pointer_math 
a[3]:3 str[3]:l

Knowing what you know, the output is not consistent. When you add 3 to the array of integers a, you adjust the pointer by 12 bytes so that you now reference the value 3. However, when you add 3 to the string pointer, you adjust the pointer by 3 bytes to reference the value 'l'.

The reason for this has to do with pointer arithmetic consideration of typing. When you declare a pointer to reference a particular type, C is aware that adding to the pointer value should consider the type of data being referenced. So when you add 1 to an integer pointer, you are moving the reference forward 4 bytes since that is the size of the integer. If we were to print the pointer values (in hex) and do numerical arithmetic we would see this to be true:

printf("a=%p a+3=%p (a+3-a)=%d\n",a,a+3, ((long) (a+3)) - (long) a);

printf("str=%p str+3=%p (str+3-str)=%d\n",str,str+3, ((long) (str+3)) - (long) str);
aviv@saddleback: demo $ ./pointer_math 
a[3]:3 str[3]:l

a=0x7fffa5c4d260 a+3=0x7fffa5c4d26c (a+3-a)=12

str=0x7fffa5c4d280 str+3=0x7fffa5c4d283 (str+3-str)=3

In the first part a+3 changed the pointer value by 0xc in hex which is 12, while str+3 only changes the character value by 0x3 or 3 bytes. More starkly you can see that if we treat the pointer values as longs and do numerical arithmetic after doing pointer arithmetic you see this more clearly.

8.2 Character Arrays as Arbitrary Data Buffers

Now you may be wondering, how do I access the individual bytes of larger data types? The answer to this is the final peculiarity of character arrays in C.

Consider that a char data type is 1 byte in size, which is the smallest data element we work with as programmers. Now consider that an array of char's matches exactly that many bytes. So when we write something like:

char s[4];

What we are really saying is: "allocate 4 bytes of data." We like to think about storing a string of length 3 in that character array with one byte for the null terminator, but we do not have to. In fact, any kind of data can be stored there as long as it is only 4-bytes in size. An integer is four bytes in size. Let's store an integer in s.

aviv@saddleback: demo $ cat pointer_casting.c
#include <stdio.h>
#include <stdlib.h>

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

  char s[4];

  s[0] = 255;
  s[1] = 255;
  s[2] = 255;
  s[3] = 255;

  int * i = (int *) s;
  printf("*i = %d\n", *i);

}

What this program does is set all the bytes in the character array to 255, which is the largest value 1-byte can store. The result is that we have 4-bytes of data that are all 1's, since 255 in binary is 11111111. Four bytes of data that is all 1's. Next, consider what happens with this cast:

int * i = (int *) s;

Now the pointer i references the same memory as s, which is 4-bytes of 1's. What's different is that i is an integer pointer not a character pointer. That means the 4-bytes of 1's is an integer not characters from the perspective of i. And when we dereference i to print those bytes as a number, we get:

aviv@saddleback: demo $ ./pointer_casting 
*i = -1

Which is the signed value for all 1's (remember two's compliment?). What we've just done is use characters as a generic container for data and then used pointer casting to determine how to interpret that data. This may seem crazy — it is — but it is what makes C so low level and useful.

We often refer to character arrays as buffers because of this property of being arbitrary containers. A buffer of data is just a bunch of bytes, and a character array is the most direct way to access that data.

9 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 as these are processed as an array of strings, which are arrays themselves, thus double arrays.

9.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.

9.2 The type of a double array

Let's think a bit more about what a double array really is given our understanding of 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.

9.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 an 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.

10 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

10.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:

/*print_args.c*/
#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

10.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!)

10.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() dereferences 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.