Dynamic Memory
Dynamic Memory: Memory allocated at run time whose lifetime is independent of the scope in which the allocation occurs.
Free Store / Heap: The memory pool used for dynamically allocated objects.
C++ provides two broad ways to manage dynamic memory:
- smart pointers
- direct use of
newanddelete
The preferred approach is usually to use library types that manage lifetime automatically.
Required header:
#include <memory>
Important smart pointers are:
shared_ptr
unique_ptr
weak_ptr
12.1 Dynamic Memory and Smart Pointers
Smart Pointer: A class that behaves like a pointer while also managing the lifetime of the object to which it points.
The three main smart pointers have different ownership models.
| Smart Pointer | Ownership |
|---|---|
shared_ptr<T> | Shared ownership |
unique_ptr<T> | Exclusive ownership |
weak_ptr<T> | Non-owning observation |
Dynamic-memory bugs commonly include:
- memory leaks
- dangling pointers
- use after delete
- double delete
Smart pointers reduce these risks by connecting resource lifetime to object lifetime.
1) The shared_ptr Class
shared_ptr: A smart pointer that allows several smart pointers to share ownership of one object.
std::shared_ptr<int> pointer;
A default-initialized shared_ptr is null.
Test it like a built-in pointer:
if (pointer)
{
std::cout << *pointer;
}
Basic shared_ptr Operations
| Operation | Meaning |
|---|---|
shared_ptr<T> p | Null smart pointer |
p | Tests whether an object is managed |
*p | Access managed object |
p->member | Access managed object's member |
p.get() | Return underlying built-in pointer |
p.swap(q) | Exchange managed objects |
p.use_count() | Number of owning shared_ptrs |
p.unique() | Whether this is the only owner |
Example:
auto pointer =
std::make_shared<std::string>(
"hello"
);
std::cout
<< pointer->size();
make_shared()
make_shared < T() >: Allocates and initializes an object and returns a shared_ptr<T> that owns it.
auto number =
std::make_shared<int>(42);
Conceptually:
dynamic int object
42
↑
|
shared_ptr<int>
Arguments are passed to the constructor of the allocated object.
auto text =
std::make_shared<std::string>(
5,
'A'
);
The managed string contains:
AAAAA
Value Initialization
Calling make_shared<T>() without constructor arguments value-initializes the object.
auto number =
std::make_shared<int>();
The managed int has value:
0
Copying shared_ptrs
Copying a shared_ptr creates another owner of the same object.
auto p1 =
std::make_shared<int>(42);
auto p2 = p1;
Conceptually:
p1 ──┐
├──> 42
p2 ──┘
Both pointers refer to the same dynamically allocated object.
*p2 = 100;
std::cout << *p1;
Output:
100
Reference Count
A shared_ptr implementation tracks how many owning smart pointers share the object.
auto p1 =
std::make_shared<int>(42);
std::cout
<< p1.use_count();
After:
auto p2 = p1;
the ownership count increases.
When an owning shared_ptr is destroyed or reassigned, the count decreases.
The exact counting mechanism is an implementation detail, but the ownership behavior is defined by the library.
Automatic Destruction
When the last owning shared_ptr disappears, the managed object is destroyed automatically.
void function()
{
auto pointer =
std::make_shared<std::string>(
"data"
);
// use pointer
}
Conceptually:
enter function
↓
create shared_ptr
↓
dynamic string exists
↓
leave function
↓
last shared_ptr destroyed
↓
dynamic string destroyed
No explicit delete is required.
Reassignment and Ownership
auto p =
std::make_shared<int>(10);
auto q =
std::make_shared<int>(20);
p = q;
After assignment:
preleases ownership of the first objectpbegins sharing the object owned byq- the first object is destroyed if no other owner remains
Dynamic-Lifetime Resources
Dynamic allocation is useful when the lifetime of shared data must not match the lifetime of the object that originally created it.
A common design is:
several ordinary objects
↓
share one dynamically allocated resource
shared_ptr expresses this ownership model directly.
StrBlob
A simplified StrBlob can share a dynamically allocated vector<string>.
#include <initializer_list>
#include <memory>
#include <string>
#include <vector>
class StrBlob
{
public:
using size_type =
std::vector<
std::string
>::size_type;
StrBlob();
StrBlob(
std::initializer_list<
std::string
> values);
size_type size() const
{
return data->size();
}
bool empty() const
{
return data->empty();
}
void push_back(
const std::string& value)
{
data->push_back(value);
}
void pop_back();
std::string& front();
std::string& back();
private:
std::shared_ptr<
std::vector<std::string>
> data;
void check(
size_type index,
const std::string& message) const;
};
StrBlob Constructors
StrBlob::StrBlob()
: data(
std::make_shared<
std::vector<
std::string
>
>())
{
}
Initializer-list constructor:
StrBlob::StrBlob(
std::initializer_list<
std::string
> values)
: data(
std::make_shared<
std::vector<
std::string
>
>(values))
{
}
Each StrBlob stores a shared_ptr to the vector.
Shared Copy Semantics
StrBlob a{
"one",
"two"
};
StrBlob b = a;
The default copy operation copies the shared_ptr.
Conceptually:
a.data ──┐
├──> vector<string>
b.data ──┘
Therefore both StrBlob objects share the same underlying elements.
Bounds Checking
void StrBlob::check(
size_type index,
const std::string& message) const
{
if (index >= data->size())
{
throw std::out_of_range(
message
);
}
}
Element operations can reuse this helper.
std::string&
StrBlob::front()
{
check(
0,
"front on empty StrBlob"
);
return data->front();
}
2) Managing Memory Directly
C++ also provides explicit dynamic-memory management through:
new
delete
This is lower level and more error-prone.
new
new: Allocates memory, constructs an object in that memory, and returns a pointer to it.
int* pointer =
new int;
The object remains alive until it is explicitly deleted.
Default Initialization
int* pointer =
new int;
For a built-in type, the value is indeterminate.
It should not be read before being initialized.
Value Initialization
int* pointer =
new int();
The dynamic integer is value-initialized to:
0
Direct Initialization
int* pointer =
new int(42);
The allocated object contains:
42
Class example:
std::string* text =
new std::string(
5,
'A'
);
List Initialization
std::vector<int>* values =
new std::vector<int>{
1,
2,
3
};
The dynamically allocated vector contains three elements.
Dynamically Allocated const Objects
A const dynamic object must be initialized when required by its type.
const int* pointer =
new const int(42);
The returned pointer points to const.
std::cout << *pointer;
but:
// Error:
// *pointer = 100;
Memory Exhaustion
Ordinary new throws:
std::bad_alloc
when allocation fails.
int* pointer =
new int;
Required header:
#include <new>
A nonthrowing form is:
int* pointer =
new (std::nothrow) int;
If allocation fails:
pointer == nullptr
rather than an exception being thrown.
delete
delete: Destroys a dynamically allocated object and returns its memory to the free store.
int* pointer =
new int(42);
delete pointer;
A delete expression performs two actions:
- runs the object's destructor
- releases the object's storage
Valid Pointers for delete
The operand of delete must be:
- null, or
- a valid pointer obtained from a corresponding
new
Valid:
int* p =
new int(42);
delete p;
Also valid:
int* p =
nullptr;
delete p;
Deleting null is harmless.
Invalid delete
Wrong:
int value = 10;
int* pointer =
&value;
// Undefined behavior:
// delete pointer;
The object was not allocated by new.
Dynamic Object Lifetime
A built-in pointer going out of scope does not destroy the dynamic object.
void function()
{
int* pointer =
new int(42);
}
When the function ends:
pointer is destroyed
dynamic int remains allocated
```
The program has lost the address of the allocation.
That is a memory leak.
---
### Memory Leak
**Memory Leak:** Dynamically allocated memory that can no longer be released because the program has lost the owning pointer.
~~~cpp
int* pointer =
new int(42);
pointer =
new int(100);
The address of the first allocation is lost.
The first object cannot now be deleted through pointer.
Use After Delete
int* pointer =
new int(42);
delete pointer;
// Undefined behavior:
// std::cout << *pointer;
After delete, the old pointer value no longer denotes a live object.
Dangling Pointer
A pointer that still contains the address of a destroyed object is a dangling pointer.
A common defensive step is:
delete pointer;
pointer = nullptr;
Now this particular pointer clearly refers to no object.
However, aliases remain dangerous.
int* p =
new int(42);
int* q = p;
delete p;
p = nullptr;
// q is still dangling
Resetting one pointer does not repair other pointers that referred to the same deleted object.
Double Delete
int* pointer =
new int(42);
delete pointer;
// Undefined behavior:
// delete pointer;
Deleting the same allocation more than once is undefined behavior.
Prefer Smart Pointers
Instead of:
int* pointer =
new int(42);
// ...
delete pointer;
prefer:
auto pointer =
std::make_shared<int>(42);
when shared ownership is needed.
Use direct memory management only when the ownership requirements genuinely require it.
3) Using shared_ptrs with new
A shared_ptr can take ownership of a pointer returned by new.
std::shared_ptr<int>
pointer(
new int(42)
);
The constructor is explicit.
Therefore this is not a valid implicit conversion:
// Error:
// std::shared_ptr<int> p
// = new int(42);
Use direct initialization.
Prefer make_shared()
Instead of:
std::shared_ptr<int>
pointer(
new int(42)
);
prefer:
auto pointer =
std::make_shared<int>(42);
when possible.
It is clearer and directly expresses managed allocation.
Do Not Mix Owning Raw Pointers and Smart Pointers
Dangerous:
int* raw =
new int(42);
std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw);
p1 and p2 have independent ownership state.
Both may eventually try to delete the same object.
The correct way to share ownership is:
auto p1 =
std::make_shared<int>(42);
auto p2 = p1;
get()
get() returns the underlying built-in pointer.
auto smart =
std::make_shared<int>(42);
int* raw =
smart.get();
The raw pointer does not gain ownership.
Use it only for temporary non-owning access.
Do not delete it:
// Wrong:
// delete raw;
Never Create Another Owner from get()
Dangerous:
auto p1 =
std::make_shared<int>(42);
int* raw =
p1.get();
// Wrong:
std::shared_ptr<int> p2(raw);
p1 and p2 now believe independently that they own the same object.
This can lead to double deletion.
reset()
A shared_ptr can release its current ownership.
auto pointer =
std::make_shared<int>(42);
pointer.reset();
Afterward:
pointer == nullptr
If it was the last owner, the object is destroyed.
Reset to a New Object
pointer.reset(
new int(100)
);
The old managed object is released according to the ownership rules, and the smart pointer takes ownership of the new pointer.
Using make_shared() for new allocations is generally clearer when possible.
4) Smart Pointers and Exceptions
Smart pointers provide automatic cleanup during stack unwinding.
void function()
{
auto pointer =
std::make_shared<
std::string
>("data");
operation_that_may_throw();
}
If an exception leaves the function, the local shared_ptr is destroyed automatically.
If it is the last owner, the managed object is destroyed.
Raw Pointer Exception Risk
void function()
{
int* pointer =
new int(42);
operation_that_may_throw();
delete pointer;
}
If the operation throws before delete executes, the allocation may leak.
This illustrates why resource management should normally be tied to object lifetime.
Custom Deleters
A smart pointer can use custom cleanup code instead of ordinary delete.
Suppose an external API has:
Resource* open_resource();
void close_resource(
Resource*);
A shared_ptr can be created with a custom deleter:
std::shared_ptr<Resource>
resource(
open_resource(),
close_resource
);
When the smart pointer releases the resource, it calls:
close_resource(pointer);
rather than delete.
Resource Management beyond Memory
Smart pointers can manage resources such as:
- file-like handles
- connections
- library objects requiring explicit release functions
The deleter connects resource cleanup to C++ object lifetime.
This is an application of RAII.
5) unique_ptr
unique_ptr: A smart pointer that exclusively owns its managed object.
std::unique_ptr<int>
pointer(
new int(42)
);
Only one unique_ptr owns a given object at a time.
Exclusive Ownership
unique_ptr
|
v
object
There is no shared reference count.
When the owning unique_ptr is destroyed, its object is destroyed automatically.
unique_ptr Cannot Be Copied
Invalid:
std::unique_ptr<int>
p1(
new int(42)
);
// Error:
// std::unique_ptr<int> p2 = p1;
Copying would create two exclusive owners, which contradicts the ownership model.
unique_ptr Operations
| Operation | Meaning |
|---|---|
unique_ptr<T> p | Null unique pointer |
p = nullptr | Delete current object and become null |
p.release() | Give up ownership and return raw pointer |
p.reset() | Delete current object and become null |
p.reset(q) | Delete current object and take ownership of q |
release()
std::unique_ptr<int>
pointer(
new int(42)
);
int* raw =
pointer.release();
Afterward:
pointer == nullptr
raw owns responsibility for the object
release() does not delete the object.
Therefore the returned pointer must be:
- passed to another owner, or
- eventually deleted manually
delete raw;
reset()
std::unique_ptr<int>
pointer(
new int(42)
);
pointer.reset();
The managed object is deleted and the pointer becomes null.
Transfer Ownership
A common C++11 transfer uses std::move.
std::unique_ptr<int>
p1(
new int(42)
);
std::unique_ptr<int>
p2 =
std::move(p1);
Afterward:
p1 == nullptr
p2 owns the object
Ownership moved rather than copied.
Returning unique_ptr
A function can return a unique_ptr to transfer ownership to the caller.
std::unique_ptr<int>
make_value()
{
return std::unique_ptr<int>(
new int(42)
);
}
Usage:
auto pointer =
make_value();
The caller becomes the owner.
Passing a Custom Deleter
The deleter type is part of the unique_ptr type.
void close_resource(
Resource* resource)
{
// release resource
}
Pointer type:
using ResourcePtr =
std::unique_ptr<
Resource,
void (*)(Resource*)
>;
Object:
ResourcePtr resource(
open_resource(),
close_resource
);
Cleanup occurs automatically when resource is destroyed.
6) weak_ptr
weak_ptr: A non-owning smart pointer that observes an object managed by shared_ptr.
auto shared =
std::make_shared<int>(42);
std::weak_ptr<int>
weak(shared);
Both refer to the same object, but only shared owns it.
Weak Ownership
Creating a weak_ptr does not increase the owning reference count.
Conceptually:
shared_ptr ──owns──> object
^
|
weak_ptr ──observes────┘
If the last shared_ptr disappears, the object is destroyed even if weak_ptrs remain.
Why a weak_ptr Cannot Be Dereferenced Directly
The observed object might already have been destroyed.
Therefore this is not the interface:
// no direct *weak access
Instead, use:
lock()
lock()
std::weak_ptr<int>
weak(shared);
if (auto pointer =
weak.lock())
{
std::cout
<< *pointer;
}
If the object still exists, lock() returns a shared_ptr.
If not, it returns a null shared_ptr.
expired()
if (weak.expired())
{
std::cout
<< "object is gone\n";
}
expired() is true when there are no owning shared_ptrs.
Weak Pointer Operations
| Operation | Meaning |
|---|---|
weak_ptr<T> w | Null weak pointer |
weak_ptr<T> w(sp) | Observe object managed by sp |
w.reset() | Become null |
w.use_count() | Owning shared_ptr count |
w.expired() | Whether no owner remains |
w.lock() | Obtain shared_ptr if object still exists |
StrBlobPtr
A pointer-like class can use weak_ptr so that it does not keep a StrBlob's underlying vector alive.
class StrBlobPtr
{
public:
StrBlobPtr() = default;
StrBlobPtr(
StrBlob& blob,
std::size_t position = 0)
: data(blob.data),
current(position)
{
}
private:
std::weak_ptr<
std::vector<std::string>
> data;
std::size_t current = 0;
};
The idea is:
StrBlob owns vector through shared_ptr
StrBlobPtr observes vector through weak_ptr
The pointer-like object must verify that the vector still exists before accessing it.
Lifetime Check with lock()
auto pointer =
data.lock();
if (!pointer)
{
throw std::runtime_error(
"unbound StrBlobPtr"
);
}
If lock() succeeds, the returned shared_ptr keeps the vector alive while the operation runs.
12.2 Dynamic Arrays
Dynamic Array: A dynamically allocated sequence of objects whose size can be chosen at run time.
Direct dynamic arrays are lower level than standard containers.
Prefer:
std::vector<T>
when a vector naturally satisfies the requirement.
1) new and Arrays
Array allocation syntax:
new T[n]
Example:
std::size_t size = 10;
int* values =
new int[size];
The size can be determined at run time.
Returned Type
Although multiple objects are allocated, the result is a pointer to the first element.
new int[10]
↓
int*
The pointer itself does not encode the array size.
No Container Interface
A dynamically allocated array does not provide members such as:
size()
begin()
end()
```
and the pointer alone cannot determine the allocation length.
The program must track the size separately.
---
### Default Initialization
~~~cpp
int* values =
new int[10];
Built-in elements are default initialized and therefore have indeterminate values.
Value Initialization
int* values =
new int[10]();
The elements are value initialized.
For int, each element becomes zero.
List Initialization
int* values =
new int[5]{
1,
2,
3
};
The first three elements are initialized from the list.
Remaining elements are value initialized.
Result:
1 2 3 0 0
Zero-Length Dynamic Array
This is legal:
int* values =
new int[0];
The returned pointer must not be dereferenced.
It can represent the end of an empty dynamic range.
The pointer must still be released using the matching delete form.
delete[]
A dynamic array must be freed with:
delete[] values;
not:
// Wrong:
// delete values;
Matching forms:
new T -> delete
new T[n] -> delete[]
Using the wrong delete form is undefined behavior.
Destruction Order
For class-type elements:
Object* objects =
new Object[count];
then:
delete[] objects;
destroys every constructed element, generally in reverse order of construction.
unique_ptr<T[]>
unique_ptr provides an array specialization.
std::unique_ptr<int[]>
values(
new int[10]()
);
Array elements can be accessed with subscripting.
values[0] = 42;
values[1] = 100;
When the smart pointer is destroyed, it automatically uses:
delete[]
shared_ptr and Arrays in the C++11 Model
The C++11 shared_ptr interface does not provide the same built-in array specialization used by unique_ptr<T[]>.
When a shared_ptr manages an array in this model, it needs a deleter that calls delete[].
std::shared_ptr<int>
values(
new int[10],
[](int* pointer)
{
delete[] pointer;
}
);
This is another reason to prefer vector for ordinary dynamic sequences.
2) The allocator Class
allocator: A library class that separates raw memory allocation from object construction.
Required header:
#include <memory>
Example:
std::allocator<std::string>
allocator;
Allocate raw storage:
auto pointer =
allocator.allocate(10);
At this point:
memory exists
objects do not yet exist
Why Separate Allocation and Construction?
An array allocation normally allocates and constructs every element immediately.
allocator allows:
allocate raw storage
↓
construct only needed objects
↓
destroy constructed objects
↓
deallocate storage
This is useful when implementing low-level containers.
allocate()
auto begin =
allocator.allocate(10);
There is enough raw memory for ten std::string objects.
But none of those objects has been constructed yet.
construct()
In the C++11 model used by this chapter:
auto current =
begin;
allocator.construct(
current,
"hello"
);
++current;
A std::string object now exists at the first position.
Construct another:
allocator.construct(
current,
5,
'A'
);
The object contains:
AAAAA
Use Only Constructed Objects
Raw allocated memory must not be used as though an object already existed.
Conceptually wrong:
// raw memory only:
// *begin = "hello";
Construct the object first.
destroy()
Destroy a constructed object without releasing the raw storage:
allocator.destroy(
current
);
When several objects were constructed, destroy only those objects.
A common pattern destroys them in reverse order.
while (current != begin)
{
allocator.destroy(
--current
);
}
deallocate()
After all constructed objects are destroyed:
allocator.deallocate(
begin,
10
);
The count must match the allocation size used for that block.
Complete allocator Pattern
std::allocator<std::string>
allocator;
auto begin =
allocator.allocate(3);
auto current =
begin;
allocator.construct(
current++,
"one"
);
allocator.construct(
current++,
"two"
);
allocator.construct(
current++,
"three"
);
while (current != begin)
{
allocator.destroy(
--current
);
}
allocator.deallocate(
begin,
3
);
Uninitialized-Memory Algorithms
The library provides algorithms that construct objects in raw memory.
Important functions include:
| Algorithm | Meaning |
|---|---|
uninitialized_copy() | Construct copies from a range |
uninitialized_copy_n() | Construct a specified number of copies |
uninitialized_fill() | Construct a range from one value |
uninitialized_fill_n() | Construct a specified number from one value |
These are defined in:
#include <memory>
uninitialized_copy()
Suppose:
std::vector<std::string> source{
"one",
"two",
"three"
};
Allocate raw memory:
std::allocator<std::string>
allocator;
auto begin =
allocator.allocate(
source.size()
);
Construct copies:
auto end =
std::uninitialized_copy(
source.begin(),
source.end(),
begin
);
The destination now contains constructed std::string objects.
uninitialized_fill_n()
auto end =
std::uninitialized_fill_n(
begin,
5,
std::string("data")
);
This constructs five strings in previously unconstructed memory.
12.3 Using the Library: A Text-Query Program
The chapter combines earlier library facilities into a small query system.
The program:
- reads a text file
- stores every input line
- records the line numbers on which each word occurs
- answers queries for a word
- prints the matching lines
The important design lesson is how several standard-library components work together.
1) Design of the Query Program
The program uses:
vector<string>
stores the input lines
istringstream
separates a line into words
map
maps each word to its line-number set
set
stores unique line numbers in order
shared_ptr
lets query results share stored data safely
Data Flow
Conceptually:
input file
↓ getline
vector<string>
↓
each line
↓ istringstream
words
↓
map<string, set<line_no>>
A query later uses the map to locate the matching line numbers.
TextQuery
A simplified declaration:
class QueryResult;
class TextQuery
{
public:
using line_no =
std::vector<
std::string
>::size_type;
explicit TextQuery(
std::ifstream& input);
QueryResult query(
const std::string& word) const;
private:
std::shared_ptr<
std::vector<std::string>
> file;
std::map<
std::string,
std::shared_ptr<
std::set<line_no>
>
> word_map;
};
Why the File Is Shared
TextQuery stores the complete file.
QueryResult also needs access to that file when results are printed.
Copying the entire vector for every query would be wasteful.
Instead:
TextQuery ──┐
├──> shared vector<string>
QueryResult ┘
Both objects share the same stored text.
Word Map
The map has the conceptual type:
word
↓
shared_ptr<set<line_no>>
For example:
"hello" -> {0, 3, 5}
"world" -> {1, 3}
```
The set keeps line numbers:
- unique
- ordered
---
### `TextQuery` Constructor
A compact implementation is:
~~~cpp
TextQuery::TextQuery(
std::ifstream& input)
: file(
std::make_shared<
std::vector<std::string>
>())
{
std::string text;
while (std::getline(
input,
text))
{
file->push_back(text);
const auto line_number =
file->size() - 1;
std::istringstream line(
text
);
std::string word;
while (line >> word)
{
auto& lines =
word_map[word];
if (!lines)
{
lines =
std::make_shared<
std::set<line_no>
>();
}
lines->insert(
line_number
);
}
}
}
Constructor Processing
For each input line:
getline()
↓
store full line in vector
↓
determine zero-based line number
↓
create istringstream
↓
extract each word
↓
find/create set for word
↓
insert current line number
Why auto& lines
auto& lines =
word_map[word];
The map subscript returns the mapped value.
The mapped value is:
std::shared_ptr<
std::set<line_no>
>
Binding by reference means assigning to lines modifies the actual map element.
First Occurrence of a Word
For a new word:
word_map[word]
inserts a value-initialized mapped value.
Because the mapped value is a shared_ptr, it is initially null.
Therefore:
if (!lines)
{
lines =
std::make_shared<
std::set<line_no>
>();
}
creates the line-number set only when needed.
QueryResult
A result object stores:
- the searched word
- the matching line-number set
- the shared input file
class QueryResult
{
friend std::ostream&
print(
std::ostream&,
const QueryResult&);
public:
using line_no =
TextQuery::line_no;
QueryResult(
std::string word,
std::shared_ptr<
std::set<line_no>
> lines,
std::shared_ptr<
std::vector<std::string>
> file)
: sought(std::move(word)),
lines(std::move(lines)),
file(std::move(file))
{
}
private:
std::string sought;
std::shared_ptr<
std::set<line_no>
> lines;
std::shared_ptr<
std::vector<std::string>
> file;
};
The important concept is data sharing, not copying.
query()
Query lookup should not add missing words to the map.
Therefore use:
find()
rather than:
operator[]
Implementation:
QueryResult
TextQuery::query(
const std::string& sought) const
{
static auto no_data =
std::make_shared<
std::set<line_no>
>();
auto found =
word_map.find(sought);
if (found == word_map.end())
{
return QueryResult(
sought,
no_data,
file
);
}
return QueryResult(
sought,
found->second,
file
);
}
Why a Shared Empty Set?
If the word does not exist, the query still returns a valid QueryResult.
Instead of returning a null line set, it returns a shared empty set.
This means client code can uniformly do:
lines->size()
and iterate the set without special null handling.
Printing Query Results
std::ostream&
print(
std::ostream& output,
const QueryResult& result)
{
output
<< result.sought
<< " occurs "
<< result.lines->size()
<< " times\n";
for (auto number
: *result.lines)
{
output
<< "(line "
<< number + 1
<< ") "
<< (*result.file)[number]
<< '\n';
}
return output;
}
Zero-Based vs. User Line Numbers
Internally, the vector uses zero-based indices.
first stored line = 0
second stored line = 1
```
When presenting results to users:
~~~cpp
number + 1
converts the stored index into conventional one-based line numbering.
Query Program Architecture
The program demonstrates separation of responsibilities.
TextQuery
owns/indexes input data
QueryResult
represents one query result
print()
formats the result
Shared pointers allow the result object to keep the required data alive without copying the complete input file.
Essential Study Checklist
- Dynamic memory has a lifetime independent of the scope that created it.
- The free store is also commonly called the heap.
- Smart pointers are defined in
<memory>. shared_ptrrepresents shared ownership.unique_ptrrepresents exclusive ownership.weak_ptrobserves ashared_ptr-managed object without owning it.make_shared()allocates and constructs an object managed by ashared_ptr.- Copying a
shared_ptradds another owner of the same object. - Destroying or reassigning a
shared_ptrreduces its ownership. - The managed object is destroyed when the last owning
shared_ptrdisappears. use_count()reports the number of owningshared_ptrs.shared_ptrcan be used to implement shared-state classes such asStrBlob.- Copying a class containing a
shared_ptrnormally copies the ownership relationship. newallocates and constructs dynamic objects.new Tdefault initializes the object.new T()value initializes the object.new T(args)directly initializes the object.new T{args}performs list initialization.- Ordinary allocation failure from
newthrowsstd::bad_alloc. new (std::nothrow)returns null instead of throwing on allocation failure.deletedestroys a dynamic object and releases its storage.- Only null pointers or valid pointers from
newmay be passed to the matchingdelete. - Destroying a raw pointer variable does not destroy the object it points to.
- Losing the last usable pointer to an allocation causes a memory leak.
- Accessing an object after
deleteis invalid. - A dangling pointer refers to storage where its former object no longer exists.
- Setting one deleted pointer to
nullptrdoes not fix other aliases. - Deleting the same allocation twice is undefined behavior.
- Smart pointers should generally be preferred over direct
newanddelete. - A
shared_ptrcan directly take ownership of a raw pointer returned bynew. - Prefer
make_shared()when ordinary shared allocation is required. - Never create independent
shared_ptrs from the same raw pointer. get()provides non-owning access to the underlying raw pointer.- Never use
get()to create another independent owner. reset()releases a smart pointer's current ownership.- Smart pointers automatically clean up during exception unwinding.
- Raw
newallocations can leak if exceptions bypass the matchingdelete. - Custom deleters let smart pointers manage resources that require special cleanup.
unique_ptrhas exactly one owner.unique_ptrcannot normally be copied.- Ownership of a
unique_ptrcan be transferred. release()gives up ownership without deleting the object.- A raw pointer returned by
release()must later acquire an owner or be manually deleted. reset()deletes the currently owned object before taking a new one.- A function can return a
unique_ptrto transfer ownership. - A
unique_ptrcustom deleter type is part of its type. - Creating a
weak_ptrdoes not increase the owning reference count. - The observed object may disappear while a
weak_ptrstill exists. weak_ptr::expired()tests whether the observed object has been destroyed.weak_ptr::lock()safely obtains ashared_ptrwhen the object still exists.weak_ptris useful for non-owning pointer-like relationships.new T[n]allocates a dynamic array.- Dynamic-array size may be chosen at run time.
- Array
newreturns a pointer to the first element. - The returned pointer does not itself store the array length.
new T[n]()value initializes the dynamic-array elements.new T[n]{...}supports list initialization.new T[0]is legal but the returned pointer must not be dereferenced.- Dynamic arrays allocated by
new[]must be released withdelete[]. unique_ptr<T[]>manages a dynamic array and usesdelete[].- In the C++11 model,
shared_ptrarray management requires an appropriate custom deleter. - Prefer
vectorover manually managed dynamic arrays for ordinary resizable sequences. allocator<T>separates storage allocation from object construction.allocate()obtains raw unconstructed storage.- Raw allocated storage cannot be used as an object until construction occurs.
construct()creates an object in raw storage in the C++11 allocator model.destroy()destroys a constructed object without freeing its storage.deallocate()releases the raw storage after objects have been destroyed.- Destroy only objects that were actually constructed.
uninitialized_copy()constructs copied objects in raw storage.uninitialized_fill()constructs repeated values in raw storage.- The text-query program combines
vector,map,set,istringstream, and smart pointers. vector<string>stores the complete input file.istringstreamseparates each input line into words.mapassociates each word with line-number information.setstores unique line numbers in sorted order.TextQueryandQueryResultuse shared ownership to avoid copying the stored file.TextQuery::query()usesfind()so lookup does not insert missing words.- A shared empty set can represent a valid query with no matches.
- Query results convert zero-based stored indices to one-based line numbers when displayed.