Lab 7. Pointers and Dynamic Memory

[Illustration]

Introduction

Every variable your program uses lives somewhere in memory, and every location in memory has a numeric
address. Until now you have never needed to know an address: you wrote score and the compiler worked
out where score was. This lab is about getting hold of that address and using it directly.

The Hunter College campus is a useful analogy. It is a large, heavy thing — you cannot carry it around or
hand it to a friend. Its address, 695 Park Ave, New York, NY 10065, is a short string you can write on a
scrap of paper, copy, pass around, and keep in a drawer. A pointer variable is that scrap of paper: a few
bytes holding the address of something that may be very much larger.

Two operators move between a thing and its address:

They are exact opposites, and almost everything in this lab is one of them applied carefully.

string book = "In a hole in the ground there lived a hobbit...";

string *p;      // p is a pointer to a string — it holds an address
p = &book;      // store the address of book in p

cout << *p << endl;   // dereference: prints the hobbit sentence
cout << p << endl;    // the pointer itself: a hexadecimal address, e.g. 0x7ffd3a2b
& takes you to the address; * brings you back string book "In a hole in the ground there lived a hobbit..." lives at 0x7ffd3a2b string *p 0x7ffd3a2b &book *p a few bytes: just the address the actual data, however large & goes from a variable to its address. * goes from an address back to the variable. Copying p copies the scrap of paper, never the building.

The type matters: string *p; declares a pointer that may hold the address of a string and nothing else.
A double * cannot point at an int. The star belongs to the variable, not the type — in
int *a, b; only a is a pointer and b is a plain int, which is why most people write the star against
the name.


Task A. A point in space, reached through a pointer

A point in 3D space is three coordinates (x, y, z) measured along three perpendicular axes from the
origin O = (0, 0, 0). A point P together with the origin defines a vector OP, whose length is

\[\text{length of } OP = \sqrt{x^2 + y^2 + z^2}\]

Create a program 3d-space.cpp holding this class:

class Coord3D
{
    public:
        double x;
        double y;
        double z;
};

Write a function that takes the address of a point and returns its distance from the origin:

double length(Coord3D *p);
Coord3D pointP = {10, 20, 30};
cout << length(&pointP) << endl;      // 37.4166

The function receives an address, so it must dereference before it can reach a member. Both of these work:

(*p).x          // dereference p, then take member x
p->x            // the same thing, written the way everybody writes it

The parentheses in the first form are not optional. *p.x means *(p.x) — the dot binds tighter than the
star — and will not compile. The arrow operator -> exists precisely so that nobody has to think about
that again. Use it.

Task B. Which point is farther?

In the same file, add:

Coord3D* fartherFromOrigin(Coord3D *p1, Coord3D *p2);

It takes two addresses and returns the address of whichever point is farther from the origin. Note the
return type: this function hands back a pointer, not a copy of a point.

Coord3D pointP = {10, 20, 30};
Coord3D pointQ = {-20, 21, -22};

Coord3D *ans = fartherFromOrigin(&pointP, &pointQ);
cout << "Address of P: " << &pointP << endl;
cout << "Address of Q: " << &pointQ << endl;
cout << "Farther:      " << ans    << endl;

Run it and compare the three addresses by eye to see which point won. Then make the program say so itself —
comparing two pointers with == asks whether they hold the same address, so ans == &pointP is how a
program answers the question:

Length of OP: 37.4166
Length of OQ: 36.4005
Address of P: 0x7ffdf2fad310
Address of Q: 0x7ffdf2fad330
Farther:      0x7ffdf2fad310  (that is P)

Your addresses will differ from mine and from each other’s — they are wherever your machine happened to put
the variables on that run.

Task C. Changing a point through its address

An object moving at constant velocity has its velocity described in the same three components, as
displacement per second. After dt seconds its position has changed by vel times dt along each axis.

Add to 3d-space.cpp:

void move(Coord3D *ppos, Coord3D *pvel, double dt);

It returns nothing. Instead it updates the point ppos points at:

Coord3D pos = {0, 0, 100.0};
Coord3D vel = {1, -5, 0.2};

move(&pos, &vel, 2.0);
cout << pos.x << " " << pos.y << " " << pos.z << endl;   // 2 -10 100.4

Nothing was passed by reference here, and yet pos changed. The function was given the address of pos, so
it wrote to pos itself — there was never a copy to write to instead. This is the same mechanism that made
arrays behave strangely in Lab 6, which is the subject of the next task.

Watch the axes when you write it: each coordinate advances by its own velocity component, so y uses
pvel->y, not pvel->x.

Task D. Arrays are addresses

In Lab 6 you were told that passing an array does not copy it, and that the reason was Chapter 10’s business.
This is it.

An array’s name is the address of its first element. Not a copy of the array, not a special array
object — just an address, which is why a function receiving one can reach the caller’s elements, and why the
size has to be passed separately. Write pointer-walk.cpp and see it:

const int SIZE = 5;
double reading[SIZE] = {12.5, 9.0, 14.25, 7.75, 11.0};

cout << reading     << endl;    // these two print
cout << &reading[0] << endl;    // exactly the same address

Because the name is an address, pointer notation and subscript notation are two spellings of one idea:

reading[i]        is        *(reading + i)

And reading + i does not add i bytes. Pointer arithmetic counts elements, not bytes: adding 1 to a
double * advances by one whole double. Print the address of every element and you will see them eight
apart.

Your program should:

  1. Print reading and &reading[0] and confirm they match.
  2. Print the address and value of every element, using *(reading + i) rather than reading[i].
  3. Walk the array with an actual pointer instead of an index:
for (const double *p = reading; p < reading + SIZE; p++)
{
    cout << *p << " ";
}
  1. Write and call double sumArray(const double *arr, int size); which totals the array without using
    square brackets anywhere
    — only * and pointer arithmetic.

That const is the same promise you met in Lab 6, now in pointer spelling. const double *arr means the
doubles at that address may not be modified through this pointer
. Try breaking it — add arr[i] = 0; to
sumArray — and read the error before deleting the line.

Example

reading     = 0x7ffc63c51630
&reading[0] = 0x7ffc63c51630
&reading[0] = 0x7ffc63c51630   reading[0] = 12.5
&reading[1] = 0x7ffc63c51638   reading[1] = 9
&reading[2] = 0x7ffc63c51640   reading[2] = 14.25
&reading[3] = 0x7ffc63c51648   reading[3] = 7.75
&reading[4] = 0x7ffc63c51650   reading[4] = 11
walking with a pointer: 12.5 9 14.25 7.75 11
sum = 54.5

Memory that outlives its scope

Every variable so far has been automatic: memory is set aside when execution enters the variable’s scope
and reclaimed when it leaves. You have relied on that without thinking about it.

It becomes a problem the moment you want to hand something back. This function is broken:

string* createAPoem()
{
    string poem = "2B or not 2B?";
    return &poem;                  // the address of something about to vanish
}

The address is real when it is returned and worthless immediately after, because poem ceases to exist as
the function returns. The caller holds the address of memory that now belongs to somebody else — a
dangling pointer. Compile it: g++ -Wall catches this one and says
warning: address of local variable 'poem' returned. Unlike the array overruns in Lab 6, here the compiler
is on your side. Do not ignore it.

new and delete

The fix is to ask for memory that is not tied to any scope. The new operator allocates it and returns its
address; the memory stays until you say otherwise:

string *ppoem = new string;        // allocate
*ppoem = "Roses are red";          // use it
return ppoem;                      // safe: nothing reclaimed it

“Until you say otherwise” is the catch. Memory from new is never reclaimed automatically, so every new
needs a matching delete:

int *p = new int;
*p = 1234;
cout << *p << endl;
delete p;                          // give it back

Forget the delete and the memory is gone for as long as the program runs — a memory leak. For arrays
the operators grow brackets, and the two forms must not be mixed:

double *data = new double[n];      // allocate n doubles
// ... use data[0] through data[n-1] ...
delete[] data;                     // bracketed delete for a bracketed new
Automatic memory is reclaimed; memory from new is not AUTOMATIC — dies with the function FROM new — outlives the function inside the function string poem = "2B..."; return &poem; inside the function string *p = new string; return p; returns returns memory reclaimed the returned address now points at somebody else's data memory still there and stays there until somebody calls delete on that address One delete for every new. Too few leaks memory; too many corrupts it.

Task E. Stop the leak, and measure it

Here is poem.cpp. The allocation is correct — the poem really does survive the function — but the program
never gives any of it back:

#include <iostream>
#include <string>
using namespace std;

string* createAPoemDynamically()
{
    string *p = new string;
    *p = "Roses are red, violets are blue";
    return p;
}

int main()
{
    for (long i = 0; i < 2000000; i++)
    {
        string *p = createAPoemDynamically();
        // the poem is finished with at this point
    }
    return 0;
}

Fix it so that it still makes two million poems and ends with no memory outstanding. The change is one line.

To see that it worked, watch the program’s memory while it runs. In one terminal start the program; in
another, run top or ps -o rss= -p $(pgrep poem), which reports the process’s memory in kilobytes. The
leaking version climbs steadily past 150,000 KB; the fixed version sits flat at a few thousand. Report both
numbers in a comment
in your program.

Task F. Allocating and releasing objects

Back in 3d-space.cpp, add two functions that allocate a point and release it:

Coord3D* createCoord3D(double x, double y, double z);   // allocate and initialize
void deleteCoord3D(Coord3D *p);                         // free

The pair is the point of the exercise: every function that hands out memory should have a partner that takes
it back, so a reader can check them against each other.

Coord3D *ppos = createCoord3D(10, 20, 30);
Coord3D *pvel = createCoord3D(5.5, -1.4, 7.77);

move(ppos, pvel, 10.0);
cout << ppos->x << " " << ppos->y << " " << ppos->z << endl;   // 65 6 107.7

deleteCoord3D(ppos);
deleteCoord3D(pvel);

Note that move needed no change at all. It always wanted an address; where that address came from — a local
variable’s &, or new — was never its concern.

Example

$ ./3d-space
Enter position: 10 20 30
Enter velocity: 5.5 -1.4 7.77
Coordinates after 10 seconds: 65 6 107.7

Task G (Bonus). A class that owns memory

Write particle.cpp, declaring a class Particle that holds a position and a velocity as pointers, each
allocated with new:

class Particle
{
    public:
        Coord3D *pos;
        Coord3D *vel;
};

Provide this interface:

Particle* createParticle(double x, double y, double z,
                         double vx, double vy, double vz);
void setVelocity(Particle *p, double vx, double vy, double vz);
Coord3D getPosition(Particle *p);
void move(Particle *p, double dt);
void deleteParticle(Particle *p);

createParticle performs three allocations — the particle and its two members — so deleteParticle must
perform three deletions, in the right order: release the members first, then the particle. Delete the
particle first and you no longer have a legal way to reach p->pos.

This driver models accelerated motion by raising the velocity at every step:

int main()
{
    Particle *p = createParticle(1.0, 1.5, 2.0, 0.1, 0.2, 0.3);
    double time = 0.0;
    double dt = 0.1;
    while (time < 3.0)
    {
        setVelocity(p, 10.0 * time, 0.3, 0.1);
        move(p, dt);
        time += dt;
        cout << "Time: " << time << " \t";
        cout << "Position: "
             << getPosition(p).x << ", "
             << getPosition(p).y << ", "
             << getPosition(p).z << endl;
    }
    deleteParticle(p);
    return 0;
}

Example

$ ./particle
Time: 0.1 	Position: 1, 1.53, 2.01
Time: 0.2 	Position: 1.1, 1.56, 2.02
Time: 0.3 	Position: 1.3, 1.59, 2.03
...
Time: 2.9 	Position: 41.6, 2.37, 2.29
Time: 3 	Position: 44.5, 2.4, 2.3

Check your own work. The compiler cannot tell you whether you freed everything, but a tool can. Rebuild
with the address sanitizer and run it:

$ g++ -fsanitize=address -g particle.cpp -o particle
$ ./particle

A correct deleteParticle prints nothing extra. Delete only the particle and forget its two members, and the
run ends with SUMMARY: AddressSanitizer: 48 byte(s) leaked in 2 allocation(s). Try it both ways — seeing
the tool catch a leak you planted is the fastest way to trust it when it catches one you did not.

Looking ahead. Remembering to call deleteParticle is entirely on the programmer, and one forgotten
call leaks. In Lab 8 you will build a class that owns a whole chain of allocations, and C++ offers a way to
have the cleanup happen automatically when the object goes out of scope: the destructor you met in
Lab 4, which exists for exactly this.

How to submit your programs

Submit through Gradescope

Submit the source files:

3d-space.cpp    pointer-walk.cpp    poem.cpp    particle.cpp (bonus)

Do not submit compiled executables. Each program should start with a comment containing your name and a short
description, for example:

/*
Author: your name
Course: CSCI-135
Instructor: their name
Assignment: Lab 7

Here, briefly, at least in one or a few sentences
describe what the program does.
*/