Module 3: Pointers, strings, arrays, malloc
Pointers
What is a pointer?
- A pointer stores a memory address.
- A pointer usually has an associated type, e.g.,
an int pointer vs. a char pointer.
- Pointers (memory addresses) are themselves numbers and can
be added (if it makes sense to do so).
Let's start with an example:
int main ()
{
int i = 5;
int *intPtr;
// Extract the address of variable i into the pointer:
intPtr = & i;
// Print this address:
printf ("Variable i is located at address %p\n", intPtr);
// Use the address:
*intPtr = *intPtr + 1;
// See what happens:
printf ("i = %d\n", i); // What is in i now?
}
Note:
- The ampersand operator (&) extracts a memory address:
intPtr = & i;
- This address (a number) goes into the variable
intPtr
- The contents of this variable
intPtr
can be printed using the
%p
format specifier:
printf ("Variable i is located at address %p\n", intPtr);
- What good is a memory address if we can't use it?
- To use a memory address, one uses the star operator:
*intPtr = *intPtr + 1;
- What this is saying:
- Look at the right side first.
- Add 1 to whatever
intPtr
is pointing to.
- Left side of assignment: put the result in whatever
intPtr
is pointing to.
Exercise 3.1:
Type up, compile and execute the above program. Add a second
pointer variable that points to
i
and modifies
i
using only the second pointer variable. Can two pointers point
to the same thing?
A more complex example:
(source file)
// Declare some global integers:
int i = 5;
int j = 6;
int sum;
int main ()
{
// Int pointer declarations:
int *intPtr;
int *intPtr2;
int *intPtr3;
// First, let's print the address of variable i:
printf ("Variable i is located at address %p\n", &i);
// Now extract the address of variable i into the pointer:
intPtr = & i;
// Print.
printf ("The int at memory location %p is %d\n", intPtr, *intPtr);
// Let's do some arithmetic.
intPtr2 = & j;
sum = (*intPtr) + (*intPtr2);
printf ("The sum of %d and %d is %d\n", *intPtr, *intPtr2, sum);
// Now for something stranger:
printf ("The integer at location %p is %d\n", (intPtr+1), *(intPtr+1));
printf ("The integer j=%d is at location %p\n", j, &j);
// Let's see what the default initial value of intPtr3 is:
if (intPtr3 == NULL) {
printf ("intPtr3 has been initialized to NULL\n");
}
else {
printf ("intPtr3 has been initialized to %p\n", intPtr3);
}
}
Note:
- The (asterisk) operator * is used in pointer
declarations and in dereferencing pointers.
- The (ampersand) operator & is used for extracting
memory addresses:
// Now extract the address of variable i into the pointer:
intPtr = & i;
- Pointers (memory addresses) can be printed using the
%lu format specifier.
- Dereferenced pointers can be used in expressions:
sum = (*intPtr) + (*intPtr2);
- Pointers themselves can be used in expressions:
printf ("The integer at location %lu is %d\n", (intPtr+1), *(intPtr+1));
In this case, we examine the next address beyond i, which
happens to be where j is stored.
- Pointers can be compared (equality comparison) to NULL.
- Curiously, NULL is not a reserved word but a constant.
- ANSI C99 initializes pointers to NULL, but prior versions of
C do not.
- Important: it's best to assume pointers and variables are not initialized.
Consider an example with char pointers:
(source file)
int main ()
{
int i = 5;
char *charPtr; // Pointer declaration
int j;
// Make the char pointer point to the start of the integer:
charPtr = (char*) (& i);
// Extract the byte, store it in the integer j and print j.
j = (int) *charPtr;
printf ("First byte: %d\n", j);
// Get the next byte and print:
j = (int) *(charPtr+1);
printf ("Second byte: %d\n", j);
// Get the third byte and print:
j = (int) *(charPtr+2);
printf ("Third byte: %d\n", j);
// Get the fourth byte and print:
j = (int) *(charPtr+3);
printf ("Fourth byte: %d\n", j);
}
Exercise 3.2:
In addition to the value in
j
also print the address in
charPtr
in the first
printf
statement.
Note:
- The integer i occupies four bytes, only one of
which contains "5".
- To extract the first byte, make the char pointer charPtr
point to the start of the integer and extract the byte.
- Every increment of the charpointer will point it to the next byte.
- A char pointer is declared with the asterisk symbol to the left
the variable:
char *charPtr; // Pointer declaration
- Similarly, a pointer is dereferenced using the asterisk symbol:
j = *charPtr; // Retrieve whatever charPtr points to.
- The byte so referenced is cast into an int
type for printing (since C has no byte type).
- The term (charPtr+1) adds "1" to the pointer
and thus points to the next memory address. To dereference,
we apply the asterisk.
Examining pointers in a debugger:
- Let's compile the second example with the debug option
gcc -g -o pointer2 pointer2.c
and run inside the debugger:
gdb pointer2
- Inside the debugger, we will set a breakpoint, step through and
print the pointer intPtr and what it points to:
(gdb) l
7
8 int main ()
9 {
10 // Int pointer declarations:
11 int *intPtr;
12 int *intPtr2;
13 int *intPtr3;
14
15 // First, let's print the address of variable i:
16 printf ("Variable i is located at address %p\n", &i);
(gdb) break 16
Breakpoint 1 at 0x10720: file pointer.c, line 16.
(gdb) run
Starting program: pointer2
Breakpoint 1, main () at pointer2.c:16
16 printf ("Variable i is located at address %p\n", &i);
(gdb) p intPtr
$1 = (int *) 0x0
(gdb) next
Variable i is located at address 134000
19 intPtr = & i;
(gdb) next
22 printf ("The int at memory location %p is %d\n", intPtr, *intPtr);
(gdb) p intPtr
$2 = (int *) 0x20b70
(gdb) p *intPtr
$3 = 5
(gdb)
|
The l (list) command lists the program.
The break command creates a breakpoint so you can
stop midway through execution.
The p (print) command prints variables.
The next command executes the next statement.
|
Exercise 3.3:
Write a program in
pointertopointer.c
to use a pointer to a pointer to an int.
Fill in the needed assignments below to make the program
print "5".
int main ()
{
int i = 0;
int *p = NULL;
int **p2 = NULL;
// Fill in the assignments here to make the program work:
// Should print "5";
printf ("i = %d\n", **p2);
}
Strings
Strings in C:
- There is limited support for strings in C.
- There is no separate string type.
- Strings are simply a sequence of char's where the
last character is the special character \0.
- String variables are declared as pointers to type char.
Example:
(source file)
int main ()
{
char *hStr = "hello"; // String initialization.
char *wStr = "world";
char ch;
// Note the use of "%s"
printf ("%s %s\n", hStr, wStr);
printf ("3rd char in wStr is %c\n", *(wStr+2)); // Prints 'r'
// Let's get the 6-th char: should be '\0'
ch = *(wStr+5);
if (ch == '\0') {
printf ("char is string terminator\n");
}
else {
printf ("char is not string terminator\n");
}
}
Note:
- Although not visible, the character immediately after the last
real character in a string is the "null" character \0.
- Strings are enclosed in double-quotes.
- String variables are declared as char * variables.
- Strings can be printed using the %s print-format specifier.
Arrays
There are two ways of creating an array in C:
- Declare an array with a static size.
- Declare an array variable without a size, and allocate
memory dynamically.
Let's look at an example:
(source file)
int main ()
{
int A[10]; // Statically-sized unidimensional array.
double B[20][20]; // 2D array.
int *A2 = NULL; // Declaration of 1D array variable.
double **B2 = NULL; // Declaration of 2D array variable.
int i, j; // For-loop variables.
int sum; // For use in examples.
double dSum;
// Static example. The space is already allocated, so the
// array can be used immediately.
for (i=0; i < 10; i++) {
A[i] = i * 100; // Fill the array with some numbers.
}
sum = 0;
for (i=0; i < 10; i++) {
sum += A[i]; // Compute the sum of array's numbers.
}
printf ("sum = %d\n", sum); // Print sum.
// Dynamic version of example using "malloc" to allocate space.
// Note the use of the "sizeof" keyword.
A2 = (int*) malloc (sizeof(int) * 10);
for (i=0; i < 10; i++) {
A2[i] = i * 100; // Fill the array with some numbers.
}
sum = 0;
for (i=0; i < 10; i++) {
sum += A2[i]; // Compute the sum of array's numbers.
}
printf ("sum = %d\n", sum); // Print sum.
// 2D example with static array.
for (i=0; i < 20; i++) {
for (j=0; j < 20; j++) {
B[i][j] = i*j; // Fill the array with some numbers.
}
}
dSum = 0;
for (i=0; i < 20; i++) {
for (j=0; j < 20; j++) {
dSum += B[i][j]; // Compute the sum of array's numbers.
}
}
printf ("dSum = %lf\n", dSum); // Print sum.
// Dynamic version of 2D example. Note the two-step allocation.
// Allocate space for 20 pointers-to-double
B2 = (double **) malloc (sizeof(double*) * 20);
for (i=0; i < 20; i++) {
// For each such pointer, allocate a size-20 double array.
B2[i] = (double*) malloc (sizeof(double) * 20);
}
for (i=0; i < 20; i++) {
for (j=0; j < 20; j++) {
B2[i][j] = i*j; // Fill the array with some numbers.
}
}
dSum = 0;
for (i=0; i < 20; i++) {
for (j=0; j < 20; j++) {
dSum += B2[i][j]; // Compute the sum of array's numbers.
}
}
printf ("dSum = %lf\n", dSum); // Print sum.
// Must free allocated memory when not needed anymore:
free (A2);
free (B2);
}
Note:
- Although ANSI C99 allows declaration of for-loop variables in
the for-statement, our example is compatible with earlier versions
of C.
- Dynamic memory allocation is done using malloc
and is returned using free.
- Consider the first example:
A2 = (int*) malloc (sizeof(int) * 10);
Note:
- The variable A2 is declared as a pointer to
int:
int *A2 = NULL; // Declaration of 1D array variable.
-
The argument to malloc is the total amount of
space (in bytes) needed.
- We need space for 10 integers.
- Since we don't know how much space an int takes up,
we get the current system's size-of-an-
int
by
using the sizeof operator.
Thus, an alternative way of writing the same code is:
spaceNeededInBytes = sizeof(int) * 10;
A2 = (int*) malloc (spaceNeededInBytes);
Finally, note that malloc's return value
is declared as void* (i.e., pointer to anything).
It must be cast as the desired pointer:
A2 = (int*) malloc (sizeof(int) * 10);
Consider the second example:
B2 = (double **) malloc (sizeof(double*) * 20);
for (i=0; i < 20; i++) {
B2[i] = (double*) malloc (sizeof(double) * 20);
}
Note:
- The first statement allocates an array of pointers (to
double):
B2 = (double **) malloc (sizeof(double*) * 20);
- We need space for 20 double-pointers.
- Since we don't know the size of a pointer-to-double, we
use sizeof(double*).
- Note that malloc's return value (a pointer)
must be cast to the type of B2.
- Once we've allocated the array of pointers, each of
those pointers must be made to point to an array of
double's:
for (i=0; i < 20; i++) {
B2[i] = (double*) malloc (sizeof(double) * 20);
}
Consider what it means to access B2[4][6]:
- B2[4] is the 5-th element in the array pointed
to by B2.
- This 5-th element is itself a pointer - to an array of double's.
- Thus, B2[4][6] is the 7-th element in the array
pointed to by B2[4].
Memory and malloc
Let's start with a simple example:
(source file)
#include <stdio.h>
#include <stdlib.h>
int main ()
{
// Declare a pointer to an int.
int *p;
// Get the space from malloc:
p = (int*) malloc (sizeof(int));
// Assign a value and print:
*p = 5;
printf ("Int value: %d\n", *p);
// Free the memory when done:
free (p);
}
Note:
- The pointer declaration
// Declare a pointer to an int.
int *p;
simply declares the pointer variable. The pointer may itself is
invalid since it hasn't been assigned a value yet.
- In C99, pointers are initialized to zero, e.g.,
int main ()
{
// Declare a pointer to an int.
int *p;
printf ("Initial value of pointer: %p\n", p); // Prints 0 (C99)
}
Thus, the "picture" after declaration is:
- To make the pointer point to something, there are two ways:
- Assign it an address of a known variable, e.g.
int main ()
{
int *p;
int i;
// Extract the address of i and put that in p:
p = &i;
}
- Ask malloc to create some space and return the start address
of that space:
int main ()
{
int *p;
// Get the space from malloc:
p = (int*) malloc (sizeof(int));
}
- Suppose the space assigned is at location 1023.
The picture of memory after the allocation is:
- After the space is assigned, it can be used:
*p = 5;
printf ("Int value: %d\n", *p);
After this assignment, the picture is:
- The argument to malloc is the number of bytes desired.
- The program could also have been written as:
int main ()
{
int *p;
p = (int*) malloc (4);
*p = 5;
printf ("Int value: %d\n", *p);
}
This works if we happen to know that an int takes up
4 bytes.
- Notice that a larger number will work:
int main ()
{
int *p;
p = (int*) malloc (16);
*p = 5;
printf ("Int value: %d\n", *p);
}
Here, 16 contiguous bytes are allocated, of which only 4 are used.
- A smaller number will appear to work, but may in fact tamper
with existing memory allocations.
- If you wish to know the number of bytes required for a type,
simply print the sizeof value:
printf ("The number of bytes needed by an integer: %d\n", sizeof(int));
printf ("The number of bytes needed by a double: %d\n", sizeof(double));
- Notice that the return value is cast into the pointer
type:
p = (int*) malloc (sizeof(int));
An easy way to remember this is: "the cast type is the same as
the argument
to sizeof but with a *
appended".
- Thus, if the sizeof argument is an int,
the return value should be cast into int* (an int pointer).
- Similarly, if the sizeof argument is an double,
the return value should be cast into double*.
- It looks more complicated with 2D arrays, as we will see, but
the essence is the same.
- After the pointer has a value, we can actually print the
address:
#include <stdio.h>
#include <stdlib.h>
int main ()
{
// Declare a pointer to an int.
int *p;
// Print the initial value of the pointer:
printf ("Initial value of pointer: %p\n", p); // Prints 0 (C99)
// Get the space from malloc:
p = (int*) malloc (sizeof(int));
// Print the address of the memory block returned by malloc:
printf ("Current pointer: %p\n", p);
}
- Whenever we're done with memory allocated by malloc,
we need to return it:
// Free the memory when done:
free (p);
Another example:
(source file)
#include <stdio.h>
#include <stdlib.h>
int main ()
{
double *doublePtr;
char *charPtr;
printf ("The number of bytes needed by a double: %d\n", sizeof(double));
printf ("The number of bytes needed by a char: %d\n", sizeof(char));
// Get the space from malloc:
doublePtr = (double*) malloc (sizeof(double));
charPtr = (char*) malloc (sizeof(char));
// Print the address of the memory block returned by malloc:
printf ("double pointer is at location: %p\n", doublePtr);
printf ("char pointer is at location: %p\n", charPtr);
// Assign values and print:
*doublePtr = 3.141;
*charPtr = 'A';
printf ("double value = %lf char value = %c\n", *doublePtr, *charPtr);
// Free the memory when done:
free (doublePtr);
free (charPtr);
}
Note:
- Observe the pattern in calling malloc:
doublePtr = (double*) malloc (sizeof(double));
charPtr = (char*) malloc (sizeof(char));
Arrays
An example using arrays:
(source file)
#include <stdio.h>
#include <stdlib.h>
int main ()
{
int A[10];
int *B;
printf ("BEFORE malloc: Address A=%p Address B=%p\n", A, B);
// Prints an address for A, but zero for B
// Assign space to B:
B = (int*) malloc (sizeof(int) * 10);
printf ("AFTER malloc: Address A=%p Address B=%p\n", A, B);
// Prints an address for A and newly allocated address for B.
// Two ways of assigning values:
A[3] = 5;
*(A + 4) = 6;
printf ("A[3]=%d A[4]=%d\n", A[3], A[4]); // Prints 5 and 6.
// Two ways of assigning values:
B[3] = 7;
*(B + 4) = 8;
printf ("B[3]=%d B[4]=%d\n", B[3], B[4]); // Prints 7 and 8.
// Done.
free (B);
}
Note:
- The memory picture after malloc is called is:
- Array variables are really pointers and can be used as
pointers:
A[3] = 5;
*(A + 4) = 6; // Fifth position in array A.
- You should use regular array indexing (A[3]=5)
where possible, instead of the pointer manipulation (*(A+4) = 5).
- The pointer version explains how arrays work:
- A is the address of the start of the array.
- A+4 is the address 4 positions into the array,
i.e., the address of A[4].
- The expression *(A+4) follows the address.
Thus, the assignment
*(A + 4) = 6;
assigns the value 6 into this location.
Next, let's consider 2D arrays:
(source file)
#include <stdio.h>
#include <stdlib.h>
int main ()
{
double A[10][10]; // Static allocation of space to array A.
double **B; // We'll use malloc to assign space to B.
int i;
printf ("BEFORE malloc: Address A=%p Address B=%p\n", A, B);
// Prints an address for A, but zero for B
// Assign space for first dimension of B (an array of pointers-to-double)
B = (double**) malloc (10 * sizeof(double*));
for (i=0; i<10; i++) {
B[i] = (double*) malloc (10 * sizeof(double));
}
printf ("AFTER malloc: Address A=%p Address B=%p\n", A, B);
// Prints an address for A and newly allocated address for B.
// Two ways of assigning values:
A[3][4] = 5.55;
*((double*)A + 5*10 + 6) = 6.66;
printf ("A[3][4]=%lf A[5][6]=%lf\n", A[3][4], A[5][6]); // Prints 5.55 and 6.66.
// Twy ways of assigning values:
B[3][4] = 7.77;
*((*(B + 5)) + 6) = 8.88;
printf ("B[3][4]=%lf B[5][6]=%lf\n", B[3][4], B[5][6]); // Prints 7.77 and 8.88
// Free the individual small arrays:
for (i=0; i<10; i++) {
free (B[i]);
}
// Free the array of double-pointers:
free (B);
}
Note:
- The array A is given a chunk of space for 100 double's.
- The picture of memory after the first call to malloc:
- After the subsequent calls to malloc:
- Both arrays are accessed using standard array indexing:
A[3][4] = 5.55;
B[3][4] = 7.77;
- However, it is also possible (not recommended) to use the
actual addresses and perform address arithmetic:
*((double*)A + 5*10 + 6) = 6.66;
*((*(B + 5)) + 6) = 8.88;
- Let's explain the first one:
*((double*)A + 5*10 + 6) = 6.66;
- A double occupies 8 bytes.
- Thus, a 10 x 10 array requires 100*8 = 800 bytes.
- To get to location A[5][6], we compute
the address of the first byte in the 8 bytes allocated to A[5][6].
- Note that A[5] is the 6-th row and so, we need to
skip past five rows: 5*10.
- Then we skip past 6 positions.
- The total number of positions to skip past: 5*10 + 6.
- Adding this to the start address gives the start address of
position A[5][6].
- Finally, because A is declared as a 2D array,
we need to cast A into a pointer type to perform arithmetic.
- The second one is a little more complicated:
*((*(B + 5)) + 6) = 8.88;
- Recall that B is a pointer that points to a block
of double-pointers.
- Thus, B+5 is the sixth such pointer in the block.
- The expression *(B+5) follows that address, which is
the address of the block of 10 elements assigned to the 6-th row.
- We now add 6 to this address to get the exact address of
element B[5][6].
- Lastly, we follow the address (pointer) with the (outermost)
* operation.
- Note that the individual array blocks need to be
free'd before the double-pointer block pointed to by
B is free'd.
Exercise 3.4:
Draw the "memory picture" for arrays
B and
B2
in the first arrays example above
(arrays.c)
just before the
free()
function calls are executed.
That is, draw a picture with sample memory addresses
that shows how these arrays are located in memory.
Put this in the "answers" PDF for this module.
Exercise 3.5:
In
pascal.c,
create a 2D array to store and print Pascal's triangle
(Each element is the sum of the two elements immediately
above, slightly to the left and to the right).
Ensure
that only as much space as needed is used for the array,
so that the first row is of size 1, the second of size 2,
and so on. Sample output:
1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
© 2003, Rahul Simha (revised 2017)