OOP Search

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.

Objectives
  • 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)

Open in Godbolt

Pointer members

When a class owns memory through a pointer, the compiler-generated functions do the wrong thing:

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
}
STACK
a.data: ●
b.data: ●
HEAP
0 1
0 1 2

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

1. A class has the member const int id;. Where can you give id its value?

2. How many copies of a static int count; data member exist when the program has 10 objects of that class?

3. Which of these can a static method do directly?

4. What does the program print?
#include <iostream>
using namespace std;
class A {
public:
    static int n;
    A() { n++; }
};
int A::n = 0;
int main() {
    A x, y;
    A z, w;
    A* p = new A;
    cout << A::n;
    delete p;
}

5. C is the normal constructor, K the copy constructor, A operator=. What does the program print?
#include <iostream>
using namespace std;
class P {
public:
    P() { cout << "C"; }
    P(const P&) { cout << "K"; }
    P& operator=(const P&) { cout << "A"; return *this; }
};
void f(P) {}
int main() {
    P a;
    P b(a);
    f(a);
    b = a;
}

6. A class has the member int* data; and no operator= of its own. What does a = b; do?

7. What does the program print?
#include <iostream>
using namespace std;
class Engine {
public:
    Engine() { cout << "E"; }
    ~Engine() { cout << "e"; }
};
class Wheels {
public:
    Wheels() { cout << "W"; }
    ~Wheels() { cout << "w"; }
};
class Car {
    Engine engine;
    Wheels wheels;
public:
    Car() { cout << "C"; }
    ~Car() { cout << "c"; }
};
int main() {
    Car x;
}

8. Where should int Product::nextId = 1; go when Product is split into Product.h and Product.cpp?

Score: 0/8

Code in repository: L05

Romanian version