The computer's physical memory is used to store the data contained in variables, arrays, etc. In quantum chemistry programs, where the size of the computed data can range from megabytes to terabytes (and petabytes are approaching rapidly), efficient use of memory is vital. The purpose of this section is to explain the fundamentals of C++-language memory allocation/deallocation.
Each piece of data used by a program (integers, doubles, characters, etc.) is associated with a memory location/address, often referred to by a hexadecimal number (e.g., 0x8130). A pointer is a data type that can be used to store such addresses. For example, imagine we have an integer variable named “natom” whose address we need [e.g., to pass to a function]. We can obtain the address using the “address of” operator (also called the “reference” operator):
int natom; ... great_function(&natom);
Note carefully the difference between natom and &natom: the former refers to the value of the variable natom; the latter refers to the memory address of the value of the variable natom. (An explanation of the need to pass a pointer in this case can be found in the section on scope.)
If we needed to store this address for later use, we could declare a variable of type “pointer-to-int”:
int *natom_address; natom_address = &natom;
Notice the use of the “*” syntax to identify the pointer variable. This syntax can be used with any data type, int, double, char, etc. (even “void”).
What if we know the pointer (address), and we'd like to know the value stored there? We can access that using the “indirection” (dereference) operator:
cout << "Number of atoms: " << *natom_address << endl;
(If you instead tried to print natom_address instead of *natom_address, above, you would most likely get a strange-looking integer value, i.e. the hexadecimal representation of the actual memory address.)
The notion of a pointer also means that we can indirectly change the value of natom using its address. For example, the following code will print the number 10, not 7:
natom = 7; natom_address = &natom; *natom_address = 10; cout << "Number of atoms: " << natom << endl;
One way to declare an array in C++ is using the standard bracket notation, e.g.
int zvals; /* declare an array of 10 integers */ ... cout << "The z-value of the first atom is: " << zvals << endl;
Notice that in C++, array indices begin at element “0”. However, the variable named “zvals” is actually a pointer-to-int, meaning we could also use the syntax:
cout << "The z-value of the first atom is: " << *zvals << endl;
How do we access the other elements of the array using this notation? We can take advantage of so-called “pointer arithmetic”:
cout << "The z-value of the second atom is: " << *(zvals+1) << endl; // Same as zvals
The notation “zvals+1” refers to the memory address of the next element in the array, which we dereference with the “*” operator.1) Hence, we could print all the elements of the array using the bracket notation:
for(i=0; i < 10; i++) cout << "Z-value of atom " << i << " is " << zvals[i] << endl;
for(i=0; i < 10; i++) cout << "Z-value of atom " << i << " is " << *(zvals+i) << endl;
Static allocation means that the memory is allocated when the program is compiled (aptly named "compile-time"), as in cases when one uses syntax like:
Dynamic means that the memory is allocated when the program is executed (“run-time”). The advantage of dynamic allocation is that the program itself can determine how much memory it needs as it runs (e.g. based on input data).
In C++, a common approach to allocate memory dynamically is using the new operator:
int *zvals = new int; ... delete zvals;
Alternatively, one can use the malloc() function:
int *zvals = (int *) malloc(10 * sizeof(int)); ... free(zvals);
In these examples, we explicitly declare the variable zvals to be a pointer-to-int, but until we assign it a value, it's meaningless. The new operator takes the data type (in this case, “int”) and the number of elements needed and returns a pointer. The malloc() function takes as an argument the number of bytes needed and returns a pointer to the allocated memory.3)4) The “(int *)” syntax to the left of malloc() is called a “cast”; it converts the value returned by malloc() (called a “pointer-to-void” or “void *”) into a pointer-to-int, to match the declared type of zvals. Note also the use of stdlib.h, which provides the necessary declaration of malloc().
If the call to new or malloc() is successful, we may then access the elements of zvals using the usual syntax: zvals, zvals, etc.
After we are finished with the memory, we must release it back to the computer. If we allocated it using new, then we return it using the delete operator:
If we used malloc(), then we return it with the free() function:
It's important to delete/free() memory allocated with new/malloc(), not only because it's good coding practice, but also because it can often reveal subtle memory access errors elsewhere in your program.
As with arrays, C++ provides syntax to declare matrices and higher-dimensional structures:
With the above syntax, the matrix “geom” would be allocated at compile-time (statically) using 30 consecutive integers in memory stored “row-wise” or in “row-major” ordering (i.e., the first three elements in memory would constitute row 0, the next three row 1, etc.).5) We access the elements of the matrix using the usual bracket notation, e.g. “geom” would yield the value on the 7th row and 3rd column of the matrix.
An equivalent way to achieve the above using dynamic memory allocation is to declare geom as a “pointer-to-pointer-to-int”:
int **geom = new int* ; for(int i=0; i < 10; i++) geom[i] = new int;
This is complicated the first few times you go through it, so be patient:
Note that the static allocation of a matrix above yields a set of consecutive addresses in memory (“contiguous” memory allocation), while the dynamic method above only guarantees that each row of the matrix is contiguous. A method for dynamically allocating a matrix with contiguous storage will be described elsewhere.
To release the memory after we are finished with it, we could use:
for(int i=0; i < 10; i++) delete geom[i]; delete geom;
Notice that we have one delete call for every new call above and that the release of the rows occurs before the release of the pointers to the rows.