MTRN2500
Week 3 Lab01
WEDNESDAY 4PM - 6PM
THURSDAY 4PM - 6PM
Slides by Alvin Cherk (z5311001)
Info
These slides are based upon the lab document on Teams or on GitLab.com
Today
- Storage duration.
- Dynamic memory allocation.
- Dangers of raw pointers.
- Smart pointers (unique and shared).
-
make_unique
andmake_shared
- Passing-by-pointer
- References.
- Passing-by-reference.
Heap vs Stack Memory
Array Memory Layout
Array Memory Layout
An array is a linear data structure that stores a collection of similar data types at contiguous locations in a computer's memory
Array Memory Layout
The address of std::array
is the same as the address of its first element.
#include <array>
#include <iostream>
int main() {
std::array<int, 4> arr{1, 2, 3, 4};
std::cout << "Address: " << &arr << " == " << arr.data() << std::endl;
// Address: 0x7ffcf7562b70 == 0x7ffcf7562b70
}
Array Memory Layout
The elements of std::array
are contiguous
#include <array>
#include <iostream>
int main() {
std::array<int, 4> arr{1, 2, 3, 4};
for (std::size_t i{0}; i < arr.size(); i++) {
std::cout << i << ": " << &arr.at(i) << std::endl;
}
// Actual values differs from each run
// 0: 0x7ffdc616b400
// 1: 0x7ffdc616b404
// 2: 0x7ffdc616b408
// 3: 0x7ffdc616b40c
}
Vector Memory Layout
Vector
Dynamically sized array that doubles in size when it reaches capacity.
It is one of the most common and most used STL container.
You can probably do everything in a vector, but that doesn't mean you should
int main() {
// Creates a vector of integers using an initialiser list
std::vector<int> a = {0, 1, 2, 3, 4};
// Creates a vector of length 3, with all value defaulted at 2. Using constructor
std::vector<int> b(3, 2);
// Vector with {1, 2} as elements. Using constructor
std::vector<int> c{1, 2};
// Creating a copy. Using constructor
std::vector<int> d = a;
// Assignment {2, 2, 2}
c = b;
}
When declaring a std::vector
, a size is not needed, only the type.
How am I supposed to understand all the constructors or member functions/methods?
Read the C++ documentation on cppreference.com
cppreference generally better than cplusplus
Vector Memory Layout
The address of std::vector
is not the same address of its first element.
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec{1, 2, 3, 4};
std::cout << "Address: " << &vec << " != " << vec.data() << std::endl;
// Address: 0x7ffea4df2110 != 0x55e7531c8eb0
}
Vector Memory Layout
The elements of std::vector
are contiguous
int main() {
std::vector<int> vec{1, 2, 3, 4};
std::cout << vec.capacity() << std::endl;
for (std::size_t i{0}; i < vec.size(); i++) {
std::cout << i << ": " << vec.data() + i << std::endl;
}
// 0: 0x55ba8abe3eb0
// 1: 0x55ba8abe3eb4
// 2: 0x55ba8abe3eb8
// 3: 0x55ba8abe3ebc
}
Vector Memory Layout
How is a std::vector
represented in memory
--- --- --- ---
Stack | b | e | c | a |
--- --- --- ---
| | |
| --------
| |
v v
--- --- --- ---
Heap | 1 | 2 | 3 | |
--- --- --- ---
The vector itself will be allocated on the stack i.e.:
-
b
: Beginning address of the vector. -
e
: One-past-the-end address of the vector. -
c
: Capacity of the vector. -
a
: Allocator class (how the vector allocates its elements in memory).
Vector Fun Fact
std::vector
stores 1 type differently
How many bits/bytes are needed to store a single boolean?
1 bit. 0 => false, 1 => true
But the smallest data we can store is 1 byte (like an ascii character)
Not course content (I think)
Storage Duration
also known as Object Lifetimes
Storage Duration
Three different types of storage durations in C++
- Static: Object persists throughout program life time
- Automatic: Object persists within scope life time
- Dynamic: Object life time defined by us
int variable_1{10};
int main() {
int* variable_2{new int};
delete variable_2;
{
int variable_3{3};
}
}
-
variable_1
: Static -
variable_2
: Dynamic -
variable_3
: Automatic
Dynamic Memory Allocation
Dynamic Memory Allocation
What are the disadvantages of using C-style dynamic memory allocation?
#include <iostream>
int main() {
int* int_ptr{(int*)malloc(sizeof(int))};
*int_ptr = 3;
std::cout << *int_ptr << std::endl;
free(int_ptr);
}
-
malloc
is verbose and requires the user to give the number of bytes to allocate. -
malloc
needs to be type-cast to the assigned pointer. - Pointer is not initialised upon creation.
Code Demo
cpp_memory_allocation.cpp
Dynamic Memory Allocation
Convert the program you created to use C++-style dynamic memory allocation i.e. new
and delete
.
What are the advantages?
#include <iostream>
int main() {
int* int_ptr{new int{42}};
std::cout << *int_ptr << std::endl;
delete int_ptr;
}
-
new
just requires the datatype and does not require the number of bytes to allocate. -
new
returns anint*
directly so there is no need to type-cast. - The pointer can be initialised upon creation. If we don't want to initialise the pointer then:
int* int_ptr{new int}; // int_ptr is a nullptr.
#include <iostream>
int main() {
int* int_ptr{(int*)malloc(sizeof(int))};
*int_ptr = 3;
std::cout << *int_ptr << std::endl;
free(int_ptr);
}
Pointers are Dangerous
Pointers are Dangerous
Why are pointers dangerous?
Wild Pointers: Pointers used before initialisation
int *ptr;
std::cout << *ptr << std::endl; // Using uninitialised pointer.
Dangling Pointers: Pointers whose memory that it is pointing to has been freed.
int *ptr1{new int{4}};
int *ptr2{ptr1};
delete ptr1; // ptr2 becomes a dangling pointer.
delete ptr2; // This line results in double free.
Memory Leaks: Pointers that are never freed so memory usage increases over time.
int *ptr{new int};
// do ptr stuff // Forget to free.
References
References
Why are pointers bad?
They are dangerous. They can cause runtime errors if you are not careful.
Solution?
C++ introduces references (&) that sort-of replaces pointers.
A reference is an alias for another object. You can use it just as you would the original object.
Similar to a pointer, but:
- Don't need to use -> to access elements
- Can't be null
- Can't change what a variable is referenced to after initialisation
References
#include <iostream>
auto swap(int* x, int* y) -> void {
int temp = *x;
*x = *y;
*y = temp;
}
auto main() -> int {
int x = 100;
int y = 0;
std::cout << "Before (" << x << ", " << y << ")\n";
// Before (100, 0)
swap(&x, &y);
std::cout << "After (" << x << ", " << y << ")\n";
// After (0, 100)
return 0;
}
Simple swap()
function that swaps the values passed in the arguments using pointers
References
#include <iostream>
auto swap(int& x, int& y) -> void {
int temp = x;
x = y;
y = temp;
}
auto main() -> int {
int x = 100;
int y = 0;
std::cout << "Before (" << x << ", " << y << ")\n";
// Before (100, 0)
swap(x, y);
std::cout << "After (" << x << ", " << y << ")\n";
// After (0, 100)
return 0;
}
Simple swap()
function that swaps the values passed in the arguments using references
References
#include <iostream>
int main() {
int a{3};
int& b{a};
std::cout << a << " " << b << std::endl;
a = 4;
std::cout << a << " " << b << std::endl;
}
What is the output of the following program?
3 3
4 4
References
References can also be dangerous (*if you do dumb things)
References can dangle (i.e. the resource is destroyed while the reference is valid). Attempting to access a dangling reference is undefined behaviour which may work, may give some unexpected value, or may crash.
#include <iostream>
int& foo() {
int var{42};
return var;
}
int main() {
int& var_2{foo()};
// This is undefined behaviour.
std::cout << var_2 << std::endl;
}
- In
foo()
,var
is an automatic managed variable (at the end of the scope, its deleted) - As such,
foo()
is returning a reference to a deleted variable
References
Pass by Value / Reference
Pass by Value
By default in C++, all arguments passed into functions are copied into the parameters.
auto swap_value(int x, int y) -> void {
auto const tmp = x;
x = y;
y = tmp;
}
auto main() -> int {
auto i = 1;
auto j = 2;
std::cout << i << ' ' << j << '\n';
// 1 2
swap_value(i, j);
std::cout << i << ' ' << j << '\n';
// 1 2 (not swapped)
}
The actual value of the argument is copied into the memory being used to hold the formal parameters value during the function call/execution
Pass by Reference
Sometimes, we don't want to copy the variable (as it may not be copyable, or it is expensive to copy it). The solution is to use references.
auto swap_reference(int& x, int& y) -> void {
auto const tmp = x;
x = y;
y = tmp;
}
auto main() -> int {
auto i = 1;
auto j = 2;
std::cout << i << ' ' << j << '\n';
// 1 2
swap_reference(i, j);
std::cout << i << ' ' << j << '\n';
// 2 1 (swapped)
}
Code Demo
pass_by_references.cpp
Code Demo
What is the output?
#include <iostream>
void foo(int& arg1, int arg2) {
arg1++;
arg2++;
}
int main() {
int a{3};
int& b{a};
int c{4};
std::cout << a << " " << b << " " << c << std::endl;
foo(a, b);
std::cout << a << " " << b << " " << c << std::endl;
foo(c, a);
std::cout << a << " " << b << " " << c << std::endl;
}
3 3 4
4 4 4
4 4 5
Loops Revisited
Loops Revisited
So what are some actual usages of references in C++?
auto loop() -> void {
auto numbers = std::vector<int>{1, 2, 3, 4, 5};
for (auto number : numbers) {
number = 0;
}
for (auto number : numbers) {
std::cout << number << std::endl;
}
// {1, 2, 3, 4, 5}
}
No you can't, as the value in the array is copied into a local variable number
. Any changes to this variable is not reflected in the actual vector itself
What if I want to use a for-range loop and I want to change the values in a vector?
Loops Revisited
What if I want to use a for-range loop and I want to change the values in a vector?
References can be used as they are an alias for the actual value in the vector
auto loop_4() -> void {
auto numbers = std::vector<int>{1, 2, 3, 4, 5};
for (auto& number : numbers) {
number = 0;
}
for (auto const& number : numbers) {
std::cout << number << std::endl;
}
// {0, 0, 0, 0, 0}
}
Loops Revisited
I have a 2D vector and I am looping through it.
auto loop_5() -> void {
auto matrix = std::vector<std::vector<int>>{{1, 2, 3, 4, 5}, {0, 0, 0, 0, 0}};
for (auto row : matrix) {
//
}
}
Since the for-range loop copies the elements of the vector into the row
variable, we are effectively creating redundant copies.
This is a performance and memory issue.
We can use references to "point" to the vector elements instead of copying them.
auto loop_6() -> void {
auto matrix = std::vector<std::vector<int>>{{1, 2, 3, 4, 5}, {0, 0, 0, 0, 0}};
for (auto& row : matrix) {
// row is a std::vector<int> reference that can be changed
}
for (auto const& row : matrix) {
// row is a const std::vector<int> reference that cannot be changed
}
}
Smart Pointers
Smart Pointers
Class wrapper of un-named heap objects (i.e., raw pointer) in named stack objects so that object lifetimes can be managed much easier (makes them automatic)
#include <memory>
int main() {
{ // ptr will only exist within these braces
// and will not require manual deallocation.
std::unique_ptr<int> ptr{new int{3}};
}
// There are no memory leaks since ptr will free int(3).
}
Smart Pointers
There are 3 types of smart pointers:
- Unique pointers
- Shared pointers
- Weak pointers
Type | Shared Ownership | Take Ownership |
---|---|---|
std::unique_ptr<T> | No | Yes |
raw pointer | No | No |
std::shared_ptr<T> | Yes | Yes |
std::weak_ptr<T> | No | No |
Unique Pointer
- Unique pointers owns the object
- When the unique pointer is destructed, the underlying object is destructed too
- No additional/very tiny overhead compared to raw pointer
- Has no copy constructor/assignment
-
make_unique<T>()
is safe for creating temporary variables and assigning them to aunique_ptr
Unique Pointer
#include <iostream>
#include <memory>
int main() {
// 1 - Worst - you can accidentally own the resource multiple
// times, or easily forget to own it.
auto* silly_string = new std::string{"Hi"};
auto up1 = std::unique_ptr<std::string>(silly_string);
auto up11 = std::unique_ptr<std::string>(silly_string);
// 2 - Not good - requires actual thinking about whether there's a leak.
auto up2 = std::unique_ptr<std::string>(new std::string("Hello"));
// 3 - Good - no thinking required.
auto up3 = std::make_unique<std::string>("Hello");
}
Shared Pointer
- Shared pointers allow several pointers to share ownership over an object
- When a shared pointer is destructed, if its the only shared pointer left pointing at the object, then the object is destroyed
#include <iostream>
#include <memory>
int main() {
auto x = std::make_shared<int>(5);
auto y = x; // Using copy constructor
std::cout << "use count: " << x.use_count() << "\n"; // 2
std::cout << "value: " << *x << "\n"; // 5
x.reset(); // Memory still exists, due to y.
std::cout << "use count: " << y.use_count() << "\n"; // 1
std::cout << "value: " << *y << "\n"; // 5
y.reset(); // Deletes the memory, since
// no one else owns the memory
std::cout << "use count: " << x.use_count() << "\n"; // 0
// std::cout << "value: " << *y << "\n";
}
Weak Pointer
- Weak pointers are used when:
- You don't want to add to the reference count
- You want to be able to check if the underlying data is valid before using it
#include <iostream>
#include <memory>
int main() {
auto x = std::make_shared<int>(1);
auto wp = std::weak_ptr<int>(x); // x owns the memory
auto y = wp.lock(); // Creates a shared_ptr
if (y != nullptr) { // x and y own the memory
// Do something with y
std::cout << "Attempt 1: " << *y << '\n';
}
}
Extra info, not in course.
Weak Pointer
Weak pointers are also used to avoid circular references which can result in memory leaks.
Extra info, not in course.
Smart Pointers
When to use which?
- Almost always want to use a unique pointer over shared
- Use a shared pointer when:
- An object has multiple owners, and you don't know which one will stay around the longest
Code Demo
Convert to Smart Pointers
Code Demo
Convert the following program so that raw pointers are replaced with smart pointers (prefer std::unique_ptr
over std::shared_ptr
when possible). The following program may or may not be correct.
int main() {
int* ptr1{new int{1}};
int* ptr2{new int{2}};
int* ptr3{ptr2};
int var{5};
int* ptr4{new int{var}};
int* ptr5{new int{var}};
delete ptr1;
delete ptr2;
delete ptr3;
delete ptr4;
delete ptr5;
}
int main() {
int* ptr1{new int{1}};
int* ptr2{new int{2}};
int* ptr3{ptr2};
int var{5};
int* ptr4{new int{var}};
int* ptr5{new int{var}};
delete ptr1;
delete ptr2;
delete ptr3;
delete ptr4;
delete ptr5;
}
#include <memory>
int main() {
std::unique_ptr<int> ptr1{new int{1}};
std::shared_ptr<int> ptr2{new int{2}};
std::shared_ptr<int> ptr3{ptr2};
int var{5};
std::unique_ptr<int> ptr4{new int{var}};
std::unique_ptr<int> ptr5{new int{var}};
}
-
ptr1
is astd::unique_ptr<int>
. -
ptr2
andptr3
arestd::shared_ptr<int>
becauseptr3
points toprt2
's resource. -
ptr4
andptr5
arestd::unique_ptr
because memory is allocated forvar
for both pointers. -
delete
is not used since the deletion of the resource given to smart pointer is managed automatically.
Code Demo
Convert to smart pointers 2
Code Demo
Convert the following program so that it uses smart pointers instead of raw pointers.
#include <cstdlib>
#include <memory>
void foo(int* ptr) {
return;
}
int main() {
int* ptr{(int*)malloc(sizeof(int))};
*ptr = 42;
foo(ptr);
free(ptr);
}
Code Demo
Convert the following program so that it uses smart pointers instead of raw pointers.
#include <cstdlib>
#include <memory>
void foo(int* ptr) {
return;
}
int main() {
int* ptr{(int*)malloc(sizeof(int))};
*ptr = 42;
foo(ptr);
free(ptr);
}
#include <memory>
void foo(std::shared_ptr<int>& ptr) {
return;
}
int main() {
auto ptr{std::make_shared<int>(42)};
foo(ptr);
}
Note the reference in foo(...)
, we don't want to create a copy of the shared_ptr
Code Demo
Challenge: Implement a Shared Pointer
Code Demo
Implement a shared pointer to an int
in a class called SharedPointer
. The specification for SharedPointer
is given below:
or look at the pdf on teams
Code Demo
#ifndef SHARED_POINTER_HPP
#define SHARED_POINTER_HPP
class SharedPointer {
public:
SharedPointer();
SharedPointer(int* pointer);
~SharedPointer();
int* get();
int const* get() const;
int& operator*();
int const& operator*() const;
long use_count() const;
private:
int* pointer_;
};
#endif // SHARED_POINTER_HPP
Note the const
correctness
Code Demo
int main() {
auto* ptr = new int{42}; // Malloc the memory for an integer with a value of 42
auto sp_1 = SharedPointer(ptr); // Create an instance
std::cout << "count: " << sp_1.use_count() << std::endl;
auto sp_2 = SharedPointer{ptr};
std::cout << "count: " << sp_1.use_count() << std::endl;
{
auto const sp_3 = SharedPointer{ptr};
std::cout << "count: " << sp_1.use_count() << std::endl;
} // sp_3 gets deleted
std::cout << "count: " << sp_1.use_count() << std::endl;
auto const sp_4 = SharedPointer{ptr};
auto sp_4_ptr = sp_4.get(); // Gets me the pointer as a const
std::cout << sp_4_ptr << " vs " << ptr << std::endl;
auto& test2 = *sp_1; // Gets me a reference of the integer
std::cout << test2 << std::endl;
test2 = 100; // Change the value using the reference
std::cout << *(sp_1.get()) << std::endl; // Get the address of the integer and deference to
// print value
auto const& test3 = *sp_4; // Get a const reference of integer
std::cout << test3 << std::endl;
}
Feedback
MTRN2500 Week 3 Lab 1 22T3
By kuroson
MTRN2500 Week 3 Lab 1 22T3
- 395