Unit 09: Pointers

  1. Memory addresses
    1. Memory addresses
    2. Address-of operatro &
  2. Pointer variables
    1. What is a pointer?
    2. Declaring a pointer
    3. Pointing to addresses
    4. Dereferencing pointers
    5. Why use pointers?
  3. Debugging pointers
    1. Dereferencing invalid memory
    2. What happens with ptr1 = ptr2?
  4. Program memory space
    1. Program memory - stack vs. heap

  5. Pointer quick reference
  6. Example code and additional resources

This week's stuff:

Pointers and Memory

Memory addresses

When we declare a variable, what we're actually doing is telling the computer to set aside some memory (in RAM) to hold some information. Depending on what data type we declare, a different amount of memory will need to be reserved for that variable.

Data type boolean character integer float double
Size 1 byte 1 byte 4 bytes 4 bytes 8 bytes

(A bit is one digit: 0 or 1. A byte is 8 bits.)


Any time we declare a variable, it is given a space in memory to store its data. You can think of this as being spots in RAM, but the Operating System actually gives each program a "virtual" memory space and is an intermediate between system resources (RAM) and the software/programs.

Address-of operator &

We can use the address-of operator & to see the memory address of any variable.

Example:

 cout << &number1 << "\t" << &number2 << endl;

Program output:

0x7ffd3a24cc70	0x7ffd3a24cc74

Each time the program runs, different memory addresses are shown. When a program ends, the variables we've declared are destroyed and that memory is freed up. When a program starts, then the program variables take whatever memory spaces are available.

(Note: We've used & before for pass-by-reference parameters. When it's after the data type in a variable declaration it's pass-by-reference. When it's before a variable name (not in a declaration), it's address-of.)

What is a pointer?

A pointer is special a type of variable. Most variables store data like ints, chars, floats, etc., but a pointer variable stores memory addresses as its value. We can grab the address of any other variable and point to it.

A pointer declaration looks like a normal variable declaration, except with an extra symbol *. We specify a DATA TYPE and a VARIABLE NAME during declaration.

Where TYPE should be the type of data we're pointing to - such as an integer, float, etc. We tell it a type because each data type has a different amount of space it takes up in memory.

We can name the pointer variable anything along with our normal variable naming rules, but it is common to prefix your pointer variable name with "ptr" just to make it obvious that it is a pointer.

Declaring a pointer

A pointer declaration takes this form: TYPE* NAME;

 int* ptrNumber = nullptr;
float* ptrPrice = nullptr; 

It is best to assign all pointers to nullptr when they're not in use. nullptr is an obviously "invalid memory address", and we can use this for error checking.

If we don't initialize our pointer to nullptr, then it starts off with "garbage data". We can't tell if garbage data is a valid memory address or not.

After a pointer is declared, we can point it to different memory addresses at any time in our program - we're just assigning it a new value.

Pointing to addresses

To assign an address to a pointer variable we use the address-of operator & to access a variable's address.

int * ptr = nullptr; // Declare pointer
int var = 10; // Declare normal variable
ptr = &var; // Store var's address in the ptr variable

If we use cout to display ptr, it will give us the same thing as if we used cout on &var...

cout << "var address: " << &var << endl;
cout << "ptr value:   " << ptr << endl;
var address: 0x7ffc3d6028b0
ptr value:   0x7ffc3d6028b0

Dereferencing pointers

When a pointer is pointing to some address, we often want to access the value stored at that address - either to write it out somewhere or to overwrite the value. We can do this by dereferencing our pointer.

Dereferencing a pointer takes this form: *PTR

int * ptr;    // Declare pointer
int var = 10; // Declare normal variable

ptr = &var;   // Store var's address in the ptr variable

cout << "var value:        " << var << endl;  // Original variable
cout << "ptr value:        " << ptr << endl;  // Pointer's value is an address
cout << "ptr dereferenced: " << *ptr << endl; // Dereferencing the pointer 
var value:        10
ptr value:        0x7ffd21de775c
ptr dereferenced: 10

Why use pointers?

We're just starting to cover pointers and it can be hard to come up with "good reasons" for them this early on. Here are a few reasons we use pointers:

  • Point to the current item being modified instead of writing duplicate code for modifying each item.
  • Can have pointer parameters and pass pointers; similar use as pass-by-reference, just different notation.
    void Quadratic_Reference( float a, float b, float c, float & x1, float & x2 ); // Pass by ref version
    void Quadratic_Pointer  ( float a, float b, float c, float * x1, float * x2 ); // Pass by pointer version
  • Dynamically allocate memory for variables and arrays...
    • Dynamically allocated memory for variables: Build link-based data structures (Linked Lists, Binary Search Trees) - CS 250 topic :)
    • Dynamically allocated memory for arrays: Can create "resizable" arrays; don't need to know array size at compile time.

Debugging - Dereferencing invalid memory

Pointers open us up to ways to accidentally cause our programs to crash. In particular, we need to look out for dereferencing invalid addresses.

Remember that if we declare an int without setting a value, it will start with "garbage" as its value:

int number;
cout << number << endl;
32741

The same issue happens with a pointer. If we don't initialize it, it will get "garbage", and when we display it it will look like a memory address, but it's an invalid one!

int * ptr;
cout << ptr << endl;
0x7f7efe1fc934

If we dereference this invalid memory address, the program will crash or give some kind of undefined behavior. This is why we initialize our pointers to nullptr!

ptr1 = ptr2?

If you set one pointer equal to another pointer, it will copy the memory address over, so both pointers will be pointing to the same location. Sometimes this is what you want, but it can also cause errors if you're not expecting it!

Program memory - stack vs. heap

When we run a program, each program has its own program space with different types of memory. You'll learn about this more in a computer architecture class, but a couple pieces of memory we care about are:

  • Stack storage: Local variables and parameter variables are stored in stack memory space. There is a limited amount of stack storage available to a program. This is why if your recursive function has a bug that would make it repeat infinitely, it will eventually crash - stack overflow from declaring its parameter variables too many times in memory.
  • Heap storage: There is a virtually unlimited amount of available storage in heap memory space (as long as there is free memory in general). However, this memory can only be accessed via pointers. When we use the new keyword with a pointer, we allocate memory dynamically in heap space.

We'll allocate memory later on while learning about arrays.

Pointer quick reference

Declare a pointer
int * ptrInt;
float* ptrFloat; // I prefer this way
string *ptrString;
Assign pointer to address
ptrInt = &intVar;
ptrFloat = &floatArray[2];
Dereference a pointer
cout << *ptr;
cin >> *ptr;
getline( cin, *ptrString );
Assign to nullptr
ptr = nullptr;
Copy memory address from B to A
ptrB = &someVar;
ptrA = ptrB; // now both pointers point to &someVar

Example code and additional resources

Example code: (repository)

Archived videos: