10B_MemoryManagement
10B_MemoryManagement
The C++ programming language gives its users incredible control in how
memory is managed. Although when writing simple user-defined data
structures and when using templated classes like std::vector we do not
need to worry about individual chunks of memory, it is important to
understand what is going on “under the hood.” When we write more
complicated classes that need to manage memory directly or wish to
understand such classes, we need a working knowledge of pointers and
different types of references; different types of constructors; and the
means of managing memory safely.
Pointers
std::cout << (*jp)++ << " "; // prints 8 and now j==9
7 8
Pointers
std::cout << (*jp)++ << " "; // prints 8 and now j==9
7 8
Pointers
++(*ip);
7 8
Pointers
++(*ip);
7 8
Pointers
Remark: the addresses we see displayed when printing pointers are not
the physical address of the variable in memory. In general, we can only
see the virtual address in memory, an abstraction that allows for safer
running of programs.
Pointers
std::string s = "window";
std::string *sp = &s;
6
6
Found it!
Pointers
The type of object to which a pointer points cannot be changed and to
assign a pointer a value, the value it is assigned to must match in the
type it points to (except for void* to be discussed much later).
double d = 3.14;
double *dp = &d;
std::string s("...");
std::string *sp = &s;
c14eb424 1 3 9 2
Placement of const keyword
As a rule, the const keyword modifies the item to its left, unless it cannot
modify anything to its left in which case it modifies the item to its right...
++xp; // valid
*xp = 123; // fine
I yp is a pointer to a constant int, const int*: we can change where
yp points, but we cannot modify the value *yp.
++yp; // valid
*yp = 0; // ERROR: yp points to const int!
Const and Pointers
int x[] = { 42, 43, 44 };
int *xp = x; // xp points to 42
*zp = 0; // okay
++zp; // ERROR: zp is a const pointer
I wp is a constant pointer to a constant int, const int * const: we
cannot change where wp points nor can we change the value *wp
through wp.
Consider:
int i = 322;
const int *ip = &i;
The above would indirectly give the ability to modify i through ip, but ip
treats i as const...
References
int j = 22;
double &k = j; // cannot bind double reference to an int: type mismatch
The lifetime of the rvalue is then extended for the lifetime of the reference.
Lvalue References
Basically:
I We cannot bind an lvalue reference to an rvalue.
I We cannot bind a normal (non-const) reference to something that is
const because that indirectly gives permission to modify the const
object through that reference.
I We cannot modify an object, even if it is not really const, through a
reference to const.
Const References
The proper term for a variable like y below is reference to const, not a
"const reference".
int x = 4;
const int& y = x;
Syntax & and *
double d = 0;
double &x = d; // x is a REFERENCE to d
double *y = &x; // y is a POINTER to x (and d), &x is the address of d
++(*y); // *y IS d
std::cout <<d; // prints 1
Syntax & and * I
Consider Cat and a Person classes showing how people are owned by
cats and cats have people as servants... close enough to reality.
The classes store pointers to one another. Compilers only allow a symbol
to be used after it has been declared: observe the first line of declaring a
Person class (more on declarations/definitions later). Just like a function
is declared by giving a signature, no body required, a class is declared by
listing it as a class, no interface required.
Syntax & and * II
class Person; // declare that Person is a class
class Cat {
private:
public:
So each class stores a pointer to const for the other class, initially being
nullptr.
Note how the assign_ functions accept a pointer (as pointer to const)
and do pointer assignment, while the is_ functions accept a reference (as
reference to const) and compare addresses!
Cat cotton;
Person patricia;
cotton.is_servant(patricia); // true
Initialization and Assignment
Multiple variables can be defined (and initialized) as a single statement.
Initialization is a left-to-right operation.
Basically we include the pure type on the left and we need to add a * or &
to the left of a variable to make it into a pointer or reference to the type on
the left.
Initialization and Assignment
s1 = s2 = "new message";
/* s2 is assigned to "new message" and its new value is returned and
used to assign s1 to the same value */
Const Casts
++dr; // nope!
double *dp2 = dp; // not allowed!
/* cast away constness so have regular double*, can initialize dp2 from
that */
double *dp2 = const_cast<double*>(dp);
When the a program is run, the loader allocates memory in the RAM for
the operations of the program. This includes regions known as the text
segment, the heap, and the stack.
Within the text segment are found the code memory (the raw machine
code for the program to operate) of lowest address and the data of higher
address. The data stores the static and global variables: the variables
that have memory allocated for them or are initialized before int main()
runs.
Stack
The stack stores local variables created during the running of the
program and, in the case of functions, the return address where to send
the return value, etc.
Stack variables can be quickly accessed by the CPU and do not require
the use of pointers. We use stack variables when we know how many
variables, i.e., how much memory, we need to begin with.
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Stack
Recall that a set of braces defines a scope:
{
int x = 4;
{
double y;
y = std::sin(x); /* The sine function (probably) creates
local variables to capture x to do computation */
} // y goes out of scope and is destroyed
double w = 0;
} // x and w go out of scope: first w is destroyed, then x
Heap
The heap (also called “free store”) stores objects in so called dynamic
memory. This is useful when we don’t know exactly how much memory
we may require (so memory allocation may need to be dynamic).
Data access is slower for heap memory, and we can only access heap
memory through pointers (i.e. knowing the memory addresses).
Heap
std::string myString("chalk");
std::string also likely stores its data terminating by the null character.
new and delete
The keywords new and delete are used for dynamic memory storage.
These are very delicate operations and using these operations without
great care can cause serious bugs due to accessing invalid locations in
memory and memory leaks (when heap memory is tied up but can no
longer be accessed).
Modern C++ offers smart pointers to help with these dangerous memory
management issues. We will look at both.
new and delete
With the () syntax, variables are value-initialized so that class types are
default constructed and fundamental types are set to 0.
new and delete
// u points to unsigned int on the heap, value 13
unsigned *u = new unsigned(13);
The error of allocating heap memory but not releasing that memory when
it is no longer needed is called a memory leak. This can lead to
programs slowing down or crashing due to memory exhaustion.
To properly free the memory, we need to request that from the compiler
with the delete expression.
delete p;
Recall v and s are pointers that have been deleted but do not point to
null. u has been deleted and now points to null.
// cannot delete v again - v was already deleted but not set to nullptr!
delete v;
Note that the return of the new [] expression is a pointer to the first
element, not an array!
The array size parameter need not be a const value like for static arrays.
new and delete
size_t sz = 42;
int iarr[sz]; // ERROR: sz is not constant!
The values in the contiguous block can be: default constructed (for
fundamental types this means uninitialized), value initialized (so
fundamental types would be set to 0); or, if the first several values are set
by the user, the remaining values will be value initialized.
{ // some scope
std::string msg("hi");
} // end of scope, msg will be destroyed and the heap memory freed
{ // some scope
char *msg = new char[3] { 'h', 'i', '\0' };
} // end of scope, memory leak has taken place!
There are also smart pointers that can be used like pointers, but which
fulfill RAII.
Smart Pointers
3.14 0
!!!
0
30.3
Shared Pointers
Recall that the logical operators && and || are evaluated lazily: for &&,
the first false encountered results in a value of false; for ||, the first true
that is encountered results in an evaluation of true.
Unique Pointers
// okay ...
std::unique_ptr<double> dp = std::make_unique<double>( 10.6 );
400400
The std::make_unique function can also be used for dynamic arrays but
besides a default constructor, it can only accept a size parameter for the
array.
There are different types of constness of smart pointers, too. Here’s the
parallels between them:
In C++, all expressions (things that can be evaluated) have a type and
value category.
int x; // x is an lvalue
double d = 3.14 - 6; // d is an lvalue, 3.14-6 is an rvalue
Besides the “ordinary” lvalue references (&), there are also rvalue
references (&&).
An rvalue reference can only bind to rvalues. They can help make
various constructions and assignments more efficient.
Note: some entities don’t have "names" but are still lvalues (like elements
of an array/vector)!
xvalue also includes objects that are prvalues upon which member
access has been invoked.
C++ Standard: "An xvalue is a glvalue that denotes an object ... whose
resources can be reused (usually because it is near the end of its
lifetime)... Certain kinds of expressions involving rvalue references yield
xvalues, such as a call to a function whose return type is an rvalue
reference or a cast to an rvalue reference type."
R-,GL-,PR-,X-, and L-Values
"abcdefg"; /* lvalue and glvalue: string literals (of type const char*)
have memory locations */
double g = 9.8;
double *g = &g; // rvalue and prvalue
std::string word("rain");
std::move(word); /* xvalue, glvalue, and rvalue: the move function
returns an x-value */
// function signatures
std::string F();
const std::string& G(const std::string&);
std::string&& H(std::string&);
// ...
std::string lvalue("L");
Then:
decltype(foo()) x = 4; // x is double
const decltype(x) y = 8; // y is const double
decltype
namespace nameOfSpace {
/* stuff */
}
#ifndef _EXAMPLE_
#define _EXAMPLE_
namespace example {
#endif
Namespaces
Y.cpp
#include<iostream>
#include "Header.h"
example::Y::bar(const example::Y& y)
Namespaces
baz.cpp
#include "Header.h"
main.cpp
#include "Header.h"
int main() {
example::Y y; // create a Y object as defined in example namespace
example::baz(y); // calls Y::bar to call Y::foo to print "foo"
return 0;
}
Constructors and Destructors
A constructor creates a class object and a destructor specifies what to
do when the object has reached the end of its lifetime.
There are two other constructors that are often compiler generated, but
which we can write ourselves: the copy constructor and move
constructor.
The variables b_copy and b_copy2 are copy constructed from b. The
variable b is unchanged through these constructions.
std::string s = "Samantha";
For the classes that we write, the compiler-generated copy and move
constructors memberwise (i.e. variable-by-variable) copy or “move” data
from the constructed-from object to create the new object.
Constructors and Destructors
The signatures of these constructors and the destructor are below for a
class T:
T ( ); // default constructor
~T ( ); // destructor
Constructors and Destructors
The copy constructor should copy all the values stored in the
assigned-from object: care is needed when dealing with heap memory
because we generally want a separate, independent copy.
The move constructor generally takes the pointers and values from the
constructed-from object, giving them to the constructed object, and
leaves that constructed-from object in a state suitable for destruction.
The destructor should ensure all the resources of the object are cleared
up. When the new expressions have been used, there should be calls to
delete expressions when raw pointers are being used.
Constructors and Destructors
In the following slides, we imagine that T is a class type that stores some
memory directly on the stack and also stores a pointer that manages
heap memory.
We will write our own simplified string class to explore these ideas. It will
be defined within a namespace basic.
namespace basic {
class string {
private:
size_t sz; // the size of the object (counting null char)
char *ptr; // points to heap memory of chars
A Simple String Class from Scratch II
public:
string(); // default constructor
string( const char* ); // accept a string literal input
string( const string& ); // copy constructor
string ( string && ); // move constructor
string& operator=(const string&); // copy assignment
string& operator=(string&&); // move assignment
~string(); // a destructor
void concat( const string& ); // a concatenation function
void display() const; // print to screen
char& at(size_t i); // at function for non-const strings
char at(size_t i) const; // at function for const strings
};
}
A Simple String Class from Scratch
Often a deep copy that takes into account heap memory is preferred.
Thus, we need to write our own copy/move constructors.
A Simple String Class from Scratch
// sz is now the size of the string literal, including the null character
ptr = new char[sz]; // allocate large enough dynamic array
for (size_t i = 0; i < sz; ++i) { // loop over string literal chars
ptr[i] = c[i]; // c[i] same as *(c+i)
}
}
A Simple String Class from Scratch
Within the constructor initializer list, one construtor can "delegate" its
work to another constructor by giving the constructor name and
arguments. We chose to default construct the string initially.
After it being default constructed, we swap the resources from the default
object (storing only the null character) and the object we harvest from.
This has a nice property of making sure the "moved from" object is still in
a valid state (it’s just the empty string).
A Simple String Class from Scratch
Illustration of move constructor:
A Simple String Class from Scratch
Illustration of move constructor:
A Simple String Class from Scratch
Illustration of move constructor:
A Simple String Class from Scratch
Illustration of move constructor:
A Simple String Class from Scratch
Illustration of move constructor:
A Simple String Class from Scratch
Illustration of move constructor:
A Simple String Class from Scratch
Because we are not using smart pointers, we have to write our own
destructor to free the heap memory. Fortunately, there isn’t much work to
do here.
string::~string() {
// call the proper delete function for the dynamically allocated array
delete [ ] ptr;
}
The at function, which just returns the char at a given index is said to be
overloaded on const. If the object really is const, then only a copy of the
char is returned, preventing mutations; but if the object is not const, a
char& is returned, allowing for mutations.
// copy assignment: x’s old data will be gone and it will be a copy of y
x = y;
/* move assignment: y’s old data will be gone and it takes the value of the
temporary value */
y = basic::string("Z");
Within every class, there is a special value, this, which is a pointer to the
class itself.
Remark 1: the check this == &rhs tests whether the address of the
assigned-from and assigned-to objects are the same. If so, we avoid the
work of extra copying.
struct Foo{
Foo(int _i) : i(_i) {}
int i;
};
// ...
struct Bar{
explicit Bar(int _i) : i(_i) {}
int i;
};
// ...
class A {
public:
A(const A&) = delete;
// OTHER STUFF
};
Then we would not define A(const A&) anywhere (it has already been
defined as deleted).
class X {
public:
X() { } // default constructor
X(const X &) { } // copy constructor
// there is no move constructor!!!
};
We could wonder: how many times is the copy constructor invoked in the
line
X x = f(); ?
This eliding of the copy construtor is called copy elision and is part of
C++ return value optimization. Modern compilers are required to elide
copies and moves when a direct construction is permissible. They can
even handle cases of named variables:
std::string foo() {
std::string s("FOO");
return s;
}
// ...
This is also why, although technically not a direct construction, the code
below could yield a direct construction anyway:
std::string s = std::string("pita");
This content is protected and may not be shared, uploaded, or distributed.
PIC 10B, UCLA
©Michael Lindstrom, 2016-2022
Move Constructors for Classes with Class Member
Variables
struct val_msg {
int val;
basic::string msg;
// ...
};
Move Constructors for Classes with Class Member
Variables
The int value can be taken directly since ints are fundamental types
taking up little memory. But the basic::string should be transferred
efficiently...
Move Constructors for Classes with Class Member
Variables
val_msg(val_msg&& right) : val(right.val), msg(std::move(right.msg)) { }
Even though right references an rvalue (an object that will soon no
longer exist and that we should harvest from), right itself is an lvalue (it
has a name!). And right.msg is also an lvalue. If we just wrote
(and later define it) and such an operator would work with std::cout, or
an output file stream object (std::ofstream), or an output string stream
object (std::ostringstream) because an std::ostream& can bind to all
of them when passed as an argument to the operator.
When dealing with large amounts of data, it could be possible to run out
of memory. In that case, the new expression can throw an exception.
We can also request that new not throw an exception but instead return
nullptr if the memory allocation fails.