Week 05 / 14 Object-Oriented Programming
Member types, operator = and the "has a" relationship
Const and static members, static methods, what pointer members demand from constructors and the destructor, the assignment operator, class composition, and how to split a class into a header and a source file.
- Declare const members and initialize them in the initializer list
- Use static data members and static methods, and say what they cannot access
- Explain why a class with a pointer member needs its own copy constructor and destructor
- Write operator= with a self-assignment check that returns *this
- Model 1:1 and 1:M "has a" relationships with composition
- Split a class into a .h and a .cpp file protected by include guards
Const members
A const member gets its value once, when the object is created, and never changes after that. A student ID or a serial number are good examples.
class Product {
const int id;
double price;
public:
Product(int i, double p) : id(i), price(p) {} // OK: initializer list
// Product(int i, double p) { id = i; } // error: assignment to a const member
};
The initializer list runs before the constructor body. By the time the body starts, id already exists, so assigning to it there is an assignment, not an initialization. The compiler refuses it.
Static members and static methods
A static data member belongs to the class, not to an object. There is exactly one copy of it, shared by all objects, and it exists even when no object exists.
class Product {
static int nextId; // declaration, inside the class
// ...
};
int Product::nextId = 1; // definition, outside the class, exactly once
A static method is called through the class name: Product::created(). It has no this, so it can only touch static members directly.
The constructor counts objects up, the destructor counts them down. What does the program print?
#include <iostream>
using namespace std;
class Counter {
public:
static int count;
Counter() { count++; }
~Counter() { count--; }
};
int Counter::count = 0;
int main() {
Counter a, b;
{
Counter c;
cout << Counter::count << " ";
}
cout << Counter::count;
} Change the program below: add a second static method that returns the price of the most expensive product created so far.
#include <iostream>
using namespace std;
class Product {
const int id; // fixed for the lifetime of the object
double price;
static int nextId; // one counter for the whole class
public:
Product(double p) : id(nextId++), price(p) {}
int getId() const { return id; }
static int created() { return nextId - 1; }
};
int Product::nextId = 1;
int main() {
Product a(10), b(25.5);
Product c(7);
cout << a.getId() << " " << b.getId() << " " << c.getId() << endl;
cout << "created: " << Product::created() << endl;
}Input (stdin)
Pointer members
When a class owns memory through a pointer, the compiler-generated functions do the wrong thing:
- the generated copy constructor copies the address, not the data;
- the generated destructor frees nothing;
- the generated operator= copies the address and loses the old block.
So the class needs all three written by hand: destructor (delete[]), copy constructor (allocate and copy), and operator=. This is the Rule of Three: if you need one of them, you almost certainly need all three.
The assignment operator
Box b = a; creates a new object, so it calls the copy constructor. c = a; changes an object that already exists, so it calls operator=.
C is the normal constructor, K the copy constructor, A the assignment operator.
#include <iostream>
using namespace std;
class Box {
public:
int v;
Box(int x) : v(x) { cout << "C"; }
Box(const Box& b) : v(b.v) { cout << "K"; }
Box& operator=(const Box& b) { v = b.v; cout << "A"; return *this; }
};
int main() {
Box a(1);
Box b = a;
Box c(2);
c = a;
cout << " " << b.v << " " << c.v;
} For a class with a pointer member, operator= has four steps: check self-assignment, free the old memory, copy the other object, return *this.
Buffer& Buffer::operator=(const Buffer& b) {
if (this == &b) return *this; // a = a; must not delete its own data
delete[] data;
size = b.size;
data = new int[size];
for (int i = 0; i < size; i++) data[i] = b.data[i];
return *this; // so that a = b = c; works
}
Buffer a(2), b(3); - each object owns its own block.
Step 1/3
Class composition: βhas aβ
A class can have objects of other classes as members. A car has an engine (1:1). A group has many students (1:M).
class Engine { int power; /* ... */ };
class Car {
Engine engine; // 1:1
};
class Group {
Student students[30]; // 1:M, fixed capacity
int count;
};
Members are constructed before the body of the ownerβs constructor runs, in the order they are declared. Destruction goes in reverse.
#include <iostream>
using namespace std;
class Engine {
public:
Engine() { cout << "E"; }
~Engine() { cout << "e"; }
};
class Car {
Engine engine;
public:
Car() { cout << "C"; }
~Car() { cout << "c"; }
};
int main() {
Car x;
} If a member has no default constructor, the owner must call one of its constructors in the initializer list: Car(int hp) : engine(hp) {}.
Header and source files
Larger programs put each class in two files. The header says what the class is, the source file says how it works.
// Product.h
#pragma once // or the classic include guard below
// #ifndef PRODUCT_H
// #define PRODUCT_H
class Product {
const int id;
double price;
static int nextId;
public:
Product(double p);
int getId() const;
static int created();
};
// #endif
// Product.cpp
#include "Product.h"
int Product::nextId = 1;
Product::Product(double p) : id(nextId++), price(p) {}
int Product::getId() const { return id; }
int Product::created() { return nextId - 1; }
main.cpp only includes "Product.h". The guard stops the class from being defined twice when two headers include the same file. The static member is defined in the .cpp, never in the header, or you get a link error for multiple definitions.
Check yourself
Score: 0/8