C++ help: Understanding pointers?

I’m working my way through an e-book called Jumping Into C++ and the past four chapters have been on Pointers. I picked up all the stuff up til Pointers very easily, but this is just frying my brain. I’m working my way through it, but I just got to “pointers of pointers” and I’m starting to freak out >.>

I’m going to continue reading later tonight, but if anyone has anything to help me (and others!) understand Pointers, it would really be appreciated :slight_smile: Whether it be understanding how, when, or why to use them, everything helps!

Pointers are exactly what they sound like, if you have encountered ‘References’ a pointer is very similar. A pointer is literally the memory address of a piece of data, the type you assign to the pointer dictates what you expect to find at that address (such as a class, an array, or an integer).

You use a pointer typically to avoid moving around big chunks of memory. If you imagine the basic robot class, it has a large number of variables for motors drive systems etc, all of these are stored in memory. If you call a funtion and pass in the ACTUAL robot, you end up making a copy of the whole robot (remember if you change a variable you pass into a function, you are changing a copy, not the original) which can be a lot of memory to copy and is quite wasteful. If you simply pass a pointer, you essentially say “our robot is located a memory address 0x800f000” and that pointer (the address only) becomes the variable which is copied into memory, when you look up that memory address you get the real robot, so if you change attributes of the robot, you are not changing a copy, but rather the real robot.

A semi-similar example would be if you were trying to send someone a file you downloaded from the internet, you could either email the file itself (giving them a copy of the same file) or email them a link to the file. In both cases they have access to the file, but the latter is more efficient.

Usually “a pointer to a pointer” is used with 2d arrays. The first pointer points to an array of pointers, which each point to their own arrays.

The first time I encountered a pointer to a pointer, I was writing a function that set the address of a block of memory I allocated to the address that was passed in.

Can get pretty complicated, usually it’s easier to draw a picture.

The way I try to teach pointers to our kids is to analogize them to book pages.

If I have a book of 100 blank pages, then a pointer is like me saying “hey george, write something on page 5”. That “page 5” is telling george the address that I want him to write to.

A pointer to a pointer would be: “hey george, go to the page number you find on page 5”. Whenever you say “page __”, that’s a pointer. A pointer to a pointer to a pointer would be “hey george, go to the page number you find on the page you found on page 5”.

The main problem with this analogy is that ‘page’ is a term for a related memory concept, but you don’t need to learn about that until much much later.

Don’t feel bad… I think pointers are one of the hardest thing for most beginning programmers to understand (along with proper memory allocation and management).

It helps to have an understanding of how information is physically stored in a computer. You can picture it like a tall apartment building or hotel. There are a ton of blocks for data, and each one has an address. Each of those rooms, with their unique address, then contains different people (or in the case of a computer, information). So to access a particular piece of information, you need to know where its stored - you have to know its address.

For the most part, this is taken care of for you. You create a variable, and behind the scenes the variable knows where its stored and can access that data without you needing to do anything.

When you work with a pointer, however, you aren’t actually storing data. You’re storing that address. So you can think of a pointer as a business card sitting in a Rolodex - it isn’t the data you want, but it tells you where that data is and how to access it.

For pointer usage, think about how you call functions in C++. If you send a function a couple of variables, that function uses a copy of those variables. Any changes to those variables in the function is not reflected in the caller. With pointers, however, you can pass the pointer to the data, so your function can make updates to the data and have that immediately reflected in the caller. Using pointers this way is called “pass by reference”, where as the “normal” way of just passing a variable to a function is called “pass by value”. Try it out - create a small program with a function in it that simply adds one to the value passed in. If you just pass in an integer, you’ll see that after the function is finished, your initial value hasn’t changed in the caller. If you pass in a pointer to the integer, you’ll see that the value has actually changed.

Pointers can also be useful when dealing with lists, multidimensional arrays, or tree structures, especially when dealing with searching or sorting… but it’s been too long since I’ve really had to use them for me to be any good describing them to you (I’ve been doing nothing but Java for about 6 years now).

Okay so I know that pointers simply point to another variable/memory location and can both change locations or change the value of the pointed-to’s variable. I know pointers are used to borrow memory when you need it.
I DON’T know when/why you need extra memory and when you need to/dont need to use pointers.
I DON’T know how new/delete work all the way. here’s an example of what I’m confused on for them:

int Number;
int *p_Number = new int;
int p_Number = Number;

So this creates an integer (Number), creates a pointer (p_Number) and gets enough memory for an integer (“new int”), then makes the pointer point to the original integer.

  1. Is this how you would go about making room for a variable, making that variable and setting it to the pointer?
  2. Is there a certain order or does it not matter?
  3. Why do I need to get room (via “new”) if the program will run fine without having to grab memory for it? And doesn’t it defeat the purpose if I create an integer before getting room for one?

P.S. Why are there references? They’re just half-pointers… Why not use pointers when you’d use references and have one less thing to remember?

Very, very few people would ever run the
int* p = new int;
code. As you deduced, it’s practically useless. It uses at least twice the memory (4 bytes for the int, 4 bytes for your pointer p) of just declaring the integer normally, plus the ‘new’ operator incurs a significant performance penalty because the operating system has to hunt for enough space for your 4-byte allocation.

What new does is it asks the operating system to reserve a certain amount of memory for your problem. In the case of “new int”, it reserves 4 bytes (usually), then returns a pointer to those 4 bytes.

What you usually use new for is things like this:
int* buffer = new int[1000]; // allocates 1000 ints.

This asks the operating system to allocate 1000 ints on the heap. Since you have limited space on the stack (where your conventionally-declared variables end up), you can’t create really massive buffers there. In windows, a thread usually has a maximum stack size on the order of a few megabytes. If you’re loading an image or a database, you often need far more than a few megabytes, so you need to store it on the heap (or be very clever). The stack is also far less flexible - you almost always have to know at compile-time how big your buffer needs to be, but if you’re an “image-loading” program, you may have wildly varying buffer sizes for your decoded pixels.

‘delete’ tells the operating system you’re done with a given block of memory on the heap. You can only delete things that you’ve told the operating system to allocate via ‘new’. A very concrete example of why deleting is important is to make this program:
int main(void)
{
while(true)
{
int* p = new int[100]; // allocate array, but never delete it
p[5] = 5;
}
}
Then open task manager - you’ll see your program rapidly gobble up all the memory your system can give it, and eventually crash (new returns NULL when it can’t find enough space for your request, and p[5] = 5 would try to write to invalid memory)

To relate this to my book-page analogy - “int* buffer = new int[100]” is like saying “hey george, find me 100 consecutive blank pages, and tell me where the first page of that block starts”. ‘delete] buffer’ would say “hey george, I’m done with those 100 pages, you can let other people use them now”. In this case, ‘george’ is my lame representation of the operating system.

Related reading
http://en.wikipedia.org/wiki/Dynamic_memory_allocation - all about why you might want new and delete
http://en.wikipedia.org/wiki/New_(C%2B%2B) - new!

My professor (from the dark ages) explained pointers like this. Think of memory as a bunch of numbered mailboxes. If you declare a simple variable that is like going to a mailbox (the address) and getting your letter (data). A pointer is going to a mailbox (address of the pointer) and getting a slip of paper that has another mailbox number (address of the data) on it. You then open the next mailbox and their is your letter (data). Make sense?

Pointers are not just an arbitrary concept put in languages to cause us headaches. They mirror capabilities in the hardware. Most computers have machine instructions that use “indirect addressing” or “indirected indexed addressing” modes. Pointers in C and C++ are indirect addressing in a high level language.

Anyways - add the concept that pointers increment by the size of the object they point to (so incrementing a pointer to an integer adds 4) and you are good to go.

:slight_smile: Great comment! New C programmers always list pointers high on their WTF list.

If you continue in CS you will take courses in Data Structures and Algorithms, where the beauty and utility of pointers starts to become apparent.

Things like graphs, trees, and other structures, whose size and connections are not known at compile time, are easy to create, traverse, add too, delete from, sort, etc., when using pointers.

Hang in there, learning to use them will be a great benefit to you when you reach that stage.

Thanks everyone for the support!

To continue on from Bongle’s Post #7: So you don’t need to allocate memory for programs with a smallish amount of variables, only large amounts like 1,000+? And either way, what’s the order to allocate memory for an integer and create a normal integer in that space?

Everything I’m learning I’m also trying to apply to how I’ll use it next year. I’ll be my teams only programmer, we don’t have a programming mentor or anything (small team, just 15 or so guys and a teacher), and I’m trying to learn C++ enough to be able to code the robot in that instead of LabVu (since I’ll need to know C++ for my career it’d be a lot better for me to get a good foundation now).
So with that said: when/why/what do YOU use pointers for the robot? Any/all details help :slight_smile:

Also, are pointers/memory allocation with them related to how to save memory long-term? That’s one thing I’m looking forward to – writing a program where, for example, I can get a user input assigned to an integer, close the program, start it back up later and it remember what that value was

So, to answer your question of when/why…

Typically, nobody creates a pointer for an intrinsic type such as int, bool,float etc, unless there is a special reason (such as making an array later)

You typically use a pointer for interacting with functions that either return pointers or take pointers. When you are writing these functions, you should choose to pass arguments as pointers for two reasons: either because you need to modify the value (and not the copy) of the variable (such as a swap function), or because the object you are passing is larger than a pointer. When you pass parameters to a function, there is some overhead from making the copy of those objects that gets passed in, if you have a large class such as an image, you will pay a big penalty for copying that image when you call the function; however if you pass the object by pointer, you only copy it’s address, which is a significantly smaller amount of memory.

As you start getting your feet wet in the WPI lib code, you’ll start finding some functions require you to pass in pointers, such as when you are passing in an image. There is no magical advantage to making all your variables pointers (in fact you pay a penalty for it because you store the object and the pointer to it in memory).

to make some corrections on your snippet of example code:


int Number;
int *p_Number = new int; //you create an integer using new, which gives you a pointer to the int
*p_Number = Number; //you put a * (and this is where pointers get confusing) 
//because you want to set the value at the address p_Number to be Number, 
//the * when used this way dereferences the pointer.
int *p_Number2 = &Number; //this will set p_Number2 to point to Number.
delete p_Number; //This frees the memory allocated by the new int above, so you can reuse p_Number again below without making another pointer variable
p_Number = p_Number2; //this sets the address pointed to by p_Number to the same address pointed to by p_Number2

When you use &Number, you are referencing Number, this operation gets the address at which Number resides, that address has type (int *), so you can set p_Number2 which is also of type (int *).

As far as when to use new/delete. The above example is probably the simplest. You create p_Number, a pointer, and create an int to point it to. Later you want to point it to a different int, so you delete p_Number (which deletes the int it points to) and set p_Number to point somewhere else. Had you not deleted p_Number before changing it to point elsewhere, the int you new’d would no longer have any pointers referencing it, and it would live in memory forever (this is called a memory leak).
Alternatively, if you tried to delete p_Number2 (which points to Number), you would be deleting Number which you aren’t responsible for managing, and cause your program to crash when it leaves your function. The reason it crashes is that when you declared Number, the compiler creates this memory location to be used within the function, and it knows that it needs to delete the memory location when it leaves; but if you have already deleted Number (by calling ‘delete p_Number2;’) then the compiler tries to delete memory it no longer owns.

In short. Don’t lose hope, it will make sense eventually (hopefully my explanation helps), everyone pretty much has the same reaction to pointers/references/memory allocation/etc when they first encounter it; and the complexity is necessary, because these concepts are what give C/C++ a lot more power than many other languages. Just remember that you really are working with real memory (as in RAM in your PC, or on the cRIO), learning how memory allocation works is a good first step to a deeper understanding of how computers work.

First, be patient, take your time, and read as much as you can. There are lots of good discussions and documents online explaining the complexities of pointers. Pointers can be a difficult subject - I avoided them like the plague when I first learned C - but it’s important to know them well. What you want to avoid is what one of my professors termed “Ninja coding”: “If it doesn’t compile, just throw a star at it”. That causes confusion and chaos wherever you turn.

References are really useful when you get into object-oriented programming, particularly when overloading operators (be patient, you’ll get to this eventually). In short, they allow you to make functions that look and feel like pass-by-value without the overhead of actually doing a full copy. Do they do anything you couldn’t do with pointers? No, but just like array bracket notation, they make some things more natural.

Now back to debugging a pointer bug in my homework…

Okay so after reading all of this and getting (what I think is) a better understanding of pointers, I attempted my book’s chapter’s next practice problem:

Write a function that takes 3 arguments, a length, width and height, dynamically allocates a 3-dimensional array with those values and fills the 3-dimensional array with multiplication tables. Make sure to free the array when you are done.

I’m just filling it with an incrementing value rather than multiplication tables

Here’s the code I wrote. It has 2 main errors that repeat many times that the compiler picked up and I wouldn’t be surprised if there were several more. It looks like a lot, but you’ll quickly realize that it’s very small, quick, and (hopefully) simple so please don’t be put off by its size.
So what’s wrong with this and how can I fix it?
Please ignore the fact that I made it a 3x? 2dimensional array instead of a 3dimensional array. I noticed that towards the end of writing this but that’s not the main focus of the exercise so I’ll fix that later.

//get 3 user-input values and make a 3dimensional array with those dimensions.  Allocate that much memory.

#include <iostream>
using namespace std;

void setValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree]);
void showValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree]);

int main()
{
    int firstNumber;
    int *p_firstNumber = & firstNumber;
    int secondNumber;
    int *p_secondNumber = & secondNumber;
    int thirdNumber;
    int *p_thirdNumber = & thirdNumber;

    //get the 3 sizes
    cout << "Give me a length, width, and height!
";
    cout << "Length: "; cin >> firstNumber;
    cout << "Width: "; cin >> secondNumber;
    cout << "Height: "; cin >> thirdNumber;

    //create the pointer version of the 3dimensional array
    int **p_p_fullArray = new int[3];
    int *p_firstArray = new int[firstNumber];
    int *p_secondArray = new int[secondNumber];
    int *p_thirdArray = new int[thirdNumber];

    //set the 3 1dimensional arrays to the full array
    p_p_fullArray[0] = & p_firstArray;
    p_p_fullArray[1] = & p_secondArray;
    p_p_fullArray[2] = & p_thirdArray;

    //create normal variables for the 3dimensional array
    int firstArray[firstNumber];
    int secondArray[secondNumber];
    int thirdArray[thirdNumber];

    //set the 3 1dimensional pointers to the 3 1dimensional variables
    p_firstArray = & firstArray;
    p_secondArray = & secondArray;
    p_thirdArray = & thirdArray;

    //assign something to every cell in the array
    setValues(int *p_firstNumber, int *p_secondNumber, int *p_thirdNumber, int *p_firstArray], int *p_secondArray], int *p_thirdArray]);

    //show the array (easy way to make sure the program worked)
    cout << "
Cool thanks.  Here's your array:" << endl;
    showValues(int *p_firstNumber, int *p_secondNumber, int *p_thirdNumber, int *p_firstArray], int *p_secondArray], int *p_thirdArray]);

    //free the allocated memory and make sure the pointers don't grab on to anything random and cause issues
    delete p_p_fullArray;
    p_p_fullArray = NULL;
    delete p_firstArray;
    p_firstArray = NULL;
    delete p_secondArray;
    p_secondArray = NULL;
    delete p_thirdArray;
    p_thirdArray = NULL;
    delete p_firstNumber;
    p_firstNumber = NULL;
    delete p_secondNumber;
    p_secondNumber = NULL;
    delete p_thirdNumber;
    p_thirdNumber = NULL;
}

void setValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree])
{
    int value = 0;

    for (int i = 0; i < a; i++)
    {
        *arrayOne* = value;
        ++value;
    }

    for (int j = 0; j < b; j++)
    {
        *arrayTwo[j] = value;
        ++value;
    }

    for (int k = 0; k < c; k++)
    {
        *arrayThree[k] = value;
        ++value;
    }
}

void showValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree])
{
    cout << "first row: ";
    for (int i = 0; i < a; i++)
    {
        cout << arrayOne* << " ";
    }

    cout << "
second row: ";
    for (int j = 0; j < b; j++)
    {
        cout << arrayTwo[j] << " ";
    }

    cout << "
third row: ";
    for (int k = 0; k < c; k++)
    {
        cout << arrayThree[k] << " ";
    }
}

**

the function pointer tutorial website

http://www.newty.de/fpt/index.html

You’re very close with your code, but you actually need a 3rd level of indirection.

int*** p3Dimensions = new int**[width];
for(int x = 0;x < width; x++)
{
p3Dimensions[x] = new int*[height];
for(int y = 0;y < height; y++)
{
p3Dimensions[x][y] = new int[depth];
for(int z = 0;z < depth; z++)
{
p3Dimensions[x][y][z] = xyz;
}
}
}

I’m pretty tired from practice day at waterloo, so I’ll let you figure out how that works … if it compiles, which it might not.

So I looked at your code and fixed it. As I did, I added comments and showed what I changed it from. To distinguish them, my comments are not indented:

//get 3 user-input values and make a 3dimensional array with those dimensions.  Allocate that much memory.

#include <iostream>
using namespace std;

// See at implementation
void setValues(int *a, int *b, int *c, int arrayOne], int arrayTwo], int arrayThree]); // void setValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree]);
void showValues(int *a, int *b, int *c, int arrayOne], int arrayTwo], int arrayThree]); // void showValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree]);

int main()
{
    int firstNumber;
    int *p_firstNumber = & firstNumber;
    int secondNumber;
    int *p_secondNumber = & secondNumber;
    int thirdNumber;
    int *p_thirdNumber = & thirdNumber;

    //get the 3 sizes
    cout << "Give me a length, width, and height!
";
    cout << "Length: "; cin >> firstNumber;
    cout << "Width: "; cin >> secondNumber;
    cout << "Height: "; cin >> thirdNumber;

    //create the pointer version of the 3dimensional array
    int **p_p_fullArray = new int*[3]; // int **p_p_fullArray = new int[3];
    int *p_firstArray = new int[firstNumber];
    int *p_secondArray = new int[secondNumber];
    int *p_thirdArray = new int[thirdNumber];

    //set the 3 1dimensional arrays to the full array
    p_p_fullArray[0] = p_firstArray; // p_p_fullArray[0] = & p_firstArray;
    p_p_fullArray[1] = p_secondArray; // p_p_fullArray[1] = & p_secondArray;
    p_p_fullArray[2] = p_thirdArray; // p_p_fullArray[2] = & p_thirdArray;

// this part is trying to statically allocate an array with a length derived from
// a runtime value (illegal), and then leaks memory because you're removing the arrays
// you allocated using new
/*
    //create normal variables for the 3dimensional array
    int firstArray[firstNumber];
    int secondArray[secondNumber];
    int thirdArray[thirdNumber];

    //set the 3 1dimensional pointers to the 3 1dimensional variables
    p_firstArray = & firstArray;
    p_secondArray = & secondArray;
    p_thirdArray = & thirdArray;
*/

// functions are called by passing values, not by declaring what types those values are
    //assign something to every cell in the array
    setValues(p_firstNumber, p_secondNumber, p_thirdNumber, p_firstArray, p_secondArray, p_thirdArray); // setValues(int *p_firstNumber, int *p_secondNumber, int *p_thirdNumber, int *p_firstArray], int *p_secondArray], int *p_thirdArray]);

    //show the array (easy way to make sure the program worked)
    cout << "
Cool thanks.  Here's your array:" << endl;
// same as before
    showValues(p_firstNumber, p_secondNumber, p_thirdNumber, p_firstArray, p_secondArray, p_thirdArray); // showValues(int *p_firstNumber, int *p_secondNumber, int *p_thirdNumber, int *p_firstArray], int *p_secondArray], int *p_thirdArray]);

// delete is not enough for arrays. delete normally just calls a destructor and deallocates memory for a single instance.
// If you want to deallocate an array, you should use delete ].
//
// Although setting pointers to NULL when you're done with them is a good idea, if you aren't
// going to use them again (in a situation like, for example, your program ending), there's no point
    //free the allocated memory and make sure the pointers don't grab on to anything random and cause issues
    delete ] p_p_fullArray; // delete p_p_fullArray;
    p_p_fullArray = NULL;
    delete ] p_firstArray; // delete p_firstArray;
    p_firstArray = NULL;
    delete ] p_secondArray; // delete p_secondArray;
    p_secondArray = NULL;
    delete ] p_thirdArray; // delete p_thirdArray;
    p_thirdArray = NULL;
// you tried to delete statically allocated memory. This messes stuff up.
/*
    delete p_firstNumber;
    p_firstNumber = NULL;
    delete p_secondNumber;
    p_secondNumber = NULL;
    delete p_thirdNumber;
    p_thirdNumber = NULL;
*/
}

// a note on parameters: arrays, when used as function parameters, degenerate and become identical
// to pointers. Therefore, you're asking for 3 int**s, which is definitely not what you passed. Pick
// either * or ], whichever better represents how you use it.
void setValues(int *a, int *b, int *c, int arrayOne], int arrayTwo], int arrayThree]) // void setValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree])
{
    int value = 0;

// you have to dereference a pointer to be able to use it as a normal value
//
// also, i j and k each go out of scope after each loop, so you don't have to
// choose a different name for each. Name them all i and they'll technically
// be different variables, but will still be called i. Consistency is good, and
// coming up with a new name for the iterator in each loop is pointless.
    for (int i = 0; i < *a; i++) // for (int i = 0; i < a; i++)
    {
// since you don't pass in an int**, don't use it as one        
        arrayOne* = value; // *arrayOne* = value;
        ++value;
    }

    for (int j = 0; j < *b; j++) // for (int j = 0; j < b; j++)
    {
        arrayTwo[j] = value; // *arrayTwo[j] = value;
        ++value;
    }

    for (int k = 0; k < *c; k++) // for (int k = 0; k < c; k++)
    {
        arrayThree[k] = value; // *arrayThree[k] = value;
        ++value;
    }
}

// same as above
void showValues(int *a, int *b, int *c, int arrayOne], int arrayTwo], int arrayThree]) // void showValues(int *a, int *b, int *c, int *arrayOne], int *arrayTwo], int *arrayThree])
{
    cout << "first row: ";
// same situation. Dereference, dereference, dereference!
    for (int i = 0; i < *a; i++) // for (int i = 0; i < a; i++)
    {
        cout << arrayOne* << " ";
    }

    cout << "
second row: ";
    for (int j = 0; j < *b; j++) // for (int j = 0; j < b; j++)
    {
        cout << arrayTwo[j] << " ";
    }

    cout << "
third row: ";
    for (int k = 0; k < *c; k++) // for (int k = 0; k < c; k++)
    {
        cout << arrayThree[k] << " ";
    }
}

However, as you said, this does not actually do what it was supposed to do, and there were some things about it that could have been done better, so I wrote my own as an example:

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

// once created, arr*[j][k] will be the product i*j*k
int*** makeMultTable(int l,int w,int h) {
    if(l <= 0 || w <= 0 || h <= 0)
        return NULL;
    // allocate top-layer containing array
    int*** ret = new int**[l];
    for(int i=0;i<l;++i) {
        // allocate array to store rows
        ret* = new int*[w];
        for(int j=0;j<w;++j) {
            // allocate individual row
            ret*[j] = new int[h];
            // set each row element to its value in the multiplication table
            for(int k=0;k<h;++k)
                ret*[j][k] = i*j*k;
        }
    }
    return ret;
}

void printTable(int*** table,int l,int w,int h) {
    if(!table || l <= 0 || w <= 0 || h <= 0)
        return;
    // find longest possible int
    // used for prettifying the grid
    stringstream digitTest;
    // if i, j, and k are less than l, w, and h respectively,
    // then the largest value of i*j*k is (l-1)*(w-1)*(h-1)
    digitTest << (l-1)*(w-1)*(h-1);
    // find greatest amount of digits for prettification
    int digitLength = digitTest.str().length();
    // go by layers because text is only 2 dimensional at best
    for(int i=0;i<l;++i) {
        cout << "i = " << i << ":" << endl;
        for(int j=0;j<w;++j) {
            // go through each element in the row
            for(int k=0;k<h;++k) {
                // reset prettifying stream
                digitTest.str("");
                // single grid element
                digitTest << table*[j][k];
                // prettify the element
                while(digitTest.str().length() < digitLength)
                    digitTest << " ";
                // no extra space needed at the end of a row
                if(k != h-1)
                    digitTest << " ";
                // output prettified element
                cout << digitTest.str();
            }
            // row done
            cout << endl;
        }
        // layer done
        cout << endl;
    }
}

void freeMultTable(int*** table,int l,int w,int h) {
    if(!table || l <= 0 || w <= 0 || h <= 0)
        return;
    // delete contents from the inside out
    for(int i=0;i<l;++i) {
        for(int j=0;j<w;++j)
            // delete individual row
            delete ] table*[j];
        // delete one layer
        delete ] table*;
    }
    // delete whole table
    delete ] table;
}

int main() {
    int l,w,h;
    
    cout << "Length? ";
    cin >> l;
    cout << "Width? ";
    cin >> w;
    cout << "Height? ";
    cin >> h;
    
    int*** table = makeMultTable(l,w,h);
    
    printTable(table,l,w,h);
    
    freeMultTable(table,l,w,h);
    
    return 0;
}

Then, since I noticed that the table could be well-represented as a class, I converted it to OOP:

#include <iostream>
#include <sstream>
#include <stdexcept>
using namespace std;

class MultTable {
    int*** table;
    int l,w,h;
public:
    MultTable(int l_,int w_,int h_) : l(l_),w(w_),h(h_) {
        if(l <= 0 || w <= 0 || h <= 0)
            throw runtime_error("Invalid dimensions");
        // allocate top-layer containing array
        table = new int**[l];
        for(int i=0;i<l;++i) {
            // allocate array to store rows
            table* = new int*[w];
            for(int j=0;j<w;++j) {
                // allocate individual row
                table*[j] = new int[h];
                // set each row element to its value in the multiplication table
                for(int k=0;k<h;++k)
                    table*[j][k] = i*j*k;
            }
        }
    }
    ~MultTable() {
        if(!table || l <= 0 || w <= 0 || h <= 0)
            return;
        // delete contents from the inside out
        for(int i=0;i<l;++i) {
            for(int j=0;j<w;++j)
                // delete individual row
                delete ] table*[j];
            // delete one layer
            delete ] table*;
        }
        // delete whole table
        delete ] table;
    }
    
    int get(int i,int j,int k) const {
        if(i <= 0 || j <= 0 || k <= 0 ||
           i >= l || j >= w || k >= h)
            throw runtime_error("Index out of range");
    }
    
    string toString() const {
        stringstream out;
        // find longest possible int
        // used for prettifying the grid
        stringstream digitTest;
        // if i, j, and k are less than l, w, and h respectively,
        // then the largest value of i*j*k is (l-1)*(w-1)*(h-1)
        digitTest << (l-1)*(w-1)*(h-1);
        // find greatest amount of digits for prettification
        int digitLength = digitTest.str().length();
        // go by layers because text is only 2 dimensional at best
        for(int i=0;i<l;++i) {
            out << "i = " << i << ":" << endl;
            for(int j=0;j<w;++j) {
                // go through each element in the row
                for(int k=0;k<h;++k) {
                    // reset prettifying stream
                    digitTest.str("");
                    // single grid element
                    digitTest << table*[j][k];
                    // prettify the element
                    while(digitTest.str().length() < digitLength)
                        digitTest << " ";
                    // no extra space needed at the end of a row
                    if(k != h-1)
                        digitTest << " ";
                    // output prettified element
                    out << digitTest.str();
                }
                // row done
                out << endl;
            }
            // layer done
            out << endl;
        }
        return out.str();
    }
};

ostream& operator<<(ostream& o,const MultTable& multTable) {
    return o << multTable.toString();
}

int main() {
    int l,w,h;
    
    cout << "Length? ";
    cin >> l;
    cout << "Width? ";
    cin >> w;
    cout << "Height? ";
    cin >> h;
    
    MultTable table(l,w,h);
    
    cout << table;
    
    return 0;
}

I tried to comment on some of the things that might confuse you. I hope these help!****************