Pointers - AlbertGBarber/PixelSpork GitHub Wiki

Intro:

Pointers can be one of the more intimidating aspects of C/C++, especially for newer programmers. In Pixel Spork, I try to shield you from having to work with pointers directly as much as possible, but there are occasions where they are needed, so I am providing this guide to some of their more common uses.

Jump to:

Pointer Basics:

You can skip this section if you are already familiar with pointers (skip).

What are Pointers?:

When you upload a program to a micro-controller, you are storing the program in the micro-controller's memory. This includes all the program's variables. These variables all have an address that tells the program where in memory the variable is physically stored. When the program asks for the value of a variable, it's actually asking to read the value of memory at the variable's address. Pointers are variables that store the memory address of another variable. When you read from a pointer, you're actually reading from another variable's memory!

As an alternate explanation, think of a program/micro-controller as a house. Inside the house are various items, like furniture, books, etc. These are its variables. I might ask you to tell me the color of the couch in the living room on the ground floor. The couch's color is its value, while the physical location is its memory address. A pointer would be a note telling you the location of the couch. To use the pointer, I'd ask you to tell me the color of the couch listed on the note.

This detached memory aspect is what makes pointers special, but also potentially dangerous. Anyone with a pointer can read and change the value of whatever is at its memory address. This means pointers are great if you need to, say, pass an array to a function; you can pass a pointer to the array instead (passing the array directly would force the function to make a wasteful local copy of the array). However, now you've given the function the power to both read and write to the array, even if you only wanted the function to read from it. Likewise, if the array is moved, deleted, etc, the pointer doesn't follow it, so it's left pointing to an empty bit of memory. To return to our house analogy, anyone with the note can find the couch, potentially allowing them to move or change it.

This doesn't mean that pointers are bad, just that we need to be careful when using them!

Creating and Using Pointers:

Pointers are declared via two methods: using the Address-of Operator (&) or the Dereference operator (*).

A pointer using the address-of operator (&) will have the value of the address itself.

For Example:

foo = &bar; 

Will set the value of foo to the address of bar. Using the house analogy, it's like copying the couch note.

A pointer using the dereference operator (*) stores the value at the address.

For Example:

foo = *bar;

Will set foo equal to the value pointed to by bar. Using the house analogy, it's like copying the color of the couch.

Combining the address-of and dereference operators allows you to store variables remotely using pointers:

For Example:

myVar = 10; //Create a variable of value 10
*ptr = &myVar; //Create a pointer, pointing to myVar

If we output *ptr we get 10, but critically, because ptr is tied to the address of myVar, if myVar is changed, the value of *ptr also changes. Returning to the house analogy, if the couch color is changed, and I ask you to use the note, then you'll report the changed couch color.

Not only that, but you can use the pointer to change the value of myVar:

*ptr = 20;

Will change the value of myVar to 20. This is because we're changing the value at the pointer's address, which is shared with myVar. In the house analogy, I can ask you to change the color of the couch on the note (assuming you can do that!), and the color will be changed for everyone.

This is even more useful in the case of arrays and other objects:

For example, say we have an array:

myArray[3] = {1, 2, 3};

If we want to pass this array to function like myFunt(myArray), we'd need to copy the whole array object into the function as a new variable. Not only is this slow, and a waste of memory to store the copy, but it also prevents you from changing the original array, because your copy is local to the function.

Instead, if we create a pointer to the array, and pass it to the function:

*ptr = myArray; //Note that we don't use &myArray here, this is a specific interaction in C for arrays.
myFunct(ptr); 

We can access and change the array from the function. In Arduino, this would look like:

To read a value:

myVar = ptr[0]; 

Where [0] indicates the value of the array at the 0th index.

To write a value:

ptr[0] = 5; 

Would set the 0th array index to 5.

For pointers to objects other than arrays, you use (->) to access the object members.

For example, say we have a pointer to a Segment Set:

*ptr = &mySegmentSet;

We'd access the Segment Set's variables via (->):

ptr->numSegs; //numSegs is a variable in the mySegmentSet object

Pointer typing:

Pointers are always typed to match what they're pointing at. This tells the program how to read what's at the pointer's address.

For example:

int *ptr;

Declares a pointer to an int, while:

SegmentSetPS *segSetPtr;

Declares a pointer to a SegmentSetPS.

Returning to the house and note analogy again, I would tell you that the note was for a couch, not for some other piece of furniture. So, when you went to the location on the note, you'd know to look for a couch.

In most cases, if a pointer is declared incorrectly, the Arduino compiler will let you know, usually by telling you the types don't match. If the type is correct, but you're getting a pointer type error, try changing from the address-of operator (&) to the dereference opperator (*) or vice versa.

Common Bugs:

By design, pointers don't have a lot of safeguards. This allows you to create some truly mysterious bugs.

When using Pixel Spork, the most common bug you may fall-prey-to is creating a pointer without an address (ie int *ptr;). The address will default a random memory value. Reading the pointer is still fairly harmless, you just get random junk back, but trying to change what's at the pointer's address is a mess. Maybe you get lucky, and you write to an empty bit of memory, or maybe you write over any existing variable, and possibly breaking anywhere from everything to nothing at all.

Due to the randomness, a program may appear to work fine, even for a long time, but then mysteriously crashes, or, even worse, everything does continue to work, but a rogue pointer mysteriously changes a variable, causing unexpected behavior (and often tricking you into trying to de-bug a section of perfectly functioning code, while the actual bug is elsewhere with the pointer).

The simple way to avoid these types of bugs is to always set a pointer's address when creating it. If there is nothing for it to point to, set it to nullptr (NULL). Having a null pointer may still cause your program to crash, but it will be predictable, and at the exact point where you try to use the pointer. Likewise, make sure whatever your pointer points to has a value, pointing a pointer to an unassigned variables (ie int a, where the value of a is not set), otherwise you'll just be reading back junk.

There are a number of other common pointer bugs, but you probably won't run into them in Pixel Spork unless you're doing something out of the ordinary (such as chaining multiple utility outputs together, swapping around parts of segment sets during runtime, etc). You can read more about them here.

Common Pointer Usage in Pixel Spork:

There are three common ways you may encounter or use pointers in Pixel Spork:

1. For pointing an effect variable, such as a background color, the update rate, etc to a variable outside the effect.

Because pointers allow you to tie one variable to another, it is useful for some effect variables to be pointers. The exact pointers will vary from effect to effect, but they should be recorded in the effects' documentation. There is a common setup to these pointers that I'll go over below:

Using the effect update rate as an example:

The update rate (*rate) for every effect is a pointer. By default it's bound to the effect's local variable rateOrig, which is set to the rate value provided in the effect's constructor.

In code form:

rateOrig = Rate; //Rate is provided as a constructor argument        
rate = &rateOrig; //Bind the effect's rate pointer to rateOrig

So, to change the rate, you can change rateOrig, or you can tie the rate pointer to an external variable. For Example:

//Changing the rate directly by setting the rateOrig var
yourEffect.rateOrig = 80;
        
//Binding the rate pointer to newRate, an external var
uint16_t newRate = 80;
yourEffect.rate = &newRate; 

//To tie two effect's rates together
//The below sets the rate of "yourEffect" to always be the same as "yourOtherEffect"'s rate
yourEffect.rate = yourOtherEffect.rate 

Note that, once the rate is bound to newRate, any changes to newRate will also change the effect's rate. This is great for easily controlling the rates of multiple effects, without having to update each rate individually. Furthermore, note that because rate no longer points to rateOrig, changing rateOrig will not effect the update rate.

Other single variable effect pointers usually follow a similar setup, such as the background color (bgColor), which is a pointer, and by default bound to bgColorOrig. These kind of pointers will be noted on the effect wiki page.

2. Object pointers within effect, such as palettes, patterns, etc.

The easiest way to pass objects to effects is via a pointer. The effect will have a corresponding pointer variable to store the address. For example, one of the constructors for the LavaPS effect takes a palette address as an argument:

LavaPS(SegmentSetPS &SegSet, palettePS &Palette, uint16_t BlendSteps, uint16_t BlendScale, uint16_t Rate);
//                                  ^------The palette address argument  

(Any arguments using an & or * will be passed via address/pointer)

Meanwhile, the effect has a palette pointer variable, which is then bound to the passed in palette:

PalettePS *palette; //The effect's palette pointer is declared in the class definition
palette = &Palette; //During construction, the palette pointer is bound to the passed in palette address

Usually these pointers are public, so you can change the effect's palette by binding it to a new palette. For example:

yourLavaEffect.palette = &cybPnkPal_PS; 

Will change your Lava effect palette to point to the cybPnkPal_PS, which will then be used for the effect's colors. As with other pointers, if you change the colors in the cybPnkPal_PS, these will also be reflected in the effect.

You can also change an effect's palette using any palette function. For example, to shuffle the lava effect's palette you would:

paletteUtilsPS::shuffle(*yourLavaEffect.palette); //shuffle(&palette), where *yourLavaEffect.palette is the address of the lava effect's palette

Note that effects sometimes will create palettes automatically, usually if you want a random set of colors. The Lava effect has this as one of its constructor options, where you ask for a number of colors rather than supplying a palette. In these cases, the effect still uses its palette pointer, but instead binds it to local palette it creates (usually named paletteTemp):

paletteTemp = paletteUtilsPS::makeRandomPalette(numColors); //creates a random palette with a set number of colors, storing it locally in paletteTemp
palette = &paletteTemp; //The effect's palette pointer is bound to paletteTemp, just as it would be bound to an external palette. 

The local paletteTemp behaves exactly like any other palette, but it is local to the effect, so it only exists while the effect does.

Other Object Pointers:

Other types of object, such as patterns, will also be passed as pointers and are usually handled in a similar way to palettes. For instance a pattern will be tied to a pattern pointer, can be modified using pattern functions, and may be bound locally to an effect's patternTemp if created within the effect. Likewise, an effect's segment set is tied to the effect's SegSet pointer variable. To switch to a different segment set, you'd change what set SegSet points to.

Unless otherwise noted, assume all an effect's objects are handled as pointers within the effect.

3. Pointers are used when creating more complex objects, such as SegmentSets, paletteSets, particleSets, EffectSets, etc.

In some cases, we need to store an array of objects, like an array of Segments. The easiest way to do this is with an array of pointers. Each pointer in the array points to an object.

These complex arrays will all usually be declared in a similar way. Using a paletteSet as an example:

//(assuming you've already declared palettes 1 and 2, etc)
palettePS *paletteArr[] = { &palette1, &palette2 }; //The array of pointers to palettes
paletteSetPS paletteSet = { paletteArr, SIZE(paletteArr), SIZE(paletteArr) }; //The paletteSet struct

The first line creates the array of pointers, filling it with addresses of palettes 1 and 2. The second line creates the paletteSet struct, which contains a pointer to the paletteArr and the length of the array (ignore the second SIZE(paletteArr), you can read more about that on the palette's page linked above).

With these more complex objects, I always provide a sample declaration as part of the object's documentation (the above palette example is take from the palette docs), so hopefully you'll be able to get it working even if you don't 100% understand how it works. Note that you should usually create these objects at a global scope (before the Arduino Setup() function), to ensure that all effects have access to them.