Answers to exercises in section 12.1 of Chapter 12 of C++ Primer

Chapter 12 dynamic memory of C++ Primer

Introduction this chapter introduces smart pointer and dynamic memory management, including:

● the basic concept of smart pointer, especially the benefits of using it to manage dynamic memory.

● use allocator to manage dynamic arrays.

● use a large example such as text query to show dynamic memory management.

The focus of this chapter's exercise is to familiarize the reader with the use of smart pointers, including the use of shared_ptr and unique_ Some problems that should be paid attention to when PTR manages dynamic memory; Using allocator to manage dynamic arrays; And some larger exercises based on text query examples.

Section 12.1 answers to dynamic memory and smart pointer exercises

Exercise 12.1: how many elements do b1 and b2 contain at the end of this code?

StrBlob b1;
{
    StrBlob b2 = {"a", "an", "the"};
    b1 = b2;
    b2.push_back("about");
}

[Topic setting ideas]

Understand the basic characteristics of smart pointers.

[answer]

Because the data member of StrBlob is a shared vector pointing to a string_ PTR, so the assignment of StrBlob will not copy the content of vector, but multiple StrBlob objects share the same vector object (created in dynamic memory space). Line 3 of the code provides a list of three strings when creating b2, so a vector object containing three strings will be created and a shared object will be created_ PTR points to this object (reference count is 1). In line 4, when b2 is assigned to b1, create a shared_ptr also points to the vector object just created, and the reference count becomes 2. Therefore, when you add a string to b2 in line 4, it will be added to the vector shared by the two strblobs. Finally, at the end of the code, both b1 and b2 contain four strings.

Exercise 12.2: write your own StrBlob class, including the const version of front and back.

[Topic setting ideas]

Practice the simple use of smart pointer.

[answer]

Refer to the code in the book and supplement the overload of const by front and back to complete your own StrBlob class:

#ifndef SYSTRBLOB_H
#define SYSTRBLOB_H

#include <vector>
#include <string>
#include <initializer_list>
#include <memory>
#include <stdexcept>

using namespace std;

class StrBlob
{
public:
    typedef vector<string>::size_type size_type;
    StrBlob();
    StrBlob(initializer_list<string> i1);
    size_type size() const { return data->size(); }
    bool empty() const { return data->empty(); }
    //Add and remove elements
    void push_back(const string &t) { data->push_back(t); }
    void pop_back();
    //Element access
    string& front();
    const string& front() const;
    string& back();
    const string& back() const;

private:
    shared_ptr<std::vector<std::string>> data;
    //If the data[i] is illegal, an exception is thrown
    void check(size_type i, const std::string &msg) const;
};

#endif // SYSTRBLOB_H
#include "SYStrBlob.h"

StrBlob::StrBlob():data(make_shared<vector<string>>())
{

}

StrBlob::StrBlob(initializer_list<string> i1):data(make_shared<vector<string>>(i1))
{

}

void StrBlob::check(size_type i, const string &msg) const
{
    if(i >= data->size())
        throw out_of_range(msg);
}

string& StrBlob::front()
{
    //If the vector is empty, check throws an exception
    check(0, "front on empty StrBlob");
    return data->front();
}

//const version front
const string& StrBlob::front() const
{
    check(0, "front on empty StrBlob");
    return data->front();
}

string& StrBlob::back()
{
    check(0, "back on empty StrBlob");
    return data->back();
}

//const version back
const string& StrBlob::back() const
{
    check(0, "back on empty StrBlob");
    return data->back();
}

void StrBlob::pop_back()
{
    check(0, "pop_back on empty StrBlob");
    data->pop_back();
}
#include <iostream>
#include "SYStrBlob.h"

using std::cout;
using std::endl;

int main(int argc, const char * argv[])
{
    StrBlob b1;
    {
        StrBlob b2 = {"a", "an", "the"};
        b1 = b2;
        b2.push_back("about");
        cout << "b2.size==================" << b2.size() << endl;
    }
    cout << "b1.size==================" << b1.size() << endl;
    cout << b1.front() << "  " << b1.back() << endl;

    const StrBlob b3 = b1;
    cout << b3.front() << "  " << b3.back() << endl;
    return 0;
}

Operation results:

 

Exercise 12.3: StrBlob requires const version of push_back and pop_back? If necessary, add it. Otherwise, explain why not.

[Topic setting ideas]

Understand the differences between const and non const versions.

[answer]

push_back and pop_ The semantics of back are to add elements to and remove elements from the vector object shared by the StrBlob object. Therefore, we should not overload the const version for it, because the constant StrBlob object should not be allowed to modify the content of the shared vector object.

Exercise 12.4: in our check function, we do not check whether i is greater than 0. Why can i ignore this check?

[Topic setting ideas]

Understand the difference between private member functions and public member functions.

[answer]

We define check as a private member function, that is, it will only be called by the member function of StrBlob, not by the user program. Therefore, we can easily ensure that the value of i passed to it meets the requirements without checking.

Exercise 12.5: we did not write to accept an initializer_ Constructor for the list explicit (see section 7.5.4, page 264) parameter. Discuss the advantages and disadvantages of this design strategy.

[Topic setting ideas]

Review the differences between implicit and explicit class type conversions.

[answer]

An explicit constructor that accepts an initialization list parameter is not written, which means that the implicit type conversion from list to StrBlob can be carried out, that is, where StrBlob is required (such as function parameters), list can be used for substitution. Moreover, initialization in the form of copy (such as assignment) can be carried out. This makes the programming more simple and convenient.

But this implicit conversion is not always good. For example, not all values in the list may be legal. For another example, for a function that accepts StrBlob, pass it a list, create a temporary StrBlob object, initialize it with the list, and then pass it to the function. When the function is completed, the object will be discarded and can no longer be accessed. For these cases, we can define explicit constructors to prohibit implicit class type conversion.

Exercise 12.6: write a function that returns a dynamically allocated vector of ints. Pass this vector to another function, which reads the standard input and saves the read value in the vector element. Then pass the vector to another function to print the read value. Remember to delete vector at the right time.

[Topic setting ideas]

This exercise uses new and delete to manage memory directly.

[answer]

The key to direct memory management is that whoever allocates memory should remember to release it. In this program, the main function calls the allocation function to create the vector of int in the dynamic memory space. Therefore, after reading and printing data, the main function should be responsible for releasing the vector object.

#include <iostream>
#include <vector>
#include <new>

using std::cout;
using std::endl;
using std::vector;
using std::nothrow;
using std::cin;

vector<int> *new_vector(void)
{
    return new (nothrow) vector<int>;
}

void read_ints(vector<int> *pv)
{
    int v;
    while(cin >> v)
    {
        pv->push_back(v);
    }
}

void print_ints(vector<int> *pv)
{
    for(const auto &v: *pv)
        cout << v << " ";
    cout << endl;
}

int main(int argc, const char * argv[])
{

    vector<int> *pv = new_vector();
    if(!pv)
    {
        cout << "Out of memory!" << endl;
        return -1;
    }

    read_ints(pv);
    print_ints(pv);
    delete pv;
    pv = nullptr;
    std::cout << "Hello, World!\n";
    return 0;
}

Operation results:

  Exercise 12.7: redo the previous question and use shared this time_ PTR instead of built-in pointer.

[Topic setting ideas]

This exercise uses smart pointers to manage memory.

[answer]

Compared with the previous question, the program is not much different, mainly changing the vector < int > * type to shared_ PTR < vector < int > > type, space allocation is no longer new but make_shared, it is no longer necessary to actively free memory at the end of the main function. The significance of the last point is not obvious for this small program, but it is very important for large programs. It saves programmers from releasing memory and can effectively avoid memory leakage.

#include <iostream>
#include <vector>
#include <memory>

using namespace std;

shared_ptr<vector<int>> new_vector(void)
{
    return make_shared<vector<int>>();
}

void read_ints(shared_ptr<vector<int>> spv)
{
    int v;
    while(cin >> v)
        spv->push_back(v);
}

void print_ints(shared_ptr<vector<int>> spv)
{
    for(const auto &v: *spv)
        cout << v << " ";
    cout << endl;
}

int main(int argc, const char * argv[])
{
    auto spv = new_vector();

    read_ints(spv);
    print_ints(spv);

    return 0;
}

Operation results:

 

Exercise 12.8: are there any errors in the following functions? If yes, explain the reason for the error.

bool b() {
    int *p = new int;
    // ...
    return p;
}

[Topic setting ideas]

Understand the difference between success and failure of allocating memory with new, and review type conversion.

[answer]

From the program fragments, it can be guessed that the programmer's intention is to distinguish the success or failure of memory allocation through the pointer value returned by new - successfully return a legal pointer, convert it to integer is a non-zero value, and convert it to bool value true; Allocation failed. p gets nullptr, whose integer value is 0, which can be converted to bool value false.

But ordinary new calls throw an exception bad when allocation fails_ Alloc instead of returning nullptr, so the program cannot achieve the desired purpose.

You can change new int to new (nothrow)int to make new return nullptr instead of throwing an exception when allocation fails. But this is still not a good method. You should return true or false by catching exceptions or judging the returned pointer, rather than relying on type conversion.

Exercise 12.9: explain the results of the following code execution:

int *q = new int(42), *r = new int(100);
r = q;
auto q2 = make_shared<int>(42), r2 = make_shared<int>(100);
r2 = q2;

[Topic setting ideas]

Understand the difference between directly managing memory and smart pointers.

[answer]

This code shows the advantages of smart pointers in memory management.

For the ordinary pointer part, two int objects are allocated first, and the pointers are saved in p and r respectively. Next, the value of pointer q is assigned to r, which leads to two very serious memory management problems:

1. First of all, there is a direct memory leakage problem. r and q both point to the memory address of 42, while the memory of 100, the original address saved in r, is no longer managed by pointer and becomes "orphan memory", resulting in memory leakage.

2. The second is a "null pointer" problem. Because r and q point to the same dynamic object, if the program is not written properly, it is easy to release one pointer and continue to use the other pointer. The pointer that continues to be used points to a piece of memory that has been released. It is an empty pointer. Continuing to read and write the memory it points to may lead to a serious problem of program crash or even system crash.

And shared_ptr can solve these problems well. First, two shared objects are allocated, which are pointed to by the shared pointers p2 and q2 respectively, so their reference count is 1. Next, give q2 to r2. The assignment operation will assign the address of the object pointed to by q2 to r2, reduce the reference count of the object originally pointed to by r2 by 1, and increase the reference count of the object pointed to by q2 by 1. In this way, the reference count of the former becomes 0, and the memory space occupied by it will be released without memory leakage. If the reference count of the latter becomes 2, its memory space will not be released due to the destruction of one of r2 and q2, so it will not cause the problem of dangling pointers.

Exercise 12.10: the following code calls the process function defined on page 413 to explain whether this call is correct. If not, how should I modify it?

shared_ptr<int> p(new int(42));
process(shared_ptr<int>(p));

[Topic setting ideas]

Understand the use of smart pointers.

[answer]

This call is correct. Use p to create a temporary shared_ptr gives process parameters ptr, P and ptr all point to the same int object, and the reference count is correctly set to 2. After the process is executed, the ptr is destroyed and the reference count is reduced by 1, which is correct -- only p points to it.

Posted by carefree on Thu, 07 Oct 2021 12:03:45 -0700