C++_Pointers - RicoJia/notes GitHub Wiki
========================================================================
========================================================================
-
new and delete: new must be deleted, otherwise there will be a memory leak!!. see code
- new
- new will call default initialization (if we don't have args ofc).
- new[]:size must be provided as part of the signature
initialize an array using new: new int[10](); new int[5] {1,2,3}; // without 5 wouldn't compile``
-
new[] must match delete[]. A simple delete will yield undefined behaviour.
- delete should not be used on heap allocated objects
- delete vs delete[]
- delete will take not into account the number of objects, but delete[] will.
- delete[] should not be used on regular char*, unless it is
char* c = new char[size_];
.
- new
-
struct that contains ptrs, and we create struct using new
- ptr's default initialization is nullptr,so no memory allocation is done. so we need to call new to create that memory
-
Use Pointers
- Initialization
- ptr in class won't be initalized to null_ptr - pointers do not get initialized automatically!! cuz ptr is a variable too. So you need to initialize them to nullptr explicitly.
- Not safe to get size of array using
sizeof(int*)/sizeof(int)
- When to use raw pointers:
- in small code where performance is crucial, and ownership has no confusion about it.
- If your function doesn't use the lifetime semantics of the smartpointer, use a raw pointer, or a reference. You don't pay for what you don't need!.
//good example: void f(widget& w){w = 1; } //bad: void f(shared_ptr<widget>& w){*(w.get())=3;} // because if another shared_ptr may also be pointing to the same thing (which you don't know), and you do w.reset(new something ()), you do not modify that pointer's object.
- After freeing/deleting a pointer, always set it to nullptr
free(ptr_); ... realloc(ptr_, size); // segfault, since ptr is not null but its memory has been freed. delete ptr_ ; ptr_ = nullptr // this will not automatically set ptr_ to null!
- Reason can
delete
rvalue. But=
wouldn't work with that.
- Reason can
- Cautions
- Always initialize your pointer!
- Initialization
-
Drawbacks:
int* i
- Array?
- should delete this? Ownership?
- If yes, how? thru a dedicated destruction function, or delete? If delete, is it delete[] or delete?
- Dangling pointer?
- missing one of the poitner's occurances?
-
int* const is a const ptr, const* int is ptr to const
-
const shared_ptr
is equivalent toT* const
, where p itself is const, not the pointee. - (also if you want pointer to const, do shared_ptr).
- must be initialized in initializer_list!
-
-
arr[1] = 10
will not throw error even if arr doesn't own the array.- but may throw segfault when reading arr[1]
-
arr[1][2]
array requiresarr
to beint**
, not just int. -
No easy way to convert pointer back to array
-
Basic Example:
// int pf(int) is actually function pointer // int (pf)(int) // int (*pf)(int) are all valid void f (int pf(int)){ pf(1); } f(some_func);
-
Function overloading with func pointers: templated functions pointer can be casted, too.
// Function overloading with func pointers ========================= // int pf(int) is actually function pointer void f2 (int pf(int)){} //function trying to forward a function pointer. int processVal(int){return 0;}; int processVal(int, int){return 0;}; // We have two overloaded functions //Tricky template <typename T> int someFunc(T param){ return 0; } using FuncType = int (*)(int); void forward_func_ptr_works(){ f2(processVal); // fine, because function signature is clear // f2(someFunc); //Error: You need to specify the signature of someFunc! f2(static_cast<FuncType>(someFunc)); }
-
std::nothrow
: disable exceptions during new or new[], and get nullptr for return instead.#include <new> while(1){ int *ptr = new (std::nothrow) int[100000000ul]; // This will fail if (ptr == nullptr) cout<<"failed to allocate memory"<<endl; }
-
placement new
- pros:
- when memory is limited, there might be memory failure on heap. Placement new allows us to allocate memory on stack
- Faster pointer memory construction, and we know the memory address
- Easier memory management (don't need to free that memory yourself), also delete cannot free stack memory
- Peligros: placement new will yield segfault: the pointer passed in is
- void pointer
- nullptr
- not pointing to anything
- example
// Basic syntax: can specify address // new (address) (type) initializer #include <iostream> #include <string> using namespace std; int main() { //example 1 char *buf = new char[sizeof(string)]; // pre-allocated buffer string *p = new (buf) string("hi"); // placement new, takes less time to construct string *q = new string("hi"); // ordinary heap allocation // Example 2 while(1){ int *ptr = new (std::nothrow) int[100000000ul]; // This will fail silently char alt_arr[100000000ul * sizeof(int)]; int* ptr2 = new(alt_arr) int(5); //put a single 5 in the slot } return 0; }
- pros:
-
std::experiment::propagate_const
(c++14): treats a weapped pointer as a pointer to const, when the upstream caller is const.#include <experimental/propagate_const> #include <iostream> #include <memory> using std::cout; using std::endl; struct X{ void g(){cout<<"non const g"<<endl; } void g() const {cout<<"const g"<<endl; } }; struct Y{ Y(): m_ptrX (std::make_unique<X>()){} void f(){ cout<<"non const f"<<endl; m_ptrX->g(); } void f() const { cout<<"const f"<<endl; m_ptrX->g(); } std::experimental::propagate_const<std::unique_ptr<X>> m_ptrX; }; int main(){ Y y; y.f(); // see "non const g()"; const Y cy; cy.f(); // see "const ()" }
========================================================================
========================================================================
-
Motivation: a scoping object that can hold RAII object, where resources are allocated on heap. C++ is not a garbage collecting language, this is important.
- raw pointers are encapuslated by a smart pointer, so the smart pointer is responsible for the raw pointer.
- The purpose of smart ptrs is to prevent memory leaks, and to gurantee the liveliness of an object pointed to by multiple pointers. Also, we do not need to explicit delete a smart ptr.
-
Short Summary:
-
auto_ptr
is C++ 98's unique_ptr - after move
std::move
, an unique_ptr will be nullptr. - can do
unique_ptr -> shared_ptr
, but notshared_ptr -> unique_ptr
-
-
Common things to notice
- to initialize smart pointer reference:
- for const ptr&, you can use nullptr, for ptr&, you can't
- Initialization (shared ptr, unique_ptr)
- swap: see code
std::unique_ptr<int> ptr; ptr.swap(ptr2); std::shared_ptr<LargeObject> pLarge(nullptr); pNewLarge.swap(pLarge); // reminder // But this can cause crash, because this is not default ctor, it's just nullptr auto default_ptr = std::unique_ptr<crash_t>(); // This is the real default ctor using unique_ptr default_ptr = std::unique_ptr<crash_t>(new crash_t());
- swap: see code
- reset
// can be used to destruct unique_ptr.reset(); // but in shared ptr reset is just resetting the ptr itself, and --count shared_ptr.reset() // or shared_ptr can do shared_ptr = nullptr
- Conversion: no shared_ptr -> unique_ptr, but you can do unique_ptr -> shared_ptr
- get()
unique_ptr.get(); shared_ptr.get();
- to initialize smart pointer reference:
-
Differences
-
Working with arrays: shared_ptr doesn't work with C arrays, use std::vector instead. unique_ptr can
std::unique_ptr<int []> unique_ptr;
-
You can pass raw ptr into unique_ptr, Never pass a raw pointer into shared_ptr:
- The raw ptr might be owned by multiple shared_ptr.
- So only the first one destructing it can delete it successfully. Otherones will give undefined errors.
- only time when you have to use a raw ptr is when you have custom deleter. Then, use new
-
Initialization
- ofc shared ptr can do but unique_ptr can't do
auto pNewLarge(pLarge); pNewLarge = pLarge;
- ofc shared ptr can do but unique_ptr can't do
-
Destruction
- shared_ptr is more flexible as it doesn't require custom deleter as part of its type.
#include <memory> auto customDeleter1 = [](Foo* f){delete f}; auto customDeleter2 = [](Foo* f){delete f}; std::shared_ptr<Foo> ptr1 (new Foo, customDeleter1); std::shared_ptr<Foo> ptr2 (new Foo, customDeleter2); //This allows you to put these in a more generic vector: std::vector< std::shared_ptr<Foo> >{ptr1, ptr2};
- shared_ptr is more flexible as it doesn't require custom deleter as part of its type.
-
shared_ptr see Count references
pLarge.use_count();
-
========================================================================
========================================================================
-
Motivation
- Ownership can be only one. Can be moved to another object, but not by copying
-
std::unique_pointer
is smaller and more efficient thanboost::scoped_ptr
. The size is only one raw pointer
-
Basics
- Internally has a pointer pointing to heap memory, then delete it using delete.
- destruct the object by
reset()
- return ptr and gives up ownership:
release()
-
Create a
std::unique_ptr
-
Summary
#include<memory> // 1. make unique std::unique_ptr<LargeObject> pLarge = make_unique<LargeObject> (); // or using copy ctor: pLarge2 = make_unique<LargeObject>(*pLarge); // 2. create using reset. std::unique_ptr<LargeObject> ptr; ptr.reset(new LargeObject()); // 3. very convenient! - wrapping around a new pointer, but may not be exception safe using Ptr = std::unique_ptr<int>; auto i = new int(1); Ptr ptr(i); // Note: in ctor you can Foo(int &i):j(new int(i)){ } //But you can't do j = new int(i); //Because IMPLICIT CONVERSION RAW POINTER -> SMART POINTER might cause problems. unique_ptr = some_ptr; // not working, have to use reset. // array std::unique_ptr<unsigned char[]> testData(new unsigned char[16000]());
-
Make_unique (C++14)
- make_unique can is doing
std::forward()
on args, for non-array type. - For array types, you should specify size (TODO)
-
Make_unique uses template, whose type will be deduced. So, if you use initializer list, it won't be deduced
std::make_unique<std::vector<int>>({1,2,3}); //Bad: Initializer list will not be deduced std::make_unique<std::vector<int>>(std::vector<int>{1,2,3}); //Good: Initializer list will not be deduced
- make_unique can is doing
-
Suicidal
int main(){ auto ptr1 = std::unique_ptr<int>(new int(1)); if (ptr1) cout<<"ptr1"<<endl; { // freed ptr1's resource, though after this ptr1 still not null auto ptr2 = std::unique_ptr<int>(ptr1.get()); } if (ptr1) cout<<*ptr1<<endl; }
-
-
Other Basic Operations
- Get the raw pointer
pLarge.get();
- Ease to convert to shared_ptr: //thru move semantics, or copy elision
std::unique_ptr<int> arr_ptr (new int{1}); std::shared_ptr<int> ptr2 = std::move(arr_ptr);
- This can be useful in Factory method, as the factory function can allow the pointer being used either as unique_ptr, or shared_ptr in the future
- Can be converted to
std::shared_ptr<void>
std::unique_ptr<int> u_ptr (new int{1}); std::shared_ptr<void> ptr = u_ptr;
- Get the raw pointer
-
Destruction
- if
unique_ptr.get() == nullptr
, the destructor has no effect. - but if you have custom deleter, you have to call delete on the pointer!
- if
-
ref to object created by unique_ptr, what if unique_ptr is nullptr? How to check the reference? using
&
.#include <memory> #include <iostream> using std::cout; using std::endl; void foo(int & i){ if (&i == nullptr) cout<<"ha"<<endl; else cout<<"he"<<endl; } int main(){ std::unique_ptr<int> i; foo(*i); }
- Typical use is factory method.
class Investment; class Stocks: public Investment; class Bonds: public Investment; template <typename T> std::unique_ptr<Investment> makeInvestment(T&& args); //Factory function that returns a ptr to investment, on demand
- Pass unique pointer into/out of function
- Transfer ownership, by std::move (for lvalues) or rvalues (copy ellision)
void Foo(std::unique_ptr<Bar> bar_ptr); Foo(std::unique_ptr<Bar>(new Bar())); //OR std::unique_ptr<Bar> bar_ptr_(new Bar()); Foo(std::move(bar_ptr_)); //move semantics for lvalues.
- polymorphism using unique_ptr
class Base{...}; class derived{...}; std::unique_ptr<Base> Func(std::unique_ptr<Base> base){ std::unique_ptr<Derived> derived_ptr(new Derived()); }
- Transfer ownership, by std::move (for lvalues) or rvalues (copy ellision)
-
Except for copy elision, we cannot copy a unique_ptr, because of
unique_ptr(const unique_ptr&) = delete;
. So we have to meet all requirements for copy ellision- why wouldn't this work?
#include <memory> #include <utility> using Ptr = std::unique_ptr<int, void (*)(int*)>; Ptr foo(){ Ptr ptr1 (new int(100), [](int* p){delete p;}); // return Ptr (nullptr, nullptr); // 1: this is fine // return ptr1; // 2: this is fine too return (1==1)? ptr1 : Ptr (nullptr, nullptr); // 3: this will pop up an Error "error: use of deleted function 'std::unique_ptr<>..." Why? // unique_ptr(const unique_ptr&) = delete; But 1 works because of Return-Value-Optimization, an optimization that uses copy-elision. // Copy-elision allows the caller function directly get the local variable being returned. // However, copy-elision is disabled if which object to return is decided during run time, specifically, the ternary statement in 3. // A fix is simple: either not use ternary, or use std::move to move_construct the returned object (which in many cases is a swap of pointers) // return (1==1)? std::move(ptr1) : Ptr (nullptr, nullptr); } int main() { auto ptr = foo(); return 0; }
- why wouldn't this work?
-
use unique_ptr reference?
- If you have lambda expression like
[&](){ptr...}
, sure go for it. - don't use pointer to a smart pointer. That totally loses its purpose.
- don't use reference to smart pointer either. Bad.
- If you have lambda expression like
-
unique_ptr is meant to replace new, delete.
- so it doesn't work with ptrs to stack allocated objects. Otherwise if unique_ptr takes in a pointer not created by
new
, there'd be double-freeing issues.
- so it doesn't work with ptrs to stack allocated objects. Otherwise if unique_ptr takes in a pointer not created by
-
Clearly distinguish pointer to a single obj, or to an array
std::unique_ptr<int[]> arr_ptr (new int[10] {1,2,3}); cout<<arr_ptr[2];
-
unique_ptr: wouldn't make sense to use unique_ptr, as it is not gonna be freed.
-
C++ unique_ptr signature allows a custom deleter (second template argument):
template<class T, class Deleter=std::default_delete<T>> class unique_ptr; //So a unique_ptr is actually: | raw_ptr | deleter |
-
Custom Deleter: a custom deleter could be a functor, a lambda, or a function pointer
struct CustomDeleter{ void operator()(Obj* obj)const{ //Define a functor here. cout<<"HAHAHA"<<endl; delete obj; } } int main(){ Obj* obj_ptr = nullptr; // or some function that returns a raw pointer // Method 1: empty (no-data members) functor: auto rp = std::unique_ptr<Obj, CustomDeleter>(obj_ptr, CustomDeleter()); // Method 2: capture-less lambda, also smallest implementation, need decltype, like in priority_queue auto lam = [](Obj* p){ delete p; }; auto rp2 = std::unique_ptr<Obj, decltype(lam)>(obj_ptr, lam); // Method 3: function pointer, 16 bytes for that. slightly larger void func(obj*); std::unique_ptr<Obj, void(*)(int*)>; //or decltype(&func) // Method 4: std::function, which is MUCH LARGER std::unique_ptr<obj, std::function<void(int*)>>; }
-
Restrictions
- you can't make unique_ptr directly a nullptr, you have to pass it in.
std::unique_ptr<AVFrame, void (*)(AVFrame*)> output_frame = unique_frame(nullptr, nullptr)
- reset only needs ptr part, not the deleter part.
std::unique_ptr<Foo, D> up(new Foo(), D()); // up owns the Foo pointer (deleter D) up.reset(new Foo()); // calls deleter for the old one
- you can't make unique_ptr directly a nullptr, you have to pass it in.
-
Change deleter: need deleter to be of the same time. Then can use
=, move, get_deleter()
- you can do move:
IntUniquePtr p1(new int(42), deleteEvenNumber); IntUniquePtr p2(new int(43), deleteOddNumber); p1 = move(p2);
- You can manually change the deleter, as .get_deleter() will return the deleter as non const reference, so you can change that
p1.get_deleter() = deleteOddNumber;
- **Neatest: use = **
IntUniquePtr p1(new int(42), deleteEvenNumber); p1 = IntUniquePtr(new int(43), deleteOddNumber);
- you can do move:
========================================================================
========================================================================
-
Motivation
- ownership can be multiple. The raw pointer won't be freed until the last owner is out of scope.
-
Basics
- make shared pointers
#include <memory> std::shared_ptr<LargeObject> pLarge = make_shared<LargeObject>(); //not recommend std::shared_ptr<LargeObject> pLarge(new LargeObject),since it's slower and not exception safe. // Because you first allocate memory for the control block, then allocate memory for the object. // If you do make_shared, you will allocate the two in one call, which reduces construction overhead. std::shared_ptr<LargeObject>pLarge(nullptr);
- frees the object pointed to by the pointer, delete a pointer.
pLarge.reset(); //sets the pointer to null, minus the reference count by 1. pLarge = nullptr; //sets the pointer to null. pLarge = make_shared<LargeObject>(); //frees the object, and point to a new object.
- Shared_ptr
reset()
anduse_count()
#include<iostream> #include<memory> using std::cout; using std::endl; int main(){ std::shared_ptr<int> pt = std::make_shared<int>(3); cout<<pt.use_count()<<endl; // see 1. std::shared_ptr<int> pt2 = pt; cout<<pt.use_count()<<endl; // see 2. cout<<pt2.use_count()<<endl; // see 2. pt.reset(); cout<<pt.use_count()<<endl; // see 0. cout<<pt2.use_count()<<endl; // see 1. auto pt3 = pt2.get(); cout<<pt2.use_count()<<endl; // see 1; // pt2.get() will return a raw pointer, and the shared_ptr will not change the ref count. return 0; }
- make shared pointers
-
Uses
-
static_pointer_cast
vsdynamic_pointer_cast
:-
RTTI
is run time type information, if a base class has at least a virtual function, then base class ptr can be dynamic_cast to derived class (downcast, upcast is derived -> base) -
static_cast
happens during compile time, no RTTI is needed.dynamic_cast
is only for downcasting. dynamic_cast can happen in runtime. If downcasting fails (not derived class), nullptr is returned. If you're sure you can downcast, use static_cast since it's cheaper, and the language allows you to do so -
static_pointer_cast
vsstatic_cast
:static_pointer_cast
works on shared_ptrs, because you can't cast its type directly.#include <iostream> using namespace std; class B { //virtual void fun() {} // NEED TO ADD THIS!! }; class D : public B { }; int main() { B* b = new D; D* d = dynamic_cast<D*>(b); // 1. use dynamic_cast if we're not sure if we can succeed if (d != NULL) cout << "works"; else cout << "cannot cast B* to D*"; // 2. cpp still allows you to use static_ptr_cast std::static_pointer_cast<DerivedClass>(ptr_to_base)->f(); // 3. even static_pointer_cast static_cast<DerivedClass*>(ptr_to_base.get())->f(); // equivalent to above return 0; }
-
-
std::shared_ptr<void>
can be used as a replacement for void*, which can store any type of raw pointer, or unique_ptr!!!std::shared_ptr<void> ptr (new Foo()); //creating a new Foo ptr ptr.get(); // return a std::shared_ptr<void> *(std::static_pointer_cast<int>(ptr)); //This is how you use it, Note that we're using std::static_pointer_cast
- Cool thing: ptr can call Foo type deleter, which means we don't need to worry about deletion! YAYYY even tho ptr itself is
void
- Cuz ptr stores a custom deleter during initialization. Whether the actual type is a child class, or in this case Foo, their default deleter is passed in here.
- Therefore, even tho .get() will return a
shared_ptr<void>
, the actual deleter is the default
- Cool thing: ptr can call Foo type deleter, which means we don't need to worry about deletion! YAYYY even tho ptr itself is
-
-
Cautions:
- So you can use
std::enable_shared_from_this
to address this specific issue- if you pass "this" pointer to an external shared_ptr, be careful**
class Foo{ void processFoo(std::vector<std::unique_ptr<Foo>> processed&){ ... // Here this is implicitly converted into a new std::shared_ptr, whose control block //has nothing to do with this object processed.push_back(this); //So the external processed vector might delete this object AFTER it gets destructed } }
-
std::enable_shared_from_this
is called a "base class template". Internally, enable_shared_from_this will share the same control block with an existing shared_pointer. So, you need to make sure at least one shared_ptr has been created, which could be done by Factory method
class Foo : public std::enable_shared_from_this<Foo>{ public: Foo& createFoo(); static void processFoo(std::vector<std::unique_ptr<Foo>> processed&){ processed.emplace_back(shared_from_this()); // Built in enable_shared_from_this function }; private: //ctors }
- if you pass "this" pointer to an external shared_ptr, be careful**
- no [] or ptr arithmatic on shared pointers. so use a raw pointer from the shared ptr
- So you can use
-
size is two pointers, one is for the raw pointer, one is for the ptr control block.
- shared_ptr also has a virtual function that makes sure the right derived class is deleted
- control block: a reference counter: a copy of custom deleter, and a copy of an allocator.
- constructed when a new object is created
-
One key thing about shared_ptr is reference count: stored in heap memory (i.e control block of the object, see below), incrememented only by copy operations.
- Things will create a ref count (as well as a control block)
- make_shared(creating a new object)
- unique_ptr
- Things will change ref count:
- using a raw ptr
- from another shared_ptr
- Things won't increment the ref count:
- move assignment / construction
- Ref must be changed atomically, so shared_ptr is a bit slower.
- Things will create a ref count (as well as a control block)
-
Prefer std::make_shared to new
- std::make_shared perfect forward arguments to the object's dynamic constructor
-
Advantage 1: in make_shared, new and ptr storage happens atomically, while in new, they're two separate steps, which in some cases can lead to memory leaks. So, don't use
new
, unless you must do custom deletion.- Example: say we have this function
// In runtime, ```some_small_func()``` can happen anytime. This is because the compiler doesn't generate code to make sure about that order. // here the shared_ptr creates memory and stores the raw ptr atomically void processFoo(std::shared_ptr<Foo> ptr, some_small_func()); // If we use ```new```, ```some_small_func()``` may get executed between ```new``` and constructing ```std::shared_ptr<Foo>```. If ```some_small_func()``` fails, the memory created by ```new``` won't be claimed back processFoo(std::shared_ptr<Foo> (new spw), some_small_func()); // here some_small_func is computed between creating the new pointer, and storing it with the shared_ptr!
- Example: say we have this function
-
Advantage 2: make_shared has the memory and the control block in the same place, so one memory call is fine, which decreases the static size of the program
- Speed might be faster as we allocate memory only once
- Disadvange 1: cannot have custom deleter
-
Disadvange 2: might have a delay between the destruction of
shared_ptr
destruction of object- This is because the memory block, for the control block and the object, are stored together.
- So the memory it occupies cannot be released, just because the object is destructed,
- That's because: weak pointer might also be pointing to the obj.
- Weak count will count how many weak_ptrs there are, but weak count will use the primary count.
- **Disadvange 3 ** Do not take in braces (we've seen that, because make_shared takes in template argument, and {} is not type deducible)
std::make_shared<std::vector<int>>({1,2,3}); //illegal
========================================================================
========================================================================
-
Motivation:
-
This can be used to break the circular reference between two shared_ptr
class Foo{ std::shared_ptr<Bar> ptr; } class Bar{ //Option 1 std::shared_ptr<Foo> ptr; //this is circular, neither object will be deleted; // Option 2 Foo* ptr_2; // This doens't tell you the liveliness of Foo // Option 3 std::weak_ptr<Foo> ptr_3; // This will not try to delete A, so won't cause circular issue. }
-
Can access an object owned by shared_ptrs. serving as an augmentation of shared_ptr
-
But this does not participate in ownership, so no lifetime managing, hence nothing interferes with a reference count.
-
Applications where you don't want to manage the livetime of tthe obeject, but you do care about their liveliness, such asCache or the observer pattern
#include <memory> std::shared_ptr<Foo> returnCachedPtr(id){ auto obj_ptr = cache[id].lcok(); // here cache can be std::unordered_map. if (obj_ptr != nullptr){ obj_ptr = load_Object(); } return obj_ptr; }
-
-
Basics
- no guarantee the referenced object is still alive. (Dangling pointer)
- But you can check it, by reading the exising shared_ptr's weak reference!!!
- created by shared_ptr and can return a shared_ptr, or null (if the pointee is not alive)
- Cannot be dereferenced directly
- E.g
std::shared_ptr<Foo> ptr1 = std::make_shared<Foo>(args); std::weak_ptr<Foo> ptr2(ptr); //create like this. //checking liveliness of the pointee, only if (ptr2.expired()) // Checks the liveliness and Return a shared_ptr ATOMICALLY std::shared_ptr<Foo> ptr3 = ptr.lock(); // Returns a shared_ptr and throws a std::bad_weak_ptr error: std::shared_ptr<Foo> ptr4(ptr2);