Week 04 / 14 Object-Oriented Programming
Constructors, destructor and accessors
How an object gets a valid state from the first moment, how it cleans up after itself, what the copy constructor does when the class owns heap memory, and how getters and setters keep the data private.
- Write constructors with no parameters, with all parameters and with fewer parameters
- Initialize members in the initializer list
- Replace several constructors with one that has default arguments
- Write a copy constructor for a class with a dynamically allocated member
- List the situations in which the copy constructor is called
- Explain when the destructor runs and what it must release
- Write getters and setters that keep the class encapsulated
Why constructors
Last week the object was created first and filled in afterwards, with an init function or by assigning members one by one. Between those two moments the object exists in an invalid state, and nothing forces you to call init at all.
A constructor is a member function with the same name as the class and no return type. It runs automatically when the object is created. Its job is to make the object valid from the first moment.
If you write no constructor at all, the compiler generates a default one that does nothing for int members. If you write any constructor, the compiler no longer generates the default one.
Kinds of constructors
A class can have several constructors, as long as their parameter lists differ:
- without parameters (the default constructor):
Point(); - with all parameters:
Point(int x, int y); - with fewer parameters:
Point(int v); - copy constructor:
Point(const Point& p), covered below.
Which constructor runs for each object?
#include <iostream>
using namespace std;
class Point {
int x, y;
public:
Point() : x(0), y(0) { cout << "A "; }
Point(int v) : x(v), y(v) { cout << "B "; }
Point(int x, int y) : x(x), y(y) { cout << "C "; }
};
int main() {
Point p1;
Point p2(5);
Point p3(1, 2);
Point p4 = 7;
} Point p4 = 7; calls the constructor with one int parameter. It is not an assignment.
The initializer list
The initializer list comes after : and before the body. Members are initialized there, before the body runs. In the body you can only assign values to members that already exist.
class Product {
const int id; // const: can only be initialized
string name;
double price;
public:
Product(int id, string name, double price)
: id(id), name(name), price(price) {}
};
id(id) is not ambiguous: outside the parentheses id is the member, inside it is the parameter.
You must use the initializer list for const members, reference members, and members of a class type that has no default constructor.
Default arguments
Instead of three constructors you can write one, with default values in the parameter list:
class Point {
int x, y;
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
};
Point a; // (0,0)
Point b(3); // (3,0)
Point c(3, 4); // (3,4)
Default values go from right to left: once a parameter has one, all the parameters after it need one too. Write them only in the declaration, not again in a definition outside the class.
The destructor
The destructor is called ~ClassName(), has no parameters and no return type. It runs automatically when the object’s lifetime ends:
- a local object: at the closing
}of its block; - an object created with
new: atdelete; - a global or
staticobject: at the end of the program.
Local objects are destroyed in the reverse order of their creation.
#include <iostream>
#include <string>
using namespace std;
class Tracer {
string name;
public:
Tracer(string n) : name(n) { cout << "+" << name << " "; }
~Tracer() { cout << "-" << name << " "; }
};
int main() {
Tracer a("a");
{
Tracer b("b");
Tracer c("c");
}
Tracer d("d");
} Write a destructor when the object owns a resource, such as memory taken with new. The destructor gives it back.
Copy constructor and dynamic members
The copy constructor builds a new object from an existing one. If you don’t write it, the compiler generates one that copies every member as it is. For a pointer, that copies the address, not the data it points to.
class Student {
char* name;
public:
Student(const char* n) {
name = new char[strlen(n) + 1];
strcpy(name, n);
}
~Student() { delete[] name; }
};
This class is wrong as soon as you copy an object: Student b = a; leaves both name pointers on the same array. The first destructor frees it, the second one frees it again. That is a double delete, which is undefined behaviour.
Student a("Ana"); - the constructor allocates the text on the heap.
Step 1/4
The fix is a copy constructor that allocates new memory and copies the content:
Student(const Student& s) {
name = new char[strlen(s.name) + 1];
strcpy(name, s.name);
}
The parameter must be a reference. With Student(Student s), passing s by value would itself need a copy, so the compiler rejects it.
When is the copy constructor called
- When you initialize an object from another one:
Box b = a;orBox c(a);. - When you pass an object by value to a function.
- When a function returns an object by value. Here the compiler is allowed, and in some cases required, to skip the copy, so don’t count on seeing it.
#include <iostream>
using namespace std;
class Box {
public:
Box() { cout << "default" << endl; }
Box(const Box&) { cout << "copy" << endl; }
};
void byValue(Box) {}
void byReference(const Box&) {}
int main() {
Box a;
Box b = a;
Box c(a);
byValue(a);
byReference(a);
} Accessors: get and set
Data stays private. The class decides what can be read and how it can be changed.
- A getter returns a value and does not change the object, so it is
const. - A setter validates before it changes anything. Invalid values are rejected.
- Not every member needs a setter. An id that never changes gets only a getter.
const char* getName() const { return name; } // read only
void setGrade(int g) {
if (g >= 1 && g <= 10) grade = g; // the invariant holds
}
Return const char*, not char*. Otherwise the caller can write through the pointer straight into your private array.
Run the program below. Then add setName(const char*): what must it do with the old array before it stores the new name?
#include <iostream>
#include <cstring>
using namespace std;
class Student {
char* name;
int grade;
public:
Student(const char* n = "unknown", int g = 0) : grade(g) {
name = new char[strlen(n) + 1];
strcpy(name, n);
cout << "constructor " << name << endl;
}
Student(const Student& s) : grade(s.grade) {
name = new char[strlen(s.name) + 1]; // own memory for the copy
strcpy(name, s.name);
cout << "copy constructor " << name << endl;
}
~Student() {
cout << "destructor " << name << endl;
delete[] name;
}
const char* getName() const { return name; }
int getGrade() const { return grade; }
void setGrade(int g) {
if (g >= 1 && g <= 10) grade = g; // reject invalid grades
}
};
void show(Student s) {
cout << s.getName() << ": " << s.getGrade() << endl;
}
int main() {
Student a("Ana", 9);
Student b = a;
b.setGrade(15); // ignored
b.setGrade(7);
show(b);
cout << a.getGrade() << " " << b.getGrade() << endl;
}Input (stdin)
Check yourself
Score: 0/8