C++ classes are similar to Java classes in many ways, but there are also important differences. Below is an example of a C++ class named IntList to be used to represent a list of integers; operations to add a value to the end of the list and to print the list are provided. The implementation uses a dynamically allocated array to store the integers; when the array is full, a new array of twice the size is allocated.
The code that defines the class would be split into two files: the first part specifies what member functions (methods) and data members (fields) the class will have. That code goes into a header file: a file with the extension .h. It is usually a good idea (though not a requirement as in Java) to give the file the same name as the class (so the file would be named IntList.h).
The second part of the class definition gives the code for the function bodies. That code goes in a source file (e.g., IntList.C).
The reason for splitting up the code is that it is generally a good idea to try to separate the interface from the implementation. Someone who wants to use an IntList really only needs to know what IntList operations are available; it is not necessary to know all the details about how an IntList is implemented. However, splitting up the code in this way is not required by C++. Some people prefer to include code for the member functions in the .h file when that code involves only one or two statements.
Here is the code that would be in IntList.h:
#include <iostream> class IntList { public: IntList(); // constructor; initialize the list to be empty void AddToEnd(int k); // add k to the end of the list void Print(ostream &output) const; // print the list to output private: static const int SIZE = 10; // initial size of the array int *Items; // Items will point to the dynamically allocated array int numItems; // number of items currently in the list int arraySize; // the current size of the array };Things to note about the example so far:
#include "IntList.h" IntList::IntList(): Items(new int[SIZE]), numItems(0), arraySize(SIZE) { } void IntList::AddToEnd(int k) { ... } void IntList::Print(ostream &output) const { ... }Things to note about this part of the example:
IntList::IntList() { Items = new int[SIZE]; numItems = 0; arraySize = SIZE; }
The main reason to use a member initialization list is when a data member is itself a class object, and you don't want the default initialization of that object. If you initialize the data member inside the body of the constructor function it will already have been initialized using its default (no-arg) constructor, which is a waste of time.
As in Java, constructor functions can be overloaded (there can be
multiple constructors for a class, as long as each has a different number
and/or type of parameters).
In C++, a constructor function is called either when a class object is
declared:
To use a constructor with parameters, just put the values for the parameters
in parentheses as follows:
To use the vector class you must #include <vector>.
A vector is similar to an array, but vectors provide some operations that
cannot be performed using C++ arrays, and vectors can be passed both by value
and by reference (unlike C++ arrays, which are always passed by reference).
Unfortunately, there is no bounds checking for vectors (i.e., an index out
of bounds does not necessarily cause a runtime error).
Test your function with the following main function:
Every class that has a pointer data member should include the
following member functions:
An object's destructor function is called when that object is about to
"go away"; i.e., when:
For example, consider the following function, with line numbers included
for reference:
The scope of variable L1 is the body of the while loop (lines 4 to 6).
L1's constructor function is called at the beginning of every
iteration of the loop, and its destructor function is called at the
end of every iteration of the loop. Note that if the loop included a
break or continue statement, the destructor would still
be called.
Variable p is a pointer to an IntList. When an
IntList object is allocated
using new at line 2, that object's constructor function is called.
When the storage is freed at line 7, the object's destructor function is
called (and then the memory for the Intlist itself is freed).
Why isn't the destructor function of a reference parameter
called at the end of the function?
Destructor functions are defined using syntax similar to that used for the
constructor function (the name of the class followed by a double colon
followed by the name of the function). For example, the definition of the
Intlist destructor function would look like this:
NOTE: If you don't write a destructor function for a class that includes
pointers to dynamically allocated storage, your code will still work, but
you will probably have some storage leaks.
To understand more about storage management and destructor functions,
let's consider a simpler version of the example code give above:
An object's copy constructor is called (automatically, not by the
programmer) when it is created, and needs to be initialized to be a
copy of an existing object.
This happens when an object is:
On line 6, variable L1 is passed as a value parameter to function
f.
The corresponding formal parameter is L. When the call is executed,
L's copy constructor is called to initialize L to be a
copy of the actual parameter, L1.
On line 10, variable tmp1 is declared to be an IntList,
initialized to be the same as variable L. When line 10 is executed,
tmp1's copy
constructor is called to initialize tmp1 to be a copy of L.
Similarly, when line 11 is executed, tmp2's copy constructor is called
to initialize tmp2 to be a copy of L.
On line 13, variable tmp1 is returned as the result of calling
function f.
When line 13 is executed, a copy constructor is called to make a copy of
tmp1 to be returned. (Later, that copy is used as the right-hand side
of the assignment on line 6.)
If you don't write a copy constructor, the compiler will provide one that
just copies the value of each data member. If some data member is a
pointer, this causes aliasing (both the original pointer
and the copy point to the same location), and may lead to trouble.
For example, consider the following code:
Consider the StrList class defined below. A StrList stores
a list of strings in a linked list pointed to by the StrList's
head field.
The Lookup operation determines whether a given string is in the list;
if it is there, it is moved to the front of the list, and the value
true is returned (otherwise, the list is unchanged, and the
value false is returned).
Recall that the declaration of a class's copy constructor is similar to
that of its default (no-argument) constructor: the function has no return
type (not even void), and its name is the same as the name of
the class.
However, unlike the default constructor, the copy constructor has one
argument: its type is the class, and it is a const reference
parameter. The argument is the object that the copy constructor is
supposed to copy.
For example:
Constructor Functions
IntList L; // L's no-arg constructor function is called
or when the object is dynamically allocated:
IntList *p; // no constructor called yet
p = new IntList; // now the no-arg constructor is called
IntList L(1, 10); // L's 2-arg constructor will be called
IntList *p;
p = new IntList(0, 5, 22); // L's 3-arg constructor will be called
Two Useful Standard Classes: string and vector
The string class
To use the string class you must #include <string> (be sure
that you do not include string.h, because then you will get the
header file for C-style strings rather than for the C++ string class).
string s1; // s1 is initialized to the empty string
string s2("hello"); // s2 is initialized to the string "hello"
string s3 = "goodbye"; // s3 is initialized to the string "goodbye"
string s1, s2 = "hello";
cout << s1.size(); // s1's size is 0
cout << s2.size(); // s2's size is 5
string s1("abc");
string s2;
s2 = "abc";
if (s1 == s2) ... // yes! the two strings ARE equal
string s1("hello");
string s2("goodbye");
string s3 = s1 + " and " + s2; // the value of s3 is "hello and goodbye"
string s1 = "hello";
for (int k=s1.size()-1; k>=0; k--) cout << s1[k]; // write s1 backwards
for (int k=s1.size()-1; k>=0; k--) s1[k] = 'a'; // change s to "aaaaa"
s[10] = 'a'; // ERROR! s only has 5 chars
The specified size can be any expression that evaluates to a non-negative
value.
vector <int> v1(10); // v1 is a vector of 10 integers
vector <char> v2(5); // v2 is a vector of 5 characters
vector <int> v(10);
for (int k=0; k<10; k++) {
v[k] = 3;
}
vector <int> v1(10);
vector <double> v2(5);
cout << v1.size(); // v1's size is 10
cout << v2.size(); // v2's size is 5
vector <int> v(1);
v[0] = 10;
v.resize(2*v.size());
v[1] = 20;
v.resize(1);
The resize operation preserves as many of the old values as possible
(so in the example, after the first resize operation, v[0]
is still 10; after the second resize operation, v[0]
is still 10, but there is no element of v equal to 20).
vector <int> f( ) { ... }
void f( vector
"hello", "", "bye, "", "", "!"
then function NonEmpty should create and return a vector
that contains the 3 strings:
"hello", "bye, "!"
#include <iostream>
#include <vector>
int main() {
vector
When you run this program, the output should be:
vector size before calling Expand: 1
vector size after calling Expand: 2
vector size before calling Expand: 2
vector size after calling Expand: 4
vector size before calling Expand: 4
vector size after calling Expand: 8
vector size before calling Expand: 8
vector size after calling Expand: 16
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ]
Classes with Pointer Data Members
The IntList class, defined above, includes a pointer to a dynamically
allocated array. Here is the
declaration of the IntList class again, augmented to include
declarations of the class's destructor, copy
constructor, and assignment operator (in red for emphasis):
class IntList {
public:
IntList(); // constructor; initialize the list to be empty
~IntList(); // destructor
IntList(const IntList &L); // copy constructor
IntList & operator=(const IntList &L); // assignment
void AddToEnd(int k); // add k to the end of the list
void Print(ostream &output) const; // print the list to output
private:
static const int SIZE = 10; // initial size of the array
int *Items; // Items will point to the dynamically allocated array
int numItems; // number of items currently in the list
int arraySize; // the current size of the array
};
The main purpose of the destructor function is to free any dynamically
allocated storage pointed to only by a data member of that
object.
(Note that it is up to the programmer to
ensure that no other pointers are pointing to that storage.)
[1] void f(IntList L) {
[2] IntList *p = new IntList;
[3] while (...) {
[4] IntList L1;
[5] ...
[6] }
[7] delete p;
[8] }
In this example, the scope of value parameter L is the whole function;
L goes out of scope at the end of the function (line 8). So when
function f ends, L's destructor function is called.
(Note: if f had
one or more return statements, L's destructor function would
be called when a return was executed).
IntList::~IntList() {
delete [] Items; // free the dynamically allocated array pointed to by Items
}
[1] void f() {
[2] IntList *p = new IntList;
[3] ...
[4] delete p;
[5] }
Assume that just before line 4, we have the following situation:
p: ---------> +---------------+
| | +---+
| Items: ----------> | 2 |
| | |---|
| numItems: 10 | | 6 |
| | |---|
| arraySize: 10 | | |
| | |...|
+---------------+ | |
+---+
If there is no IntList destructor, then when delete p is
executed, the storage for the IntList object pointed to by p
(which was alloacted at line 2) is freed.
However, the array pointed to by the IntList's Items
field is not freed, and will never be freed, so that is a storage
leak.
If the IntList destructor given above (that deletes the array
pointed to by Items) is provided, then it is called when line 4
is executed. That call frees the array storage, and then the delete
operator frees the storage pointed to by p (namely, the
storage for the IntList itself), and there is no storage leak.
The purpose of the copy constructor is to make a copy of the
Example
Here are two functions that illustrate when copy constructors are called:
[1] IntList f( IntList L );
[2]
[3] int main() {
[4] IntList L1, L2;
[5] ...
[6] L2 = f( L1 ); // copy constructor called here to copy L1
[7] }
[8]
[9] IntList f( IntList L ) {
[10] IntList tmp1 = L; // copy constructor called here to copy L
[11] IntList tmp2(L); // copy constructor called here to copy L
[12] ...
[13] return tmp1; // copy constructor called here to copy tmp1
[14] }
void f(IntList L) {
L.AddToEnd(11);
}
int main() {
IntList I;
for (int k=1; k<11; k++) I.AddToEnd(k);
// I's array "Items" is now full
f(I);
...
}
If the IntList class does not include a copy constructor, the compiler
will supply one that just copies the value of the pointer Items.
Here are pictures illustrating the result of the call to I's copy
constructor, which initializes the formal parameter L to be a copy
of I.
Note that both I.Items and L.Items point to the same array.
+---------------+
| | +----+
I: | Items: -----------> | 1 |
| | +-> |----|
| | | | 2 |
| numItems: 10 | | |----|
| | | | 3 |
| arraySize: 10 | | |----|
+---------------+ | | . |
| | . |
+---------------+ | | . |
| | | |----|
L: | Items: ---------+ | 10 |
| | +----+
| |
| numItems: 10 |
| |
| arraySize: 10 |
+---------------+
Now think about what happens when the body of function f executes.
L.AddToEnd discovers that the array is full, so it
allocates a new array, copies the values from the old array to the new array,
and returns the old array to free storage.
Unfortunately, L.AddToEnd doesn't know that I.Items
is also pointing to
the old array, so when that array is returned to free storage, I.Items
becomes a dangling pointer, and any attempt to access the array
it points to is likely to lead to trouble.
class StrList {
public:
// constructor
StrList();
// modifiers
void AddToFront(string s);
bool Lookup(string s);
// other operations
void Print(ostream &output) const;
private:
struct ListNode {
string data;
ListNode *next;
};
// pointer to the first node of the list
ListNode *head;
};
Consider the following code:
void f(StrList L) {
L.Lookup("b");
}
int main() {
StrList S;
S.AddToFront("c"); S.AddToFront("b"); S.AddToFront("a");
// S.head points to the linked list:
// "a" -> "b" -> "c"
f(S);
...
}
Note that there is no StrList copy constructor (so the compiler will
supply one).
Draw variables S and L as they would appear at the very
beginning of function f (just after L's copy
constructor is called to initialize it to be a copy of S).
Draw a second picture to illustrate what happens as a result of the call
to L.Lookup in function f.
What goes wrong because there is no StrList copy constructor?
The Copy Constructor Declaration
class IntList {
public:
IntList(); // default constructor
IntList(const IntList &L) // copy constructor
...
};