Class Constructor and Destructor
Dr. Lawlor, CS
202, CS, UAF
OK, so you've got classes--code and data together. The typical problem is that the data needs to be:
- Set up
- Used
- Cleaned up
And, you've got to do them in that exact order. For example, the "company" class from the last lecture really needs to initialize its totals to zero at startup, and really needs to send a check off to the IRS afterwards.
Constructor and Destructor Syntax
So C++ classes can start with a "constructor", which is like a method
that runs to set up the class. The constructor is named the same
thing as the class; so for a class named "Sammy", the constructor is
also named "Sammy", and it runs whenever you allocate a Sammy object:
class Sammy {
int value; // <- keeps a running total
public:
Sammy(void) { // constructor
value=0;
cout<<" Hi, I'm Sammy!\n";
}
};
int foo(void) {
cout<<"About to make a class...\n";
Sammy s;
return 0;
}
(Try this in NetRun now!)
The constructor performs step 1, data set up. Object methods use
the data, step 2. To clean up the data, you need some code to run
when the class is finished. C++ supports a "destructor", which is
called when the object is deleted. Destructors are named like
constructors, but with a tilde, a "~", in front of the name.
Here's an example with both a constructor and a destructor. Note
that the constructor is called when the class is created, and the
destructor is called when the class is destroyed at the end of the
function.
class Sammy {
int value;
public:
Sammy(void) { // constructor
value=0;
cout<<" Hi, I'm Sammy!\n";
}
void add(int v) {
value+=v;
cout<<" My total is now "<<value<<"!\n";
}
~Sammy() { // destructor
cout<<" Bye!\n";
}
};
int foo(void) {
cout<<"About to make a class...\n";
Sammy s;
cout<<"Calling class's add method...\n";
s.add(14);
cout<<"Returning from function...\n";
return 0;
}
(Try this in NetRun now!)
Constructors with Arguments
If you need to pass some data into a class to set it up, you can pass parameters to the constructor:
class Sammy {
int value;
public:
Sammy(int v) { // constructor, taking initial value
value=v;
cout<<" Hi, I'm Sammy!\n";
}
void add(int v) {
value+=v;
cout<<" My total is now "<<value<<"!\n";
}
~Sammy() { // destructor
cout<<" Bye!\n";
}
};
int foo(void) {
cout<<"About to make a class...\n";
Sammy s(100); // pass 100 to Sammy's constructor
cout<<"Calling class's add method...\n";
s.add(14);
cout<<"Returning from function...\n";
return 0;
}
(Try this in NetRun now!)
You can even have several different constructors taking different
arguments, or no arguments (the "default constructor"). For
example, my standard "vec3" class has a whole set of constructors:
class vec3 {
public:
float x,y,z; // components of the vector
vec3(float x_,float y_,float z_) { x=x_; y=y_; z=z_; } // call like: vec3 A(3,4,5);
vec3(float v) {x=y=z=v; } // call like: vec3 B(0.0);
vec3(float *arr) {x=arr[0]; y=arr[1]; z=arr[2]; } // call like: vec3 C(&floatarr[9]);
vec3(void) {x=y=z=0.0; } // default constructor: vec3 D;
...
};
Destructors can't take arguments, so there's only one destructor.
Sentinals
Because destructors always run when your function exits, sometimes
people make classes that do work only inside the destructor. A
class like this is called a "sentinal", since it waits silently until
it is called upon for some task.
For example, if you keep forgetting to flush the log file (and so
getting an empty log file), you could make an "AutoFlush" class that
will flush the file when your code returns:
class AutoFlush {
fstream *file; // to flush
public:
AutoFlush(fstream *f) {file=f;}
~AutoFlush() {file->flush();} // flush in destructor
};
fstream outfile("log.txt",ios::out);
void doWork(void) {
AutoFlush flusher(&outfile); // <- guarantees flush before return
for (int i=0;i<17;i++) {
outfile<<"still alive at step "<<i<<"\n";
if (i==12) { return; } // <- oops!
}
outfile.flush(); //<- you remembered to flush here...
}
int foo(void) {
doWork();
cat("log.txt");
return 0;
}
Resource Aquisition Is Initialization
One extremely common and powerful trick in C++ is to allocate memory in
your constructor, and release the memory in your destructor. This
makes it much harder to forget to deallocate memory. For example,
here's a "smart array" class that checks array bounds, and
automatically deletes the data when your function returns.
class SmartArray {
int length;
int *data;
public:
SmartArray(int len) {
length=len;
data=new int[length];
}
// Get this value from the array, or exit if a bad index
int &get(int index) {
if (index<0 || index>=length)
{ // index is out of bounds!
cout<<"Index "<<index<<" out of bounds 0.."<<length-1<<"!\n";
exit(1);
}
return data[index];
}
~SmartArray() {
delete[] data;
}
};
int foo(void) {
SmartArray arr(10); // <- array length is constructor parameter
for (int i=0;i<10;i++) {
arr.get(i)=1000+i;
}
return arr.get(7);
}
(Try this in NetRun now!)