Lecture 06: C Strings, String Library, and Pointer Arithmetic
Table of Contents
1 Review Arrays in C
In this lesson we will be discussing C Strings, which are essentially
arrays of char
's that are NULL
terminated. Due to the close link
between arrays, strings, and pointers, it's good to review some that
material first before diving into the nuances of C Strings.
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 you are 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.
2 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 expliciteNULL
termination.
We will tackle each of these in turn below.
2.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 is actually happening in this simple program:
#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_declerations 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.
2.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 declaring 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:
#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",a); //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.
2.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 seen 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:
#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.
#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 inedible 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.
3 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:
#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:
#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.
3.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 must 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!
3.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) then it returns 1, and if the first string is less then (smaller alphabetically) then it returns -1.
Plugging in strcmp()
into our secrete message program, we get the
desired results.
#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!
3.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.
4 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 refere0NCOs 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.
4.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 pointer arithmetic on these arrays to dereference the third index:
#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.
4.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 1111111. 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 difference 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.