array.h ------- class Array { public: Array(int n); // Constructor: // Create array of n elements Array(); // Default constructor: // Create array of 0 elements ~Array(); // Destructor:Destroy array(comment 1) Array(const Array &); // Copy array (comment 2: // Copy constructor) int numElts(); // Number of elements Array & operator = (const Array &); // Array assignment Array & operator = (float); // Scalar assignment (comment 3) void setSize(int n); // Change size float &operator[] (int i); // Subscripting (comment 4) private: int num_elts; // Number of elements float *ptr_to_data; // Pointer to data void copy(const Array & a); // Copy elements of a }; void error(const char *s); // For bounds checking array.cc -------- #include "array.h" Array::Array(int n) { num_elts = n; ptr_to_data = new float[n]; } Array::Array() { num_elts = 0; ptr_to_data = 0; } Array::~Array() // Destructor { delete[] ptr_to_data; } Array::Array(const Array& a) // Copy constructor { num_elts = a.num_elts; ptr_to_data = new float[num_elts]; copy(a); // Copy a's elements } void Array::copy(const Array& a) { // Copy a's elements into the elements of *this float *p = ptr_to_data + num_elts; float *q = a.ptr_to_data + num_elts; while (p > ptr_to_data) *--p = *--q; } float& Array::operator[](int i) { #ifdef CHECKBOUNDS if(i<0 || i>num_elts) error("out of bounds"); #endif return ptr_to_data[i]; } int Array::numElts() { return num_elts; } Array& Array::operator=(const Array& rhs) { if (ptr_to_data != rhs.ptr_to_data) { setSize(rhs.num_elts); copy(rhs); } return *this; } void Array::setSize(int n) { if (n != num_elts) { delete[] ptr_to_data; // Delete old elements, num_elts = n; // set new count, ptr_to_data = new float[n]; // and allocate new elements } } Array& Array::operator=(float rhs) { float *p = ptr_to_data + num_elts; while (p > ptr_to_data) *--p = rhs; return *this; } void error(const char *s) { cerr << endl << s << endl; //1st "endl" is in case program is printing cout << endl << s << endl; //something out when the error occurs cout.flush(); //write the output buffer to the screen //or wherever the output of cout goes. abort(); }
The destructor member has the name as the class but with a tilde
(~)
added to the front. ~
Array() is the destructor of the
class Array. (The tilde is the symbol for NOT in C.)
Like a constructor, the destructor has no return type.
If you don't explicitly provide a destructor for the class,
the compiler will provide one for you. Often this is fine, but
if you explicitly allocated resources, (e.g., using new), then
you need to explicitly provide a destructor to deallocate these resources
(e.g., using delete).
In our Array example, the destructor frees up the memory
occupied by the array:
Array::~Array() // Destructor { delete[] ptr_to_data; }
Array(const Array &)
is an example of a copy constructor.
In general the standard syntax for a copy constructor is
X::X(X&)
, where X is any class name. That is, it is
the constructor of class X which takes as its argument a reference
to an object of class X. The copy constructor makes a copy
of an existing object. Since you usually do not want to
change the original object, the copy constructor should be
written as X::X(const X&)
.
When would you need a copy of an existing object?
A function call is a good example.
void func(Array sfa); main() { Array mymatrix(5); func(mymatrix); }The statement Array mymatrix(5) calls the ordinary constructor Array(int n). C++ calls the copy constructor to make a copy of mymatrix to pass to func(). The copy is destroyed at the return from func(). The original object, mymatrix, is destroyed at the end of main.
If you don't provide your own copy constructor, C++ provides one for you. The automatic copy constructor performs a member-by-member copy. So why bother ever writing your own copy constructor? When would you ever want to do anything but make a member-by-member copy? The answer has to do with asset allocation. Assets are things like memory off the heap, open files, ports, allocated hardware like printers, etc. Consider what happens if the constructor allocates an asset such as memory off the heap. If the copy constructor simply makes a copy of that asset without allocating its own, you end up with a troublesome situation: two objects thinking they have exclusive access to the same asset. This becomes nastier when the destructor is invoked for both objects and they both try to put the same asset back. What's needed is a copy constructor that allocates the new object its own assets. You can see that this is done by Array's copy constructor:
Array::Array(const Array& a) // Copy constructor { num_elts = a.num_elts; ptr_to_data = new float[num_elts]; // Allocate new memory for copy copy(a); // Copy a's elements }Another way to say this: Suppose a class has pointers as data members. Automatic copies of these pointers duplicate the pointer values, not the objects pointed to (e.g., the elements in an array). This is called a shallow copy. Programmer-defined copy constructors can copy the object pointed to if this is desired. This is called a deep copy. A good rule of thumb is that if your class requires a destructor to deallocate assets, it also requires a copy constructor.
In the copy constructor, look at the line:
num_elts = a.num_elts;Notice that the copy constructor has access to the private members of another object of the same class. In this case it has access to
a.num_elts
. In general an object of a class has access to
the private members of another object of the same class.
Recall that we can overload functions that have the same name but take different arguments. For example,
void func(int); void func(float);Operators, such as +, -, and =, are also functions; they just have a peculiar syntax. The functional name of an operator is the operator symbol preceded by the keyword operator and followed by the appropriate argument types. For example, the + operator that adds an int to another int and returns an int is called int operator+(int,int). The programmer can overload, or redefine, existing operators for newly defined types or classes. The only operators that can't be overloaded are
.
(member selection)
,::
(scope resolution), and .*
(member selection through
pointer to function (see Stroustrup, 3rd ed., section 15.5)).
The programmer cannot invent new operators. Nor can the
precedence or format of the operators be changed. In addition, the
operators cannot be redefined when applied to intrinsic types.
The above files have 3 examples of operator overloading:
float &operator[] (int i); // Subscripting Array & operator = (const Array &); // Array assignment Array & operator = (float); // Scalar assignment
The declaration Array & operator = (const Array &)
overloads
the assignment operator ``=''. The argument of this function
(const Array &)
will appear on the right hand side.
In this case the argument is a reference to an object of the Array class.
The assignment operator function returns a reference to the
``current'' Array. We could return a copy of the values in the array,
but copying the values can take a lot of time and memory, so it
is often better to return a reference to the array.
Assignment member functions should work correctly when the left
and right operands are the same object.
Typically an assignment operator has two parts. The first part
resembles a destructor in that it deletes the assets that the
object already owns. The second part resembles a copy constructor
in that it allocates new assets. In other words, you delete existing
stuff before replacing it with new stuff. We can see this in the
following example. Here array assignment is implemented by setting
the size of the left operand to the size of the right operand
and copying the elements.
Array& Array::operator=(const Array& rhs) { if (ptr_to_data != rhs.ptr_to_data) { setSize(rhs.num_elts); copy(rhs); } return *this; }The test handles the case in which an array is assigned to itself. Without the test, setSize() might delete the elements of the left operand, which would also be the elements of the right operand, before the values are copied.
The size of an Array is set by deleting its elements, saving the new size, and allocating space in memory for an array with the appropriate number of elements.
void Array::setSize(int n) { if (n != num_elts) { delete[] ptr_to_data; // Delete old elements, num_elts = n; // set new count, ptr_to_data = new float[n]; // and allocate new elements } } void Array::copy(const Array& a) { // Copy a's elements into the elements of *this float *p = ptr_to_data + num_elts; float *q = a.ptr_to_data + num_elts; while (p > ptr_to_data) *--p = *--q; }
Notice that the assignment operator is much like the copy constructor. In use the two look almost identical.
void fn(Array &matrix) { Array new_matrix=matrix; //This is the copy constructor. new_matrix=matrix; //This is the assignment operator. }The difference is that the copy constructor is used when a new object, that didn't already exist, is being created. The assignment operator is used if the left-hand object already exists. Like the copy constructor, the assignment operator should be provided whenever a shallow copy is not appropriate.
float &operator[] (int i)
overloads the
subscript operator [ ]. When this operator is used, the
argument i is placed between the square brackets, as in
x[i]. float &
means that the operator function returns
a reference to a float. In particular, x[i] returns a reference
to the ith element of the array. This can be seen from the definition:
float& Array::operator[](int i) { #ifdef CHECKBOUNDS if(i<0 || i>num_elts) error("out of bounds"); #endif return ptr_to_data[i]; }
Array is a class dealing with a one dimensional array. But suppose we have a two dimensional array. How do we access the elements? The problem is that the operator [ ] can only take one argument. That's why two dimensional arrays in C have the form matrix[i][j]. The solution is to use the operator (), which can take more than one argument. Then we can write matrix(i,j) just like in Fortran. So in our class definition, we would write
float& operator()(int,int);In the definition of the overloaded operator, we would write:
float& Array2D::operator()(int i, int j) { return ptr_to_data[i][j]; // indices start at 0 or return ptr_to_data[i * num_columns + j]; // indices start at 0 }Other operators can be overloaded. We will discuss this further in lecture 6.
a.out < infile > outfile
<
and >
are called redirection operators.
Now the program will write output into outfile and get
input from infile. But cerr will still write to the
screen.
int operator+(int a,int b)
returns by value. That is,
a + b adds a and b and then generates a
temporary object into which it can store the result of the addition.
It then returns this object by value to the caller. What if we
decided to return by reference instead? We would write
int& operator+(int a,int b)
. This would compile fine, but
it would generate flaky results. The problem is that the
returned reference refers to an object, call it result,
whose scope is local to the function. result is out of scope
by the time it can be used by the calling function. The moral of
the story is that you should not return a reference to an object
whose scope is local to a function and will soon disappear. Return
references to objects that are going to be around for a while.
Returning references to the ``current'' Array qualifies. Indeed,
assignment operators usually return references. Another time when
you would return a reference is when you pass an argument by
reference and then the operator changes the value of that argument.
For example, int& operator++(int& i)
. Here the prefix operator
takes a reference to an int as an argument, adds 1 to it,
and returns a reference to the resulting int. The argument will
last awhile since it came from the calling function. Input
arguments can always be referential.