Dynamic Allocation and Copy Constructors
Dr. Lawlor, CS
202, CS, UAF
(See Gaddis Chapter 9.8 for more info on new and delete, and Chapter 14.4 and 14.5 for info on copy constructors.)
So the basic dynamic allocation syntax is pretty simple: you call "new"
to get a pointer, use the space, and finally call "delete" to give up
that space.
The typical use is to allocate arrays at runtime:
- "someptr=new int[n];" allocates space for an array of n integers,
and returns you a pointer to the first integer. Unlike just declaring
an array, dynamic allocation with new allows "n" to be a runtime
variable.
- "delete[] someptr;" frees the array. The "[]" looks like some sort of weird typo, but it indicates you're deleting the whole array.
You can also allocate a single object, although this isn't nearly as common:
- "someptr=new int;" allocates space for one single integer, and returns you a pointer to the integer.
- "delete someptr;" frees up the space again. You really need
to free any space you allocate, especially in a long-running program.
Note that for both array and individual allocations, you get a bare
pointer back. This is annoying, because it means you MUST
remember whether you've got an array or an individual object, so you
can access the space properly.
Here's an example of array allocation:
int foo(void) {
int n=5; cin>>n; // how many integers?
int *ptr=new int[n]; // make an array
for (int i=0;i<n;i++) // write data in
ptr[i]=10*i;
for (int i=0;i<n;i++) // read data back out
cout<<"ptr["<<i<<"]="<<ptr[i]<<"\n";
delete[] ptr; // free allocated space
return 0;
}
(Try this in NetRun now!)
Here's an example of allocating a single integer:
int foo(void) {
int *ptr=new int; // make a pointer
*ptr=7; // write data in
int retval=*ptr; // read data back out
delete ptr; // free allocated space
return retval;
}
(Try this in NetRun now!)
Unfortunately, there are a bunch of things you MUST do with dynamic
allocations, and the compiler often can't detect any of these!
- Setup: You MUST initialize your pointers before using them.
Luckily, the compiler can usually warn you about uninitialized
pointers, and uninitialized pointers usually crash immediately.
- Embezzlement: You MUST access your pointers within the array
bounds. If you asked for [10] elements, just reading from [13]
might cause you to crash, or you might read garbage. Writing is
even worse--if you don't crash, you'll overwrite some other part of the
program, which will then crash at some unknown later date.
- Amnesia: You MUST remember to call delete. If you don't
call delete, memory marked as being in use will build up in your
program (a "memory leak"), until the machine runs out of memory or your
program exits.
- Doppleganger: You MUST call the correct version of delete:
"delete[]" for arrays, and plain "delete" for individual
pointers. Unfortunately, the compiler
doesn't detect when you use the wrong delete; it just silently screws
up memory so your program crashes sometime in the distant future.
- Overkill: You MUST not call delete more than once on the same
pointer. You can protect against this by zeroing out your
pointers after deleting them (like "delete[] someptr;
someptr=0;"). This "double delete bug" is actually common enough
that some machines' "delete" has explicit code to check for it.
But it's really hard to detect if you allocate some space and delete
it, then somebody else allocates the same space and you then delete their space!
- Zombies: You MUST not access a pointer after you've deleted
it. Unfortunately, these "living dead" pointers usually work, and
some of your data is often still there, but of course that space could
be reused by anybody else at any time, resulting in hideous weird
crashes.
Because bare pointers are so error-prone, it's common to wrap them in a nicer class interface.
Building a "Wrapper Class" for Nicer Pointers
Here's a simple class that puts a slightly nicer interface onto an
array of integers. The constructor calls "new", there's an
overloaded bracket operator to check accesses to the elements of the
array, and the destructor calls "delete".
// A nice wrapper around a dynamically allocated array of integers.
class SmartArray {
private:
int *data;
int length;
public:
SmartArray(int len) {
data=new int[len]; // allocate space
length=len;
}
int &operator[](int index) {
if (index<0 || index>=length) { // bounds-check the array index
cout<<"Index "<<index<<" out of bounds!\n";
exit(1);
}
return data[index];
}
~SmartArray() {
delete[] data; // free space
data=0; /* zero out our pointer, to indicate we're gone */
length=-1;
}
};
int foo(void) {
int n=5; cin>>n;
SmartArray arr(n); // array has n integers
for (int i=0;i<n;i++) arr[i]=10*i;
for (int i=0;i<n;i++) cout<<"arr["<<i<<"] = "<<arr[i]<<"\n";
return 0; // destructor automatically deallocates array!
}
(Try this in NetRun now!)
The nice part about this is:
- The array remembers how long it is, and checks to make sure each access is inside this range.
- The destructor will be called automatically by the compiler, so you can't possibly forget it.
This is an example of a standard C++ trick called Resource Aquisition Is Initialization (RAII): the constructor allocates, and the destructor deallocates.
Copy Constructor and Assignment Operator
One problem with the above "SmartArray" class is that the C++ compiler
automatically (and stupidly) allows people to make a simple shallow
copy of a SmartArray object. Unfortunately, the two copies share
the same pointer, so the pointer will be deleted twice! For
example:
class SmartArray {
private:
int *data;
int length;
public:
SmartArray(int len) {
data=new int[len]; // allocate space
length=len;
cout<<"Running SmartArray constructor: data="<<data<<"\n";
}
~SmartArray() {
cout<<"Running SmartArray destructor: data="<<data<<"\n";
delete[] data; // free space
data=0; /* zero out our pointer, to indicate we're gone */
length=-1;
}
};
int foo(void) {
SmartArray arr(2); // array has 2 integers
if (2==2) { // make a copy of the array
cout<<"Making another SmartArray...\n";
SmartArray evilArr=arr; // compiler-generated assignment operator!
// evilArr's destructor will delete arr's pointer!
}
cout<<"Returning from foo...\n";
return 0;
// uh oh! arr's destructor calls delete *again*!
}
(Try this in NetRun now!)
There are two different ways the compiler might make a copy of "arr":
- "SmartArray somebody(arr);" makes a new SmartArray as a copy of "arr"'s values. This is called a "copy constructor".
- "somebody = arr;" overwrites an existing SmartArray with an
assignment of "arr"'s values. This is called an "assignment
operator".
Unfortunately, the compiler's automatically generated copy constructor
and assignment operator WILL cause big problems in any class with
pointers. And people tend to copy and assign classes a lot, for
example to pass them into a function or return them. So you often
need to write your own copy constructor and assignment operator.
This is known as the "Law of the Big Three": if you've got any one of a destructor, copy constructor, or assignment operator, then you probably need all three of them.
One weird trick is to declare a private copy constructor and assignment
operator. That way nobody can call them. If nobody calls
them, you don't even need a body for these functions:
class SmartArray {
private:
int *data;
int length;
// You can't copy or assign SmartArrays (yet)
SmartArray(const SmartArray &from); // copy constructor
SmartArray& operator=(const SmartArray &from); // assignment operator
public:
... rest of stuff ...
};
(Try this in NetRun now!)
This makes any attempt to copy or assign SmartArrays a compile error,
which is way better than getting a horrible crash at runtime.
If you're really ambitious, you can write the copy constructor and
assignment operator to do the right thing, making a new copy of the
class's data. This is trickier than it sounds, especially if
speed is important, or for the case where some joker assigns an
instance to itself (self assignment, like "x=x;"). The
question is, to implement "a=b;" for arrays, do you just copy the
pointers, a "shallow copy" like the compiler does? If so, how do
you keep track of when to delete the array data? (There are some
cool implementations like "reference counting" and "garbage collection"
out there.) Or do you just copy all the data in the array?
This is called a "deep copy", which uses more time and space, but is a
little easier to write.
Here, I've written a deep copy implementation, and added some new
little utility methods called "alloc" and "free" to do the data
allocation.
/*
A nice wrapper around a dynamically allocated array of integers.
*/
class SmartArray {
public:
SmartArray(int len) { alloc(len);} // ordinary constructor
SmartArray(const SmartArray &from) { // copy constructor
alloc(from.length);
for (int i=0;i<length;i++) data[i]=from[i];
}
SmartArray& operator=(const SmartArray &from) { // assignment operator
if (data==from.data) return *this; // self assignment!
free(); // throw away our old data
alloc(from.length);
for (int i=0;i<length;i++) data[i]=from[i];
return *this;
}
// Array indexing:
int &operator[](int index) { check(index); return data[index]; }
// Constant array indexing:
const int &operator[](int index) const { check(index); return data[index]; }
// Destructor
~SmartArray() { free(); }
private:
int *data;
int length;
void alloc(int len) {// allocate space for len bytes of data
data=new int[len];
length=len;
}
void free(void) {// deallocate data
delete[] data; // free space
data=0; /* zero out our pointer, to indicate we're really gone */
length=-1;
}
void check(int index) const {// bounds-check array index
if (index<0 || index>=length) { // bounds-check the array index
cout<<"Index "<<index<<" out of bounds!\n";
exit(1);
}
}
};
int foo(void) {
int n=5; cin>>n;
SmartArray arr(n); // array has n integers
for (int i=0;i<n;i++) arr[i]=10*i;
if (2==2) { // make a copy of the array
cout<<"Making another SmartArray...\n";
SmartArray evilArr=arr; // our own assignment operator (OK)
// evilArr's destructor will delete its own separate pointer
}
for (int i=0;i<n;i++) cout<<"arr["<<i<<"] = "<<arr[i]<<"\n";
cout<<"Returning from foo...\n";
return 0;
}
(Try this in NetRun now!)
Clearly, this is not something you want to write very often. So
you should re-use something like "SmartArray" rather than writing it
from scratch each time!