Classes and Objects


This article will use a CheckingAccount class as a running example.

Writing classes

When writing classes one has to consider the behaviors and data members (data) represented by the class. A class is like a blueprint and objects are like buildings constructed from the blueprint.

Class definitions

Here is the first cut of the CheckingAccount class:
checkingaccount.h:
#ifndef CHECKING_H
#define CHECKING_H
class CheckingAccount
{
    public:
        CheckingAccount(double balance);
        ~CheckingAccount();
        void deposit(double amount);
        bool withdraw(double amount);
        double getBalance() const;
    private:
        double mBalance;
} 
#endif
checkingaccount.cpp: Note that the :: operator is the scope resolution operator.
CheckingAccount::CheckingAccount(double balance) //constructor
{
    mBalance = balance;
}

void CheckingAccount::deposit(double amount)
{
    mBalance += amount;
}

bool CheckingAccount::withdraw(double amount)
{
    if((mBalance - amount) > 0)
    {
        mBalance -= amount;
        return true;
    }
    return false;
}

double getBalance() const
{
    return mBalance;
}
//Destructor not implemented as there is no dynamic memory allocated.

Access control

  • public: Any code can call a public member function or access a public data member.
  • protected: Only the class and its derived classes can access the member function or data member
  • private : Only the class can access the member function or data member.

In-Class Member Initializers

One can initialize member variables directly in the class definition like so:
class CheckingAccount
//....
private : 
    double mBalance = 0.0;

this pointer

Each member function of a class has an implicit pointer parameter called this which points to the current object. It can be used to disambiguate parameters from data members. It can also be used to pass a reference to the current object to a function that takes a reference or const reference to the object.

Objects on the stack

C++ allows objects to be allocated on the stack as well as the heap. Example:
CheckingAccount chkAccount(1000.00);
cout << "Balance is " << chkAccount.getBalance() << endl;

Objects on the heap/free store

Objects can be allocated on the heap using either raw pointers or one of the smart pointers. Example:
auto myChkAccount = std::make_unique<CheckingAccount>(10000.00); //smart pointer
cout << myChkAccount->getBalance() << endl;

CheckingAccount * chkaccount = new CheckingAccount(10000.00); //prefer smart pointer over this
cout << chkaccount->getBalance() << endl;
delete chkaccount;

Object Life Cycles

Object life cycle consists of creationdestruction, and assignment.

Creation

When an object is created, all the objects it embeds are also created. Example:
#include <string>

class Foo
{
    private: 
        std::string mAddress;
}

int main()
{
    Foo fooObj;
    return 0;
}
The string object is created when fooObj is created and is destructed when fooObj is destructed (in this case, goes out of scope).

Constructors

A constructor is a special member function used for initializing values for a class. A default constructor is one that doesn't take any parameters, or one where all data members are given default values.

Constructors on the Stack

CheckingAccount account(5000.00);

Constructors on the Heap

auto chkAccount = std::make_unique<CheckingAccount>(5000.00);
CheckingAccount anotherAccount = nullptr;
anotherAccount = new CheckingAccount(100.00);

When default constructors are needed

When creating arrays of objects, default constructors are needed, as there is no option to call any other constructor:
CheckingAccount accounts[5];
CheckingAccount* accounts = new CheckingAccount[5];
Example of default constructor is as follows:
CheckingAccount::CheckingAccount()
{
    mBalance = 0.0;
}
Note that when using the default constructor on the stack, one must omit the parentheses as shown below:
CheckingAccount chkAccount;
chkAccount.deposit(100.00);
If the programmer does not write the default constructor, the compiler generates one. However, if any constructor is programmed, then the compiler omits the constructor generation.

Explicitly deleted constructors

If you only have static methods, and don't want a constructor or have one generated by the compiler, do the following:
class CheckingAccount
{
    public: 
       CheckingAccount() = delete;
}

Constructor initializer

There is an alternative method to initialize data members in the constructor called the constructor initializer:
CheckingAccount:: CheckingAccount(double balance) : mBalance(balance)
{ 
}
When C++ creates an object, embedded objects' constructors themselves must be called. A constructor initializer (ctor-initializer) allows thoese constructors to be called because in the body of the constructor the values are modified but are not initialized.
If an embedded object has a default constructor, there is no need to initialize it in the constructor initializer. Otherwise, initialize the object in the initializer. Some types must be initialized in constructor initializer such as follows:
  • const data members: can only be created and assigned once.
  • References: can only exist when referring to a variable
  • Embedded objects with no default constructor
  • Base classes without default constructors
Note that constructor initalizers initialize data members in the order they appear in the class definition, and not the order they appear in the initializer list.

Copy Constructors

Copy constructors let you create an object that is an exact copy of another object. If programmer doesn't provide one, compiler generates one that initializes each data member from the corresponding one in the source object.
For embedded objects, their copy constructors are called. Example:
class CheckingAccount
{
    public:
       CheckingAccount(const CheckingAccount &src);
}
Implementation:
CheckingAccount::CheckingAccount(const CheckingAccount &src) : mBalance(src.mBalance)
{

}
Given data members n1,n2,nM, compiler generates a copy constructor that can be visualized as follows:
cname::cname(const cname& src)
: n1(src.n1), n2(src.n2),...,nM(src.nM) {}
So in many cases there is no need to explicitly specify a copy constructor.

When the Copy Ctor is called

Whenever an object is passed by value to function or method, the copy constructor is called. Example:
void foo(std::string name)
{
    cout << "Name is "<<name<<endl;
}

int main()
{
    string name = "John";
    foo(name); //copy constructor
}

When the copy constructor is called explicitly

Example:
CheckingAccount account(5000.00);
CheckingAccount accountCopy(account); 

Passing objects by Reference

Here are the guidelines:
  • Pass objects by const reference for performance (unless modifications are desired)
  • Pass primitives by value
  • Pass string_view by value since it is just a pointer and length
  • Do not pass references to objects on the stack. Return a copy.
To disallow passing an object by value, delete the copy constructor:
CheckingAccount(const CheckingAccount &src) = delete;

Initializer list constructors

initializerlist constructor is a constructor with std::initializer_list<T> as the first parameter, with no additional params or with additional params given default values.
class Sequence
{
    public :
        Sequence(initializer_list<int> params)
        { 
            for(const auto& value: params)
            {
                values.push_back(value); //note push_back takes const reference parameter and makes a copy of it internally
            }
        }
    private:
        vector<int> values;
}

Delegating constructors

Delegating constructors enable constructors to call other constructors from within the ctor-initializer (it must be the only member initializer).

Object Destruction

When an object is destroyed, the object's destructor method is called and if the destructor is implemented correctly the allocated memory is freed. If programmer doesn't create a destructor,
compiler creates one that does recursive memberwise destruction. Recall objects on stack are destroyed when they go out of scope. Objects on stack are destroyed in the reverse order of their construction.

Overloading assignment operator

Overloading the assignment operator is different from implementing the copy constructor as "copying" only occurs when the object is being initialized. This operator is also known as the copy assignment operator.
Example:
CheckingAccount& operator=(const CheckingAccount &rhs);
A reference to the object is returned to allow chaining assignments. Example of overloaded assignment operator:
CheckingAccount& CheckingAccount::operator=(const CheckingAccount& rhs)
{
    if(this == &rhs)
    {
        return *this; //have to do this as self assignment is allowed in C++
    }
    mBalance = rhs.mBalance;
    return *this;
}

Distinguishing Copy from Assignment

Examples of copy construction:
CheckingAccount acct1(10.0);
CheckingAccount acct2(acct1);

CheckingAccount acct3 = acct2;
Example of assignment:
acct3 = acct1;

Objects as Return values

For example if a std::string is returned from a function and assigned as follows, the copy constructor is called and then the assignment operator is called:
string s1;
s1 = getString();
In this case two copy constructors are called:
string s1 = getString(); //getString calls copy constructor and s1's copy constructor is called.
Compilers often do Return Value Optimizations (RVO) to eliminate copy constructor calls for return values.

Copy constructors and object data members

The compiler generated copy constructor calls the copy constructors of each of the embedded objects recursively. However, when the programmer implements, only using ctor-initializer ensures the copy constructor for the embedded object is called:
CheckingAccount::CheckingAccount(const CheckingAccount &src) : mBalance(src.mBalance) {}
If one instead does the following, one is using the assignment operator, because the compiler calls the default constructor by the time the body executes:
CheckingAccount::CheckingAccount(const CheckingAccount &src)
{
    mBalance = src.mBalance; //assignment operator
}

References:

Gregoire, M. (2018). Professional C++. Indiana, IN: John Wiley & Sons.

Comments

Popular posts from this blog

QTreeView and QTableView dynamic changes

C++ strings and string_view