diff --git a/README.md b/README.md index f18801a..f8398fc 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,8 @@ Also (hopefully) a nice read for new [Husky Satellite Lab](https://huskysat.org/ - [Representing Numbers](notes/numerical-representations.md) - [Primitive Data Types](notes/primitive-data-types.md) +- [Pointers](notes/pointer-basics.md) +- Smart pointers: + - [Part 1: `unique_ptr`](notes/smart-pointers-1.md) + - [Part 2: `shared_ptr`](notes/smart-pointers-2.md) diff --git a/images/control-block.PNG b/images/control-block.PNG new file mode 100644 index 0000000..0ad2eaf Binary files /dev/null and b/images/control-block.PNG differ diff --git a/notes/pointer-basics.md b/notes/pointer-basics.md new file mode 100644 index 0000000..d4ab27f --- /dev/null +++ b/notes/pointer-basics.md @@ -0,0 +1,221 @@ +# Pointers + +Contributors: Edward Zhang + +--- +A **pointer** is a type of variable that stores an address + +Pointers are most commonly used to indirectly access another object at some address +- Thus, a pointer "points" to whatever data is located at the address it stores + +Example: +```C++ +int x = 333; +int *p = &x; +// Print p's value. Here, it's the address of variable x +std::cout << p << std::endl; +// prints something like: 0x7fffb2cf5bc0 +``` + +--- +## Declaring a pointer + +A pointer declaration gives our pointer variable a name and specifies the *type of object* to which our variable "points" + +Generic definition: `typeName* variableName;` + +Example: +```C++ +int *p; // Pointer variable with name "p" points to some integer +std::string *p2; // p2 points to some string object + +``` +- Writing `typeName *variableName;` also works + +--- +## Setting the value of a pointer + +A pointer stores the address of some object +- Thus a pointer variable's "value" is the address of some object, NOT the value of the object itself + +You can get the address of an object using the **address-of operator (&)** + +```C++ +int x = 333; +int *p = &x; +``` +- Now `p` holds the address of `x`, aka `p` is a pointer to `x` + + +Notice that the type of the pointer and the object to which it points must match + +```C++ +double x = 333; +int *p = &x; // Compiler error +``` + +You also shouldn't treat the value of a pointer as a number. For example, you cannot assign an int to a pointer like this: + +```C++ +int *p = 808; // Compiler error +``` + +--- +## Dereferencing a pointer + +So how do we actually access the object a pointer POINTS to? + +We can use the **dereference operator (*)** to get the object to which the pointer points + +```C++ +int x = 333; +int *p = &x; // p points to x +std::cout << *p << std::endl; // prints 333 + +``` + +This means we can also assign to the object a pointer points to +- Only if the pointer is not `const` though + +```C++ +int x = 333; +int *p = &x; + +*p = 124; +std::cout << *p << std::endl; // prints 124 now +``` + +--- +## Null pointers + +Null pointers do not point to any object +- I.e. they point "nowhere" + +Dereferencing a null pointer will cause a segfault + +To get a null pointer: +```C++ +int *p = 0; +int *p2 = nullptr; +int *p3 = NULL; +// All 3 ways of initializing null pointer are basically equivalent + +``` +- C++ introduced `nullptr`, which has type `std::nullptr_t` +- C's `NULL` macro is defined by `cstdlib` as `0` +- However, `nullptr` avoids some ambiguity when resolving function overloads since `NULL` tends to get treated as an `int` + +--- +## Pointer arithmetic + +You can perform arithmetic on pointers, for example: +```C++ +char *p = ... +int *q = ... +long *r = ... + +// Note: the "..." isn't valid C code, just filler +// For the purposes of the example, let's assume: +// p has value = 0x1000, q = 0x2000, r = 0x3000 +// Remember the value of a pointer variable is some address + +p += 3; +q += 3; +r += 3; + +// What are the values of p, q, and r now? + +``` + +Answer: +- `p = 0x1003, q = 0x2012, r = 0x3024` + + +Why? + +First, the type of the object a pointer points to matters. In the above example, `q` points to an int object. + +You might've already guessed the pattern. In general, for `typeName* ptrName`, the expression `ptrName + k` really evalutes to `ptrName + k * sizeof(typeName)` + +So in the above example `r + 3` would be the same as `r + 3*sizeof(long) = r + 3*8 = 3000 + 24 = 3024` +- Intuitively, this means we are moving the pointer right by 3 `long`s worth of memory + + +--- +## void* Pointer + +The type `void*` is a special pointer type + +Pointer variables of this type can hold the address of any object +- Basically the pointer holds an address, but we don't know the type of object at that address + +```C++ +int x = 333; +void *p = &x; + +// Now p holds the address of x +// You could cast p to be an int pointer, then dereference it to get x's value +std::cout << *((int*)p) << std::endl; + +``` + + +--- +Now let's examine some common use cases for pointers + +--- +## Output parameters + +Remember that C/C++ passes arguments by value +- Thus the callee receives a local copy of the argument +- If the callee modifies a parameter, the caller's copy is NOT modified + +Bugged example: +```C++ +void foo(int x){ + x = 333; +} + +int main(){ + int x = 351; + foo(x); + std::cout << x << std::endl; // what gets printed? +} + +``` +- Answer: `351` is printed! Updating `x` in function `foo` does not modify the caller (`main`'s) version of `x` + +Solution: we can mimic pass-by-reference using pointers! +- Sure, the pointer VALUE (some address of an object) is passed by copy, but the address still indirectly gives access to the same object + +Fixed example: +```C++ +void foo(int *x){ + *x = 333; +} + +int main(){ + int x = 351; + foo(&x); + std::cout << x << std::endl; // Now prints 333 +} +``` + +--- +## Dynamic Memory Allocation + +Both `malloc()` and `new` allocate a block of memory on the heap, then return a pointer (if allocation succeeded) to the start of the block + +```C++ +int main() { + int *p = new int(333); + std::cout << *p << std::endl; // prints 333 + + int *q = (int*)malloc(sizeof(int) * 3); + q[1] = 124; + std::cout << q[1] << std::endl; // prints 124 + + free(q); + delete p; +} +``` \ No newline at end of file diff --git a/notes/smart-pointers-1.md b/notes/smart-pointers-1.md new file mode 100644 index 0000000..46bd5f9 --- /dev/null +++ b/notes/smart-pointers-1.md @@ -0,0 +1,247 @@ +# Smart Pointers, Part 1: unique pointers + +Contributors: Edward Zhang + +--- +## The problem with raw pointers + +Raw pointers are tricky to work with due to a variety of reasons + +- Its declaration doesn't tell you if the pointer points to a single object or an array + - ... do you use `delete` or `delete[]`? +- Its declaration doesn't tell you if the pointer owns the object it points to + - ... should you destroy the pointed-to object after you're done using the pointer? +- Easy to cause dangling pointers (i.e., objects are destroyed but we still have pointers to them) +- Easy to cause double frees +- Easy to cause memory leaks + +--- +## Smart pointers to the rescue! + +Smart pointers can help us! + +A **smart pointer** is an object that stores a pointer to a heap-allocated object +- Think of them as wrappers around raw pointers +- Smart pointers look and behave like regular pointers + +However, smart pointers can automatically delete the object to which it points, at the right time + +Let's take a look at 3 kinds of smart pointers + +--- +## `std::unique_ptr` + +A `unique_ptr` is a kind of smart pointer +- Use this for managing resources with exclusive ownership semantics +- Very little overhead, can be used even when memory is limited + +Since it is a kind of smart pointer, a unique pointer will take ownership of a pointer to an object + +The `unique_ptr`'s destructor will invoke `delete` on the owned pointer when the `unique_ptr` object is destroyed + +General syntax: +``` +std::unique_ptr varName(p) +``` +- Where `p` is a pointer of type `typeName*` to heap-allocated memory + +Example: +```C++ +#include // std::cout, std::endl +#include // std::unique_ptr + +void foo(){ + std::unique_ptr x(new int(333)); + // You can dereference x as if it were a normal pointer + *x += 20; + std::cout << *x << std::endl; + + // No leaks, even though we never used delete! + // When x falls out of scope, we run delete on its owned ptr +} + +``` + +--- +## `unique_ptr` Operations + +For unique pointer `x`: + +| Function | Purpose | +|--- |--- | +| `x.get()` | Returns a pointer to the pointed-to object | +| `*x` | Returns value of pointed-to object, basically normal dereference | +| `x.reset(ptr)` | Run `delete` on the current owned pointer, then store a new one, `ptr` | +| `x.release()` | Returns the pointer, sets wrapped pointer to `nullptr`- releases responsibility for explicitly freeing back to user | + +Example: +```C++ +#include // unique_ptr +#include // cout +#include // EXIT_SUCCESS + +int main(){ + // x is a unique pointer that owns an int* + std::unique_ptr x(new int(333)); + + // x.get() returns pointer to pointer-to object + int* ptr = x.get(); + std::cout << *x << std::endl; // prints 333 + + // delete current pointer, stores a new one + x.reset(new int(124)); + std::cout << *x << std::endl; // prints 124 + + // smart pointer releases responsibility for freeing back to user + ptr = x.release(); + delete ptr; + + return EXIT_SUCCESS; +} + +``` + +--- +## `unique_ptr` is not copyable + +As its name implies, `std::unique_ptr` cannot be copied +- Copy constructor and copy-assignment operator are disabled +- Compiler error if you try + +Bugged example: +```C++ +std::unique_ptr x(new int(333)); +std::unique_ptr y(x); // Compiler error - cctor disabled + +std::unique z; // z is nullptr +z = x; // Compiler error - copy-assignment disabled + +``` + +--- +## Bug #1: double free + +We've seen that you can't copy unique pointers, but you can still wrap the same raw pointer with multiple unique pointers. This causes a problem + +Example: +```C++ +int main() { + int* p = new int(333); + std::unique_ptr x(p); + std::unique_ptr y(p); + + // What happens here at the end of main? +} +``` +- Now both `x` and `y` "own" pointer `p` +- When `y` falls out of scope, it will call `delete` on `p`, which it owns +- But then when `x` falls out of scope, it also will call `delete` on `p`, which has already been deleted! +- Causes a double-free! + +--- +## Bug #2 + +Also remember that smart pointers are intended to wrap raw pointers to HEAP-ALLOCATED memory + +You should never try to `delete` memory on the stack anyways + +Bugged example: +```C++ +int main(){ + int x = 333; + std::unique_ptr p(&x); // BUG +} +``` +- This will still compile +- However, `&x` is a pointer to memory on the stack, so when the unique pointer tries to `delete` it will cause undefined behavior + + +--- +## Transferring ownership + +You can transfer ownership of a pointer from one `unique_ptr` to another using `release()` and `reset()` in combination + +```C++ +int main() { + std::unique_ptr x(new int(333)); + + std::unique_ptr y(x.release()); + // y now owns x's old pointer + x.reset(new int(124)); + + std::cout << *x << std::endl; // prints 124 + std::cout << *y << std::endl; // prints 333 + +} +``` + +You can also use move semantics (will cover later) + +```C++ +std::unique_ptr x(new int(333)); +std::unique_ptr y = std::move(x); + +std::cout << x.get() << std::endl; // prints 0 +std::cout << y.get() << std::endl; // prints something like 0x602000000010 +``` + +--- +## `unique_ptr` and STL + +Unique pointers can be stored inside STL containers +- Since `unique_ptr` supports move semantics, STL will move rather than copy + +However if you try to sort, say a vector of unique pointers, you'll be sorting based on pointer address comparison, not the pointed-to objects + +Instead, provide a custom comparator function + +```C++ +bool cmp(const unique_ptr &x, const unique_ptr &y) { + return *x < *y; +} + +void printFunction(unique_ptr &x){ + cout << *x << endl; +} + +int main(){ + vector> vec; + vec.push_back(unique_ptr(new int(333))); + vec.push_back(unique_ptr(new int(124))); + + // Warning: don't do this + sort(vec.begin(), vec.end()); + // Do this + sort(vec.begin(), vec.end(), &cmp); + for_each(vec.begin(), vec.end(), &printFunction); +} +``` + +--- +## `unique_ptr` and arrays + +One last thing about unique pointers- they can also wrap arrays + +```C++ +std::unique_ptr x(new int[5]); +``` + +When `x` is destroyed, it will call `delete[]` appropriately on its owned pointer + +Note: instead of using a `std::unique_ptr` for arrays, it might be better to use `std::array` or `std::vector` + +--- +## Conversion to `std::shared_ptr` + +`std::unique_ptr` can be converted into a `std::shared_ptr` +- More on shared points in [Smart Pointers Part 2](./smart-pointers-2.md) + +For example, you can write a factory function that returns a unique pointer + + +--- +## Moving on! + +Just from looking at one type of smart pointer, the `unique_ptr`, we've resolved all of the issues described in the [first section](#the-problem-with-raw-pointers) + +In the [next chapter](./smart-pointers-2.md), we'll look at another type of smart pointer, the `shared_ptr` \ No newline at end of file diff --git a/notes/smart-pointers-2.md b/notes/smart-pointers-2.md new file mode 100644 index 0000000..56e20b5 --- /dev/null +++ b/notes/smart-pointers-2.md @@ -0,0 +1,210 @@ +# Smart Pointers, Part 2 + +Contributors: Edward Zhang + +--- +## `std::shared_ptr` + +Let's look at shared pointers! + +These are similar to unique pointers, but now we allow objects to be shared / "owned" by multiple shared pointers + +--- +## Reference counting + +If multiple shared pointers can own an object, how do we know when to `delete` the pointed-to object? + +Quite intuitively, we use the notion of **reference counting**. For each object, we keep a count of how many shared pointers are pointing to it +> NOTE: this section provides some nice intuition for reference counts. However, keep reading to see how complications can arise because of the actual implementation + +The reference count for an object is adjusted like so: +- When we make a new shared pointer to the object, increment its reference count by 1 +- When a shared pointer to it is destroyed, decrement reference count by 1 + +Note that shared pointers can be copied +- I.e., its copy constructor and copy-assignment operators are defined +- Copy-assignment can also increment or decrement reference counts (see next section) + +Finally, when a shared pointer sees that the object's reference count is 0, destroy the pointed-to object + +--- +## Creating shared pointers + +Example 1: +```C++ +int main(){ + int *p = new int(333); // pointing to an int object + std::shared_ptr x(p); + // Now reference count to int object is 1 + + // y also shares ownership of int object, + // reference count = 2 + std::shared_ptr y(x); + + // y now falls out of scope and is destroyed + // ref count for int object is 2-1 = 1 + + // x now falls out of scope and is destroyed, ref count = 0 + // Since ref count = 0, delete int object + + // Again, no memory leaks! +} +``` + +Example 2: copy-assignment +```C++ +int main(){ + // p points to an int object, let's call the object "P" + int *p = new int(333); + std::shared_ptr x(p); // ref count for P = 1 + + int *q = new int(124); // q points to another int object, "Q" + std::shared_ptr y(q); // ref count for Q = 1 + + std::shared_ptr z(x); // ref count for P = 2 + x = y; + // x now goes off to share ownership of "Q", abandoning "P" + + // What are the ref counts? + // Answer: ref count for P = 2-1 = 1, ref count for Q = 1+1 = 2 + +} +``` + +Earlier I said that "for each object, we keep a count of how many shared pointers are pointing to it." So Example 3 should work just fine right? + +Example 3: bug bug bug +```C++ +int main(){ + // p points to an int object, let's call it "P" + int *p = new int(333); + std::shared_ptr x(p); // shared pointer #1 to P + std::shared_ptr y(p); // shared pointer #2 to P + + // So what is the reference count for P? + // You'd expect 2 right? + // Right? + // ... +} +``` + +Actually no, running Example 3 causes a runtime error (double free) + +Why? Let's see the next section for how reference counts are implemented. Long story short, in Example 3 we create two DIFFERENT reference counts for object `P`, resulting in a double free + +--- +## Implementing reference counts + +As an exercise, think about how you would implement reference counts +- Hint: conceptually the reference count is associated with the object being pointed to, but is this feasible to implement? + +Simplified answer: each shared pointer keeps track of its pointed-to object's reference count + +Real answer: each `std::shared_ptr` object contains a pointer to a data structure called the **control block** + +The control block stores a reference count for the object the shared pointer is pointing to, among other things +- The control block can also contain a copy of some custom deleter and something called a weak count (not covered here) + +Here's a nice visual: + +![](../images/control-block.PNG) + +--- +## Control block creation + +The key thing is to understand when a control block is actually created + +Rules for control block creation: +1. `std::make_shared` (which we will cover later) ALWAYS creates a control block. This makes sense since `make_shared` creates a new object to point to +2. When the `std::shared_ptr` constructor is called with a RAW POINTER, it creates a new control block +3. Calling a `std::shared_ptr`'s copy constructor does NOT create a new control block, just increments the existing control block's reference count by 1 + +--- +## `shared_ptr` double free bug, revisited + +Control block creation rule #2 in the last section tells us why our earlier [Example 3](#creating-shared-pointers) failed + +Example 3: double free bug +```C++ +int main(){ + // p points to an int object, let's call it "P" + int *p = new int(333); + std::shared_ptr x(p); + std::shared_ptr y(p); + + // double free! +} +``` +- First, `x` has a field pointing to a new control block with reference count = 0 for `P` +- Then, since we create `y` by passing in a raw pointer to its constructor, `y` has a field pointing to a new control block, also with reference count = 0 for `P` +- We have multiple reference counts! +- When `y` falls out of scope, we decrement its reference count for `P` to 0. Since reference count = 0, destroy `P` +- Then when `x` falls out of scope and its reference count for `P` falls to 0, we try to destroy `P` again - double free!! + + +> In practice, avoid creating `std::shared_ptr`s from variables of raw pointer type + +--- +## Some extra notes on `shared_ptr` + +Before C++17, shared pointers did NOT support wrapping arrays, which meant you had to provide a custom deleter. As of C++17, you can safely do things like: +```C++ +std::shared_ptr x(new int[10]); +``` + +
+ +As you may have realized, `shared_ptr` has more overhead than raw pointers or `unique_ptr`, typically twice as big +- Try to use `unique_ptr` unless you truly need shared management + + +
+ +Increment and decrement of the reference count must be atomic +- This must be the case, otherwise threading will cause problems + +--- +## Be careful! + +Be careful when working with shared pointers, there are quite a few ways to inadvertently give yourself a headache + +What is wrong with the following example? +```C++ +int main(){ + int* x = new int(333); + { + // Temporary inner scope + std::shared_ptr y(x); + } + std::cout << *x << std::endl; // What does this print? +} +``` +Answer: +- You might expect `333` to be printed +- Actually no, we expect undefined behavior here + +
+ +I'll write out the example again with comments explaining why + +```C++ +int main(){ + // x is pointing to some int object "X" + int* x = new int(333); + { + // Temporary inner scope + + // Ok, y has field pointing to control block + // with reference count for X = 1 + std::shared_ptr y(x); + + // End of block, y falls out of scope + // y's reference count for X is 0 + // So destroy X! + } + std::cout << *x << std::endl; + // Whoops, we're trying to access freed memory... +} +``` +--- +See the next chapter for one more type of smart pointer \ No newline at end of file