This section is about a trick where one variable can point to another. We can’t use it with the variables we have now – we have to use a class, which is like a struct, but uses pointers. Pointers also require you to create variables in a different way than declaring them – you new them – so this section is also about that.
That’s a lot of stuff at once. This is one of the more complicated things in programming, and especially C#, so don’t worry if you have to read it a few times.
But pointers are very useful. They’re one of the main concepts of an intermediate level coder. I promise that hurting your head reading this chapter will be worth it.
Before giving the rules and theory, I’ll give a working simple example using classes, pointers and new (which is completely different from the new with structs).
I’ll say what happens in each step, but I’ll save the real explanation for later,
including what they’re good for:
A class is defined and used the same as a struct – fields and the dot-rule. This defines a class Dog. It’s exactly like a struct Dog would be, except for the word class in front:
A special rule for classes says you can never declare actual Dog’s. When you think you’re declaring one, you’re really making a pointer to a Dog:
These are real variables, which can point to any Dog. We have to create the Dog separately. Since we didn’t yet, these point to nothing.
We can prove it. Try d1.name="spike";. For a struct, that would work. But now
we get a ‘Use of unassigned variable’ error. It’s telling us d1 isn’t pointing to a Dog
yet.
The only way to create a real Dog is with the command new Dog(). It creates a fresh free-floating Dog with no name. If we put d1= in front, it hooks d1 up to it:
In the picture, the box on the right is not d1. It’s the Dog we made with new, and
it doesn’t have a name. d1 happens to be pointing to it, but that could
change.
Even though it has this funny set-up, we can now use d1 like a struct. These lines understand to follow d1 to where it points:
These dots are doing one more step than the old dots – they have to follow the arrow coming out of d1. But the end result is the same.
This next part is where arrows matter. d2=d1; causes d2 to point at the same dog as d1. This is a totally new thing and something we can only do with pointers:
There’s one free-floating dog, and two ways to get to it. We can show this dog-sharing by using one variable to change and the other to read:
If you understand how d1 and d2 are looking at the same dog, this should
make sense. If we co-own a poodle and I shampoo it, “your” poodle was also
shampooed.
I’ll add one more line to the example, another d1=new Dog();. It makes a second free-floating Dog with d1 changed to aim at it:
d2 is now the sole owner of the original Dog. They no longer share one. d1.name="Scruffles"; no longer changes d2.
This section has the rules for each part: pointers, how new Dog() really works, and then the exact rules C# uses for pointers and new together. It’s still a lot at once. After this are more real examples of how we use them.
A pointer variable can’t store anything on it’s own. All it’s good for is pointing to a real variable. Suppose you have int a,b,c,d; and int-pointer p1. The program only has 4 ints in it. p1 isn’t an int – it’s a way to pick out one of them. p1 can point to a, b, c or d. The most common way to think of a pointer is as an arrow.
Pointer isn’t a type by itself. It has to be a pointer to some real type – Dog d1; is a pointer to a Dog, and can only point to Dogs. There’s no such thing as a generic pointer which can point to a Dog or a Horse or any type. We just use the word pointer as a shortcut for “pointer to whatever type we were just talking about.”
Inside, all pointers are really the same – just arrows. Even if Horse was a huge
class with 20 fields, a Horse-pointer would still be just a simple arrow.
A pointer variable can do two things: you can make the arrow point somewhere, or you can follow the arrow and change what’s in that box. Since it’s a variable, you can make it point somewhere, use it, then make it point somewhere else.
This uses p to change d1, then to change d2:
We used p.name= twice. But it was aimed at a different Dog each time, so we changed two different names.
The big thing here is that new Dog() is a command. It creates a Dog every time it’s
run. Normal variables don’t work that way.
We need to back up a little into computer theory. There are two general kinds of variables: Stack and Heap. Everything we’ve used before, including structs, is a Stack variable. The computer is very good at automatically managing those.
For example, variable declarations aren’t in the final program. The compiler scans your program, finds all declares, and pre-makes a chart of them. Then it finds everywhere they’re used and replaces them with the pre-set spot in the chart. Before the program starts to run, n=7; is turned into “memory location 24 = 7”.
Functions are mostly the same. The compiler does all of that work, using the locals. The resulting chart is called a stack frame. When you call a function, it grabs enough space to hold the stack frame, in one step. Every local variable is created at once. When the function ends, the whole stack frame is popped off.
If you have a chain of function calls, each adds its stack frame on top. That’s why the computer never gets confused by re-using the same variable name for different locals. They’re stored in completely different stack frames. Computer chips even have this built-in – they have a spot for the start of the current stack frame. Local variable look-up circuits use it.
As cool as those details are, you don’t need to know them. The main point is that
normal declared variables are pre-made.
As you might guess, Heap variables are the opposite. You start with none.
new Dog() is a real command that makes a Dog. Dog’s are put into a big
area with everything else ever created using new. That’s why it’s called a
heap. It’s a big pile of whatever, in no special order and without very good
record-keeping. The only way to find anything on the heap is through a
pointer.
The stack starts at one end of memory. The Heap starts at the other. Here’s a picture of a program which has run for a while (Update has called the function doMove. And someone ran new Dog() 3 times and new Horse() once):
When doMove finishes, its part pops away. The same for Update. But those Dog’s
and the Horse are just floating on the Heap, living their lives.
One more example, a useless function that makes some Dogs:
When we run this, the system instantly creates nm, n and even age for the loop. When it quits they snap away. No Dogs are made yet. When the loop runs, it creates 2 to 5 permanent Dog’s. They don’t go away when the function ends. Every time it runs, we get 2 to 5 more Dogs. They all live together on the heap:
Heap variables don’t have names. They don’t care which function made
them. They’re just free-floating anonymous Dogs. The only way to use one
is if you have a pointer to it. So all of those D’s are useless space-wasting
garbage Dogs. That’s why new Dog(); returns a pointer – it has to. Dog
d1=new Dog(); is the pointer d1 catching the return value of the new Dog();
command.
Things on the heap live forever, sort of. Some systems have a command to
individually destroy one. Other systems, like Java and C# use what’s called Garbage
Collection. When a Dog get lost – there aren’t any more pointers to it – the system
eventually auto-deletes it (more on this later).
Fun facts about other new’s:
I mentioned heap variables don’t have names, but it seems like Dog d1=new Dog(); makes a Dog named d1. This shows how that’s not true. It uses p to make two Dogs, handing them off to d1 and d2:
d1 is aimed at a Dog originally made for p. We could think of that Dog as d1, unless we plan to hand it off somewhere else. d2 is the same way.
In general, pointers are like add-ons. You can have an int and also a pointer to an int. But several languages, including C#, simplify this with a trick. They make it so a type can either never use pointers, or has to use them. That’s simpler since you never have a choice.
Our old types are the first way – they can’t use pointers. So none of these new rules apply to ints, strings. floats or structs. You can’t have a pointer to an int, or use new int() to create one out of thin air.
A class can use pointers. It has to. You can never declare a normal Dog. This is
why class and struct look like the same thing. They are, in both flavors – never
pointer, or always pointer. In practice, when you’re creating a struct you have to
decide – struct or class? Does this need pointers?
A summary, plus some new rules:
The technical term for pointer-only types is reference type. Struct is normal. Class is a Reference Type. I personally would have called them Arrow types, but reference is like “refers to”, so not too bad.
In practice, 90% of class variables are used like structs, with no pointer tricks. The rules were specially tweaked to do this. This code would work the same whether Dog’s were classes or structs:
We declared d1 to be the forever “owner” of its Dog. We’ll never use any pointer
tricks. It’s easier to think of d1 as a normal Dog variable. d1=d2 will result in pointer
weirdness, so we won’t do that.
The other way we’ll use them is as real pointers. When we declare them, we’ll mentally decide they’ll never “own” anything. They’ll only point to other people’s dogs. Here p is a pointer-style dog:
Technically that makes 1 Dog with both d1 and p pointing to it. But logically it
makes Dog d1 and a pointer temporarily aimed at it.
Here’s a semi-realistic example. There are two “real” dogs and one pointer showing the active one. Pressing A or S switches the pointer. The age of the active dog increases:
If you see this picture in your head, you’ve got the idea of pointers:
actveDog selects pet1 or pet2, by aiming at it. activeDog.age++; is really increasing pet 1 or 2’s age.
Classes can be function inputs and return values. They’re still always pointers, but we’ll use our two ways of thinking: most of the time we’ll pretend they’re normal variables; but occasionally we’ll use them as real pointers.
We can call a function with class inputs and pretend they’re structs. This normal-looking function converts a Dog into a string:
There’s nothing special about it. But a neat thing: d is a pointer. It’s aimed back
at the actual Dog that called us.
Because of pointers, functions can change their inputs. This fills a Dog with the name and age we give:
setupDog(pet1, "Gary", 4); actually changes pet1. It works because d points back to what called us. Changing d is really changing pet1:
Even though they’re in different areas, d can aim at pet1 and change it remotely,
through the magic of pointers.
This next one is about the same. It adjusts a Dog away from wrong values:
checkMe(pet1); changes pet1 for real, if it had a bad age or name.
A common class function copies one into another. The first Dog gets the contents of the second:
copyDog(pet1, pet2); turns pet1 into a copy of pet2 without making them share a Dog. This are useful, since there’s no built-in way to copy the contents of one Dog into another.
When a function looks like it returns a Dog, it’s really returning a pointer to a dog. But most of them seem like normal functions. Only a few return a “real” pointer.
The simplest dog-returning functions work like new – they create a dog and return a pointer to it.
Here’s a very basic one which is merely a shortcut for new:
We could run it like Dog d1 = makeDog();. It works since new Dog() creates a
general purpose free-floating heap Dog. It’s not tied to the function like local
variables are. When the function ends, that Dog is still there and d1 is pointing to
it.
This version does that same thing using a middle-man variable:
When the function ends, local variable d is destroyed. But once again, the actual Dog is still there and we get it. This version shows that even more:
It’s easier to see how d is basically a temporary. It’s used to set up the Dog’s base stats, before it’s passed along to us. Dog pet1=makeDog3(); gets a 2-year old dog named dog.
Finally, here a useful version, which allows us to give a name and age:
Dog pet1=makeDog("Spike", 6); is now a nice substitute for a new and two
assignment statements.
Let’s backup a little and compare setupDog and makeDog:
MakeDog creates a Dog, setupDog assumes you already have one. It’s about that
extra step classes need to create the real object.
For more fun, suppose we call makeDog twice in a row. It’s not really a problem:
That makes 2 Dogs and throws away the first one. No one has a pointer to it, so it can never be found. A picture:
It’s the same thing that happens if you write d1=new Dog(); twice in a row. No
harm, but a wasted Dog.
Another common Dog-making function is a clone. It creates a copy of another Dog:
It’s pretty much the same thing as the function taking a name and age. Dog d2 = clone(d1); gets a copy.
To sum up:
More rare functions return a true pointer. They give you back something aimed at an existing Dog.
This function takes 2 dogs and returns a pointer to the oldest:
The advantage is that we can use the output like a pointer, like activeDog in an earlier example. We can use it to change the selected Dog:
This next one cheats a little by assuming we have globals named pet1 and pet2. It randomly chooses one:
Dog pp = randomDog(); pp.age++; would randomly age pet1 or 2. The logic is the same. In out minds pp is merely a pointer, aimed at some existing Dog, used to remotely play with it.
A neat example of how structs and classes are very different is the “change the input” trick. If Dog was a struct, this would give us a baby Dog, named after the mother:
adultDog is a copy. It’s fine to change it and return it as the answer.
But with Dog as a class this is a mess. First it changes the original Dog,
then it returns a pointer to the same Dog instead of making a fresh one.
Dog baby1 = babyDog(pet1); causes both Dogs to be aimed at the same
thing.
To make a fresh baby Dog, we need a new. This works when Dog is a class:
This version is basically a modified clone function. It gives us a copy of adultDog, but younger.
Pointers are allowed to point nowhere. The official value is null (all lower-case.)
We often use it to initialize a “real” pointer, as a hint:
As we all know from movies, a null-ray is the most devastating sort of ray. It’s common to think null in a program is like that – that it destroys what we’re pointing to. But it’s harmless. null is simply a permanent place any pointer can change itself to, that means nowhere. If it helps, null is really just 0.
An example of null not causing havok:
A common use for null is showing that you’re done using a pointer:
If the function continues, p=null; let’s us know we’re done changing the oldest
Dog, and gives an error if we accidentally use p again without aiming it somewhere
else.
The most special thing about null is, obviously, you can’t use it. Trying to is an error:
We get a run-time error: NullReferenceException: Object reference not set to an
instance of an object. That seems clear enough.
We check for null using == and !=:
Checks for !=null are very common. Often they’re error checks at times when the variable should never be null. Here’s a copy function that won’t crash if you give it null dogs:
The correct thing to do on an error can be tricky. If we have dogCopy(d1,
dOoops) and dOoops is accidentally null, what should happen? If it really should
never be null, maybe we should print an error message and crash. That gives us a
chance to fix it. Maybe I should have set the name to NULL COPY and age to
-999.
Sometimes pointing nowhere isn’t a mistake. Often we want a “nothing selected” option. null is the best way to say that. Here’s the dog-age-adder from way back where the D-key selects “no dog”:
Logically, activeDog can can be 3 things, each as good as the other: pet1, pet2,
or nothing.
The common term for a pointer set to null is a null pointer. For example if(p!=null) is checking for a null pointer.
Almost all the errors we get will be crashes from trying to follow a pointer set to
null. The error message is: nullReferenceException. Object not set to an instance of
an object.
A common way to get that error is forgetting to new it. If you remember, all global variables are auto-inited to 0 or "". Pointers are auto-inited to null:
This is a run-time error, which means the program will run normally,
then crash when it comes to the line. As usual, all of Unity won’t crash.
Getting these is no worse than getting any other error (except an infinite
loop).
The same problem with a local Dog gives a little different error:
We get a normal error this time Use of unassigned local variable ‘d1’. That’s because d1 is still a pointer, and local variables aren’t automatically initialized. It’s not even null. The computer can tell it’s definitely an error to follow an arrow which has never been set.
Oh, no!! More rules! These are some examples of the pointer == rule. If you
remember, d1==d2 checks whether they are sharing the same Dog.
Here’s an example why we like this rule. The A key switches between pet 1 and 2:
This works even if the pets have the exact same values. if(activePet==pet1)
checks whether we’re pointing exactly at pet1.
Then here’s a wrong way to use pointer ==. The test is always false since they are two different Dogs:
We’d need to write it out: if(pet1.name==pet2.name && pet1.age==pet2.age). Don’t get confused about == with non-pointers. if(pet1.name==pet2.name) is a normal string compare.
This is a fun section that’s not super important. The short version: remember how in
the top section I showed how you could waste memory by creating lots of Dogs with
new’s you didn’t need? That’s not a problem. The computer will eventually clean
that up. Don’t worry about using too many.
The slightly longer version: those extra Dog’s are called garbage. If enough get
made, memory runs out and the program crashes. Every so often, C# automatically
hand-checks and deletes them. That’s called garbage collection.
And now the long, boring version: the computer is excellent at managing regular variables, using the stack frame idea. Heap variables – Dogs made with new – are more versatile, but the system can’t manage them. When you lose your last pointer to one, the system doesn’t know.
The only way to find useless Dogs is to look at every declared Dog in the entire program, see where they point, mark those Dogs as in use; then go back and remove every unused Dog. That’s garbage collection. It happens automatically. There are some tricks, but it’s not fast.
In theory, a sloppily written program, that creates and loses lots of extra Dogs, will have a small hiccup every few minutes. It’s pausing to run garbage collection to free up memory. In practice you don’t notice.
It’s common to make garbage without trying. This program has a 1% chance to reset the Dog’s name and age, except it uses new, so turns the old Dog into garbage:
It’s not many extra Dogs, so it’s fine. I think the “slow” version with new is easier
to read
If you know you’re done using a Dog, it seems as if there would be a command to
delete it, just to speed things up, but there isn’t. d1=null; is the closest
we can come. The next garbage collection step will clean up your old, lost
Dog.
The Reference Type system was invented to make automatic garbage
collection work. That’s partly the reason we can never have pointers to int’s or
declare a normal Dog. Eliminating those options made garbage collection
possible.
There are two other systems, not in C#, to handle garbage. C++ has no garbage collection. Instead it has a delete(d1) command. If you forget to use it, the garbage is there forever. C++ programs are very careful that every new has a delete. In return, C++ pointers can be from any type and can go anywhere.
Other languages use Reference Counting. Every Dog has an extra counter for how many things point to it, automatically updated. When the count hits 0, the system destroys it. The same as C#, you don’t need to do anything. It runs a little slower, but makes it up by not needing to pause for garbage collection.
C# purposely makes it so classes and structs use new to mean different things. That’s odd. Let’s compare them again, then try to see why they made that rule.
Vector3 is a struct. It’s a normal variable. new is only used for the optional constructor shortcut to fill in several variables at once. But for a class, new is required to make it exist:
We don’t normally like to double-use like this. For example we invented == for comparing since we didn’t want to use = to mean assign and compare.
For new, the idea is that not everyone knows class vs. struct very well. Here are the “real” commands (this is how every other language does it):
That’s nice – new means to actually make something. No new means you’re not
making something new.
But C# was partly written for beginners to just jump in. They don’t know what new is, and why you’d use it one place but not the other. For them they added the rule that structs get a fake new. Everything looks the same.
C# has real pointers. You aren’t allowed to use them, but it might be nice to see them, for comparison. They require you to write out every step, so you get to see what the shortcuts are doing.
To repeat, you aren’t allow to use these. They only run in “unsafe mode”,
and no C# programs are ever written in unsafe mode (this includes Unity
programs).
This example declares ints and pointers to ints:
Remembering int* means “pointer to an int” can be a pain. That’s why Dog d1;
just knows to make a pointer to a Dog. But being able to write both types of things
can be nice.
Likewise, we can point to a real int, but need another symbol:
This is cool because the types always match. p2=p1; is two int-pointers. &n1
calculates a pointer to n1, so the types also match. It’s confusing at first to remember
those symbols, but once you do, it’s easier to read.
We have to use a special symbol to follow a pointer’s arrow:
But once you figure out the rule, (*p2)=5; spells out how p2 is a pointer
remotely changing something.
Those written-out rules allow us to use = both ways, pointer assign and copy:
You don’t need struct vs. class with real pointers. A struct can be either:
And again, this is completely not important to know. But you might get a feel for reference types. These are the original rules.