The concept of pointers has historically been a difficult one. Many texts and courses leave the details of pointers for later chapters. We prefer to teach pointers as soon as possible because mastery of so many other concepts depends on understanding pointers and addressing. Once pointers are understood, arrays become clear and strings are based on arrays.
We understand that every variable v has to have a chunk of memory set aside to hold its value. The expression v produces the value of v. The expression &v produces the memory address where that value is stored. In order to save the address of v for later use, C provides a data type called the pointer type. The pointer type allows us to declare variables that hold the address of some other variable for later use. Since a pointer is used to store an address it must be large enough to store the address value of the highest byte of memory. When we say that a machine is a 32 bit address machine we are saying that a pointer is 32 bits wide and thus requires 4 bytes of storage (assuming 8 bits/byte). On a 64 bit machine a pointer's size would be 8 bytes. Our discussions and illustrations will assume a 32 bit platform. There are still many such machines around.
The primary purpose of a pointer is to read and modify a variable without using that variable's real name. A pointer gives us an alias for a variable's name (and thus, its contents). Once we explain how to declare, initialize and use pointers, we will show you the canonical scenario that requires the use of pointers to solve a problem for which there is no other solution.
int *pi; /* creates a variable whose data type is pointer to int */
Notice the * between int and pi. The star means pi is a pointer to int, not an int, The pointer type is its own type. Furthermore every pointer has a specific sub-type in that every pointer is a pointer to some type such as pointer to int, or pointer to char, or pointer to float, etc. The only exception is the void * type which is a generic pointer not associated with any particular type. We will cover void * in our chapter on dynamic memory and again when we cover generic containers. Our pointer pi is a pointer to int. The only value you should ever assign into pi is the address of some integer variable or the NULL value which we will explain shortly. Remember that C does not initialize variables for you. When you declare pi it contains whatever bit pattern was put there by the last process that wrote to that chunk of memory. It is your responsibility to initialize pi before you use it. This fact causes problems for novices who forget it.
pi = NULL; /* assigns a 32 bit zero into pi */
-OR-
pi = &i; /* assuming i has been declared as an int variable */
The NULL value is a 32 bit zero (assuming we are on a 32 bit address platform). Having zero (i.e. NULL) in a pointer means the pointer is not pointing to anything. Address zero is not a valid address. No variable could ever be stored at this address. NULL is a "DO NOT DEREFERENCE THIS POINTER" marker value. We could have done pi = 0;
but you should not assign the number zero into a pointer. Use NULL instead to avoid compiler warnings because 0 and NULL are different types even though they are the same bit pattern. One is an integer, the other is a pointer (i.e. address) value.
The three types of zeros are:
Do not mix and match.
If you do mix them up you may get compiler warnings which in some cases might not be benign. If you read a lot of legacy (old) C code you will see literal zero 0 assigned into pointers all the time. This is an archaic practice that should be replaced by the practice of always assigning the right type of value. One notable exception is in the case where you are using char variables to represent small numbers. In this case it is acceptable and common practice to assign/compare literal 0 into/against that char variable. However, if you are storing ascii character values in that variable it is more readable to assign/compare '\0'.
A moment ago we used the word dereference when explaining the NULL value. Let's explain that term. The purpose of having a pointer is to access a variable without using that variable's real name. A pointer is used as an alias or a synonym for some variable's name (and thus its value). In order to read or write the original variable, we put the variable's address into the pointer, then dereference the pointer.
Putting a star before a pointer dereferences that pointer. A dereferenced pointer is an alias or synonym for the variable whose address is stored in the pointer.
int i; /* we need a variable for pi to point to */
int *pi; /* we have our pointer pi */
pi = &i; /* and now pi has i's address */
*pi = 5; /* this is dereference and the variable i gets the value 5 assigned into it. */
printf("value of i is: %d\n", *pi ); /* again *pi means the value at the address inside pi */
Once the pointer variable pi is loaded with i's address, the syntax *pi
is a synonym for i
, thus
*pi = 5;
is identical/interchangeable with
i = 5;
and
printf("value of i is: %d\n", *pi );
is identical/interchangeable with
printf("value of i is: %d\n", i );
p
is loaded with the address of a variable v
such as when we do p = &v
, then the expression *p
becomes a synonym for v
and the two can be used interchangeably.
/* DECLARATION */
int *pi; /* declares a single pointer variable */
int *p1, *p2, *p3; /* declares three pointers to int */
int *p4, p5, p6; /* declares only one pointer and then two plain ints. */
/* The last two need their own stars to make them pointers. The star does not "distribute" */
/* DECLARATION and INITIALIZATION at same time */
int *pi = NULL; /* the *pi is declaration NOT dereference
- OR -
int *pi = &i; /* again, the star to left of pi is specification of its type as pointer. */
/* There is no dereferencing, just declaration on left and initialization from the right */
/* DEREFERENCE */
*pi = 5;
-OR-
printf("i = %d\n", *pi ); /* once pi contains &i, using *pi is the same as using i */
You can declare a pointer to any type.
char *pc; /* pc is type pointer to char */
float *pf; /* pf is type pointer to float */
double *pd; /* pd is type pointer to double */
Even types that you define yourself. You don't know how to do this yet but you will learn in chapter 6.
typedef struct /* structs are covered in chapter 6 */
{
char * lastName,firstName;
double qpa;
int year;
char * major;
} STUDENT_TYPE;
STUDENT_TYPE *ps; /* ps can store the address of any STUDENT_TYPE variable */
STUDENT_TYPE student;
ps = &student; /* ps has the address where student is stored in memory */
You have probably seen the classical swap operation that exchanges the values of two variables using a temporary variable. Here is a program to execute and observe. It performs classical swap but does so without using the real names of the variables in the exchange. Instead it uses pointers to alias the original variables.
ptrSwap.c
|
As illustrated above, once a pointer p has been loaded with the address of variable v, the syntax *p becomes a synonym for v. It may have occurred to you that our pointer swap above is contrived. You must be asking yourself:
Good question, and you are correct. We did not need to use pointers in the above scenario.
Below is a program that executes the swap operation without using pointers. Tell me what will (or won't) happen and why.
badSwap.c
|
The badSwap program does not succeed in swapping the values because our call to badSwap only passes copies of x and y into the function. When we return from badSwap we discover that the original x and y in main were not modified by the code written in badSwap. The C language always/only passes parameters this way. This is called pass by value or pass by copy. Another way to say this is that code written in one scope cannot reference the names of variables declared in other scopes. There is no way to pass by reference in C. C++ does have a pass by reference mechanism but C does not. Fortunately for us there is a workaround for this problem. In fact we've been teaching you all about it in this lesson. See if you can apply what we've been discussing, to this problem. Can you fix the swap program?
Here is the solution: solution-1.c Don't peek until you give it a try
Our solution #1 passes the addresses of x and y instead of just passing the values. The goodSwap function then dereferences those addresses (pointers) to modify the value of x and y from main. Once inside goodSwap, *a and *b are aliases for x and y respectively. Do you see how pointers solved a problem that has no other solution? Pointers allow us to modify data from one scope using code in another scope. This means we can read and write variables even though we cannot use their names. This is the fundamental use of the pointer type.
Question: By using pointers like we just did in goodSwap, are we modifying the parameters we passed into the function ?
Answer: No - we are not!. The pass by value principal still holds. We passed in two addresses and we certainly have not changed those addresses. We only changed the data they point to.
Furthermore it helps to remember that incoming args to a function behave much like local variables to the called function. Those incoming args (local vars) get a copy of the values passed from the caller and when control returns from the function those local args are destroyed.
Pointers are the only mechanism C gives us to modify data declared in one scope, using code written in another scope. If data is declared in function1 and we want to write code in function2 that modifies the data in function1 then we must pass the addresses of the variables we want to change. The calling function sends in addresses and the receiving function must prototype those incoming args as pointers. All function2 has to do to modify the data in function1 is to dereference the pointers sent in.
Add a function int getInts( int *a, int *b );
that prompts the user for both ints and stores the values into main's x and y. getInts() should also return the number of conversions and main should examine that value before printing x and y, If two good conversions were not made then an error message should be written to stderr saying not all the values were good.
Add a function void printInts( int a, int b );
that takes a copy of the values x and y and prints them to stdout. Replace the prints in main with calls to printInts().
Now that you are using pointers and functions, you must beware of the dangers. With great power comes great responsibility. Consider this next program.
dangerous.c
|
I will tell you that syntactically everything is fine. The function returns a pointer to int and that returned pointer value is assigned into the proper type of pointer variable back in main. The right kind of values are being put into the right kind of variables etc., but something very dangerous is happening. Do you see it?
When we return from our getInt() function, the local variable
Our stale pointer discussion brings us to a discussion of why the local variable called num in getInt() has been deallocated. A variable's life span is tied to the life span of the block it was declared in. A scope block is any pair of { } s. Every function has a pair of { } enclosing its code. Even a pair of { } after an if, else or while loop is a scope block. In fact it is legal (although not useful) to just have a pair of {} s show up in the middle of a piece of code and declare variables inside that block. Any variables declared inside a pair of matching { } braces are alive from the line of their declaration until the closing curly brace of the block they were declared in. Any variables declared thusly are called automatic variables. The memory they occupy is reclaimed or deallocated automatically by the compiler as soon as execution exits the block they were declared in. In our next chapter will we discuss the other categories of allocation, dynamic and static. We will also illustrate the mechanism by which this is accomplished.
C is not an object oriented language. There are no classes, no inheritance, polymorphism or function overloading. C has no keyword "Array" or "array". C does however have the array type. C arrays are indexable contiguous storage of homogeneous values. When you declare an array you are guaranteed that all the elements are of the same type and size and that they are stored in consecutive chunks of memory. The name of your array variable can be used as the address of (pointer to) the first element. Download arrDemo1.c and open it up in your favorite editor.
Our first example of array declaration (below) illustrates literal initialization. There is no dimension value between the [ ]s. The list of integer literals on the right is parsed by the compiler to determine the capacity of the array and the initial value for each element. The array has exactly five elements and each element is initialized to the respective value indicated in the list. This syntax is identical to what you have seen in Java.
int a[] = { 2,3,5,7,11 }; /* these values can be modified. They are not constants */
Our next two lines of code declare and initialize two integers that represent the number (aCnt) of values currently stored in the array and the capacity (aCap) of the array. Remember, C has no array class so you can't query your array with a.length to find out its capacity like you would in Java.
int aCnt, aCap;
aCnt = aCap = 5; /* <-- note the associative right to left property of assignment. The 5 propagates right to left into both variables */
Our next declaration creates a second array b in the same manner.
int b[] = { 4,6,8,12,16 };
int bCnt, bCap;
bCnt = bCap = 5;
Our next array is dimensioned but not initialized. Its count and capacity get different values since the cap is 10 but the count is 0. It is very important to understand that the value between the [ ]s must be a compile time value. You are
int c[10], cCnt=0,cCap=10;
Let's look at our first function, initArray(). Notice how we prototype the incoming array parameter as an array of int with no specific dimension. This allows us to pass in an array of int with any capacity. We also pass in the capacity because we want to initialize every element of the array. The factor is just a constant we multiply the index by to get a particular element's initial value. To access an individual array element we use the [ ] operator like you would in Java.
void initArray( int arr[], int arrCap, int factor)
{
int i;
for (i=0 ; i < arrCap ; ++i )
arr[i] = i*factor;
}
Here we call our initArray() function from main. We just pass the name of the array into our function and the compiler uses that name as the address of the first element. Indexing off that address (name) lets us read and assign values into those elements.
initArray( c, cCap, 3);
Our print function is similar in that it takes the name and count and prints the elements by indexing off the array's name. Where things get interesting is our swapArrays() function below.
void swapArrays( int a[], int aCnt, int b[], int bCnt )
{
int i;
if (aCnt != bCnt )
return; /* don't try to swap diff sized arrays */
for (i=0 ; i < aCnt ; ++i)
swapInts( &a[i], &b[i] ); /* RE-USE working code since array elements are int variables */
}
Look at what we are doing. We step to each [i]'th element of both arrays and swap the values. We call swapInts(&a[i], &b[i])
and pass the addresses of the [i]'th elements of the two arrays. Looking at the syntax, which operator do you think is taking precedence? Is it the & or the [] ? A little thought should lead you to conclude that the [] operator takes precedence. Clearly we want to apply indexing first, then take the address of that indexed element. This observation leads us to an all important rule you should commit to memory to help you resolve busy syntax involving multiple operators in the same expression.
A prefix operator is an operator that is placed before the operand, such as ++i or &x. A postfix operator appears to the right of its operand, such as i++ or arr[i]. An infix operator appears between its operands, such as a + b.
By passing the addresses of the ith elements into our swapInts() function we have also exercised another all important principle called code re-use. We call a function we have already written to perform a well defined operation. We will emphasize code re-use for the rest of this course. Look at the last paragraph of code in the main and notice that when we scanf(..) for c[0], we used the array's name as the addresses of c[0]. This is another example of the array's name being correctly used as if it is a constant pointer to the first element. By constant we mean approximately the same thing as the word final in Java. A constant in C is a value that cannot be changed.
Question: In the very last line of main when we print the value of c[0], what syntax could we have used instead of c[0] to supply printf() with the value of c[0] ?
Answer: c[0] could have been replaced with *c since the name c has the address of the first element and putting a * to the left of c dereferences that address, which produces the value c[0].
The most common way to think of and use an array's name is as a pointer to the first element in the array. Although this is not precisely what an array's name is, the intention of the designers of the language is that you use the name as if it is a pointer to the first element to dereference and index off of. An array's name is actually an immediate value from the compiler. An example of an immediate value is the expression (x+4). It has some value but is not a variable and you cannot assign a value into it. Likewise an array's name is not a variable and cannot be assigned into. To prove it, try to compile illegal-1.c The compiler will give you an error message stating you cannot assign a value into the array's name. Java allows this kind of thing because in Java, an array's name is a reference variable. Not so in C.
|
Our illegal-1.c program demonstrates an attempt to assign a new value into an array's name. The only things that are allowed on the receiving end of an assignment are l-values (ell value). A variable's name is not the only kind of l-value. An expression such as array[i] can also appear on the left of an assignment too. However the array's name itself is not an l-value and cannot be assigned into. Think of the array's name as being bound to that chunk of memory for the lifetime of the array. Let's look at some code that will show us another property of an array's name, and illustrate how C passes an array's name to a function.
sizeof-array.c
|
The first two printfs will print the size of a pointer on your system, probably 4. The third value printed, however will definitely be 42 since chars are always one byte and our array is an array of 42 chars. Thus, the expression arr doesn't always behave like the expression &arr[0]. We now want to understand why sizeof(arr) in main is 42 but sizeof arr in the function is 4. The answer is simply that when we pass the array's name into a function, the compiler will not under any circumstances make a deep copy of the array for the function to work with. The compiler will instead allocate a pointer variable and initialize it to contain the address of the array's first element. Thus the arr[] parameter to the printSizeof() function is not an array. That parameter is a pointer. This leads to another question: Why won't the compiler make a copy of the entire array? The answer is efficiency and economy. Imagine the compiler making a copy of the array passed in instead of making a pointer to it. What would happen if you had a recursive function such as binary search (or worse yet a recursive linear search) which passes the array into the next recursive call. Do you see the disaster it would cause? Every recursive call would allocate another copy of the array on the call stack and that array would have to be initialized to the elements of the passed in array. Your program would run very slow and run out of memory very quickly. In the next chapter we will explain the call stack and illustrate these concepts clearly. For now suffice it that making a deep copy of the array for every function that receives the array's name would be an incredible waste of space and time and this is precisely why the compiler always converts an incoming array name to a pointer once inside the receiving function.
You might be wondering if there is a way to trick the compiler into making a copy of the entire array passed into a function. Maybe our prototype fails to force deep copy only because there is no dimension between the []s. The answer is no. For instance, what do you think will happen if you write your prototype like this:
int recursiveLinearSrch( int arr[100], int lo, int hi, int target ); /* it's legal but still does not cause a deep copy of the array */
This prototype would not cause the array to be copied. It would merely require the incoming array to be dimensioned exactly to 100 elements. Bottom line is that the compiler will never deep copy an incoming array to a function for reasons of efficiency and economy.
It may have occurred to you that if the compiler always converts an incoming array to a pointer it might be proper to prototype the parameter as a pointer. This is in fact correct. The following two forms of prototyping an incoming array parameter are equivalent to the compiler.
void foo( int arr[] );
/* EQUIVALENT TO */
void foo( int * arr );
But remember they are identical only as function parameters. They are not identical as variable declarations
/* the below syntax is not legal in C. You have to either put a dimension in the []s
or put a list of values on the right of an equal sign */
int arr[];
/* IS NOT THE SAME AS */
int * arr; /* this is legal and declares a pointer to int */
Which form is to be preferred? Its a matter of style. If you read a lot of legacy code you will see most programmers using the * syntax rather than the [] syntax. Sometimes its a matter of clarity rather than style. Remember that a pointer can either be pointing to a single value, or it can be pointing to the first value of an array. Don't use array syntax if your pointer is pointing to a single value - like this bad example:
void swap( int a[], int b[] ) /* using the [] syntax is misleading! */
{
int temp = *a;
*a = *b;
*b = temp;
}
You can't go wrong just using the * declaration form all the time when prototyping an incoming array parameter. We adopt this practice for the rest of the course. It is important however that you have seen and understand the [] form of the incoming array parameter.
It's time to talk more about what it really means for a pointer to be a pointer to a specific type. So far we have told you that if you declare pointer to char such as char *pc;
you should only put the address of a char in that pointer. Every pointer points to some specific type and every type has a storage size associated with it. On a typical 32 bit platform a short int is 2 bytes, an int is 4 bytes, a double is 8 bytes, and a char is 1 byte. As a result, pointers to these types behave accordingly when arithmetic is done to the pointer.
We have emphasized that pointers are addresses not numbers. However, you can add and subtract ints from pointers. When you do, the result is always a pointer. This is pointer arithmetic. Let's illustrate.
int arr[] = { 2,4,6,8,10 };
int *p = arr;
In the above diagram we see our array of five ints and the explicit bit pattern of each element shown. Each array element requires 4 bytes of storage. We then declare an int pointer and initialize it to the address value in arr. Pointer p now points to the same thing that arr points to - the first element of the array. We indicate this fact in two ways. Firstly we write the address 1000 in the pointer contents. Secondly we draw an array out of the pointer variable to the chunk of memory it points to. Now back to our question: What happens if we add an integer to our pointer p?
p = p+1; /* what value does p now contain? */
Notice that the pointer jumped by four instead of one. Pointers always point to a specific type and every type has a size. When you add an integer to a pointer the compiler assumes that you want to advance in increments of the size of the type pointed to. Adding one to our pointer p adds four because the next integer in the array is four bytes down the road.
p = p + 2; /* increment the pointer by 2 */
After incrementing by two we see our pointer advance by eight. Although we cannot modify the array's name we can do pointer arithmetic on the array's name by creating expressions that add or subtract from the name. Like this:
/* prints the number 6 */
printf( "arr[2] = %d\n", *(arr + 2) ); /* (arr+2) is addr of 3rd elem. The * dereferences that addr. */
These observations are explained by the following
For any array's name ( i.e. arr)
arr
may be used as if it is a (const) pointer to the first element of the array. To be precise, however, it is an immediate value from the compiler
arr = something;
is ILLEGAL because an array's name is not a variable
arr + i
produces the address of the i'th element in the array
arr[i]
is the dereference of the address of the i'th element. Thus producing the value of the i'th element
&arr[i]
is the address of the i'th element of the array. The [] operator takes precedence over & since postfix operators always bind tighter than prefix operators.
The following may be thought of as something like fundamental theorems or identites of pointer arithmetic.
arr[i]
is equivalent to *(arr + i)
arr + i
is calculated by the compiler as arr + (i * sizeof(*arr) )
Notice that we are once again using the array's name as if it is a pointer to the first element. Our sizeof-array.c program demonstrates that according to the sizeof operator, the array's name is more than just a pointer value. If you feed an array's name to the sizeof operator it will accurately tell you the total number of bytes used by the array. This value is calculated simply as the number of elements in the array (dimension) times the number of bytes used by each element of the array. However, as soon as you pass an array's name into a function, that name gets converted to a pointer (to the first element) and once inside the function, the sizeof operator will no longer tell you the size of the original array. Instead, it will always produce the sizeof a pointer. Thus on a 32 bit address platform, if you pass an array's name into a function then apply the sizeof operator on that array's name, it will always return 4 instead of the true size of the array that was passed in. As a result of this automatic conversion of an array's name to a pointer when passed into a function - some instructors and authors have taught a common oversimplification that an array's name is just a constant pointer to the first element.
In C, a string is an array of characters with a null character terminator. We will demonstrate declaration, initialization and use of one and two dimensional strings.
#include <stdlib.h>
#include <stdio.h>
#include <string.h> /* string library */
#define N 10
int main()
{
char name[ N ];
The memory allocated for the string name looks like:
name: [?][?][?][?][?][?][?][?][?][?]
0 1 2 3 4 5 6 7 8 9
The ? means the value in the cell is undefined.
Whatever was last there is still there.
printf("Enter your first name: ");
scanf( "%s", name );
assume the user types: Timothy
The memory allocated for the string name now looks like:
name: ['T']['i']['m']['o']['t']['h']['y']['\0'][?][?]
0 1 2 3 4 5 6 7 8 9
scanf copies the chars from the keyboard into their respective elements of the array and then adds the '\0' terminator to mark the end of the string. '\0' is ascii value 0 in 8 bits of storage. This terminator must be present in order for printf and the string functions to work properly. It follows that a character array of length N can safely store at most N-1 characters.
printf("Your first name is %s\n", name );
return EXIT_SUCCESS;
} /* END OF MAIN */
The printf function starts at the first char of the array and continues printing chars until it sees the null char '\0'. The null is not printed. A couple questions arise.
In this case scanf continues to copy characters into memory beyond the end of the array then adds the terminator. If the user enters: "BillyJoeBob" then memory now looks like:
We have just accessed memory via the name variable that does not belong to the name variable! Even if the memory occupied by those last 2 bytes was being used by some other variable in our program, we just trashed it's value! Unfortunately C does not guarantee to detect such a mistake for us. Your program may crash before the data is finished being copied, or worse yet it may continue to run with corrupted memory which can produce unexpected/unpredictable behavior later. These kinds of errors can be very difficult to discover since the behavior may be inconsistent from run to run. If the program does crash you will probably see an error message with the words segmentation fault in it. We will explain segfault soon. This condition is also known as buffer overflow.
int i=0;
char c = 'a';
while (i < N) /* we hardcode the contents of the string but no '\0' */
name[ i++] = c++;
and now memory looks like:
Our printf function does not know when to stop. It continues to print characters beyond the end of the array until it chance encounters a '\0' value, or crashes somewhere after the end of the array.
char* strcpy(char *dest, const char *src );
The strcpy function copies the contents of one string into another and tacks on the terminating null char. It returns a pointer to the dest string.
char foo[10];
char bar[10];
strcpy( foo, "Hello");
foo: ['H']['e']['l']['l']['o']['\0'][?][?][?][?]
0 1 2 3 4 5 6 7 8 9
Note that strcpy will accept a string literal as its source. The meaning of const in the prototype does not mean the src must be a string literal. It just means the code inside strcpy should not modify the source string. If the code inside does modify the src string, the compiler will issue a warning that a read only location is being assigned into. The compiler however will complete the compilation and let you do it. Those of you familiar with C++ may recall that C++ will refuse to compile const code that modifies a const arg. C however, as usual, lets you do something that is inconsistent. The strcpy function added a null char ('\0') to the dest string right after the 'o' in "Hello". This null char is the sentinel value that marks the end of the string. None of the string functions work correctly without this terminator being present.
strcpy( bar, foo );
bar: ['H']['e']['l']['l']['o']['\0'][?][?][?][?]
0 1 2 3 4 5 6 7 8 9
strcpy( bar, "Tim" );
this memory unchanged
______________________
bar: ['T']['i']['m']['\0']['o']['\0'][?][?][?][?]
0 1 2 3 4 5 6 7 8 9
Note that strcpy does not alter the portion of the dest string after the NULL deposited by the copy operation. Strcpy does not do any error checking for invalid arguments. If the src string is not null terminated than strcpy will read on further in memory until it crashes or chance encounters a null char. If the dest string is not big enough to hold src then strcpy will copy chars beyond the end of dest until it crashes or completes the copy. Either of these 2 cases are memory errors even though your program might not actually crash on any particular run.
int strcmp( const char *s1, const char *s2 );
The strcmp function behaves much like the compareTo method in Java Strings (or vice versa since C came first). The strcmp compares both strings one char at a time starting at the addresses passed in. It subtracts the ascii value of s2[i] from s1[i]. If the difference is non zero or if a null char is encountered in either string, the difference is returned. See the man pages for a formal description. As with all the string functions no error checking is done and bad args produce crashes or unpredictable behavior.
int strlen( const char * );
The strlen counts the number of non null chars starting at the address passed in. As with all the string functions no error checking is done and bad args produce unpredictable behavior.
char *strcat( char * dest, const char *src );
Appends the src onto end of dest starting with copying src's first char over top of the terminating null in dest. The rest of the chars from src are then copied over and a terminating null char is tacked onto the end of the resultant string. This function also returns a copy of the dest pointer.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define ROWS 3
#define COLS 5
int main()
{
char matrix[ROWS][COLS];
/*
The memory allocated for matrix looks like:
matrix: [?][?][?][?][?][?][?][?][?][?][?][?][?][?][?]
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
for convenience we view the matrix as:
0 1 2 3 4
0 [ ? ][ ? ][ ? ][ ? ][ ? ]
1 [ ? ][ ? ][ ? ][ ? ][ ? ]
2 [ ? ][ ? ][ ? ][ ? ][ ? ]
*/
scanf("%s", matrix[1] ); /* assume user types: cat */
/*
produces:
0 1 2 3 4
0 [ ? ][ ? ][ ? ][ ? ][ ? ]
1 ['c']['a']['t']['\0'][ ? ]
2 [ ? ][ ? ][ ? ][ ? ][ ? ]
*/
strcpy( matrix[2], "dog");
/*
produces:
0 1 2 3 4
0 [ ? ][ ? ][ ? ][ ? ][ ? ]
1 ['c']['a']['t']['\0'][ ? ]
2 ['d']['o']['g']['\0'][ ? ]
*/
printf("string in row 1: %s\n", matrix[1] ); /* prints cat */
printf("string in row 2: %s\n", matrix[2] ); /* prints dog */
return EXIT_SUCCESS;
} /* END OF MAIN */
In this case scanf or strcpy spills the overflow characters into the next row. If you are already at the last row then you spill over beyond the end of the entire array and you have committed a memory error which results in undefined behavior.
strcpy( matrix[0], "foobar" );
produces:
matrix: 0 1 2 3 4 0 ['f']['o' ]['o']['b' ]['a'] 1 ['r']['\0']['t']['\0'][ ? ] 2 ['d']['o' ]['g']['\0'][ ? ]
Notice that "foobar" spills over to row 1 and overwrites the first 2 chars of the word "cat". This is not an error per se since matrix owns the space but it is probably an unintended effect.
char *name = "Timmy";
The above syntax declares a string constant. By constant we mean that you cannot modify the individual characters. Watch what happens when we place this declaration in a program and try to change one of the letters in the string. We hold off on the precise explanation of segmentation fault until our chapter on dynamic memory. This segmentation fault is a run time memory error resulting from our attempt to modify data that is not a variable.
Contrast this with the following declaration and modification of a string variable.
Here is another, more typical form of declaring, initializing and modifying a string variable.
char *words[] = { "real", "programmers", "use", "emacs" };
Any attempt to modify the number of strings in this array or modify any of the chars in those strings will result in an error. Let's look now at our argv array that is part of the signature of main and allows us to pass strings into our program. Try it yourself. Try to change the word "emacs" to "vim" using any syntax you can get to compile.
#include <stdio.h> #include <stdlib.h> #include <string.h> int mystery() { char *data[] = { "I", "will", "not", "miss", "this", "one" }; char words[6][4]; int i; for (i=5; i>=0 ; --i) strcpy( words[i], data[i]); for (i=0; i<6 ; ++i) printf("%s\n", words[i]); return 0; } int main() { mystery(); return 0; } |
|
Workaround #1 uses tells fscanf to limit the number of characters to read in. This solution has a couple weaknesses that makes it unacceptable, Do you see then? There is no way to tell with certainty if your read had to be truncated or not. Also - the limit value must be a compile time constant.
|
Workaround #2 uses fgets to read an entire line from an input stream. It is superior because we just give it the actual buffer capacity (not capacity-1) and it knows to stop one short. It also allows you to pass that capacity number from a runtime variable's value and not a compile value. Lastly, it has the advantage of storing the original newline and tacking on the null char after. This means that once we read in our line, all we need do is verify it contains a newline to prove the line was not truncated. We will use fgets() for our Lab#2 program. As soon as we learn dynamic allocation we will never again use fscanf to read strings.
What we really want is a function that:
Ansi C has no such function (although C++ has a getline function). You will have all the tools to write one yourself once we learn dynamic memory allocation.
There is one other kind of overflow known as value overflow. Value overflow occurs when you add a value to a variable and the new value is larger than the capacity of the variable. For instance if you declare char c =100, then execute c += 200, the char variable c has overflowed. Another way to overflow value is to scan in a string of digits that evaluates to a number bigger than your variable can hold. C does not promise to detect overflow and throw an exception for you like Java. Also the value that gets stored is not specified by the standard. What has been observed in most compilers is a wrap-around effect analogous to what happens when your car's odometer reaches it maximum mileage then wraps back to zero and start over again.