본문으로 건너뛰기

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:

  1. smart pointers
  2. direct use of new and delete

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 PointerOwnership
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

OperationMeaning
shared_ptr<T> pNull smart pointer
pTests whether an object is managed
*pAccess managed object
p->memberAccess 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:

  • p releases ownership of the first object
  • p begins sharing the object owned by q
  • 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:

  1. runs the object's destructor
  2. 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

OperationMeaning
unique_ptr<T> pNull unique pointer
p = nullptrDelete 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

OperationMeaning
weak_ptr<T> wNull 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:

AlgorithmMeaning
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:

  1. reads a text file
  2. stores every input line
  3. records the line numbers on which each word occurs
  4. answers queries for a word
  5. 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:

  1. the searched word
  2. the matching line-number set
  3. 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

  1. Dynamic memory has a lifetime independent of the scope that created it.
  2. The free store is also commonly called the heap.
  3. Smart pointers are defined in <memory>.
  4. shared_ptr represents shared ownership.
  5. unique_ptr represents exclusive ownership.
  6. weak_ptr observes a shared_ptr-managed object without owning it.
  7. make_shared() allocates and constructs an object managed by a shared_ptr.
  8. Copying a shared_ptr adds another owner of the same object.
  9. Destroying or reassigning a shared_ptr reduces its ownership.
  10. The managed object is destroyed when the last owning shared_ptr disappears.
  11. use_count() reports the number of owning shared_ptrs.
  12. shared_ptr can be used to implement shared-state classes such as StrBlob.
  13. Copying a class containing a shared_ptr normally copies the ownership relationship.
  14. new allocates and constructs dynamic objects.
  15. new T default initializes the object.
  16. new T() value initializes the object.
  17. new T(args) directly initializes the object.
  18. new T{args} performs list initialization.
  19. Ordinary allocation failure from new throws std::bad_alloc.
  20. new (std::nothrow) returns null instead of throwing on allocation failure.
  21. delete destroys a dynamic object and releases its storage.
  22. Only null pointers or valid pointers from new may be passed to the matching delete.
  23. Destroying a raw pointer variable does not destroy the object it points to.
  24. Losing the last usable pointer to an allocation causes a memory leak.
  25. Accessing an object after delete is invalid.
  26. A dangling pointer refers to storage where its former object no longer exists.
  27. Setting one deleted pointer to nullptr does not fix other aliases.
  28. Deleting the same allocation twice is undefined behavior.
  29. Smart pointers should generally be preferred over direct new and delete.
  30. A shared_ptr can directly take ownership of a raw pointer returned by new.
  31. Prefer make_shared() when ordinary shared allocation is required.
  32. Never create independent shared_ptrs from the same raw pointer.
  33. get() provides non-owning access to the underlying raw pointer.
  34. Never use get() to create another independent owner.
  35. reset() releases a smart pointer's current ownership.
  36. Smart pointers automatically clean up during exception unwinding.
  37. Raw new allocations can leak if exceptions bypass the matching delete.
  38. Custom deleters let smart pointers manage resources that require special cleanup.
  39. unique_ptr has exactly one owner.
  40. unique_ptr cannot normally be copied.
  41. Ownership of a unique_ptr can be transferred.
  42. release() gives up ownership without deleting the object.
  43. A raw pointer returned by release() must later acquire an owner or be manually deleted.
  44. reset() deletes the currently owned object before taking a new one.
  45. A function can return a unique_ptr to transfer ownership.
  46. A unique_ptr custom deleter type is part of its type.
  47. Creating a weak_ptr does not increase the owning reference count.
  48. The observed object may disappear while a weak_ptr still exists.
  49. weak_ptr::expired() tests whether the observed object has been destroyed.
  50. weak_ptr::lock() safely obtains a shared_ptr when the object still exists.
  51. weak_ptr is useful for non-owning pointer-like relationships.
  52. new T[n] allocates a dynamic array.
  53. Dynamic-array size may be chosen at run time.
  54. Array new returns a pointer to the first element.
  55. The returned pointer does not itself store the array length.
  56. new T[n]() value initializes the dynamic-array elements.
  57. new T[n]{...} supports list initialization.
  58. new T[0] is legal but the returned pointer must not be dereferenced.
  59. Dynamic arrays allocated by new[] must be released with delete[].
  60. unique_ptr<T[]> manages a dynamic array and uses delete[].
  61. In the C++11 model, shared_ptr array management requires an appropriate custom deleter.
  62. Prefer vector over manually managed dynamic arrays for ordinary resizable sequences.
  63. allocator<T> separates storage allocation from object construction.
  64. allocate() obtains raw unconstructed storage.
  65. Raw allocated storage cannot be used as an object until construction occurs.
  66. construct() creates an object in raw storage in the C++11 allocator model.
  67. destroy() destroys a constructed object without freeing its storage.
  68. deallocate() releases the raw storage after objects have been destroyed.
  69. Destroy only objects that were actually constructed.
  70. uninitialized_copy() constructs copied objects in raw storage.
  71. uninitialized_fill() constructs repeated values in raw storage.
  72. The text-query program combines vector, map, set, istringstream, and smart pointers.
  73. vector<string> stores the complete input file.
  74. istringstream separates each input line into words.
  75. map associates each word with line-number information.
  76. set stores unique line numbers in sorted order.
  77. TextQuery and QueryResult use shared ownership to avoid copying the stored file.
  78. TextQuery::query() uses find() so lookup does not insert missing words.
  79. A shared empty set can represent a valid query with no matches.
  80. Query results convert zero-based stored indices to one-based line numbers when displayed.