Any (non-reference) variable used in a C++ program is stored somewhere in the computer’s memory when the program is being run, at some numerical memory address. Since this location is unique (we can’t have two different variables in the same location) we can refer to a variable by both its name, and its address. Because addresses are integer values, unlike variable names, we can manipulate them in powerful ways.

To find the address of a variable we use the & operator placed in front of the variable name. This gives us a new value with the pointer type, or specifically the pointer to X type, where X is whatever the type of the variable we are taking the address of; taking the address of an int gives an int pointer, taking the address of a Foo gives a Foo pointer, and so on. To denote this new type, we place a * after the type name, such as int* and Foo*.

int foo = 10;
// Print the address of foo in memory. This will differ
// every time the program is run
std::cout << &foo << std::endl;

// This is not allowed because &foo is not of type int
int ptr = &foo;

// Instead we have to create a pointer to an int. nullptr is a special
// pointer constant that works for all pointer types and is equivalent to 0
int* ptr = nullptr;

// Now the value of ptr is the address of foo
ptr = &foo;

A handy feature of pointers is that because they refer to an address, they can be used in a manner similar to references to create aliases for existing variables, which can then be passed to functions. For example, we can write an add function that increases the value of a variable by passing to it that variable’s address in the form of a pointer. To modify the value of the variable pointed to by the pointer (that is, the value at the address stored in the pointer) we have to dereference the pointer using the * operator placed in front of the pointer variable. If we were to modify the pointer’s value then we would be changing which address it stored, not which value is at the address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

// Add b to a and store in a. First argument is modified so must
// be a pointer, if we used int a then we would just modify the
// value of a inside the function and not outside it
void add(int* a, int b)
{
    // Dereference a and increase that value by b
    // A dereferenced pointer is a valid l-value and * essentially
    // converts a pointer into its variable form
    *a += b;
}

int main()
{
    int var = 10;
    // We have to take the address of var because var is not a pointer
    add(&var, 4);

    if (var == 14) std::cout << "Hooray for pointers!\n";

    return 0;
}

Arrays and Pointers

This shouldn’t be particularly exciting to you though, since we did all this with references a while ago and those didn’t require a bunch of & and * operators scattered all over the place. However, unlike references—which might not actually exist in memory (so you can’t take pointers to them!)—pointers hold actual numerical values and so can be manipulated as such. This isn’t useful in and of itself, but by knowing that the values in an array occupy a contiguous block of memory—where each value is directly after the next—we can use pointers in conjunction with arrays.

// Assume we don't know the contents of this array, but
// we do know there's a 9 there. Which element
// comes directly before the 9?
int array[8] = { 1, 3, 9, 2, 6, 5, 4, 10 };

// Using a for loop we would do this
int i = 0;
for(i = 0; i < 8; ++i)
{
    if(array[i] == 9) break;
}
std::cout << "Value before is " << array[i-1] << "\n";

// And by using pointers
int* ptr = &array[0];
while(*ptr != 9)
{
    ++ptr; // You could put this in the condition instead (*ptr++ != 9)
}
std::cout << "Value before is " << *--ptr << "\n";

In the pointer example we create a new pointer that points to the first position in the array, then if the value of the pointer is not 9 we increment it so that it points to the next address and loop. Since arrays are contiguous blocks of memory the next address will be the address of the next position in the array. We then decrement the pointer to move it to the address of the preceeding element and then dereference it to find its value.

One possible flaw of the pointer method is that whilst it is readily apparent how to obtain the fifth value in the array using indices (array[4]) it’s much more labourious if you have to create a new pointer pointer to the first array position and then increment it 4 times. Pointers don’t just support incrementing though, they support arbitrary addition and subtraction and so the following are equivalent.

int array[8] = { 2, 3, 5, 7, 11, 13, 17, 19 };

// These do the same thing
int foo = array[4];
int bar = *(&array[0]+4);

In fact, this is how the compiler internally represents arrays, by using pointers and pointer arithmetic! The [] operator is just a convenient shorthand for finding the values of elements in an array. What’s more, because arrays are implemented as pointers, if you don’t apply the [] operator to an array you obtain a pointer to its first element.

// This is the same too
int baz = *(array+4);

This doesn’t mean that the two are entirely interchangeable, when declaring arrays you still have to use [] instead of *.

Dynamic Memory

So far we have known at compile time exactly how many variables we will need to use and we can declare them appropriately. This means that if we write a program that reads a selection of data from the user, we would have to constrain the maximum amount of data they can input; arrays only have a fixed number of elements. Using pointers we can remove this limitation by dynamically allocating memory, giving the program access to a different amount of memory to what it had at compile-time. By replacing our arrays with dynamically allocated memory we can handle an arbitrary amount of data; every time the user inputs some data we can just ask the computer to give our program some more space to store the value in. We can do this via the new[] and delete[] operators.

// This pointer will point to the user's data
int* data = nullptr;

// Ask the user how much data they will enter
unsigned long n = 0;
std::cin >> n;

// Ask the computer to allocate enough memory for us to store that
// data, and then move the data pointer to the beginning of that memory
data = new int[n];

// Now we can read the data as before, pretending that data is of type
// int data[n]
for(int i = 0; i < n; ++i)
{
    std::cin >> data[i];
}
// Once we're done we tell the computer we don't need the memory any more
// and it can be used by other programs
delete[] data;

The new[] operator requires a type (or class) and the number of values of that type you want to allocate memory for. It then allocates the memory and evaluates to a pointer with value equal to the address of the first element in the new array. delete[] doesn’t require any parameters other than the pointer whose memory you want to deallocate (give back to the computer), but that pointer must be one which was already allocated using new[]. Once a pointer is deallocated or freed the data that it pointed to is lost, so don’t delete[] until you’re completely done. Make sure you do delete[] though, if you don’t deallocate a pointer then the memory will not be useable later in the program. (The operating system will automatically deallocate any allocated memory that you didn’t deallocate when the program ends, but don’t rely on this and make sure you always call delete[]!) For example, this code will break if left long enough.

int* ptr = nullptr;

while(true)
{
    ptr = new int[100];
}

Every loop iteration we’re reassigning the pointer to a different block of memory that the computer allocates, but because we aren’t deallocating the memory the computer doesn’t know that we aren’t using it anymore. The program will therefore use up more and more memory until your computer runs out of memory to allocate, in which case it will crash! Always make sure to deallocate your memory, and remember that even if you go out of scope of a pointer variable, the memory remains in use.

new[] and delete[] also have two counterparts—new and delete—which are used for allocating and deallocating memory for a single variable instead of an array. Their usage is the same but with the [] removed, and they are not compatible with each other; memory allocated with new must be deallocated with delete, and new[] with delete[].

// Allocate memory for a new int
int* int_ptr = new int;
// Allocate memory for a new double and initialize it
double* double_ptr = new double(10.5);
// Allocate memory for a new Vec3 and call its constructor
Vec3<double>* vec_ptr = new Vec3<double>(1.1, 2.2, 3.0);

// Deallocate the memory
delete int_ptr;
delete double_ptr;
delete vec_ptr;

Pointers and Classes

If you tried accessing the members of vec_ptr in the last example you’ll have run into some problems, namely that the . operator no longer works. Instead, there is a separate pointer member access operator, ->. It works in exactly the same way as the ., but is used whenever the variable you are applying it to is a pointer to an object.

Vec3<float>* vec = new Vec3<float>(1.0f, 2.0f, 3.0f);

std::cout << "(" << vec->x << ", " << vec->y << ", " << vec->z << ")\n";

delete vec;

Note that the use of -> requires that the object is a pointer, not the member

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
#include <iostream>

class A
{
public:
    int* ptr;
    A()
    {
        ptr = nullptr;
    }
    // Destructor can be used to deallocate memory
    ~A()
    {
        if (ptr != nullptr)
            delete ptr;
    }
};

class B
{
public:
    A a;
    B()
    {
        a.ptr = new int(10);
    }
};

int main()
{
    B* b = new B;
    // b is a pointer to B so we use ->
    // a is not a pointer so we use .
    // ptr is a pointer to int so we have to take the value using *
    int a_value = *(b->a.ptr);

    std::cout << a_value << "\n";

    return 0;
}

Here we’ve made use of a destructor in order to deallocate the memory that was allocated during the life of any instances of A. Because ~A is called whenever the object goes out of scope, it ensures that the memory is deallocated and returned to the operating system. We’ve also checked that ptr actually has a value before deallocating it, as calling delete on nullptr or any value not obtained from a new will cause the program to error. (This method is not entirely robust, as it assumes that ptr was allocated using new and isn’t a pointer to an existing variable instead.)

All classes also implement a special pointer, this, which points to the object using it. So far we have evaded the problem of duplicating variable names in constructors by adding p to the constructor arguments (another common practice is adding m_ to the members), but the this pointer can also be used.

class Foo
{
public:
    int a;
    double b;

    Foo(int a, double b)
    {
        this->a = a;
        this->b = b;
    }
};

It is important to remember that dynamically allocated memory is not owned by any particular object, even if it was allocated by a member function. This means that you should be very careful when intialising objects which use dynamic memory. Consider for example

#include <iostream>

class Foo
{
public:
    int* a;
    Foo(int a)
    {
        this->a = new int(a);
    }
    ~Foo()
    {
        delete this->a;
    }
};

int main()
{
    Foo f1(10);
    // f2.a == f1.a, but since f1.a is a pointer this means that
    // they point to the same address, not that they have the same
    // value (though they do by extension)
    Foo f2 = f1;

    // This will change f1.a too!
    f2.a = 5;
    if(f1.a == 5) std::cout << "Oh dear!\n";

    if(true)
    {
        // Now we're in another scope and f3.a == f2.a == f1.a
        Foo f3 = f2;
    }
    // Out of scope so f3's destructor is called, which will
    // deallocate f3.a, also deallocating f1.a and f2.a!

    // This code crashes the program, because f1.a doesn't point
    // to a valid memory location anymore!
    std::cout << f1.a << std::endl;

    return 0;
}

To combat this we need to use a copy constructor—which is called whenever copy initialisations such as Foo f2 = f1 occur—and will allocate extra memory for f2 instead of just copying the pointers. C++ provides default copy constructors as we’ve used here, but we can easily define our own when they aren’t suitable.

Foo(const Foo& b)
{
    // Allocate new memory for this that isn't shared with b,
    // but does have the same value
    this->a = new int(b.a);
}

Smart Pointers

This whole issue of remembering to deallocate pointers can be a bit tedious, but the fact that they aren’t deallocated when they leave the scope they were allocated in is rather handy; it allows us to allocate memory inside of a function but have it accessible outside of the function, something that doesn’t work when using variables. What would be useful is if memory stayed allocated until there were no longer any pointers pointing to it.

The standard library comes to the rescue with the introduction of smart pointers, or specifically the std::shared_ptr. std::shared_ptr is a class template defined in the memory header that shares the ownership of a piece of memory with other std::shared_ptrs. When the last std::shared_ptr sharing that memory is destroyed (goes out of scope) or is changed to point to another address then the memory will be deallocated, but it will remain allocated and useable until then.

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
#include <memory>
#include <iostream>

int main()
{
    // Create a new empty shared_ptr to int
    std::shared_ptr<int> foo;

    if(true)
    {
        // Create a new shared_ptr to double and let it manage
        // a pointer to a new double with value 3.1
        std::shared_ptr<double> bar(new double(3.1));

        // Allocate memory for an int
        int* ptr = new int(10);

        // Give foo control of ptr
        foo = std::shared_ptr<int>(ptr);

        if(true)
        {
            // Make another shared_ptr sharing the memory
            std::shared_ptr<int> bar(foo);
            // It goes out of scope but the memory isn't deallocated
            // because its still in use by foo
        }
    }

    // bar is out of scope so its memory has been deallocated
    // ptr has gone out of scope but its memory hasn't been deallocated
    // and is accessible via foo which can be dereferenced
    // like a normal pointer
    std::cout << *foo << "\n";

    // Now foo goes out of scope and the memory allocated by ptr
    // is deallocated
    return 0;
}

Obviously this is a very useful feature, especially because std::shared_ptr objects behave like pointers and the * and -> operators (which have been overloaded) dereference the managed pointer. The class also has various member functions which can be used to see how many pointers are sharing the managed pointer for example.

There are also two other classes which manage pointers in similar ways; std::unique_ptr and std::weak_ptr. When std::unique_ptr manages a pointer that pointer cannot be managed by any other std::unique_ptr, and when it goes out of scope the allocated memory is deallocated. std::weak_ptr is different and is used to obtain temporary ownership of an std::shared_ptr when the existence of its managed pointer is not guaranteed. This is only really useful in threaded applications when we are not directly controlling pointer allocation, which we won’t cover, but I encourage you to read the documentation if you’re interested.

Instead of having to write code like

std::shared_ptr<int> foo;
/* Things happen */
foo = std::shared_ptr<int>(new int(10));

we can make use of the std::make_shared function

std::shared_ptr<int> foo;
foo = std::make_shared<int>(10);

A similar function exists for std::unique_ptr, but only in the newer C++14 standard so your compiler may not support it yet. There is also a lot more to memory management in C++, but there’s far too much to cover here.

Exercise

Modify the Vec3 class into a general Vec class that can contain an arbitrary number of elements determined when an object is instantiated. You should define a member function to compute the magnitude as before as well as at least scalar multiplication and vector addition operators; vector addition is only defined between two vectors when they both have the same number of elements, if they do not then it’s up to you to decide the operator’s behaviour. You should also overload the [] operator to access each component of the vector. There’s no need to define a sqrt function—you may use the one defined in the cmath header (don’t forget to link the cmath library if you do, usually using -lm)—and templates are not required (the solution will use double).

To handle a variable number of arguments in the constructor, either use C-style variadic arguments, C++ style std::initializer_lists, or use a pointer/array argument and a length argument as was used in previous tutorials. Solutions for all three methods will be provided!

The following should work with your code, with the appropriate constructor syntax selected.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include "vector.hpp"

int main()
{
    // Array constructor
    double e1[3] = {1.0, 2.0, 3.0};
    double e2[3] = {0.0, 4.0,-2.0};
    Vec v1(3, e1);
    Vec v2(3, e2);

    // Initializer list
    // Vec v1({1.0, 2.0, 3.0});
    // Vec v2({0.0, 4.0,-2.0});

    // Variadic arguments
    // Vec v1(3, 1.0, 2.0, 3.0);
    // Vec v2(3, 0.0, 4.0,-2.0);

    std::cout << (v1 + v2).mag() << std::endl;
    std::cout << v1[0] << " " << v2[1] << 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/* vector.hpp */
#ifndef VECTOR_HPP
#define VECTOR_HPP

#include <cmath>
// For initializer list constructor
#include <initializer_list>
// For variadic argument constructor
#include <cstdarg>

class Vec
{
    double* m_elements;

    unsigned int m_size;

    public:

    // Array method
    Vec(unsigned int size, double elements[])
    {
        m_size = size;
        m_elements = new double[m_size];
        for(unsigned int i = 0; i < size; ++i)
            m_elements[i] = elements[i];
    }

    // Initializer list method
    Vec(std::initializer_list<double> l)
    {
        m_size = l.size();
        m_elements = new double[m_size];
        unsigned int pos = 0;
        for(auto e = l.begin(); e != l.end(); ++e)
            m_elements[pos++] = *e;
    }

    // Variadic argument method
    Vec(unsigned int size, ...)
    {
        m_size = size;
        m_elements = new double[m_size];
        va_list args;
        va_start(args, size);
        for(unsigned int i = 0; i < size; ++i)
            m_elements[i] = va_arg(args, double);
        va_end(args);
    }

    // Copy constructor shortcut using C++11 constructor delegation
    // and the array constructor; instead of just using : to call
    // constructors from the base class, you can call constructors
    // from the same class too
    Vec(const Vec& v) : Vec(v.m_size, v.m_elements) {}

    // If you didn't define an array constructor
    // Vec(const Vec& v)
    // {
    //  m_size = v.m_size;
    //  m_elements = v.m_elements;
    //  for(unsigned int i = 0; i < size; ++i)
    //      m_elements[i] = v.m_elements[i];
    // }

    // Destructor to deallocate memory
    ~Vec()
    {
        delete[] m_elements;
    }

    // Calculate the length of the vector
    double mag()
    {
        double s = 0.0;
        for(unsigned int i = 0; i < m_size; ++i)
            s += m_elements[i]*m_elements[i];
        return sqrt(s);
    }

    // Return the number of elements in the vector
    unsigned int size()
    {
        return m_size;
    }

    // Overload the element access operator [] to access
    // the vector's elements in m_elements
    double& operator[](unsigned int i) // For writing
    {
        return m_elements[i];
    }
    const double operator[](unsigned int i) const // For reading
    {
        return m_elements[i];
    }
};

Vec operator*(double lhs, Vec rhs);
Vec operator+(Vec lhs, Vec rhs);
Vec operator-(Vec lhs, Vec rhs);

#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
/* vector.cpp */
#include "vector.hpp"

Vec operator*(double lhs, Vec rhs)
{
    Vec v = rhs;
    for(unsigned int i = 0; i < rhs.size(); ++i)
        v[i] *= lhs;

    return v;
}

Vec operator+(Vec lhs, Vec rhs)
{
    // If you know how you should throw an exception here,
    // for simplicity we'll just give up and return the lhs
    if(lhs.size() != rhs.size()) return lhs;

    Vec v = lhs;
    for(unsigned int i = 0; i < rhs.size(); ++i)
        v[i] += rhs[i];

    return v;
}

Vec operator-(Vec lhs, Vec rhs)
{
    if(lhs.size() != rhs.size()) return lhs;

    Vec v = lhs;
    for(unsigned int i = 0; i < rhs.size(); ++i)
        v[i] -= rhs[i];

    return v;
}