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
andelse
with the same syntax - Loops: Use
while
andfor
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 ofchar
'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 declarei
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 callssys/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 conversionsmath.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 inputstdout
: 1 : standard outputstderr
: 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-bytesshort
: integer number : 2-byteslong
: integer number : 8-byteschar
: character : 1-bytefloat
: floating point number : 4-bytesdouble
: floating point number : 8-bytesvoid *
: 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:
- Advanced Structured Types: Create new types and formatted by combining basic types.
- Pointers: Working with references to data
- 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 variablea
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 explicitNULL
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.