Chapter 12. Dynamic Memory
Difference of stack and heap in c++
-
stack stores all the local variables created by the function, and follow the LIFO strategy. Once the function call is done, all function data and variables are freed up. Stack is a contiguous block of memory that is managed automatically by the operating system.
-
stack has a limited size, and if the program tries to allocate more memory, it will cause a stack overflow.
-
heap is a region of memory that is managed manually by the programmer. Memory is allocated on the heap using dynamic memory allocation functions like ‘new’ or ‘malloc’. The memory allocated on the heap until it’s explicitly deallocated by the programmer.
-
heap has a much larger size compared to the stack. However, managing memory on the heap can be more complex and error-prone compared to managing memory on the stack.
- Summary:
-
- Memory allocation on the stack is managed automatically by the operating system, while memory allocation on the heap is managed manually by the programmer.
-
- The stack has a limited size, while the heap has a much larger size.
-
- Memory allocated on the stack is automatically deallocated when the function returns, while memory allocated on the heap remains allocated until it is explicitly deallocated by the programmer.
-
12.1. Dynamic Memory and Smart Pointers
Dynamically allocated objects have a lifetime that is independent of where they are created; they exist until they are explicitly freed! But sometimes it will cause some bugs.
allocate in stack:
int a;
int b;
---
allocate in heap:
int *a = new int();
To make using dynamic objects safer, there exist two smart pointer types that dynamically allocated objects.
Smart pointers ensure that the objects to which they point are automatically freed when it is appropriate to do so.
Smart pointers allocate memory on heap.
-
- shared_ptr, which allows multiple pointers to refer to the same object
-
- unique_ptr, which “owns” the object to which it points.
common operations to shared_ptr and unique_ptr:
-
- shared_ptr sp; unique_ptr up; Both are null smart pointer that can point to objects of Type T.
-
- p, use p as a condition, true if p points to an object; *p, dereference p to get the object to which p points.
- swap(p,q): swap the pointers in p and q
12.1.1. The shared_ptr Class
operations:
-
- make_shared(args): return a shared_ptr pointing to a dynamically allocated object of Type T.
-
- shared_ptrp(q): p is a copy of shared_ptr q; increments the count in q.
-
- p.unique(): return true if p.use_count() is one; false otherwise.
-
- p.use_count(): return num of objects sharing with p
// shared_ptr taht points to an int with value 1
shared_ptr<int> p = make_shared<int>(1);
// q points to a string with value 999
shared_ptr<string> q = make_shared<string>(3,'9');
// r points to an int that is initialized to 0
shared_ptr<int> r = make_shared<int>();
// s points to the same object as r points to
shared_ptr<int> s(r);
cout << p.use_count() << endl; // 1
cout << r.use_count() << endl; // 2
{
shared_ptr<int> g(r);
cout << g.use_count() << endl; // 3
cout << r.use_count() << endl; // 3
} // g is destroyed
cout << r.use_count() << endl; // 2
12.1.2. Managing Memory Directly
shallow/deep copy
shallow copy:
class A{
public:
int* a_;
string b_;
A(int a, string b) : a_(new int(a)), b_(b) {
cout << "in constructor" << endl;
}
~A() {
cout << "in destructor" << b_ << endl;
delete a_;
}
};
void func() {
A a1(1,"a1");
A a2(a1); // copy construction // what we done is shallow copy, a1 and a2 points to the same object
a2.b_ = "a2";
cout << a1.a_ << endl;
cout << a2.a_ << endl;
cout << "123" <<endl;
}
int main() {
func();
cout << "hello" << endl;
return 0;
}
// in constructor
// 0x882770
// 0x882770
// 123
// in destructora2
// in destructora1
// hello
deep copy:
class A{
public:
int* a_;
string b_;
A (int a, string b) : a_(new int(a)), b_(b) {
cout << "in constructor" << endl;
}
A (const A& a) {
// this->a_ = a->a_; // shallow copy will copy the value of the pointer address.
this->a_ = new int(*(a.a_)); // deep copy will create a new object
}
~A() {
cout << "in destructor" << b_ << endl;
delete a_;
}
};
void func() {
A a1(1,"a1");
A a2(a1); // copy construction // what we done is shallow copy, a1 and a2 points to the same object
a2.b_ = "a2";
cout << a1.a_ << endl;
cout << a2.a_ << endl;
cout << "123" <<endl;
}
int main() {
func();
cout << "hello" << endl;
return 0;
}
// in constructor
// 0x7e2770
// 0x7e27c0
// 123
// in destructora2
// in destructora1
// hello
We cannot implicitly convert a pointer to a smart pointer:
shared_ptr<int> p = new int(1); // error
shared_ptr<int> p2(new int(1)); // correct
It is dangerous to use a built-in pointer to access an object owned by a smart pointer, because we may not know when that object is destroyed.
dangling pointer 悬空指针
A dangling pointer is a pointer that points to memory that has been deallocated (freed) or otherwise no longer exists. Dereferencing a dangling pointer can result in undefined behavior, since the memory it points to may have been reallocated for another purpose, or the memory may have been completely released back to the operating system.
A* a = new A(1);
{
shared_ptr<A> str(a); // use_count = 1
} // use_count = 0
cout << a.a_; // compiler will not print error, but the address of a is destroyed!
12.1.4. Smart Pointers and Exceptions:
class A {
public:
int* a_;
A(int a) : a_(new int(a)) {
cout << "This is the constructor" << endl;
}
~A() {
delete a_;
cout << "This is the destructor, you like it?" << endl;
}
};
void throwException() {
throw bad_exception();
}
void func() {
std::shared_ptr<A> a = make_shared<A> (1);
try {
throwException();
} catch(const exception& e) {
cout << e.what() << endl;
}
// other operations
int *abs = new int(2);
*abs *= 2;
//delete(abs);
} // shared_pte will be freed, but abs is stored in heap, not freed
int main() {
func();
cout << "hello world" << endl;
return 0;
}
This is the constructor
std::bad_exception
This is the destructor, you like it?
hello world
what if we want our shared_pte enter into the self-made destructor, not the default ~A():
class A {
public:
int* a_;
A(int a) : a_(new int(a)) {
cout << "This is the constructor" << endl;
}
~A() {
delete a_;
cout << "This is the destructor, you like it?" << endl;
}
};
void throwException() {
throw bad_exception();
}
void self_made_destructor(A* a) {
delete a->a_;
cout << "In self_made destructor, you like it?" << endl;
}
void func() {
// std::shared_ptr<A> a = make_shared<A> (1);
shared_ptr<A> a(new A(1), self_made_destructor);
try {
throwException();
} catch(const exception& e) {
cout << e.what() << endl;
}
// other operations
int *abs = new int(2);
*abs *= 2;
//delete(abs);
} // shared_pte will be freed, but abs is stored in heap, not freed
int main() {
func();
cout << "hello world" << endl;
return 0;
}
This is the constructor
std::bad_exception
In self_made destructor, you like it?
hello world
Unique Pointers
A unique_ptr “owns” the object to which it points. Unlike shared_ptr, only one
unique_ptr at a time can point to a given object. The object to which a
unique_ptr points is destroyed when the unique_ptr is destroyed
specific operations:
unique_ptru1: Null unique_ptrs that can point to objects of type T. Use delete to free it’s pointer.
unique_ptr<T,D>u2: use a callable object of type D to free its pointer.
u = nullptr; make u null
u.reset(): deletes the object which u points.
unique_ptr p1;
unique_ptr p2(new int(1)); // p2 points to int with value 1
No assign and copy for unique_ptr:
u = u2; // error
unique_ptru2(u); // error
weak pointer
A weak_ptr is a smart pointer that does not control the lifetime of the object to which it points. Binding a weak_ptr to a shared_ptr does not change the reference count of that shared_ptr.
When we create a weak_ptr, we initialize it from a shared_ptr:
auto p = make_shared<int>(1);
weak_ptr<int> wp(p);
// w.expired(): return true if w.use_count() is zero, false otherwise.
// w.lock(): if expired is true, returns a null shared_ptr, otherwise return a shared_ptr to which objects w points.
12.2 Dynamic Arrays
How to allocate storage for many obejects at once? For example, vector and string.
Language method: new (int *p = new int[100])
Library: a template calss called allocator to seperate allocation from initialization.
when we use new to allocate an array, we do not get an object with an array type. Instead, we get a pointer to the element type of the array. The allocated memory does not have an array type, we cannot call begin or end on a dynamic array. It is important to remember that what we call a dynamic array does not have
an array type (In C++, an array type is a type that represents a fixed-size sequence of elements of the same type. It is a fundamental data structure in C++).
// Delete Dyncamic Array
int *p = new int[10];
delete [] p; // Use delete[] to deallocate an array of objects that was allocated using new[]
// delete pa; Use delete to deallocate a single object that was allocated using new
// The compiler is unlikely to warn us if we forget the brackets when we delete a pointer to an array or if we use them when we delete a pointer to an object.
Smart Pointers and Dynamic Arrays
// To use a unique_ptr to manage a dynamic array, we must include a pair of empty brackets after the object type:
unique_ptr<int[]> up(new int[10]);
up.release(); // automatically uses delete[] to destroy its pointer
to use a shared_ptr we must supply a deleter
std::shared_ptr<int[]> p = std::make_shared<int[]>(10);
shared_ptr<int> sp(new int[10], [](int* p) {delete [] p;});
"std::shared_ptr" has a default deleter built-in that knows how to delete objects created with new. Specifically, the default deleter uses the delete operator to deallocate the memory allocated with new. This means that the std::shared_ptr<int> returned by std::make_shared<int>(1) will automatically deallocate the int object when there are no more shared pointers referencing it.
12.2.2 The allocator Class
new and delete combines memory allocation/deallocation and object construction/deconstruction.
How to decouple memory allocation from object construction?
P.S: More importantly, classes that do not have default constructors cannot be
dynamically allocated as an array.
The allocator Class
allocator<T> a: Defines an allocator object named a that can allocate memory for objects of type T.
a.allocate(n): Allocates raw, unconstructed memory to hold n objects of Type T.
a.deallocate(p, n): Deallocate memory that held m object of Type T starting at the address in the T* pointer p.
a.construct(p, args)
a.destroy(p): run the destructor on the object
void func() {
// create an allocator for std::string
allocator<string> alloc;
// allocate memory for an array of 10 strings
auto const p = alloc.allocate(10);
string* const pa = alloc.allocate(20);
// construct strings in the allocated memory
for (int i = 0; i < 10; i++) {
alloc.construct(p+i, to_string(i));
}
// print array p
for (int i = 0; i < 10; i++) {
cout << *(p+i) << endl;
}
// destroy array p
for (int i = 0; i <10; i++) {
alloc.destroy(p+i);
}
// Though we destroy the object, the memory
// still contains the values that were previously
// stored in the object.
for (int i = 0; i < 10; i++) {
cout << *(p+i) << endl;
}
// deallocate p;
alloc.deallocate(p, 10);
}
int main() {
func();
cout << "hello world" << endl;
return 0;
}
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
hello world
Chapter Sum:
In C++, memory is allocated through expressions. The library also defines an
new allocator expressions and freed through class for allocating blocks of delete dynamic memory.