CCC 9: Memory - TEAM1771/Crash-Course GitHub Wiki
The stack is like a small, temporary storage area for your program.
- When you create a variable inside a function, it goes on the stack.
Imagine the things that you may take out on your desk when you're working on homework. Most will be small, like a pencil or calculator, but you may also have the occasional textbook.
- Your desk space is limited, just like the stack.
- The stack is useful for small things you need quickly, like numbers or small pieces of information.
- Memory on the stack is automatically managed by the computer.
- When the function finishes, the variables on the stack are automatically removed.
- Every example we have seen so far in this guide has actually been with the stack.
- Most of the data we deal with in robotics is small, like commands or sensor data.
- Any variable created without specifically requesting to use the heap will be placed on the stack.
- However, some std objects store data on the heap, such as vectors, which is helpful when dealing with large amounts of data.
Example:
void drive()
{
// Both variables are allocated on the stack
auto const x_input = -frc::ApplyDeadband(Buttons::drive.GetY(), 0.08);
auto const y_input = -frc::ApplyDeadband(Buttons::drive.GetX(), 0.08);
runDrivetrain(x_input,y_input);
} // Both variables are automatically removed from the stack here.
The heap is like a big storage room where you can keep larger things for a more extended period.
In the analogy comparing stack to a desk, the heap would be your desk drawers where you store textbooks, binders, etc. for a longer period of time.
- When you require more significant space, you can ask the computer to reserve some on the heap. It's like asking the storage room manager to give you a specific shelf.
- In robotics, this might be useful when handling a large amount of time that you want to keep around, like a log of every action the robot has done, or when processing vision data.
- The heap is different from the stack because memory management is not managed automatically. In traditional C code, you ask the computer to allocate space for your data using
new
ormalloc
. - However, what you do with this space is completely up to you, and that includes letting the computer know when you're done with it by using
delete
orfree
. - If you don't, that memory will be unusable until the program exits, creating a “memory leak.”
Example:
void logRobot()
{
std::string* log = new std::string[10];
log[0] = “Robot Started”;
log[1] = “Autonomous Selected”;
...
log[10] = “Ending log”;
delete[] L; // If this line isn't used, the memory will become unusable.
}
However, this practice is highly discouraged in modern C++, because it's very easy to mess up and forget to free (or accidentally delete too early) your memory. Instead, tools such as references and smart pointers can help you manage heap data easily.
Here's a great video about stack vs heap memory.
A reference in C++ is like an alias or an alternative name for an existing variable.
- It allows you to create another name (reference) for a variable, and both names refer to the same data in memory.
- When you change the value through the reference, the original variable's value also changes.
Example 1: Basic Reference
#include <iostream>
int main()
{
int speed = 20;
int &speed_ref = speed; // speed_ref is a reference to speed
std::cout << speed << std::endl; // Output: 20
std::cout << speed_ref << std::endl; // Output: 20
speed_ref = 25; // Updating the value through the reference
std::cout << speed << std::endl; // Output: 25
std::cout << speed_ref << std::endl; // Output: 25
}
-
References are declared by using an ampersand
&
before the variable name and are assigned directly to another variable. -
References are handy when you want to share the same data between different parts of your program without making copies.
-
Most functions will take
const
references to avoid copying while preventing data modification. -
However, taking a reference can also be used to modify data from outside inside a function.
-
We typically avoid this practice as it can be confusing to understand what happens to the data.
Example 2: Using References in Functions
void makeHeadingFieldRelative(frc::Rotation2d &heading)
{
bool const is_red = isRed(); // Checks if robot is on red alliance
if (is_red) // If on red alliance, the heading needs to be mirrored around 90 deg
heading = frc::Rotation2d{180_deg} - heading;
}
int main()
{
frc::Rotation2d right_heading{30_deg};
makeHeadingFieldRelative(right_heading);
// .Degrees().value() returns the heading as a double in degrees from 0 to 360
std::cout << right_heading.Degrees().value() << std::endl; // This outputs 150_deg if on red alliance
}
- References must be declared on initialization (you have to tell it what to reference when making it)
- References cannot be changed to reference a different variable
- A reference is only valid as long as the data it is pointing to exists.
Learn more about references from this video!
Pointers are the super buff father of references.
- Each variable actually has an address used by the computer to reference to its location in memory, just like a book in a library.
- Pointers are special variables that hold the memory address of another variable
For example, a records book may store the "address" of each book in a library, rather than the data of the books themselves.
Example: Using Pointers
#include <iostream>
int main() {
int number = 42; // A regular integer variable
int* ptr = &number; // A pointer that stores the memory address of 'number'
std::cout << number << std::endl; // Output: Value of number: 42
std::cout << &number << std::endl; // Output: Memory address of number: 0x7ffc52e010ac
std::cout << ptr << std::endl; // Output: Value of ptr: 0x7ffc52e010ac
std::cout << *ptr << std::endl; // Output: Value stored at the memory address pointed by ptr: 42
return 0;
}
- A pointer variable is declared using an asterisk
*
following the variable type (int
) and needs to be assigned to the memory address of anotherint
variable by placing an ampersand&
before the variable name. - Trying to print out
ptr
will only print the memory address of the variable it is assigned to. When you want to actually manage the data it points to, use an asterisk*
before the pointer name to “deference” the pointer and actually access the data. - Pointers are necessary to work with data on the heap, but can also be used for stack variables.
- Remember to be careful when using pointers to ensure they point to valid memory addresses and avoid accessing memory that has been deleted.
Check out this video for more on pointers.
Smart pointers are special objects in C++ that behave like regular pointers but provide automatic memory management for heap data.
- Essentially, they act like an assistant that keeps track of the ownership of a heap variable and automatically deletes it when nobody needs it anymore.
- They automatically free the memory they point to when the smart pointer goes out of scope, ensuring that we don't have to manually release memory like we do with regular pointers.
- This is the recommended way to manage data on the heap.
Example: Using Smart Pointers
#include <iostream>
#include <memory> // Include the smart pointer library
int main()
{
// Using std::unique_ptr for exclusive ownership
std::unique_ptr<int> unique_ptr = std::make_unique<int>(42);
std::cout << << *unique_ptr << std::endl; // Output: Value of unique_ptr: 42
// This heap variable will "stay alive" until unique_ptr goes out of scope, after which it will automatically be deleted
// Using std::shared_ptr for shared ownership
std::shared_ptr<int> shared_ptr1 = std::make_shared<int>(100);
std::shared_ptr<int> shared_ptr2 = shared_ptr1; // Both shared_ptr1 and shared_ptr2 now share ownership
std::cout << *shared_ptr1 << std::endl; // Output: Value of shared_ptr1: 100
std::cout << *shared_ptr2 << std::endl; // Output: Value of shared_ptr2: 100
// This variable will "stay alive" until both shared_ptrs go out of scope, after which it will automatically be deleted
// Using std::weak_ptr to avoid circular references
std::shared_ptr<int> shared_ptr3 = std::make_shared<int>(200);
std::weak_ptr<int> weak_ptr = shared_ptr3; // weak_ptr observes shared_ptr3 without affecting its ownership
std::cout << *shared_ptr3 << std::endl; // Output: Value of shared_ptr3: 200
// This variable will "stay alive" until only the shared_ptr3 goes out of scope, after which it will automatically be deleted
// This means there is the possibility that weak_ptr becomes invalid
// To access the value of a weak_ptr, we have to check if it's still valid
if (auto shared_ptr4 = weak_ptr.lock()) {
std::cout << *shared_ptr4 << std::endl; // Output: Value of weak_ptr: 200
} else {
std::cout << "weak_ptr is no longer valid." << std::endl;
}
// When the shared_ptr3 goes out of scope, the memory is deallocated automatically
return 0;
}
In this example, we use three types of smart pointers:
std::unique_ptr: It provides exclusive ownership. When the unique_ptr goes out of scope or gets deleted, it automatically deallocates the memory it points to.
std::shared_ptr: It provides shared ownership. Multiple shared_ptr objects can share ownership of the same memory. When the last shared_ptr that owns the memory goes out of scope or gets deleted, the memory is deallocated.
std::weak_ptr: It observes shared ownership without contributing to the reference count. It is useful to prevent circular references between shared_ptr objects, which can lead to memory leaks.
-
Smart pointers make memory management safer and more convenient by automatically handling memory deallocation.
-
We often use unique_ptrs to allow functions to point to a variable that can only be initalized after the robot's OS fully boots and the RoboRIO is able to connect over CAN to devices such as motors.
If you wish to learn more about smart pointers, check this video out!