Created
March 15, 2020 18:45
-
-
Save Eolu/334435a88c578c9e7f670ef7e56159ac to your computer and use it in GitHub Desktop.
Some pointers on C pointers (and C++ references) for new developers trying to grasp the essentials.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Note: I use hex nomenclature (0x...) to make it obvious that the value I'm talking about | |
// is a memory address. Under the hood there isn't any particular distinction (everything is | |
// represented in binary eventually). | |
void pointerception() | |
{ | |
// i is not a pointer. | |
int i; | |
// i can be set to a value | |
i = 8; | |
// Let's say that i has a value of '8' and is stored at memory address 0x04. | |
// Let's see how we can access these things: | |
// This will print 8 | |
printf i; | |
// This will print 0x04 | |
printf &i; | |
// Those are the only 2 valid ways we can access i. Let's delve deeper now: | |
// p is a pointer | |
int *p; | |
// p can be set to a value also, but this value will be treated as a memory address. | |
// this line means that p is the null pointer | |
p = 0; | |
// We can instead set p to a REAL memory address | |
// this line means that p is a pointer to i | |
p = &i; | |
// If we just print p, we get the memory address. | |
printf p; // This will print 0x04 | |
// However, we can dereference p to get the value. | |
printf *p; // This will print 8 | |
// If we tried to do the above line while p = 0, we would've gotten a null pointer exception | |
// Homework question: What happens if I do this? | |
printf &p; | |
// Now let's use some of the functions defined below. | |
//*************************// | |
// **** PASS-BY-VALUE **** // | |
//*************************// | |
// This function will make a copy of the value given to it. If we pass in 'i', it's just got a local | |
// variable with a value of 8. | |
valueFunction(i); | |
valueFunction(*p); // This will also pass in an 8. | |
// These things are legal but probably mistakes: | |
// I can pass in just p, but it will just convert the memory address to an int (so I'm passing in 4) | |
valueFunction(p); | |
// Same thing if we dereference i. This will pass in a 4 (probably not what you want) | |
valueFunction(&i); | |
//*****************************// | |
// **** PASS-BY-REFERENCE **** // | |
//*****************************// | |
// NOTE: C++ only. This doesn't exist in C. | |
// This function will create a reference to i, NOT a copy. That means that modifying the variable | |
// inside the function will directly modify the value of i. | |
referenceFunction(i); | |
// This is a bit odd, but maybe occasionally done. We could dereference p inside this function to | |
// access or modify the value it points to, OR we could directly change the value of p inside this | |
// function to make it point to something else. Ultimate power and ultimate responsibility. | |
referenceFunction(p); | |
// This is equivalent to passing in i (assuming p is a pointer to i, as it is currently) | |
referenceFunction(*p); | |
//****************************// | |
// **** PASS-BY-POINTER? **** // | |
//****************************// | |
// Note: there's nothing really special about "pass-by-pointer". This is just pass-by-value | |
// except the value will be a pointer to the memory address of whatever is passed in rather | |
// than the value it points to. While this CAN be used to modify the value in the caller (as we | |
// did with a reference), it truly does create a local (pointer) variable in the function (unlike a | |
// reference). To put it simply, passing i into `valueFunction` creates a new variable with a value | |
// of 8, and passing i into `pointerFunction` creates a new variable with a value of 0x04. | |
// The function gets a pointer to i (so it has its own variable just like this function has p) | |
pointerFunction(i); | |
pointerFunction(*p); // This is effectively the same as above | |
// The function gets as pointer to p. That's a pointer to a pointer. To actually get to the value | |
// of i it would have to dereference twice (so **pnt). | |
pointerFunction(p); | |
pointerFunction(&i); // Same thing here, except we can't modify the value of this function's p | |
} | |
void valueFunction(int val) | |
{ | |
// I can do things with val and I'll never affect anyone outside my scope! | |
} | |
// This is valid in C++. This does not exist in C | |
void referenceFunction(int& ref) | |
{ | |
// If I modify ref, I modify the same variable the caller passed in! | |
} | |
// This is considered oldschool. References are now the preferred way | |
void pointerFunction(int* pnt) | |
{ | |
// If I dereference pnt, I can access the same memory address that the caller had access to. | |
// But if I modify the value of pnt, I make it point to a new memory address and am no longer | |
// (necessarily) modifying data from the calling function. | |
} | |
// It's legal to do crazier things, like have a pointer to a pointer like this: | |
void pointerPointerFunction(int** pnt) { /* ... */ } | |
// Or even a reference to a pointer: | |
void pointerReferenceFunction(int*& pnt) { /* ... */ } | |
// If you work with enough code from various places, you will undoubtedly run into one of these | |
// things at some point (or something semantically similar). While they may look odd, they aren't | |
// doing anything special. | |
/* | |
* At the end of the say, (almost) all of this is just syntactic sugar. | |
* | |
* In assembly there are no pointers or references. We can treat any value as a memory address, | |
* and its up to us to only dereference real memory addresses lest we break something by trying | |
* to dereference a value. | |
* | |
* In C there are pointers but not references. The existence of the asterisk in a type (eg `int* p`) | |
* defines a pointer, and it exists so YOU can tell that this is a memory address rather than a | |
* value. There's nothing stopping you from storing a memory address in a type without the asterisk | |
* and dereferencing to get the value it points to it (save for a compiler warning). The only "not- | |
* just-syntactic-sugar" construct is the &, which returns the memory address of a variable (like we | |
* did when we wrote `p = &i`). We can also put the `*` in a function declaration, which effectively | |
* means "automatically create a pointer to whatever I pass in". | |
* | |
* C++ keeps all of this but later added something very convenient: references. These (perhaps somewhat | |
* confusingly) use the `&` symbol also, but this time in a function declaration instead of in the middle | |
* of an expression. Whereas pass-by-value says "Create a variable based on a value", and pass-by-pointer | |
* says "Create a variable based on the memory-address of a value", pass-by-reference doesn't truly "create" | |
* anything. It's really just like saying "Give this function access to this variable, just as if it were in | |
* the same scope with no particular copying or dereferencing". (This isn't the WHOLE story, but it's enough | |
* to be able to use references effectively. I'll leave it as an exercise to figure out where I've somewhat | |
* misrepresented what's going on here). | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment