[Phy 405/905 Home Page] [ Lecturer ]
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).
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};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:
q[1] = q[2];
int r[5][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:
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;
double f[4,3]; // ***illegal***where the declaration of f is not going to be understood at all by the C++ compiler.
double g[4][3]; // legal
int r[3][2] = {}; // r filled with 0'sNote 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,3},{4,5,6} };
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 asAs 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.
// {{1,0,0},{2,0,0},{3,0,0},{4,5,6} }
int q[4][3];
cout << "# of elements in q = " << sizeof(q)/sizeof(int);
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];Here, the elements q[0][0 to 1] signify the first row, while q[0 to 2][0] signifies the first column.
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];
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} };will output:
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 << ' ';
1 0 2 0 3 0whereas the Fortran equivalent of the code would output what?
double q[20];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:
double *r = q; // q not accessed by its one index, is converted
int q[i][j] ... [k]; // ***note*** the "..." indicates omitted indicesthe 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}};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 (*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"
int *s = q[0]; // q declared above, q[0] is type int[2] --> int*In short (using the example above):
*s++ = 4; // just changed q[0][0]
*s++ = 5; // just changed q[0][1]
q; // expression of type int *[2], points first element of first row of qand
q[1]; // type int[2], can be implicitly converted to int*
q[1][1]; // type int
cout <<sizeof(q)/sizeof(int) << ' ';outputs
cout <<sizeof(q[0])/sizeof(int) <<' ';
cout <<sizeof(q[0][0])/sizeof(int) <<'\n';
6 2 1
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 zeroIf you want to jump over one index while keeping all others fixed:
// initialize entire array to 100
int *r = &q[0][0][0];
int *s = r + sizeof(q)/sizeof(int);
while (r < s)
*r++ = 100;
int (*t)[5][5]= q;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:
for (int i=0; i < 5; ++i)
*(int *)(t++) = 99; // sets q[0 to 4][0][0] = 99
typedef int FiveByFive[5][5];Note the use of the variable u, which points just past the last row of q.
typedef FiveByFive *FiveFivePtr;
FiveFivePtr t = q, u = q+5;
while (t < u)
*(int *)(t++) = 99;
If all else fails, you can always rely on simple int* and do the pointer arithmetic yourself:
int *t = &q[0][0][0];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:
int *u = t+sizeof(q)/sizeof(int);
const int offset = sizeof(q[0])/sizeof(int);
for ( ; t < u; t += offset)
*t = 99;
&q[0][0][0]+i * 25 + j * 5 + khas 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.
For example:
int q[10][20];Let's take the second line apart in detail - a totally equivalent statement is:
q[4][5] = 8;
*( *(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 8Now 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)The variable r can "stand in" for q, although it itself does not contain any integers.
r[i] = q[i];
q[2][0] = 4; // sets q[2][0]==4
r[2][0] = 4; // sets r[2][0]==4
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 lastonewhere 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>This code snippet illustrates the following points
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';
}
char str1[] = "hi";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:
char str2[] = "there";
char *good_array[] = {str1,str2};
char *dangerous_array[4] =
{ "hi", "there", "last string", "true last string"};
cout << (sizeof(good_array[0])==sizeof(char *)) << '\n'; // outputs '1'
cout << (sizeof(good_array[0])==sizeof(str1)) << '\n'; //outputs '0'
void f(int *q);Because the conversion is generalized to multi-dimensional arrays, the following function declarations are exactly equivalent:
void g(int q[]); // f() and g() take exactly the same argument!
int r[20];
f(r); // legal
f(r+1); // also legal
const int max_str = 4, max_buf=512;The declaration of s(), though strange-looking, is precisely how the argument is passed.
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);
In the same way, th ese declarations of main() are equivalent:
int main(int argc, char *argv[]);
int main(int argc, char **argv);
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.
#include <ctype.h> // for isspace()Things to note about this code snippet:
#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
}
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.
int *r[20]; // pointer arrayHere 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.
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
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 loopwill 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.
{
// 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;
}
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
double *x = new double; // no initializationThe 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.
double *y = new double(4.3); //initialization to 4.3
double *z = new double(); // initialization to 0.0
Incidentally, the last form is an explicit call to what we would be called the "default constructor" for objects of type double.
int *x = new int;After deleting an object, it is undefined if you try to dereference the old address or attempt to delete the object again:
// use variable x ....
// now we're done with it:
delete x; // deallocates memory that used to be associated with x
double *x = new double(4.0);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:
delete x;
*x = 4; // ***error!! **** x referred to after deletion, unknown results!
delete x; // *** error **** deleted an object twice!
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.
// integer arrayThe 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:
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>)
char **my_argv = new char[20][30]; // incorrect type!!There are some restrictions on the array sizes provided:
char (* my_argv)[30] = new char[20][30]; // okay!
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;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.
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
Another alternative is to use pointer arrays:
int num_elem = 10;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.
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];
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.
int *j = new char[20]; // new returns char*, but remember to call delete[]You need to be careful to match the forms of new and delete - the following code is guaranteed to do something evil:
delete[] j;
int *k = new char[20];
delete k; // **** oops!! ***** should have called delete[]
char *q = new char[20];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.
char *r = new char;
delete[] q; // okay
delete[] r; // ***** bad ***** should have been delete r, no []'s
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]);and the latter is located just before the address pointed to r. On the other hand, the call:
delete[] r; // okay, deletes all the elements of the matrix pointed to
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 tousing, of course, the apropriate form of delete (delete[] in both cases).
for (int i=0; i < num_elem; ++i)
delete[] q[i];
delete[]q;
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 bigassuming 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":
char *q = new char[huge];
q[0] = 'a'; // ***** bad, if q is the null pointer
#include <stdlib.h> // declares exit()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:
#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;
}
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 ]