Skip to main content

Introduction to Classes

1. Defining a class

#include <iostream>

class Date // we changed struct to class
{
public: // and added this line, which is called an access specifier
int m_day{}; // and added "m_" prefixes to each of the member names
int m_month{};
int m_year{};
};

void printDate(const Date& date)
{
std::cout << date.m_day << '/' << date.m_month << '/' << date.m_year;
}

int main()
{
Date date{ 4, 10, 21 };
printDate(date);

return 0;
}

In programming, we represent properties with variables, and actions with functions. It sure would be nice if there were some way to define our properties and actions together, as a single package. (Intuitaion of Class design)

2. Public and private members and access specifiers

The members of a struct are public by default

Public members are members of a calss type that do not have any restrictions on how they can be accessed. Public members can be accessed by anyone(as long as they are in the scope).

By default, all members of a struct are public members.

// Member function version
#include <iostream>

struct Date
{
int year {};
int month {};
int day {};

void print() // defines a member function named print
{
std::cout << year << '/' << month << '/' << day;
}
};

int main()
{
Date today { 2020, 10, 14 }; // aggregate initialize our struct

today.day = 16; // member variables accessed using member selection operator (.)
today.print(); // member functions also accessed using member selection operator (.)

return 0;
}

The members of a class are private by default

Private members are members of a class type that can be only accessed by other members of the same class.

#include <iostream>

class Date // now a class instead of a struct
{
// class members are private by default, can only be accessed by other members
int m_year {}; // private by default
int m_month {}; // private by default
int m_day {}; // private by default

void print() const // private by default
{
// private members can be accessed in member functions
std::cout << m_year << '/' << m_month << '/' << m_day;
}
};

int main()
{
Date today { 2020, 10, 14 }; // compile error: can no longer use aggregate initialization

// private members can not be accessed by the public
today.m_day = 16; // compile error: the m_day member is private
today.print(); // compile error: the print() member function is private

return 0;
}
tip

Best practice

Classes should generally make member variables private (or protected), and member functions public.

Structs should generally avoid using access specifiers (all members will default to public).

3. Access functions

An access function is a trivial public member function whose job is to retrieve or change the value of a private member variable.

Access functions come in two flavors: getters (also sometimes called accessors) and setters (also sometimes called mutators).

  • Getters are public member functions that return the value of a private member variable.
  • Setters are public member functions that set the value of a private member variable.

Getters are usually made const, so they can be called on both const and non-const objects. Setters should be non-const, so they can modify the data members.

#include <iostream>

class Date
{
private:
int m_year { 2020 };
int m_month { 10 };
int m_day { 14 };

public:
void print()
{
std::cout << m_year << '/' << m_month << '/' << m_day << '\n';
}

int getYear() const { return m_year; } // getter for year
void setYear(int year) { m_year = year; } // setter for year

int getMonth() const { return m_month; } // getter for month
void setMonth(int month) { m_month = month; } // setter for month

int getDay() const { return m_day; } // getter for day
void setDay(int day) { m_day = day; } // setter for day
};

int main()
{
Date d{};
d.setYear(2021);
std::cout << "The year is: " << d.getYear() << '\n';

return 0;
}

4. Member functions returning references to data members

Returning data members by value can be expensive

Returning data members by lvalue reference

#include <iostream>
#include <string>

class Employee
{
std::string m_name{};

public:
void setName(std::string_view name) { m_name = name; }
const std::string& getName() const { return m_name; } // getter returns by const reference
};

int main()
{
Employee joe{}; // joe exists until end of function
joe.setName("Joe");

std::cout << joe.getName(); // returns joe.m_name by reference

return 0;
}

Rvalue implicit objects and return by reference

#include <iostream>
#include <string>
#include <string_view>

class Employee
{
std::string m_name{};

public:
void setName(std::string_view name) { m_name = name; }
const std::string& getName() const { return m_name; } // getter returns by const reference
};

// createEmployee() returns an Employee by value (which means the returned value is an rvalue)
Employee createEmployee(std::string_view name)
{
Employee e;
e.setName(name);
return e;
}

int main()
{
// Case 1: okay: use returned reference to member of rvalue class object in same expression
std::cout << createEmployee("Frank").getName();

// Case 2: bad: save returned reference to member of rvalue class object for use later
const std::string& ref { createEmployee("Garbo").getName() }; // reference becomes dangling when return value of createEmployee() is destroyed
std::cout << ref; // undefined behavior

// Case 3: okay: copy referenced value to local variable for use later
std::string val { createEmployee("Hans").getName() }; // makes copy of referenced member
std::cout << val; // okay: val is independent of referenced member

return 0;
}
tip

Best Practise Prefer using the member initializer list to initialize your members over assigning values in the body of the constructor.

5. Default constructors and default arguments

If a class type has a default constructor, both value initialzation and default initialzation will call the default constructor.

Foo foo{}; // value initialization, calls Foo() default constructor
Foo foo2; // default initialization, calls Foo() default constructor
tip

Best Practise Prefer value initialization over default initialization for all class types.

Constructor with default arguments

As with all functions, the rightmost parameters of constructors can have default arguments.

#include <iostream>

class Foo
{
private:
int m_x { };
int m_y { };

public:
Foo(int x=0, int y=0) // has default arguments
: m_x { x }
, m_y { y }
{
std::cout << "Foo(" << m_x << ", " << m_y << ") constructed\n";
}
};

int main()
{
Foo foo1{}; // calls Foo(int, int) constructor using default arguments
Foo foo2{6, 7}; // calls Foo(int, int) constructor

return 0;
}

This prints:

Foo(0, 0) constructed
Foo(6, 7) constructed

6. Introduction to destructors

Destructor is a special member function that is called when an object is destroyed.

Destructor naming

Like constructors, destructors have specific naming rules:

  1. The destructor must have the same name as the class, preceded by a tilde (~).
  2. The destructor can not take arguments.
  3. The destructor has no return type.

A class can only have a single destructor.

#include <iostream>

class Simple
{
private:
int m_id {};

public:
Simple(int id)
: m_id { id }
{
std::cout << "Constructing Simple " << m_id << '\n';
}

~Simple() // here's our destructor
{
std::cout << "Destructing Simple " << m_id << '\n';
}

int getID() const { return m_id; }
};

int main()
{
// Allocate a Simple
Simple simple1{ 1 };
{
Simple simple2{ 2 };
} // simple2 dies here

return 0;
} // simple1 dies here

This program produces the following result:

Constructing Simple 1
Constructing Simple 2
Destructing Simple 2
Destructing Simple 1