Embercode
✕
Object-Oriented ProgrammingLesson 5 of 17

55 min read

Static members and the rule of three

Why it matters

Two questions come up in almost every interview about classes. The first is "how do all the objects of a class share one value?", and the answer is a static member. The second is "what goes wrong when a class holds a pointer and gets copied?", and the answer is the rule of three, which says that a class that owns memory needs a destructor, a copy constructor and a copy assignment operator. Candidates who know both answers can write their own linked lists and trees without crashes. Candidates who do not often write a class that works in a small test and then crashes in a larger program.

Static members come up in real code too. A counter that numbers every ticket, a registry of plug-ins, a cache shared by all objects of a type, and a logger that every class uses are all static. This lesson covers how static data is stored and initialised, what a static method can and cannot do, and how the rule of three keeps copies safe. It finishes with the modern alternative, the rule of zero, where the standard library does the work.

Static data: one copy for the whole class

A normal member is stored once per object. A static data member is stored once for the whole class. Every object of the class sees the same value, and changing it through one object changes it for all of them.

Think of a shared whiteboard in an office. Everyone reads and writes the same board. A normal member is more like a notepad that each person carries. The whiteboard is in one place, and the notepads are many.

Three objects each with their own id, all reading one shared static count stored once in the classThree objects each with their own id, all reading one shared static count stored once in the class

Here is the classic use: a counter that numbers each ticket as it is created. Every ticket must see the same counter, so the counter is static.

C++ · editable

Expected output:

3
3

Trace the program. The class declares count as static. Its definition, int Ticket::count = 0;, sits outside the class and gives the variable its storage and its starting value. Each of the three objects, a, b and c, runs the constructor, which increments the one shared count. The first object gets id 1, the second gets id 2, and the third gets id 3. The call c.getId() returns 3. The static method total reads the same shared counter and returns 3.

Notice the two rules that the example shows. A static member has a declaration inside the class, and it needs a definition outside, exactly once, in one source file. And the static method total can be called with the class name, Ticket::total(), without any object at all.

The definition rule and C++17's inline

Forgetting the outside definition is a common mistake, and the error appears at link time, not at compile time. The compiler accepts the declaration. The linker, which joins the parts of the program, then cannot find the variable. The next section shows the message.

Since C++17 you can avoid the separate definition with the inline keyword. The line static inline int count = 0; inside the class declares and defines the variable at once. The example above works the same way with that one change:

class Ticket {
    static inline int count = 0;   // declared and defined in the class (C++17)
    int id;
public:
    Ticket() : id(++count) {}
    int getId() const { return id; }
    static int total() { return count; }
};

This is the form to prefer in new code, because it cannot be forgotten. The older form is still common in existing projects and in interview questions, so you should be able to read both.

What a static method can and cannot do

A static method belongs to the class, not to any object. It is called with the class name, as in Ticket::total(). It has no this pointer, because there is no object to point to. That gives it one strict rule: a static method can use static members and parameters, but it cannot use ordinary members, because those belong to an object it does not have.

class Ticket {
    int id = 0;
public:
    static int makeId() { return id + 1; }   // id belongs to an object
};
error: invalid use of member 'Ticket::id' in static member function

The message names the member and explains the cause: the static function has no object, so id has no meaning there. The fix is either to make id static, if the value really belongs to the class, or to make makeId an ordinary method, if it needs an object.

A static method can be called on an object too, as in t.total(), but the object is ignored. Calling it through the class name is clearer, and most style guides recommend it.

Worked example: a line with a static ticket counter

The next program combines a static counter with a queue of waiting customers. The static counter gives each customer a ticket number, and it keeps counting across the whole program even when the line is served. The queue stores the waiting tickets in order, first in first out. The visualizer below shows the queue at work.

Type these commands into the Input box, one per line:

join
join
join
serve
waiting
serve
serve
serve
issued
C++ · editable

Expected output:

ticket 1
ticket 2
ticket 3
serving 1
2 waiting
serving 2
serving 3
nobody waiting
3 issued

Trace the three join commands. Each one increments the shared ticketsIssued and pushes the new number onto the back of waiting. After three joins the queue holds 1, 2, 3 in that order. The serve command takes the front of the queue, which is 1, so the first customer is served first. The waiting command reports 2, because 2 and 3 are still in the line. Two more serves take 2 and then 3. The last serve finds the line empty and prints nobody waiting. The issued command reports 3, which is the static counter. Serving customers does not reduce the number of tickets ever issued, because that count is a fact about the history, not about the line.

empty

removed: nothing

Items leave from the front and join at the back. An outlined box was just added or peeked.

The queue is empty. The first item added is the first one removed.

Step 1 of 8

The visualizer above shows the same behaviour step by step. Join adds at the back, serve removes from the front. The vector in our class does both jobs: push_back adds at the back, and erase at the front removes the first element. Notice that removing from the front of a vector shifts every other element one place, which costs time proportional to the length. A real queue uses a structure built for both ends, and the later modules show one.

Ordinary members versus static members

Here is a quick comparison to keep in mind.

QuestionOrdinary memberStatic member
How many copies exist?One per objectOne for the whole class
Where is it declared?Inside the classInside the class, and defined outside (before C++17)
Can a static method use it?No, not without an objectYes
How is it reached?Through an object, a.valueThrough the class, Ticket::count, or through an object
When does it exist?When the object is createdBefore main starts

The last row is worth remembering. Static data is created before main runs, and it lives until the program ends. That is why a static counter starts at zero for the whole program, and why its initial value must be a constant or a simple expression.

The shallow copy problem

Now the second topic. Suppose a class owns a block of memory through a raw pointer, and it has only the default behaviour for copying. The default copy copies the members one by one. For a pointer, that means copying the address, not the block. Two objects then point at the same block. This is a shallow copy.

When both objects are destroyed, each destructor frees the same block. Freeing a block twice is a crash, or worse, a silent corruption that shows up later in some unrelated part of the program.

A deep copy is the right behaviour for a class that owns memory. It allocates a new block for the copy and copies the values into it, so each object owns its own memory. Changing one object then leaves the other alone.

Two boxes showing a shallow copy where two objects share one memory block, and a deep copy where each object has its own blockTwo boxes showing a shallow copy where two objects share one memory block, and a deep copy where each object has its own block

The next program shows the crash that a shallow copy causes. The class holds an array and has a destructor, but no copy constructor.

#include <iostream>
using namespace std;
 
class IntArray {
    int* data;
    int size;
public:
    IntArray(int n) : data(new int[n]()), size(n) {}
    ~IntArray() { delete[] data; }
    void set(int i, int v) { data[i] = v; }
    int get(int i) const { return data[i]; }
};
 
int main() {
    IntArray a(3);
    a.set(0, 10);
    IntArray b = a;      // the default copy: the pointer is copied, not the data
    b.set(0, 99);
    cout << a.get(0) << " " << b.get(0) << "\n";
    return 0;
}

On g++ 15 this program stops at the end of main with the message below, and exit code 134. The line that prints the numbers does not appear, because the program aborts during clean-up, before the buffered output reaches the screen.

free(): double free detected in tcache 2

Read the cause in the code, not in the message. a and b both have data pointing at the same three integers. b.set(0, 99) changed that shared block, so a also sees 99. Then both destructors call delete[] on the same block. The second call frees memory that was already freed. The message is the allocator's way of saying so.

The rule of three is the fix. It is the rule that every class which owns memory must define three special functions together: the destructor, the copy constructor and the copy assignment operator.

The rule of three

If a class needs one of these three functions, it almost always needs all three.

  1. The destructor frees the memory the object owns.
  2. The copy constructor builds a new object as a copy of an existing one, with its own memory.
  3. The copy assignment operator replaces the contents of an existing object with a copy of another object, with its own memory.

Here is the corrected IntArray. It is the same class with the two missing functions added.

C++ · editable

Expected output:

10 99
10

Trace the program. IntArray b = a; uses the copy constructor, because the new object is being created from an existing one. It allocates a fresh block of three integers and copies the values across. So b has its own memory. b.set(0, 99) changes only b, and a still holds 10. Then c = a; uses the copy assignment operator, because c already exists. The operator copies a's values into c's memory, and c prints 10.

The two functions differ in one important way. The constructor creates a new object, so it has nothing to free. The assignment operator replaces an existing object, so it must free the old block before it takes the new one. That is why the assignment operator calls delete[], while the constructor does not.

Notice the order inside the assignment operator. It allocates the new block first, copies into it, and only then deletes the old block. If the allocation fails, the function throws before it has changed anything, and the object still holds its old, valid data. If the code deleted the old block first, a failed allocation would leave the object pointing at freed memory.

Why each piece is needed

The destructor frees the block when the object dies. Without it the block leaks.

The copy constructor gives the copy its own block. Without it, the default shallow copy creates the crash shown earlier.

The copy assignment operator does the same job for an existing object, and it must handle one special case: assigning an object to itself. Without the guard if (this == &o) return *this;, the code would delete its own data first and then copy from memory it has just freed. The guard returns early, because copying an object onto itself does nothing useful.

The three functions form one set. If you write one, the others must follow the same rule about memory. This is why the rule is called the rule of three, and why reviewers look for all three together.

Worked example: a name class with a char pointer

The same rule applies to any class that owns memory, including text. Here is a small Name class that stores a C string in heap memory. It has all three functions, and a method that changes one character, so you can see that copies are independent.

C++ · editable

Expected output:

Ada
Eda
Ada

Trace it. Name b = a; makes a deep copy, so b has its own characters. c = a; replaces c's text, freeing the old "Grace" first and then copying "Ada" into a new block. b.setChar(0, 'E') changes the first character of b only. So a and c still read "Ada", and b reads "Eda". Each object is independent, because each owns its own block.

The class uses new char[strlen(s) + 1]. The + 1 makes room for the terminating zero character that marks the end of a C string. Forgetting it writes one byte past the end of the block, which is undefined behaviour that may not crash in a small test and may crash in a larger one.

When the copy functions run

Students often ask which function runs in a given line of code, and the answer decides whether a copy is cheap or dangerous. The rule is simple once you look at what kind of step the code is taking. A new object being created from an old one uses the copy constructor. An existing object receiving a new value uses the copy assignment operator. Here are the common cases, each with the function that runs.

IntArray a(3);
IntArray b = a;        // copy constructor: b is created from a
IntArray c(a);         // copy constructor: the same thing, written with parentheses
IntArray d(1);
d = a;                 // copy assignment: d already exists and receives a value

Passing an object by value to a function also copies it. The parameter is a new object, so the copy constructor runs when the function is called.

void show(IntArray x) {          // x is a copy: the copy constructor runs on each call
    cout << x.get(0) << "\n";
}

Passing by const reference avoids the copy entirely. This is one more reason the references in lessons 3 and 4 were not a style choice. They were a way to avoid copying a class that owns memory.

Storing an object in a container also copies it. vector<IntArray> saved; saved.push_back(a); runs the copy constructor on the new element. When a vector grows, it copies the elements it already holds into the bigger block, and that is where the extra output in lesson 2 came from. Calling reserve avoids the repeated copies, and it is good practice when the final size is known.

Returning an object from a function is the last case, and it has two forms. If the return statement creates the object, as in return IntArray(3);, then since C++17 the compiler must build it directly where the caller will use it, so no copy happens. This is called copy elision, and the language guarantees it. If the function returns a named local variable, as in return row;, the compiler usually skips the copy too, but the language does not require it. Do not design your class around either assumption. Write correct copy functions, and the compiler will skip the copies that it can.

The table below sums up the cases.

CodeFunction that runsNotes
T b = a; or T b(a);copy constructora new object is created
b = a; where b existscopy assignment operatorthe old contents are replaced
f(a) where f takes T by valuecopy constructorthe parameter is a new object
f(a) where f takes const T&nonethe function only sees a reference
v.push_back(a)copy constructorthe vector keeps its own copy
return T(3); from a functionnoneelision, guaranteed since C++17
return local; from a functionusually noneelision is allowed but not required

A checklist for writing copy functions

When you write the rule of three by hand, check these points in order. Each one catches a mistake that students make in interviews.

  • The destructor frees every block the class allocated, with the matching form, delete for new and delete[] for new[].
  • The copy constructor allocates a new block and copies the values, and it does not copy the pointer.
  • The copy constructor copies every member, not only the pointer. Forgetting the size, for example, leaves the copy unable to read its own data safely.
  • The copy assignment operator checks for self-assignment with if (this == &o) return *this;.
  • The copy assignment operator allocates the new block before it deletes the old one.
  • The copy assignment operator returns *this by reference, so that chained assignments such as a = b = c work.
  • The three functions use the same rule for memory, so a change to one is reflected in the others.

A quick test catches most mistakes. Make a copy, change the copy, and check that the original is unchanged. Then assign one object to another, change the new one, and check again. Finally, assign an object to itself, and check that it still holds its values. If all three checks pass, the class is probably right.

The rule of zero

In modern C++ you usually do not write the three functions at all. The rule of zero says: if a class owns no raw resource, let the standard library own it, and write none of the three functions. A std::vector or a std::string already copies deeply, assigns safely and frees its memory in its destructor. A class whose members are all such types gets correct copies for free.

C++ · editable

Expected output:

red: Asha
red: Asha Ravi

The class has no destructor, no copy constructor and no assignment operator, yet the copy is deep. Team b = a; copies the vector inside, which copies its strings. Adding "Ravi" to b does not change a. The rule of zero is the goal, and the rule of three is the safety net for the cases where you must manage raw memory yourself, such as in a linked list node or a hand-written buffer.

Use std::unique_ptr for a single owned object and std::vector for a sequence. Reach for the rule of three only when you truly need a raw resource, and then write all three functions together.

Counting objects with a static member

Static members and copies meet in a useful pattern. A class can count how many of its objects are alive at once by changing a static counter in each constructor and in the destructor. A copy constructor must count too, because a copy is a new live object.

C++ · editable

Expected output:

alive 3
alive 1

Trace the counter. Probe a; makes the count 1. Inside the inner block, Probe b = a; calls the copy constructor, which makes the count 2, and Probe c; makes it 3. The first print shows 3. When the block ends, b and c are destroyed, and each destructor subtracts 1. The count returns to 1, and the second print shows 1. The object a is still alive, so it remains counted.

This pattern is useful for finding leaks in a test. If the count does not return to zero at the end of a run, some object was never destroyed.

Common mistakes with real compiler messages

Mistake 1: a static member declared but not defined. The compiler accepts the declaration, but the program fails at link time. The message comes from the linker, which is the last step of the build.

#include <iostream>
using namespace std;
class Ticket {
    static int count;
    int id;
public:
    Ticket() : id(++count) {}
    static int total() { return count; }
};
int main() {
    cout << Ticket::total() << "\n";
}
undefined reference to `Ticket::count'
collect2: error: ld returned 1 exit status

The fix is the line int Ticket::count = 0; outside the class, or the C++17 form static inline int count = 0;. The word "undefined" means the declaration exists but nothing gives the variable storage.

Mistake 2: an ordinary member used in a static method. The message names the member and the function.

error: invalid use of member 'Ticket::id' in static member function

The static method has no object, so there is no id to read. Use a static member, or make the method ordinary.

Mistake 3: a copy without a copy constructor. As shown above, a class with a destructor and no copy constructor compiles and copies shallowly. The mistake shows up at run time, as a crash at the end of the program. The message is typically:

free(): double free detected in tcache 2

Read it as a clue that the same block was freed twice. Look for a class that owns a pointer, has a destructor, and is copied somewhere. Then add the copy constructor and the copy assignment operator.

Mistake 4: forgetting delete[] for memory from new[]. Plain delete on memory allocated with new[] is undefined behaviour. Many small tests appear to work, and the bug shows up later. Match new[] with delete[], and new with delete.

Mistake 5: no self-assignment guard. Without if (this == &o) return *this;, the assignment a = a frees the block and then copies from it. The bug is silent in many cases, so write the guard whenever you write copy assignment by hand.

Mistake 6: no + 1 for the terminating zero. With a C string, new char[strlen(s)] is one byte too small. The copy writes past the end of the block. Always allocate strlen(s) + 1.

Debugging walkthrough: a crash only in the bigger program

Suppose a program with a Name class works in a small test and crashes in a larger one, with a double-free message. Here is how to find the cause.

  1. Note that the crash happens at the end of a scope or at the end of main. Destructors run at those points, so the crash is probably in a destructor.
  2. Find every class that owns memory. Look for new, new[] or malloc in a constructor, and delete or free in a destructor.
  3. For each such class, check the three functions. Does it have a copy constructor? A copy assignment operator? If either is missing, the default one is shallow, and that is the likely cause.
  4. Find the copies. Search for = with an object on the right, for objects passed by value, and for objects stored in containers. Each of these makes a copy.
  5. Fix the class with the rule of three, or better, replace the raw pointer with std::string or std::vector, which follows the rule of zero.

For a precise answer, compile with the address sanitiser: g++ -std=c++17 -g -fsanitize=address main.cpp. The sanitiser reports both the place where the block was freed the first time and the place of the second free, which shows the copy that caused the problem.

Variations and edge cases

Static members in templates and headers. A static member of a class template gets one copy per instantiation. A static data member defined in a header that several source files include can cause a link error, which the inline keyword solves. These cases come up in larger projects, and the C++17 form is the safe default.

Static local variables. A static variable inside a function keeps its value between calls. It is initialised the first time the function runs. Use it for a cache or a counter that belongs to the function, and avoid it when the value should belong to an object.

Move operations. C++11 added the move constructor and move assignment, which take the block from a temporary object instead of copying it. A full set of special functions for a class that owns memory is sometimes called the rule of five. When you write a destructor, the compiler stops generating move operations, so the rule of three is still the place to start.

Deleting copy. If a class should never be copied, such as a file handle or a unique lock, write Name(const Name&) = delete; and the matching assignment operator as deleted. The compiler then refuses any copy, which is a better failure than a silent shallow copy.

Static initialisation order. Static objects in different source files are constructed in an order the language does not fix. Relying on one static object's constructor having run before another's is a known source of bugs. Avoid it by keeping static state simple, or by using a function-local static.

Interview questions and how to answer them

What is a static data member? A member that belongs to the class, with one copy shared by all objects. It needs a definition outside the class before C++17, or the inline form in the class from C++17 on.

Can a static method use an ordinary member? No. It has no this pointer and no object, so it cannot read a non-static member.

What is the rule of three? A class that owns memory needs a destructor, a copy constructor and a copy assignment operator. Writing one usually means writing all three.

What is a shallow copy and why is it dangerous? A copy that copies a pointer rather than the block it points to. Two objects then share the block, and both destructors free it, which is a double free.

Why does the copy assignment operator allocate before it deletes? So that a failed allocation leaves the object unchanged and valid. The old data is released only after the new data is ready.

What is the rule of zero? Write none of the special functions, and let members such as std::vector and std::string manage the memory. The default functions then copy and free correctly.

Real-world uses

Static members appear in logging frameworks, where one logger object is shared, and in identifier generators, which hand out unique numbers across a program. They also appear in caches and registries, where a static table lets every object of a type find shared data.

The rule of three appears wherever a program owns a raw resource. A hand-written buffer, a custom string, a linked list or a tree of nodes all need it. In production code, most of these are replaced with standard containers and smart pointers, which follow the rule of zero. Reading an older codebase, though, you will meet the rule of three everywhere, and you must be able to check that its functions are complete.

Practice

Try each exercise before you open the solution. Use Run to check your answer.

  1. Add a static counter to IntArray that counts how many arrays are alive. Increment it in the constructors, including the copy constructor, and decrement it in the destructor. Print the count after creating two arrays, one copy, and after the copy leaves a block. Hint: static inline int alive = 0; is the simplest form.
Solution
#include <iostream>
using namespace std;
 
class IntArray {
    static inline int alive = 0;
    int* data;
    int size;
 
public:
    IntArray(int n) : data(new int[n]()), size(n) { alive++; }
    IntArray(const IntArray& o) : data(new int[o.size]), size(o.size) {
        for (int i = 0; i < size; i++) data[i] = o.data[i];
        alive++;
    }
    IntArray& operator=(const IntArray& o) {
        if (this == &o) return *this;
        int* fresh = new int[o.size];
        for (int i = 0; i < o.size; i++) fresh[i] = o.data[i];
        delete[] data;
        data = fresh;
        size = o.size;
        return *this;
    }
    ~IntArray() { delete[] data; alive--; }
    static int count() { return alive; }
};
 
int main() {
    IntArray a(3), b(2);
    cout << IntArray::count() << "\n";
    {
        IntArray c = a;
        cout << IntArray::count() << "\n";
    }
    cout << IntArray::count() << "\n";
    return 0;
}

The output is 2, then 3, then 2. The copy constructor increments the count, and the destructor of the inner copy decrements it again.

  1. Remove the copy constructor from IntArray and explain in two or three sentences what goes wrong when IntArray b = a; runs.
Solution

Without a copy constructor, the compiler generates one that copies each member, so b.data gets the same pointer as a.data. Both objects then share one block. When both are destroyed, the destructor frees the block twice, which the runtime reports as a double free and stops the program.

  1. Write the three special functions for a Pet class that holds char* name. The constructor takes a const char*. Test that a copy can be renamed without changing the original. Hint: the copy assignment operator must free the old name.
Solution
#include <cstring>
#include <iostream>
using namespace std;
 
class Pet {
    char* name;
public:
    Pet(const char* s) {
        name = new char[strlen(s) + 1];
        strcpy(name, s);
    }
    Pet(const Pet& o) {
        name = new char[strlen(o.name) + 1];
        strcpy(name, o.name);
    }
    Pet& operator=(const Pet& o) {
        if (this == &o) return *this;
        char* fresh = new char[strlen(o.name) + 1];
        strcpy(fresh, o.name);
        delete[] name;
        name = fresh;
        return *this;
    }
    ~Pet() { delete[] name; }
    void rename(const char* s) {
        delete[] name;
        name = new char[strlen(s) + 1];
        strcpy(name, s);
    }
    void print() const { cout << name << "\n"; }
};
 
int main() {
    Pet a("Tom");
    Pet b = a;
    b.rename("Jerry");
    a.print();
    b.print();
    return 0;
}

The output is Tom and then Jerry. The copy owns its own block, so renaming it does not affect the original.

  1. Write a class Team with only a string member and a vector<string> member, and no special functions. Copy it and show that the copy is independent. Explain why no special functions are needed.
Solution
#include <iostream>
#include <string>
#include <vector>
using namespace std;
 
class Team {
    string name;
    vector<string> members;
public:
    Team(string n) : name(n) {}
    void add(const string& who) { members.push_back(who); }
    void show() const {
        cout << name << ":";
        for (const string& m : members) cout << " " << m;
        cout << "\n";
    }
};
 
int main() {
    Team a("red");
    a.add("Asha");
    Team b = a;
    b.add("Ravi");
    a.show();
    b.show();
    return 0;
}

The output is red: Asha and then red: Asha Ravi. Copying Team b = a; copies the string and the vector, and both of those types copy their contents deeply. The rule of zero applies: the standard types handle memory, so the class needs no destructor or copy functions.

  1. Explain why a static method cannot read a non-static data member, and give two ways to fix a program that wants that read.
Solution

A static method belongs to the class and has no this pointer, so it has no object whose member it could read. Fix it either by making the member static, if the value belongs to the whole class, or by making the method an ordinary, non-static method that is called on an object.

  1. A class has a static int named next that starts at 100. Write Id objects that take next as their id and then increment it. Create three objects and print their ids. Expected: 100, 101, 102.
Solution
#include <iostream>
using namespace std;
 
class Id {
    static inline int next = 100;
    int value;
public:
    Id() : value(next++) {}
    int get() const { return value; }
};
 
int main() {
    Id a, b, c;
    cout << a.get() << "\n" << b.get() << "\n" << c.get() << "\n";
    return 0;
}

The constructor uses the old value of next and then increments it, so the ids are 100, 101 and 102.

  1. Explain why the copy assignment operator should allocate the new block before it frees the old one.
Solution

If the allocation fails and throws, the object has not been changed yet, so it still holds its old, valid data. If the old block were freed first, a failed allocation would leave the object pointing at freed memory, and any later use would be undefined behaviour.

  1. Write a Counter class with a static count of live objects and a copy constructor that increments it. Create one object, one copy, and print the count. Expected: 2.
Solution
#include <iostream>
using namespace std;
 
class Counter {
    static inline int live = 0;
public:
    Counter() { live++; }
    Counter(const Counter&) { live++; }
    ~Counter() { live--; }
    static int count() { return live; }
};
 
int main() {
    Counter a;
    Counter b = a;
    cout << Counter::count() << "\n";
    return 0;
}

The output is 2. The copy constructor counts the copy as a new live object.

  1. Explain in two sentences what a shallow copy is, and why it is a problem only when a class owns memory through a raw pointer.
Solution

A shallow copy copies the value of each member, so a pointer is copied as an address and the block it points to is shared. If the class owns that block, two objects will free it, which is a double free, but if the class owns no memory, the copy is harmless.

  1. Write a Stack class that stores its items in a vector<int> and has no special functions. Copy it, push onto the copy, and show that the original is unchanged. Explain which rule applies.
Solution
#include <iostream>
#include <vector>
using namespace std;
 
class Stack {
    vector<int> items;
public:
    void push(int x) { items.push_back(x); }
    int size() const { return static_cast<int>(items.size()); }
};
 
int main() {
    Stack a;
    a.push(1);
    Stack b = a;
    b.push(2);
    cout << a.size() << " " << b.size() << "\n";
    return 0;
}

The output is 1 2. The copy of the vector is deep, so the two stacks are independent. This is the rule of zero: the class owns no raw memory, so it needs none of the three special functions.

What you now know

  • A static data member is one value shared by all objects of a class. It needs a definition outside the class before C++17, or the inline form inside the class from C++17.
  • A static method has no object and no this, so it can use only static members and its parameters.
  • A shallow copy shares a block through a copied pointer, and two destructors then free it twice.
  • The rule of three says a class that owns memory needs a destructor, a copy constructor and a copy assignment operator.
  • The copy assignment operator guards against a = a, and it allocates the new block before it frees the old one.
  • The rule of zero says that a class built from std::vector and std::string needs none of the three functions.
  • Next, the module assignment brings together classes, constructors, inheritance, polymorphism, static members and copies in one program.
Practice this topic: coding problems for Object-Oriented Programming →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Polymorphism, virtual functions and abstraction