C++ is an object-orientated language, and so heavily makes use of classes and objects. Objects are to classes as variables are to types; a class describes how something is structured and what it’s used for, and an object is some instance of that class. For example, we may want to use arithmetic that is more precise than what float or double (or long double!) can provide. Ideally we would use rational numbers (i.e. fractions), but C++ doesn’t have a rational number type so instead we must implement our own using a class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Convention is to capitalise class names
class Rational
{
    private:

    // Number is of the form a/b where a and b are integers
    // and b is strictly positive
    signed long long a;
    unsigned long long b;

    void reduce();

    public:

    // Create a rational out of the given numerator x and denominator y
    Rational(signed long long x, unsigned long long y)
    {
        // Set the member variables to the arguments
        a = x;
        b = y;

        // Reduce to lowest terms
        reduce();

        // No return statement
    }
    // Create a rational from the given float
    Rational(float x);

    // Convert the rational number into an integer by doing a / b
    signed long long toi();
};

A class definition begins with the class keyword followed by the class name, similar to a variable definition (though class is not a type). Everything in the following {} is a part of the class definition, which can include access specifiers as on lines 4 and 13, member variable and function declarations as on lines 8, 9, 11, and 13, and constructors as on lines 16 and 28. As you can see a constructor declaration looks a lot like a function declaration (and likewise for constructor definitions, which we’ve also used), and indeed they’re very similar except that constructors must have the same name as the class, do not have a return type (not even void), and are only called when an object of a class is created, as follows.

// Call the first constructor
Rational foo(3, 6);
// Call the second constructor
Rational bar(1.2f);
// Not valid because there's no constructor without any arguments
// (the () are implied)
Rational wrong;

Here we’ve created two objects foo and bar which are instances of the class Rational. We have to call a constructor when we declare objects (which are still a kind of variable), and which constructor to call is determined by the number and type of the arguments just as in normal function overloading. Note that attempting to declare the object wrong without using a constructor or by calling a constructor that doesn’t exist will result in a compilation error.

When foo is created the Rational(signed long long x, unsigned long long y) constructor is called with arguments 3 and 6. The constructor then sets the member variables to these arguments. Member variables can be thought of as only being in the scope of the object they belong to, so whilst both foo and bar know about their own a and b, they are completely separate from each other and belong to their respective objects. This wouldn’t make any sense for example.

Rational foo(3, 6);
a = 10; // a doesn't exist outside of foo

The same applies for member functions; trying to call the functions reduce or toi will result in an error, because they don’t exist outside of foo or bar. However, this is where access specifiers come in, as sometimes member functions or variables can be accessed from outside the object! Access specifiers define the member access of each member variable and function (and constructors too), which determines whether or not a member can be used outside of the object it belongs to. The private specifier removes all access from outside of the object, and the public specifier grants complete access. We’ve given a and b private to hide the internal implementation of the Rational, and also to stop the situation where the object may expect a/b to be in lowest terms, but it isn’t because its been incorrectly change from outside the class.

Note that the members are still specific to the object they belong to, it’s just that they can be used in functions which are not members of its class. To reflect this, members are accessed using a . after the object name.

// Call foo's toi() function and print the result (3 / 6 = 0.5 => 0 as int)
std::cout << foo.toi() << std::endl;
// This is invalid because a and b are private
std::cout << "foo = " << foo.a << " / " << foo.b << std::endl;
// And so is this
foo.a = 10;

Sometimes we want to restrict modification of member variables but still want them to be accessible. In this case we define member getter functions which return the value of a member variable. This way we can access the variable but cannot modify it from outside of the class. There’s no special syntax for these, they’re just normal public functions with no arguments.

signed long long get_numerator()
{
    return a;
}

unsigned long long get_denominator() 
{
    return b;
}

Destructors

It’s worth noting that there is a counterpart to the constructor, the destructor, which is called whenever an object goes out of its scope. Since they’re called automatically by the compiler they can’t take any arguments, but must take the name of the class as a constructor does; the difference is marked by a ~ in front of the name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>

class Foo
{
    public:

    char id; // Keep track of which object this is

    // Constructor
    Foo(char pId)
    {
        id = pId;
        std::cout << "Hello from " << id << "\n";
    }
    // Destructor
    ~Foo()
    {
        std::cout << "Bye from " << id << "\n";
    }
};

int main()
{
    Foo a('a');
    // Enter a new scope
    if(true)
    {
        Foo b('b');
    } // b's constructor will be called as b is out of scope
    Foo c('c');

    return 0; // Now a and c go out of scope
}

The output of this program is

Hello from a
Hello from b
Bye from b
Hello from c
Bye from c
Bye from a

If you don’t define a destructor then the compiler will create one automatically that does nothing. Right now they aren’t particularly useful because most objects won’t need to do anything when they go out of scope, but they’ll become necessary later when we deal with dynamic memory and there will be things for them to do.

Manipulating Objects

An object is still a variable, and so if you can do something with a typed variable you can probably do it with an object to. For example, as well as defining objects using constructors you can also use the assignment operator =, you can create arrays of objects, you can write functions that return objects or take objects as arguments, and you can create references to objects.

It’s also possible to call the constructor of a class directly to create an r-value that can be assigned to a new variable using =.

// These are equivalent, though the first is faster because there's no copying involved
Rational r(10, 3);
Rational r = Rational(10, 3);

Of course the first is much shorter, but it requires the variable to have a name, which is not the case when creating arrays. Instead the second form is used.

Rational fractions[] = { Rational(1, 2), Rational(6, 3), Rational(1, 4) };
Rational& f = fractions[1];

// When using references the . operator is still used
std::cout << f.get_numerator() << " / " << f.get_denominator() << std::endl;

Defining Functions Outside of a Class

Member functions are still functions are heart, and so can be declared and defined in separate locations; so long as the declaration occurs before any usage of the member function, you’re fine. The declaration is required to be in the class, but the definition is not, and can even be placed in a separate file. Common convention is to place the class definition in a header file with the same name as the class and place all the constructor and function definitions in a similarly named source file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* rational.hpp */
#ifndef RATIONAL_HPP
#define RATIONAL_HPP

class Rational
{
    private:

    // Number is of the form a/b where a and b are integers
    // and b is strictly positive
    signed long long a;
    unsigned long long b;

    void reduce();

    public:

    // Create a rational out of the given numerator x and denominator y
    Rational(signed long long x, unsigned long long y);
    // Create a rational from the given float
    Rational(float x);

    // Convert the rational number into an integer by doing a / b
    signed long long toi();
};
#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* rational.cpp */
#include "rational.hpp"

Rational::Rational(signed long long x, unsigned long long y)
{
    // Set the member variables to the arguments
    a = x;
    b = y;

    // Reduce to lowest terms
    reduce();
}

void Rational::reduce()
{
    /* Find the greatest common divisor of a and b
       using the Euclidean algorithm and then divide
       both by it to reduce the fraction */
}

When definining functions (and constructors) outside of classes like this you have to specify which class the function belongs to (two classes might have a function called reduce) by placing the class name and a :: before the name of the function. Note that you don’t use a . like when accessing a member function! (This is to make the syntax consistent with namespaces, one of which—std—you’ve already seen.)

Operator Overloading

Whilst both foo and bar represent rational numbers, foo * bar will cause a compilation error, because C++ doesn’t know what * means when dealing with objects and not numeric types. We could get around this by creating a multiply function which returned a new Rational equal to the product of the existing ones, but it wouldn’t make sense for it to be a member function because we aren’t modifying anything (and foo.multiply(foo, bar) is pretty cumbersome) and sowe would have to come up with a unique name such as rational_multiply(foo, bar) so as to not conflict with any other multiply functions that might be defined somewhere. (This would be the C approach.)

/* rational.hpp */
// Faster to pass references, but this causes problems with r-values and l-values
// without additional optimisation which we can't cover yet
Rational rational_multiply(Rational lhs, Rational rhs);
Rational rational_multiply(Rational lhs, Rational rhs)
{
    // Create the new rational number equal to lhs * rhs
    // (Function arguments can be split across multiple lines)
    Rational product(lhs.get_numerator() * rhs.get_numerator(),
        lhs.get_denominator() * rhs.get_denominator());

    return product;
}

Luckily though, we can just define * to mean exactly what we want! (Note that this is still not a member function.)

/* rational.hpp */
// Define the * operator which takes two rationals passed by reference
// Same code as rational_multiply
Rational operator*(Rational lhs, Rational rhs);

Every operator we have seen so far can be overloaded, but that doesn’t always make it a good idea! If you decide to overload operators then their overloaded meaning should be consistent with the original (don’t overload + to mean division for example), and if you overload an operator you should overload its corresponding compound assigment operator (e.g. * and *=) too. A common idiom is to overload the compound operator first and specify the standard operator in terms of it, but that requires some syntax we haven’t quite covered yet. (Pointers, to be specific.)

Exercise

Build upon the vector functions from the last exercise by creating a Vec3 class which represents a vector with 3 float components. You should add a mag member function to calculate the length of the vector, and overload the + and - operators to handle vector addition and subtraction. Note these operations are defined component-wise, so . Additionally you should define the * operator for scalar multiplication, defined by .

It’s up to you what to make public and what to make private, but the following code should work with your class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include "vector.hpp"

int main()
{
    Vec3 a(-1.0f, 2.0f, 3.0f);
    Vec3 b(2.0f, 0.0f, -1.0f);

    std::cout << "|a| = " << a.mag() << std::endl;
    std::cout << "|b| = " << b.mag() << std::endl;

    Vec3 c = 3.0f * (a + b);

    std::cout << "|c| = " << c.mag() << std::endl;

    return 0;
}
Solution
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* vector.hpp */
#ifndef VECTOR_HPP
#define VECTOR_HPP

class Vec3
{
    public:

    float x, y, z;

    Vec3(float aX, float aY, float aZ)
    {
        x = aX;
        y = aY;
        z = aZ;
    }

    float mag();
};

// Scalar multiplication of float * Vec3
Vec3 operator*(float lhs, Vec3 rhs);

// Addition and subtraction of Vec3 and Vec3
Vec3 operator+(Vec3 lhs, Vec3 rhs);
Vec3 operator-(Vec3 lhs, Vec3 rhs);

// Calculate the square root of a positive floating point number
float sqrt(float a);

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* vector.cpp */
#include "vector.hpp"

// Same function as before
float sqrt(float a)
{
    // Try a / 2 as the initial guess
    float x = a / 2.0f;

    // Newton Rhapson will fail if x is close to zero
    // because we divide by x, but sqrt(x) = 0 in that case
    if(-0.00001 < x && x < 0.00001) return 0.0f;

    // Perform 10 iterations of Newton Rhapson
    // x <= (x + a/x) / 2
    for(int i = 0; i < 10; ++i)
    {
        x += a / x;
        x *= 0.5f;
    }

    return x;
}

Vec3 operator*(float lhs, Vec3 rhs)
{
    // Could create a new value and return it,
    // or since we aren't returning a reference we
    // can return the new object directly
    return Vec3(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);
}

Vec3 operator+(Vec3 lhs, Vec3 rhs)
{
    return Vec3(lhs.x+rhs.x, lhs.y+rhs.y, lhs.z+rhs.z);
}

Vec3 operator-(Vec3 lhs, Vec3 rhs)
{
    return Vec3(lhs.x-rhs.x, lhs.y-rhs.y, lhs.z-rhs.z);
}

float Vec3::mag()
{
    // Only dealing with three components so we can simplify this
    return sqrt(x*x + y*y + z*z);
}