Download Dynamic Memory Allocation and Arrays of Objects in Computing and Statistical Data Analysis and more Slides Computational and Statistical Data Analysis in PDF only on Docsity!
Computing and Statistical Data Analysis
Lecture 6
Arrays of objects
Memory allocation: static, automatic and dynamic
Memory leaks and dangling pointers
Operator overloading
Static member functions
digression on destructors, copy constructors
Class templates
Dynamic arrays
An array’s name is a pointer to its first element. We can create
a “dynamic array” using the new operator:
double* array; int len; cout << "Enter array length" << endl; cin >> len; // array length set at run time array = new double[len]; ...
When we’re done (e.g. at end of function where it’s used), we need
to delete the array:
delete [] array;
Memory allocation
We have seen two main ways to create variables or objects:
(1) by a declaration (automatic memory allocation):
int i; double myArray[10]; TwoVector v; TwoVector* vPtr;
(2) using new: (dynamic memory allocation):
vPtr = new TwoVector(); // creates object TwoVector* uPtr = new TwoVector(); // on 1 line double* a = new double[n]; // dynamic array float* xPtr = new float(3.7);
The key distinction is whether or not we use the new operator.
Note that new always requires a pointer to the newed object.
The stack
When a variable is created by a “usual declaration”, i.e., without
new, memory is allocated on the “stack”.
When the variable goes out of scope, its memory is automatically
deallocated (“popped off the stack”).
int i = 3; // memory for i and obj MyObject obj; // allocated on the stack ... } // i and obj go out of scope, // memory freed
Deleting objects
To prevent the memory leak, we need to deallocate the object’s
memory before it goes out of scope:
MyClass* ptr = new MyClass(); // creates an object MyClass* a = new MyClass[n]; // array of objects ... delete ptr; // deletes the object pointed to by ptr delete [] a; // brackets needed for array of objects }
For every new, there should be a delete.
For every new with brackets [], there should be a delete [].
This deallocates the object’s memory. (Note that the pointer to the
object still exists until it goes out of scope.)
Dangling pointers
Consider what would happen if we deleted the object, but then still
tried to use the pointer:
MyClass* ptr = new MyClass(); // creates an object ... delete ptr; ptr->someMemberFunction(); // unpredictable!!!
After the object’s memory is deallocated, it will eventually be
overwritten with other stuff.
But the “dangling pointer” still points to this part of memory.
If we dereference the pointer, it may still give reasonable behaviour.
But not for long! The bug will be unpredictable and hard to find.
Some authors recommend setting a pointer to zero after the delete.
Then trying to dereference a null pointer will give a consistent error.
Operator overloading
Suppose we have two TwoVector objects and we want to add them.
We could write an add member function:
TwoVector TwoVector::add(TwoVector& v){ double cx = this->m_x + v.x(); double cy = this->m_y + v.y(); TwoVector c(cx, cy); return c; }
To use this function we would write, e.g.,
TwoVector u = a.add(b);
It would be much easier if would could simply use a+b, but to do
this we need to define the + operator to work on TwoVectors.
This is called operator overloading. It can make manipulation of
the objects more intuitive.
Overloading an operator
We can overload operators either as member or non-member
functions. For member functions, we include in the class
declaration:
class TwoVector { public: ... TwoVector operator+ (const TwoVector&); TwoVector operator- (const TwoVector&); ...
Instead of the function name we put the keyword operator
followed by the operator being overloaded.
When we say a+b, a calls the function and b is the argument.
The argument is passed by reference (quicker) and the declaration
uses const to protect its value from being changed.
Overloaded operators: asymmetric arguments
Suppose we want to overload * to allow multiplication of a
TwoVector by a scalar value:
TwoVector TwoVector::operator* (double b) { double cx = this->m_x * b; double cy = this->m_y * b; TwoVector c(cx, cy); return c; }
Given a TwoVector v and a double s we can say e.g. v = v*s;
But how about v = s*v; ???
No! s is not a TwoVector object and cannot call the appropriate
member function (first operand calls the function).
We didn’t have this problem with + since addition commutes.
Overloading operators as non-member functions
We can get around this by overloading * with a non-member
function.
We could put the declaration in TwoVector.h (since it is related
to the class), but outside the class declaration.
We define two versions, one for each order:
TwoVector operator* (const TwoVector&, double b); TwoVector operator* (double b, const TwoVector&);
For the definitions we have e.g. (other order similar):
TwoVector operator* (double b, const TwoVector& a) { double cx = a.x() * b; double cy = a.y() * b; TwoVector c(cx, cy); return c; }
A different “static”: static members
Sometimes it is useful to have a data member or member function
associated not with individual objects but with the class as a whole.
An example is a variable that counts the number of objects of a
class that have been created.
These are called static member functions/variables (yet another use
of the word static -- better would be “class-specific”). To declare:
class TwoVector { public: ... static int totalTwoVecs(); private: static int m_counter; ... };
Static members, continued
Then in TwoVector.cc (note here no keyword static):
int TwoVector::m_counter = 0; // initialize TwoVector::TwoVector(double x, double y){ m_x = x; m_y = y; m_counter++; // in all constructors } int TwoVector::totalTwoVecs() { return m_counter; }
Now we can count our TwoVectors. Note the function is called
with class-name:: and then the function name. It is connected to
the class, not to any given object of the class:
TwoVector a, b, c; int vTot = TwoVector::totalTwoVecs(); cout << vTot << endl; // prints 3
Oops #2: digression on copy constructors
The totalTwoVec function still doesn’t work very well, since we
should count an extra TwoVector object when, e.g., we say
TwoVector v; // this increments m_counter TwoVector u = v; // oops, m_counter stays same
When we create/initialize an object with an assignment statement,
this calls the copy constructor, which by default just makes a copy.
We need to write our own copy constructor to increment
m_counter. To declare (together with the other constructors):
TwoVector(const TwoVector&); // unique signature
It gets defined in TwoVector.cc :
TwoVector(const TwoVector& v) { m_x = v.x(); m_y = v.y(); m_counter++; }
Class templates
We defined the TwoVector class using double variables. But in
some applications we might want to use float.
We could cut/paste to create a TwoVector class based on floats
(very bad idea -- think about code maintenance).
Better solution is to create a class template, and from this we
create the desired classes.
template // T stands for a type class TwoVector { public: TwoVector(T, T); // put T where before we T x(); // had double T y(); ... };