|
~ Introduction to Programming: C ~ Session 5 - Arrays, Strings & Pointers |
We have already seen a number of examples of strings and pointers already, without necessarily realising this. In this session we will be look at how this works so we can better understand and utilise how they work.
We have looked at the concept of an array when we learned about Pascal. A normal variable can store only one value. To store multiple values, we would otherwise have to create multiple variables. If we don't know how many values we will be using in the program (e.g. a name and address database), then we cannot declare the appropriate number of variables in advance.
This is where the Array comes in. An array lets you store multiple values of the same type (e.g. an integer, a character, a float etc.) in a single variable. In this way, you can use a single variable to store a list of items. For example:
int
numberlist[10];
in C declares an array of ten integers, the list given a name of numberlist. In Pascal, the equivalent statement would have been numberlist:array[0..9] of integer;
Note that when referring to elements (i.e. individual items) in an array, we typically use an index number to identify which item in the array we wish to use. In C, the first item is given the value of 0 (zero) - seemingly contrary to common sense that tells us that when we count, we start at 1 (one). There is sound reason for this, which we will look at later when we discuss pointers.
Thus, the first item in our list is referred to as numberlist[0] and the last as numberlist[9] giving us ten elements in all. For example, the following program (type it in!) will take anything up to 100 integer number inputs, until the value of 0 (zero) is entered, and then display the cumulative total.
Program 5.1
#include <stdio.h>
#include <conio.h>
main() {
int nums[100], count=-1, total=0, entry;
do {
printf("Enter value #%d (0 to finish): ",count+2);
scanf("%d",&entry);
if (entry==0) break;
count++;
nums[count]=entry;
} while (count<99);
for(int i=0;i<=count;i++) total+=nums[i];
printf("Total is %d\n", total); getch();
}
Notice that we are using a do...while loop to allow multiple numbers to be entered, counting the next index number using the count variable. The condition on the loop checks that we do not go over our self-imposed 100 item limit. If 0 is entered, we can exit the loop straight away using the break statement. Note that the counter only gets increased by 1 if 0 wasn't entered - it's important that you increase a loop counter variable in the right place. If we had placed it befor the break condition, it would have just counted an extra 0, so no problem. However what if we later change the program to find an average? See exercise 1 at the end of these session notes for an exercise relating to using arrays.
In C, there is no type of variable called a String. The C
equivalent is an array of characters. Each character in a string fills one
element in the array. In order to determine where the end of the string is, each
string needs to be terminated (i.e. have as its last character) a null character
(ASCII value 0, represented in a C string by \0). See the diagram below for
an example.
Character |
hello[0] |
hello[1] |
hello[2] |
hello[3] |
hello[4] |
hello[5] |
|
Contents |
H |
e |
l |
l |
o |
\0 |
Therefore when you declare your array of characters in which to store your string, make sure you add an extra character to accommodate the terminating null character.
You cannot simply assign a value to a string in C, as you can in Pascal. Therefore the following will give you a compiler error:-
char
hello[6];
hello = "Hello";
Try the following program intead (you will need to put it in a main() function and include the appropriate library) : -
char
hello[6];
hello[0]='H'; hello[1]='e'; hello[2]='l';
hello[3]='l'; hello[4]='o'; hello[5]=0;
printf("%s\n",hello);
This is rather a cumbersome way of setting up a string. A number of functions have been created, which are standard in all C compilers. These function can be found in the string.h library – so, make sure to use the directive #include <string.h> in your program if you wish to use these string-handling functions.
One of these functions is called strcpy (meaning string copy). It takes two input parameters - the first being the destination variable - i.e. where the string is to be copied to, and the second parameter gives the string that is to be copied from.
For example:-
#include
<stdio.h>
#include <string.h>
main() {
char hello[6];
strcpy(hello,"Hello"); printf("%s\n",hello);
}
Notice that we do not use the & operator. This is
because when we are passing an array, it is automatically assumed that we are
passing it by reference - i.e. we are changing the value of the variable
from the strcpy function. The
"Hello" is a constant string value and therefore also an array, so the
C compiler automatically uses it by reference, hence you don't need to be an
& before it.
Other commonly used String-related functions include:-
·
strcmp(x,y) where x and
y are two variables / values passed by reference. They are compared
alphabetically, and if x is before y alphabetically, a value less than 0 is
returned. If x is after y alphabetically, a value greater than 0 is returned.
Otherwise, if the two strings are identical, a value of 0 is returned. Careful
of capitalization. All capital letters are considered as being after lower-case
letters. E.g. "Alan" would come after both "alan" and "zoe",
but before "Betty" and "Zoe".
E.g. strcmp("Alan","Zoe")
returns value < 0
strcmp("Alan","zoe") returns value > 0
strcmp("zoe","zoe) returns 0
·
strlen(x) where x
is the string (passed by reference). This finds the length (i.e. number of
characters, excluding the terminating null character).
E.g. strlen("Hello")
returns 5
strlen("x") returns 1
· strcat(destvar,srcvar) takes the contents of the variables passed as reference as srcvar and adds it to the end of the variable passed as reference as destvar. The cat refers to concatenation, which basically means join-up-to. See the sprintf example for a sample of using this.
·
sprintf(destvar,format,params)
– Note this one is held in the stdio.h library. It works just like printf
except the result goes to the string variable passed by reference as destvar
instead of to the screen. For example, the following code will read integer
values in, and store them in a string separated by spaces. When a zero is
entered, the values are displayed followed by the total. Note the use of while(true)
at the end, which means loop around regardless. The exit condition is given in
the middle of the loop by the if
statement:-
program
5.2
#include <stdio.h>
#include <string.h>
#include <conio.h>
main() {
char str[500],samt[10]; int tot=0,amt=0; str[0]=0;
do {
printf("Value: "); scanf("%d",&amt);
if (amt==0) break;
sprintf(samt,"%d ",amt);
strcat(str,samt);
tot+=amt;
} while (true);
printf("Total of %sis %d\n",str,tot); getch();
}
See exercise 2 at the end of the session notes for an exercise relating to string functions.
Pointers form the building blocks of many aspects of C programming - passing variables to functions by reference, passing arrays to functions, allocating arbitrary space, working through elements in an array, copying data from one array into another, etc.
A pointer is fairly easy to visualize. If you think of your computer's memory as a large array, starting at 0 and ending with some large number, then programs, data etc. can be stored in that space at any point (that doesn't clash with another progrm/piece of data), and will take up a certain amount of room according to the size of the program/data.
When declaring variables, the computer sets aside part of this memory to store the variable. Now, if you imagine that an array of 10 characters called str1 gets stored at location (known as address) 1000, then it will end at location 1009. Location 1010 might have another array of 10 characters called str2 stored straight afterwards. Imagine that we start with str1 containing the string "Hello".
Imagine that we run the command strcpy(str2,str1)
The results afterwards would be as follows:
|
Address |
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
1009 |
|
Var Pos |
str1 |
|
|
|
|
|
|
|
|
|
|
Data |
H |
e |
l |
l |
o |
\0 |
? |
? |
? |
? |
|
Address |
1010 |
1011 |
1012 |
1013 |
1014 |
1015 |
1016 |
1017 |
1018 |
1019 |
|
Var Pos |
str2 |
|
|
|
|
|
|
|
|
|
|
Data |
H |
e |
l |
l |
o |
\0 |
? |
? |
? |
? |
What happens is that the & operator says "get me the address at which this variable is held" - i.e. &str2 would be 1010 and &str would be 1000.
By passing the address of the string rather than the string itself, the function can look at the memory, and take the data from the memory directly. In this way, we do not need to know the length of the string, or indeed anything else about the data to use it from the strcat function.
How do we refer to a variable when it has been turned into an address? The answer is to turn the address back into a variable of some type again - or dereference the address. This is done with the * operator.
To store a memory address in a variable, so that it can be
used to point to any place in memory, we prefix the variable name with a * when
we declare it. For example:-
char
str1[10], *str2;
str2=str1;
*str2='A'; *(str2+1)='l';
*(str2+2)='a'; *(str2+3)='n';
*(str2+4)=0;
printf("Name is %s\n",str1);
would print "Name is Alan". Here's what's
happening:-
· char str1[10] - Declare array of 10 characters called str1 - let's say it gets stored at position 1000. The values held between 1000 and 1009 are currently undefined, and could be anything.
· char *str2 - Next, we declare a pointer to a character called str2. Let's say this is stored at position 1010. It isn't storing an array - rather the address at which a character exists. Let's say it takes four bytes to store an address number. Thus, the address variable is held between address 1010 and 1013. Currently, the data in this part of the memory is undefined - it could be anything.
· str2=str1 - Next, we take the address of array str1 (you may think you would use &str1, but whenever we refer to an array, we are implicitly referring to its pointer or address – which in our example is 1000) and store it into our pointer variable. Thus str1 now contains th address of the start of str2.
· *str2='A' - Now we dereference str2, so that we say "take the data held at address held in pointer str2 - i.e. the character at address 1000" and assign it a value of 'A'.
· *(str2+1)='l' - Now we take the address held in str2 (1000) and add 1 to it (making 1001). We then dereference this address and assign a character to this position - the next address location after the 'A'. We do the same for letters 'a', 'n' and the terminating zero.
·
printf("Name is
%s\n",str1) - Now when we show the contents of str1, it will show as
being "Alan". Note that we could as easily done the following: -
printf("Name is %s\n",str2) – as both str2 and str1 point to the
same place.
One final point to note with arrays. When they are stored in memory, they are stored as an address to another place in memory that actually holds the array data. Thus, when you refer to an array, you are really referring to its address. For this reason, you do not need to use the & operator to find its address. When you specify an array element – e.g. nums[1] you are automatically dereferencing the array to give the data at a specific place in the array – thus, if you wanted to find the address of a specific position in an array (say element 10), you could use &nums[10], which is the same as (nums+9) – in this way, you could find the address of any array either by using &nums[0] or just nums!
If you think about a string constant, this is really just a constant array, so that is why when we pass a string constant such as "Hello" to an array, we do not need to use the & operator – by being an array, we are implicitly passing its address.
You may wonder where all this gets us. Well, if we pass an
address to a function, it can be the address of any variable, meaning we can
read from or write to a variable without knowing anything about it other than
its starting address from within a function. Let's try writing the strcpy
function for ourselves using this knowledge –
void
mystrcpy(char *dest, char *src) {
int i;
for(i=0;*(src+i)!=0;i++) *(dest+i)=*(src+i);
*dest=0;
}
This counts from 0 upwards while the character at address given by address passed via parameter src is not zero (i.e. the terminator character). However, we do need a terminating zero in the destination, so it is added outside the loop. Note that we need to use the value of i outside the loop - it identifies which character in the string is being copied / checked. Therefore, we have declared i outside the loop (just before it) to make it available anywhere within the function, rather than just within the loop.
For each time around the loop, the character at the ith position in each string will be copied.
Now consider the following situation: -
struct keep_together {
char str1[3],str2[6];
} t;
mystrcpy(t.str2,"Hello"); mystrcpy(t.str1,t.str2);
printf("str1 is %s, str2 is %s\n",t.str1,t.str2);
Don't worry about the struct statement. This is being used to ensure that the two variables str1 and str2 are kept together in memory (now referred to as t.str1 and t.str2).
You might expect this to give str1 is Hello, str2 is Hello but it is more likely to give str1
is Hello, str2 is lo - why is this? Below
is a map of the memory addresses after "Hello" has been assigned to t.str2.
|
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
|
t.str1 |
|
|
t.str2 |
|
|
|
|
|
|
|
|
|
H |
e |
l |
l |
o |
\0 |
The mystrcpy command copies the bytes from &str2
(i.e. address 1003) until a zero is found, and places them at &str1 (i.e.
address 1000). Unfortunately, str1 only has three bytes allocated to it, and as
the two variables are next to each other in memory, they will end up overlapping
as follows:-
|
1000 |
1001 |
1002 |
1003 |
1004 |
1005 |
1006 |
1007 |
1008 |
|
&str |
|
|
&str2 |
|
|
|
|
|
|
H |
e |
l |
l |
o |
\0 |
l |
o |
\0 |
Notice that the remainder of the old string (lo) from 1006 and terminating at 1008 never gets read, as the null at address 1005 terminates the string first.
This demonstrates the need to allocate the right amount of space in an array.
There is an exercise relating to the use of pointers given at the end of these session notes,
Change your program 5.1 so that it also displays the average of the values entered (remember this is the total amount divided by the number of items entered). Careful - remember the counter is 1 less than the actual number of items, as it is used to refer to the array index.
Type in program 5.2, putting it into a main() function and using the relevant #include directives.
Change the program to separate the values in the string with a comma instead of a space. To avoid having a leading or trailing comma, add a comma to the end of the string before the value just entered, if the string is not of zero length. This way, the comma does not get prefixed the first time a value is placed in the string.
Define the array length of 500 as a constant called max_str_len and change the maximum to 20 characters.
You could try exceeding this limit on purpose, but the results could be unpredictable, as you will be writing over an area of the computer's memory (after the end of the array) that could be being used for anything. At the worst, you could freeze your computer.
Change the program to check whether adding the comma and value as text will cause the array length limit of 20 to be exceeded, and break out of the loop with a warning message if this happens.
Create a function that takes two strings as parameters, taking the second string and placing it in the first string back-to-front. There are a number of way of approaching this. The easiest is probably to use a for loop from strlen(str2)-1 to 0 decreasing the counter by one each time around the loop. Test the function by reading in a string using scanf and writing it forwards and backwards to the screen.