[Phy 405/905 Home Page] [ Lecturer ]

3. Pointers - Part 5

We end our endless discussion of pointers by covering multi-dimensional arrays and pointers, and showing how objects are dynamically created in C++ (both single objects and arrays of objects).


Multi-dimensional arrays and pointers

Declaring, defining a multi-dimensional array

So far we have been considering only 1-dimensional arrays, that is, arrays declared and used as in:

int q[5] = {1,4,5,1,4};
q[1] = q[2];
Only one index is used in the declaration of and access to the array q. In C and C++, higher-dimension arrays are available, but are fundamentally as low-level as the one-dimensional arrays:
int r[5][3];
r[0][0] = 1; // assign to "first" element of r
r[4][2] = 1; // assign to "last" element of r

int q[5][5][100];
for (int i=0; i < 5; ++i) // set all elements of q == 3
for (int j=0; j < 5; ++j)
for (int k=0; k < 100; ++k)
q[i][j][k] = 3;
Arbitrary-dimension arrays can be created and used in this way. Be watchful (if you're coming from Fortran) for this common beginner's mistake:
double f[4,3]; // ***illegal***
double g[4][3]; // legal
where the declaration of f is not going to be understood at all by the C++ compiler.

Initialization of multi-dim arrays

Multi-dimensional arrays can be initialized (assigned a value at the time of definition) in the same way as one-dimensional arrays, and the same rule about "initializers smaller than the object to be initialized" (which fills in the rest of the object with 0's) applies.
int r[3][2] = {}; // r filled with 0's
int q[2][3] = { {1,2,3},{4,5,6} };
Note the order of the sub-braces, where the innermost braces correspond to the rightmost index. The "smaller initializer" rule also applied to these nested braces:
int q[2][3] = { {1}, {2} }; // same as q[2][3]={ {1,0,0},{2,0,0} };
On the other hand, nested braces are not necessary:
int q[4][3] = { 1,2,{3},{4,5,6}]; // same initializer as  
// {{1,0,0},{2,0,0},{3,0,0},{4,5,6} }
As with one-dimensional arrays, other than initialization, the values contained in a multi-dimensional array cannot be accessed other than one-at-time,through array indexing.

sizeof() and multi-dimensional arrays

Just as with one-dimensional arrays, inside the sizeof() operator is one place where a multi-dimensional array does not get implicitly converted to a pointer (of type discussed in the next section):
int q[4][3];
cout << "# of elements in q = " << sizeof(q)/sizeof(int);

Mult-dim arrays and pointers

Row-ordered vs. column-ordered

Consider a two-dimensional array:
int q[3][2] = { {1,1},{2,2},{3,3} };
There are 3*2 == 6 integer elements in q, which are contained in one block of memory. The memory layout of the array - the relative order of q[1][0] and q[2][0], for example - is determined by the fact that C++ and C have row-ordered arrays, in contrast to Fortran, whose arrays are column-ordered.

To understand what this means, define the first index of a multi-dimensional array to be a "row", with the last index signifying a "column" index. For a two-dimensional array, this corresponds to thinking of the array as a matrix, with the first index running over rows, the second over columns:

int q[3][2];
for (int i=0; i < 3; ++i) // iterate over each row
for (int j=0; j < 2; ++j) // iterate over each column
cout <<q[i][j];
Here, the elements q[0][0 to 1] signify the first row, while q[0 to 2][0] signifies the first column.

Row-ordering means that stepping through the elements of a single row corresponds to a walk through consecutive memory locations, while index-ordering means the reverse (all the elements of a column are contiguous in memory):

int q[3][2] = { {1},{2},{3} };
int *r = &q[0][0]; // pointer to run through q[][] element-by-elememt
int *s = r + sizeof(q)/sizeof(int); // just past end of q[][]
for ( ; r < s; ++r)
cout << *r << ' ';
will output:
1 0 2 0 3 0 
whereas the Fortran equivalent of the code would output what?

Array name conversion to pointer

If a multi-dimensional array name (where multi includes dimension==1) appears in an expression, but is not fully indexed, as in:
double q[20];
double *r = q; // q not accessed by its one index, is converted
it is converted to a pointer. For one-dimensional arrays of type T, the array name (as in the example above) is converted to a T* - a pointer to a single object of type T. For a N-dimensional array q of rank (i,j,...,k) - that would be declared as:
int q[i][j] ... [k]; // ***note*** the "..." indicates omitted indices
the array name q is converted to a pointer to (N-1)-dimensional array of rank (j,...,k).
int q[3][2] = { 1,0,2,0,{3,3}};
int (*r)[2] = q; // q implicitly-converted to pointer to an int[2]
cout << *r[0] << ' ' << *r[1]; // r is the pointer to an int[2],
// *r "is" an int[2]
// outputs "1 0"
Application of the subscripting operator [], because of the equivalence of a[i] and *(a+i), yields the array of dimension (N-1), which as usual is converted to a pointer to the first element of the array
int *s = q[0]; // q declared above, q[0] is type int[2] --> int*
*s++ = 4; // just changed q[0][0]
*s++ = 5; // just changed q[0][1]
In short (using the example above):
q; // expression of type int *[2], points first element of first row of q
q[1]; // type int[2], can be implicitly converted to int*
q[1][1]; // type int
and
cout <<sizeof(q)/sizeof(int)       << ' ';
cout <<sizeof(q[0])/sizeof(int) <<' ';
cout <<sizeof(q[0][0])/sizeof(int) <<'\n';
outputs
6 2 1

pointer arithmetic

Because of the conversions described above, you have several options for accessing a multi-dimensional array, where the choice is made on the basis of what kind of access you need.

For example, if you need to initialize all elements of a multi-dimensional array to a value, but don't care about the actual order, you can do the following:

int q[5][5][5] = {}; // q initialized to zero
// initialize entire array to 100
int *r = &q[0][0][0];
int *s = r + sizeof(q)/sizeof(int);
while (r < s)
*r++ = 100;
If you want to jump over one index while keeping all others fixed:
int (*t)[5][5]= q;
for (int i=0; i < 5; ++i)
*(int *)(t++) = 99; // sets q[0 to 4][0][0] = 99
This last example can be confusing because of the complicated type definitions - it could be simplified by using a typedef, which produces exactly the same effect, but allows us to break up the type into understandable chunks:
typedef int FiveByFive[5][5]; 
typedef FiveByFive *FiveFivePtr;
FiveFivePtr t = q, u = q+5;
while (t < u)
*(int *)(t++) = 99;
Note the use of the variable u, which points just past the last row of q.

If all else fails, you can always rely on simple int* and do the pointer arithmetic yourself:

int *t = &q[0][0][0];
int *u = t+sizeof(q)/sizeof(int);
const int offset = sizeof(q[0])/sizeof(int);
for ( ; t < u; t += offset)
*t = 99;
The difference between using pointer arithmetic and conventional subscripting is that the equivalent of the last example in subscripting would involve several arithmetic operations - to access q[i][j][k], the integer address:
&q[0][0][0]+i * 25 + j * 5 + k
has to be calculated, which for several dimensions is obviously going to be less efficient (for operations such as the ones discussed above) than the pointer arithmetic version.

Pointer arrays, argc and argv, and command-line arguments

The discussion of multi-dimensional arrays and their conversion to pointers points out a revealing fact about multi-dimensional arrays: they are also (convertible to ) arrays of pointers of pointers of ...

For example:

int q[10][20];
q[4][5] = 8;
Let's take the second line apart in detail - a totally equivalent statement is:
*( *(q+4) + 5) = 8;
where we have followed the rules given for array conversion to pointers.

The innermost dereferenced pointer expression, (q+4), is of type int*[20], that is, is a pointer to an array, or a pointer to a pointer. For that matter, (q+1) is a pointer to a pointer, as is (q+2), and so on, so the following is legal:

**q = 8; // set q[0][0] equal to 8
Now although all multi-dimensional arrays "are" arrays of pointers, it is not true that all pointer arrays are multi-dimensional arrays. The definition of q above allocated 10*20*sizeof(int) bytes, whereas the following:
int *r[10];
only allocates 10*sizeof(int *) bytes. The variable r does not "contain" any integers itself, though it can point to arrays of integers:
for (int i=0; i < 10; ++i)
r[i] = q[i];
q[2][0] = 4; // sets q[2][0]==4
r[2][0] = 4; // sets r[2][0]==4
The variable r can "stand in" for q, although it itself does not contain any integers.

Pointer arrays are used for more than just "copies" of multi-dimensional arrays - they're used to implement arrays with non-constant stride, i.e. , the size of the row is allowed to vary. The most basic example is for arrays of strings, which are used for handling command-line arguments.

Recall that in UNIX (and VMS, if things are set up properly, see Lecture 0, on "C++ compilation basics)"), when a C++ program is run, command-line arguments can be passed. Assuming you have compiled and linked a program named my_prog,you can run the program with the command:

my_prog 23    Hi     lastone
where three arguments have been passed to my_prog: "23", "Hi", and "lastone". The quotation marks are included because they are passed as character strings. Access within the main() function is provided through argc and argv:
#include <iostream.h>
int main(int argc, char *argv[])
{
cout << "# of arguments is "<< argc-1 <<'\n';
cout << "program name is "<< argv[0] <<'\n';
cout << "first letter of prog name is '"<<argv[0][0]<<"'\n";
char **argv_copy = argv;

while (argc--)
cout << *++argv<<'\n';

cout << "print arguments again:\n";
while (++argv_copy)
cout << *argv_copy <<'\n';
}
This code snippet illustrates the following points We can define our own pointer arrays, what we know so far:
char str1[] = "hi";
char str2[] = "there";
char *good_array[] = {str1,str2};
char *dangerous_array[4] =
{ "hi", "there", "last string", "true last string"};
Both good_array[] and dangerous_array[] are arrays of pointers, with actual allocation for storing the characters performed in two different ways (the "dangerous" only notes that string-pooling as we talked about before can lead to uncertain results for dangerous_array[]. Note that the elements of both are pointers, and not character arrays:
cout << (sizeof(good_array[0])==sizeof(char *)) << '\n'; // outputs '1'
cout << (sizeof(good_array[0])==sizeof(str1)) << '\n'; //outputs '0'

... and functions

Because of array conversion to pointers (except for use in sizeof, with &, or reference initialization discussed below), one-dimensional arrays are never passed as arrays, but only as pointers:
void f(int *q);
void g(int q[]); // f() and g() take exactly the same argument!
int r[20];
f(r); // legal
f(r+1); // also legal
Because the conversion is generalized to multi-dimensional arrays, the following function declarations are exactly equivalent:
const int max_str = 4, max_buf=512;
int my_buf[max_str][max_buf];
void q(char strlist[max_str][max_buf]);
void r(char strlist[][max_buf]);
void s(char (*strlist)[max_buf]);
// function call
q(my_buf);
The declaration of s(), though strange-looking, is precisely how the argument is passed.

In the same way, th ese declarations of main() are equivalent:

int main(int argc, char *argv[]);
int main(int argc, char **argv);

Dynamic allocation and creation

Pointer arrays and dynamically-created objects

Motivation

We just saw how pointer arrays are used for holding lists of objects of varying size, as in the particular case of the variable argv passed to the main() function:
int main(int argc, char *argv[]);
where each element of argv[] is a pointer to a single character, which is in turn the beginning of a null-terminated string.

Consider the alternative, if argv[] had been defined and declared as a true two-dimensional character array,

int main(int argc, char argv[][max_arg_length]);
where max_arg_length would set a limit on the length of each command-line argument, for example, 200 characters. This might be okay if you're used to putting up with DOS-like limits on file name lengths and so on, but totally unreasonable in general. There is no "reasonable" maximum argument length, andso argv[] is only an array of pointers to a character (the first character in each string), instead of an array of pointers to character arrays of fixed length.

An example of pointer arrays (implementing argc, argv[])

As an illustration of these points, let's consider how you'd make do if the int main(int argc, char *argv[]) version of the main() function were not available, so you had to get your own command-line and pull out the space-separated arguments yourself:
#include <ctype.h> // for isspace()
#include <iostream.h>

const int max_args = 2000; // arbitrary limitation that could be
// removed using new[](), see below
char *argv[max_args+1]; // holds pointer to each argument, last is null
int argc; // number of arguments found
void read_command_line(); // used to replace standard argc, argv[]

int main() // simple version of main(), no command-line handling provided
{
// read in command line, then print out arguments
read_command_line();
for (int i=0; i < argc; ++i)
cout << "argv["<<i<<"]: '"<<argv[i]<<"'\n";

return 0;
}

void read_command_line()
{
const int line_buffer_size = 5000;
static char command_line[line_buffer_size]; // why is this static?

cin.getline(command_line,line_buffer_size);

char *p;
for ( argc = 0, p=command_line; *p && argc < max_args; )
{
while (isspace(*p)) // find start of arg or end of command_line
++p;
argv[argc++] = p; // arg begins here
while (!isspace(*p) && *p) // find end of arg or command_line
++p;
if (*p) // null-terminate this arg if not end of command_line
{
*p++ = '\0';
while (isspace(*p)) // find beginning of next argument
++p;
}
}
argv[argc] = 0; // set last argument pointer to null
}
Things to note about this code snippet: We might well live with these last restrictions, but what if they were in fact intolerable? What if we wanted to decide all these things at run-time, letting, for example, the user type as many arguments of any length at all as she wanted? Clearly, "compile-time" declarations like:
char *argv[max_args];
are not going to cut it, because what we're after is the run-time creation of argv[]. We might even want to start off with argv[] at one size, then increase it - or decrease it. What we want is dynamic creation of objects, which is the ability to "define" an object that is not explicitly defined in the source code. Here, "definition" is intended to include allocation of memory to hold the object, just as for the object (run-time) definitions we already know about. Because we don't want to be spendthrift with memory (virtual memory only gets you so far), we'll also need the complementary dynamic destruction of objects for when we're finished using them, which will include deallocation of the memory that was used for the object, as well as certain other "housekeeping" functions.

new and delete operators

Creating objects with new()

The new operator (which is not the same as operator new(), which will be considered much later on) : An example creating 20 integers:
int *r[20]; // pointer array
for (int i=0; i < 20; ++i)
if ( !(r[i]=new int) ) // catch new() failure
{
// ***warning*** this is not fail-safe! see below
cerr << "acckk --- allocation failed on i="<<i<<"\n";
exit(1);
}
*r[0] = 4; // access newly allocated memory
Here I've included a check on the return value of new, because the last thing we'd want to do is have allocation fail - leaving some or all elements of r pointing to random places in memory - and then go and write into those memory locations. What if r[0] pointed into the memory where the code itself was located? Total, random, havoc.

On the other hand, according to the latest publicly-available draft of the C++ standard (April 1995), the default behavior of the new operator upon allocation failure is to pass control to the "exception" handler, which again by default ends program execution. So the new operator would never return in this case (let alone return a null pointer), and the code following the if() would never be executed. In fact, the precise behavior is highly customizable, and I'll outline one simple way of dealing with allocation errors in the section below on error-handling for the new operator. For the moment, assume that new operator failure is dealt with one way or another, so the examples below will omit a check for a null return.

The new operator only works for objects - functions cannot be defined in this way. The objects created by the new operator (and pointed to by the return value of the new operator) continue to exist and occupy memory until they are destroyed using the delete operator (see below). This means that the following example:

while (1) // infinite loop
{
// q is local scope, is initialized on each iteration of this loop
// and memory is allocated (and never released) on *each* iteration
int *q = new int;
}
will soon result in all the available memory being used up by the infinite number of calls to the new operator. Note that the scope in which new was called (local) has nothing to do with the lieftime of the object that q points to.

It should be noted that parentheses are not allowed in the type name following new; there is, however, a second form of the new operator, where the type name is enclose in parentheses. You can use it for more complicated declarations or just to make things more obvious:

int  (**r)[5] = new  int (*)[5];  // illegal - parsed as (new int) (*)[5]
int (**s)[5] = new (int (*)[5]); // ok - allocates a ptr to
// array of 5 ints

Initialization using new()

Objects created by new are like automatic storage objects in the sense that they are not default-initialized to zero or any other value. To initialize the created objects, use the following form of the new operator:
double *x = new double; // no initialization
double *y = new double(4.3); //initialization to 4.3
double *z = new double(); // initialization to 0.0
The last form, where no arguments are provided to the initializer part of the call to the new operator, is part of the draft standard (the Annotated Reference Manual, however, predicts unpredictable results). The current GNU compiler, 2.7.2, correctly initializes the variable z to 0.0, whereas the current DEC compiler on DIRAC does not. Only in the first form, is there no initialization taking place.

Incidentally, the last form is an explicit call to what we would be called the "default constructor" for objects of type double.

Destroying objects with delete()

You should always de-allocate an object once you're finished using it - and the way to deallocate objects create with new is the delete operator:
int *x = new int;
// use variable x ....

// now we're done with it:
delete x; // deallocates memory that used to be associated with x
After deleting an object, it is undefined if you try to dereference the old address or attempt to delete the object again:
double *x = new double(4.0);
delete x;
*x = 4; // ***error!! **** x referred to after deletion, unknown results!
delete x; // *** error **** deleted an object twice!
There is no problem deleting a null pointer however, so if there's a possibility you might for some reason try to delete a pointer twice, set it to the null pointer after each delete:
double *x = new double(4.0);
delete x;
x = 0;
delete x; / okay

For fundamental types (int, double,...), there is not usually much point in using the new operator to create/allocate single objects, as opposed to (for example) just using a static storage class variable. If you might need a float but aren't sure -why not live it up and define one? If you don't end up using it, it's not likely to be a big deal.

There's also storage overhead to consider -because of something called alignment requirements, the details of which are machine dependent but the existence of which is universal, "small objects" like char can be located at any address in memory, but "large objects" like long double might be required to fall on even addresses, or multiples of 4 bytes, or .... The main point is that because of the new operator has to work properly for general types, if asked to create a char, it will do so under the most restrictive requirements, e.g., that the address is a multiple of 4. A single-byte object (a char) will then take up 4 bytes, which is certainly wasteful, but the price of generality.

The new operator for single objects really comes into its own for more complicated objects, of types defined by the users (classes). The other importance of the new operator is for several objects at a time, i.e., creating arrays.

Creation with new[]()

The new operator can also be used to allocate arrays of objects (including character strings):
// integer array
int *j = new int[20]; // j points to allocated array

// create a copy of a character array
char str1[] = "hi there";
char *q = new int[ strlen(str1)+1 ]; // strlen() (from <string.h>) doesn't
// include final null character
strcpy(q,str1); // copy str1 TO q (strcpy() declared in <string.h>)
The syntax is simple - the same as scalar (single object) allocation, except that the array size is provided last, between square brackets. Multi-dimension arrays are allocated in the same way, just adding more []'s, but "array name conversion to pointer" is performed on the return value from new:
char **my_argv = new char[20][30];       // incorrect type!!
char (* my_argv)[30] = new char[20][30]; // okay!
There are some restrictions on the array sizes provided:
int i = 20; // non-const integer
char s[i]; // ***illegal*** array size must be CONSTANT integer
char *t = new char[i]; // okay!!!
int i=20, j=30;
char *q = (char *) new q[i][j]; // illegal!! j is not const
// an attempt without a cast:
char (*r)[j] = new q[i][j]; // illegal, but left-hand-side is also illegal
// because j is not const
This means that multi-dimensional arrays cannot be fully dynamically created, because of requiring at most one non-constant dimension. Why doesn't C++ support this? The basic reason is that there is such a thing as a "canonical" one-dimensional array (which new supports), from which fancier and higher-level one-dimensional arrays can be built up (including index range-checking, BLAS-like slices, and so on). As we saw from discussing row-ordering implemented by C++ compared to column-ordering used in Fortran, there is no "canonical" multi-dimensional array. With the use of user-defined types (classes), you can however build up your own version of dynamical n*m*p*... dimensioned matrices. Perhaps a better choice is the valarray class, which will be included in the upcoming C++ standard library, and includes several high-level matrix operations.

Another alternative is to use pointer arrays:

int num_elem = 10; 
int num_rows = 5; // non-const, but THAT's OKAY!
char **q = new char*[num_rows]; // note:
// q cannot be type char (*)[num_elem]
// (num_elem is non-const)
// so q must be char **
for (int i=0; i < num_elem; ++i)
q[i] = new char[num_elem];
which effective creates a 5*10 char "matrix" on the fly. Note that the elements of q[][] are not going to all lie in one contiguous region, in contrast to a true matrix.

This, incidentally, is how we could have created an command-line reading routine that allowed for arbitrary number of arguments and arbitrary argument length.

I haven't discussed initialization for new[] because there isn't any - sorry. If you think about it, how would you specify the initializer for an array whose size is defined at run-time? Of course, you can always explicitly assign values as desired into the array, once you've created it.

Finally, the storage efficiency of arrays of small objects (using new[]) is likely to be better than that for single objects (using new). The reason is that alignment restrictions apply to padding applied to the end end of the array - a statement like:

new char* q = new char[18];
will incur only two wasted bytes needed to pad the total size to an even multiple of 4 bytes (assuming that's what's required on the machine). There is, however, a hidden overhead of array size included for creating arrays, which I'll take up in the next section.

Destruction with delete[]()

There's a delete[] operator to go with new[]:
int *j = new char[20]; // new returns char*, but remember to call delete[]
delete[] j;
int *k = new char[20];
delete k; // **** oops!! ***** should have called delete[]
You need to be careful to match the forms of new and delete - the following code is guaranteed to do something evil:
char *q = new char[20];
char *r = new char;
delete[] q; // okay
delete[] r; // ***** bad ***** should have been delete r, no []'s
The reason is the array size that I mentioned in the last section, that is included in the allocation of every array. C++ arrays are not self-describing; they don't include the array size. Other than for null-terminated strings, there is no way to know the size of an array without being told explicitly.

This poses a problem for new[] (and delete[]): how will delete[] know how much memory the object took up (how many elements)? Clearly, the new[] operator will have to store the number of elements somewhere. At the end of the array clearly will not work (if we knew where the end was, we wouldn't have this problem), so in fact it stores the array size just before the array in memory. When delete[] is called, it looks at the number stored just before the supplied pointer. In the example above, the number right before q is 20, whereas the "number" right before r will have some fairly random number (since r was the result of new and not new[]). Chaos will ensue when delete[] destroys r. And rather than incur a further storage overhead by also requiring new to store a '1' before each single object allocated, C++ instead demands that you keep things straight and use delete[] for objects created with new[], and delete for objects created with new.

Note that a pointer to the beginning of the array will serve just as well for the call to delete[], because the only two things needed are the type and number of elements:

int *r = (int *)(new int[20][20][20]);
delete[] r; // okay, deletes all the elements of the matrix pointed to
and the latter is located just before the address pointed to r. On the other hand, the call:
delete[]  (r+1);
is likely to cause a disaster, because the array size is not located just before the address pointed to by (r+1).

A last thing to watch for is that in the pointer array example above, each element of q has to be deleted, as well as q itself:

// delete q and the objects it points to
for (int i=0; i < num_elem; ++i)
delete[] q[i];
delete[]q;
using, of course, the apropriate form of delete (delete[] in both cases).

new operator and error-handling

Here's the scoop on handling memory allocation in the new operator:

As I said before, in more current C++ compilers, a failure in the new operator will not result in the new operator returning null, because by default the new operator will never return. But in older compilers (like the current DEC compiler) the following will be problematic:

const long int huge = 0xffffffffff; // assume this is too big
char *q = new char[huge];
q[0] = 'a'; // ***** bad, if q is the null pointer
assuming that the new call fails, q will be assigned the null pointer, so the last statement will derefence the null pointer, which is thoroughly undefined. We could insert checks for null pointer returns from new (which I will usually do), but it's still not a bad idea to cover your back for these out-of-date compilers, using a "new handler":
#include <stdlib.h> // declares exit()
#include <new.h> // declares set_new_handler()

void new_error() // function called when new fails
{
cerr << "new or new[] failed to allocate\n";
exit(1);

int main()
{
set_new_handler(&new_error);
const long unsigned int i = 0xfffffffffffff;
char* x= new char[i];
x[0] = 'a'; // guaranteed that x is non-null

// done with x, delete it
delete[] x;

return 0;
}
The above is a recipe for replacing the default (no new handler) with a new_handler of your design (new_error). When the new or new[] operator fails, the current new handler is called, which presumably deletes unneeded objects to make room for the requested allocation. Then the new operator tries to allocate memory again; if it fails, it calls the current new_handler. If the new_handler is the null pointer: For "modern" compilers, the new_handler is set by default to 0, so the above code only adds an extra diagnostic message before quitting. For "old" compilers, this code prevents execution of the next statement after the call to new in the event of new failure.

This is the lecture that never ends,

it just goes on and on my friends...

I just went and - started writing it, not knowing what it was

and I'll continue writing it forever just because...

[ Phy 405/905 Home Page] [Lecturer ]