Chapter 9 - Notes
Classes
Classes are abstract objects that share common:
attributes
behaviour (methods)
Declaring a class
struct Human {
// Attributes, or member variables
std::string name;
std::string gender;
int netWorth;
// Member functions or member methods
void introduceSelf();
void addNetWorth(int amount);
};Classes provide a way to create our own data types, which allows us to encapsulate data within attributes which can change the behaviour of the corresponding objects.
Encapsulation is the ability to logically group data and functions together.
Objects and Instances
Classes are simply abstract objects, like Human, Country, or Sport. Declaring a class has no effect on the execution of a program.
These classes are simply blueprints for which we can construct one or more instances from. Using the Sport class as an example, we may create objects referring to the sports tennis, running, or golf.
Creating an instance
// Static declaration
int number;
Human some_human;
// Dynamic declaration
int number_ptr = new int(5);
delete number_ptr;
Human* human_ptr = new Human();
delete human_ptr;Accessing members
Consider the following class definition:
struct Human {
std::string name;
int age;
void introduceSelf() {
std::cout << "Hi, my name is " << name
<< " and I am " << age
<< " years old"
<< std::endl;
}
};Using the Dot Operator
Members of an instance may be accessed using the dot operator (.):
Human some_human;
// Modifying member attributes
some_human.name = "Andrew";
some_human.age = 20;
// Accessing member attributes
std::cout << some_human.name << std::endl;
std::cout << some_human.age << std::endl;
// Calling member functions
some_human.introduceSelf();Using the Pointer Operator
Members of an instance pointer may be accessed using the pointer operator (->):
Human* human_ptr = new Human;
// Modifying member attributes
(*human_ptr).name = "Andrew";
(*human_ptr).age = 20;
// Accessing member attributes
std::cout << human_ptr->name << std::endl;
std::cout << human_ptr->age << std::endl;
// Calling member functions
human_ptr->introduceSelf();
delete human_ptr;Keywords 'public' and 'private'
Class members, both attributes and methods, can be classified as private or public.
public: members that can be accessed and used by anyone in possession of an instance of the classprivate: members that can only be used within the class
class Human {
private:
// Everything within this block is private
int age;
string name;
// Everything within this block is public
public:
int getAge() {
return age;
}
void setAge(int newAge) {
if (newAge > 0) {
age = newAge;
}
}
};Human adam;
std::cout << adam.age << std::endl; // compile error
std::cout << adam.getAge() << std::endl; // OKThis prevents anyone from modifying the name and age attributes of an instance of Human.
Classes have private members by default.
Structs have public members by default.
Constructors
A constructor is the function invoked during the instantiation of a class.
Declaring and Implementing a Constructor
class Human {
public:
Human(); // Declaration
};class Human {
public:
Human() {} // Implementation
};This is admittedly not very useful, here's a useful version.
class Human {
public:
std::string name;
int age;
Human(std::string some_name, int some_age) {
name = some_name;
age = some_age;
}
};Which allows us to construct instances in the following manner:
Human some_human = Human("Andrew", 20);Overloading Constructors
Much like regular functions, constructors can also be overloaded
class Human {
private:
std::string name;
int age;
public:
Human() {
name = "unknown";
age = -1;
}
Human(int some_age) {
name = "unknown";
age = some_age;
}
Human(std::string some_name, int some_age) {
name = some_name;
age = some_age;
}
};Which allows us to construct instances in the following ways:
Human zero; // Constructor 1
Human first = Human(); // Constructor 1
Human second = Human(20); // Constructor 2
Human third = Human("Andrew", 20); // Constructor 3 Constructor with Default Parameter Values
Much like regular functions, constructor parameters can also be provided with a default value, as an alternative to overloading.
class Human {
private:
std::string name;
int age;
public:
Human(std::string some_name = "unknown", int some_age = 0) {
name = some_name;
age = some_age;
}
};Human alice = Human(); // Uses default name "unknown" and default age 0
Human bob = Human("Bob"); // Uses default age 0
Human charlie = Human("Charlie", 20); // Doesn't use any defaultsConstructor with Initialisation Lists
class Human {
private:
int age;
string name;
public:
Human(string some_name = "unknown", int humansAge = 0)
: name(some_name), age(some_age) {}
};This is the same as the previous example, but will be useful for invoking base class constructors, which we will learn about later.
Destructors
Destructors are functions that are called when an instance is destroyed. This will occur in two cases:
When an statically created object goes out of scope
When
deleteis called on a dynamically allocated object
Declaring and Implementing a Destructor
The syntax for a destructor is very similar to that of a constructor, except prefixed with a tilde (~):
struct Human {
~Human(); // Declaration
}struct Human {
~Human() {}; // Implementation
}When to use a destructor
Since the destructor is invoked when an object of a class is destroyed, it is the ideal place to reset any variables, or release any memory dynamically allocated during the lifetime of the object.
struct Human {
int* age_ptr;
Human() {
// Each `Human` object dynamically allocates an integer
age_ptr = new int(0);
}
~Human() {
// Which should be cleaned up in the destructor
delete age_ptr;
}
}Copy Constructors
Arguments passed into a function like the below are copied:
double foo(int a, int b); Arguments sent as parameters a and b are copied when foo() is invoked. This applies to objects or instances of classes as well.
Shallow copying and associated problems
Classes such as Human in the previous example contain a member variable age_ptr that points to dynamically allocated memory. When an instance of this class is copied, the pointer member is copied, but not the underlying memory that it references, resulting in two objects pointing to the same dynamically allocated integer in memory.
When the first object is destructed, delete deallocates the memory in the destructor, therefore invalidating the pointer held by the original object. This will cause a double-free error when the original object is later destructed.
Such copies are shallow and threaten the stability of the program.
Ensuring deep copies with the copy constructor
Copy constructors are special constructors that are invoked every time an object of the class is copied.
struct Human {
// ...
Human(const Human& original); // copy constructor
}The copy constructor takes an object of the same class, by reference, as a parameter. This parameter will be the source object that you will be copying from.
You would use the reference to this object to ensure a deep copy of all necessary in the source.
struct Human {
int* age_ptr;
Human() {
age_ptr = new int(5);
}
Human(const Human& original) {
if (original.age_ptr != nullptr) {
age_ptr = new int; // Create a new int ourselves
*age_ptr = *(original.age_ptr); // Copy the value
}
}
}Using const in the copy constructor ensures that the copy constructor does not modify the source object that is being referred to.
Ensuring deep copies with the copy assignment constructor
The copy constructor ensures deep copies in the case of pass-by-copy function calls, but does not fix copies via assignment:
Human first;
Human second;
second = first; // `first` is copied into `second`!
// double-free happens when the program exits Therefore, we also need to implement the copy assignment constructor:
struct Human {
int* age_ptr;
Human() {
age_ptr = new int(5);
}
Human(const Human& original) {
if (original.age_ptr != nullptr) {
age_ptr = new int; // Create a new int ourselves
*age_ptr = *(original.age_ptr); // Copy the value
}
}
Human& operator=(const Human& original) {
if (original.age_ptr != nullptr) {
age_ptr = new int; // Create a new int ourselves
*age_ptr = *(original.age_ptr); // Copy the value
}
return *this;
}
}Move Constructors
Static Class Members and Functions
When the keyword static is used on a class' data member, it ensures that the member is shared across all instances.
When static is used on a local variable declared within the scope of a function, it ensures that the variable retains its value between function calls.
When static is used on a member function, the method is shared across all instances of the class. Additionally static class methods do not require an instance to be invoked.
Different Uses of Constructors and Destructors
Singleton Classes
Singletons are classes that only permit one instance to exist, the creation of additional instances are prohibited.
class Singleton {
private:
Singleton() {}; // private default constructor
Singleton(const Singleton&); // private copy constructor
const Singleton& operator=(const Singleton&); // private copy assignment operator
std::string name;
public:
static Singleton& GetInstance()
{
static Singleton instance;
return instance;
}
std::string GetName()
{
return name;
}
void SetName(string name)
{
this.name = name;
}
}No-Copy Classes
No copy classes are classes that prohibit being copied:
through a pass-by-copy function parameter
through an assignment operator
This is done by making both the copy constructor and copy assignment operator private.
class NoCopy {
private:
NoCopy(const NoCopy&); // private copy constructor
NoCopy& operator= (const NoCopy&); // private copy assignment operator
}Heap-Only Classes
Heap only classes are classes that can only be instantiated dynamically (on the heap) and never statically (on the stack).
This is done by declaring a private destructor, and exposing the destructor through a static member function.
class VeryLargeStructure {
private:
~VeryLargeStructure(); // private destructor
public:
static void Destroy(VeryLargeStructure* instance) {
delete instance; // can be invoked here since it's within the class
}
}This is not a very common pattern, and is a remnant of older days where stack memory is limited, and expensive to unwind, compared to heap memory.
`this` Pointer
this is a reserved keyword applicable anywhere within the scope of a class, and contains the address of the object itself.
This does not have many applications, other than being able to disambiguate between local variables and member methods with the same name:
struct Human {
std::string name;
Human(std::string name) {
this->name = name; // This is ok!
}
};Note that this is not available for static class methods, since static methods do not require an instance to invoke.
Size of a class
The operator sizeof() is used to determine the amount of memory, in bytes, used by a specific type.
This operator can be used for classes too, and report the sum of bytes consumed by all the attributes contained within the class definition.
Word Padding
Let's use a worked example for this.
Friend Classes
Member functions and attributes classified as private are not accessible from functions outside of the class.
This rule can be waived for classes or functions declared as friend classes or functions using the keyword friend.
class Human {
std::string name;
int age;
friend void Describe(const Human& human); // Friend function
public:
Human(std::string name, int age) : name(name), age(age) {};
}
void Describe(const Human& human) {
std::cout << human.name << " is " << human.age << " years old." << std::endl;
}Friend functions are defined using the keyword friend followed by the signature of the function that is to be friended.
Friend classes are defined using the keyword friend class followed by the name of the class that is to be friended.
Unions
A union is a special class type where only one of its data members is active at a time. Thus, a union can accommodate multiple types of data members, but is actually only one of them.
Declaring a Union
union DayInfoValue {
char day;
bool isRaining;
float temperature;
}When to use a union
A union is often used as a member of a struct or class to model a complex data type.
The benefits of using a union is that it saves space as opposed to storing each possible data as a separate attribute.
enum DayInfoType {
DAY,
RAIN,
TEMPERATURE,
}
struct DayInfo {
DayInfoType type;
DayInfoValue value;
void display() {
switch (type):
case DayInfoType::DAY:
std::cout << "The day is " << value.day << std::endl;
break;
case DayInfoType::RAIN:
std::cout << "Today " << (value.isRaining ? "is" : "is not") << " raining" << std::endl;
break;
case DayInfoType::TEMPERATURE:
std::cout << "The temperature today is " << value.temperature << " celcius" << std::endl;
break;
}
}Aggregate Initialisation
Aggregate initialisation is initialisation with curly braces.
// With value types
int a {3};
double b {5};
float b {a};
// With array types
int nums[] = {5, 6, 10};
char word[6] = {'h', 'e', 'l', 'l', 'o', '\0'};Aggregate initialisation can be applied to any aggregate types.
Classes, structs, and unions are aggregates too.
There are some restrictions imposed by the C++ standard on the specification of structs or classes that can be aggregates. But a safe definition:
Classes and structs that comprise of only public and non-static data members
Contains no private or protected data members
Contain no virtual member functions
Feature no or only public inheritance
No user-defined constructors
can be aggregates.
The following are valid aggregates:
struct Foo {
int integer;
double decimal;
}
Foo myFoo {10, 10.2};struct Bar {
int num;
char word[6];
int numbers[5];
}
Bar myBar {42, {'h', 'e', 'l', 'l', 'o', '\0'}, {2, 4, 6, 8, 10}};Forward Declaring Classes
So far we've only defined classes. If we only wanted to declare a class for forward declaration purposes, we can do that.
class Foo {
int x;
Foo(int x); // Delcaring a constructor (not implementing!)
void bar(); // Declaring a member function (not implementing!)
};
Foo::Foo(int x): x(x) {}; // Defining
void Foo::bar() { // Defining
std::cout << x << std::endl;
}This is similar to declaring a function using its prototype, then defining it later.
int sum(int x, int y); // Declaring
// ...
int sum(int x, int y) { // Implementing / defining
return x + y;
}Last updated
Was this helpful?