How to use C + + pointer correctly

Posted by robin339 on Sat, 19 Feb 2022 12:57:30 +0100

Smart pointer

When using heap memory, when a piece of heap memory is no longer needed, this memory must be released in order to reallocate this memory.
If a programmer accidentally makes an error when writing code for object reallocation, it will lead to serious problems, such as memory leakage, which will lead to program crash.
In C ++ 11, intelligent pointer for automatic memory management is introduced. When pointers are not in scope, they will automatically reassign objects that are no longer used.

unique_ptr

If a unique pointer is used, an object is created; And if the pointer P1 points to the object, only one pointer can point to the object at one time. Therefore, we cannot share with other pointers, but we can transfer the control to P2 by deleting P1.
unique_ptr does not share its pointer. It cannot be copied to another unique_ptr; Nor can it be passed to a function by value; Or in any C + + standard library algorithm that needs to be copied. unique_ptr can only move. This means that ownership of the memory resource has been transferred to another unique_ptr, original unique_ptr no longer has it. We recommend that you limit an object to one owner, because multiple ownership increases the complexity of program logic. Therefore, when you need a smart pointer to an ordinary C + + object, use unique_ptr, and when you construct a unique_ When PTR, use make_unique helper function.
C + + standard library; Define unique in header file_ ptr. It is as efficient as raw pointers and can be used in the container of the C + + standard library. Unique_ Adding PTR instances to the C + + standard library container is effective because it is unique_ptr's move constructor eliminates copy operations.

Example 1 unique_ Generation and movement of PTR.

class Person {
public:
	string mFirstName;
	string mLastName;
	
	Person(string first_name, string last_name) : mFirstName(first_name), mLastName(last_name) {};
};

unique_ptr<Person> PersonFactory(const string& firstname, const string& lastname)
{
    // Implicit move operation into the variable that stores the result.
    return make_unique<Person>(firstname, lastname);
}

void check_quique_ptr()
{
    // Using make_unique creates a new unique_ptr pointer
    auto person = make_unique<Person>("john", "wang");

    // Reference unique_ptr pointer
    vector<string> lastnames = { person->mLastName };

    // Use std::move to move a unique_ptr to another unique_ptr.
    unique_ptr<Person> person_2 = move(person);

    // Obtain unique_ptr from function that returns by value.
    auto person_3 = PersonFactory("Michael", "Jackson");
    // The following statements will produce compilation errors
    //unique_ptr<Person> person_bad = person;
}

This example shows unique_ The basic characteristics of PTR: it can move; But it cannot be copied. "Move" is to transfer the ownership of the object to the new unique object_ ptr; And reset the old unique_ptr.

Here are some inline snippets.

void check_unique_value_ref()
{
	// Create a unique_ptr to an array of 5 integers.
	auto p = make_unique<int[]>(5);
    vector<unique_ptr<Person>> persons;
    
    // Generate unique_ PTR < person > instance,
    // And add them to a vector, using the implicit move semantics
    persons.push_back(make_unique<Person>("jackson", "Liu")); 
    persons.push_back(make_unique<Person>("John", "Lee")); 

	// Initialize the array.
	for (int i = 0; i < 5; ++i)
	{
    	p[i] = i;
    	cout << p[i] << endl;
	}
	
    // Use references and avoid copying semantics
    for (const auto& person : persons)
    {
        cout << person->mFirstName << endl;
        cout << person->mLastName << endl; 
    }    
}

Using unique_ptr reference passed. Do not use value passing, otherwise a compilation error will occur. Because unique_ptr removed the copy constructor.

class MyClass
{
private:
    // MyClass owns the unique_ptr.
    unique_ptr<ClassFactory> factory;
public:

    // Initialize by using make_unique with ClassFactory default constructor.
    MyClass() : factory (make_unique<ClassFactory>())
    {
    }

    void MakeClass()
    {
        factory->DoSomething();
    }
};

shared_ptr

If shared is used_ PTR, multiple pointers can point to the object at a time, and it uses use_count() method to maintain the reference counter.
shared_ptr type is a smart pointer in the C + + standard library, which is specially used for scenarios that may require multiple owners to manage the life cycle of objects in memory. Initialize shared_ After PTR, you can copy it, pass it by value in the function parameters, and assign it to other shared_ptr instance.
All instances point to the same object and share access to a "control block" whenever a new shared is added_ PTR, when out of range or reset, the "control block" will increment and decrement the reference count. When the reference count reaches zero, the control block deletes the memory resource and itself.

// Use make whenever possible_ Shared generate shared_ptr.
auto sp1 = make_shared<Person>("John", "Liu");

// Use the new result as the constructor variable
shared_ptr<Person> sp2(new Person("Jason", "Lee"));

shared_ptr<Person> sp5(nullptr);
sp5 = make_shared<Person>("John", "Liu");

shared_ptr usage

  1. pass by value
    Call the copy constructor, increase the reference count, and make the callee the owner. The overhead of this operation is small, which may be important, depending on the shared to be passed_ Number of PTR objects. Use value passing when the implicit or explicit code Convention between the caller and the callee requires the callee to be the owner.
    Pass shared by value_ PTR means
  • A new shared_ptr will be copied
  • The reference count as an atomic shared variable increases
  • shared_ The PTR copy is destroyed at the end of the function
  • The reference count as an atomic shared variable decreases
 void check_shared_ptr_pass_by_value(shared_ptr<Engineer> p)
 {
    cout << "value pass" << endl;
    cout << "   Engineer" << endl;
    cout << "      point = " << p.get() << endl;
    cout << "      count = " << p.use_count() << endl;
 
    shared_ptr<Person> lp = p;
    cout << "   Person" << endl;
    cout << "      point = " << lp.get() << endl;
    cout << "      count = " << lp.use_count() << endl;
 }

Output results
value pass
   Engineer
      point = 0x7fffec9ade80
      count = 2
   Person
      point = 0x7fffec9ade80
      count = 3
  1. Reference or const reference
    In this case, the reference count is not increased. As long as the caller is not out of range, the callee can access the pointer. Alternatively, the callee can create a shared based on the reference decision_ PTR and become the share owner. When the caller does not know the callee, or must pass the shared_ptr, and use reference or const reference when you want to avoid replication operation for performance reasons.

Here are some inline snippets.

void check_shared_ptr_pass(shared_ptr<Engineer> &p)
{
    cout << "reference pass" << endl;
    cout << "   Engineer" << endl;
    cout << "      point = " << p.get() << endl;
    cout << "      count = " << p.use_count() << endl;
 
    shared_ptr<Person> lp = p;
    cout << "   Person" << endl;
    cout << "      point = " << lp.get() << endl;
    cout << "      count = " << lp.use_count() << endl;
}

Output results
reference pass
   Engineer
      point = 0x7fffec9ade80
      count = 1
   Person
      point = 0x7fffec9ade80
      count = 2
  1. A base class pointer or reference is passed to the base class object.
    The callee can use the object, but cannot make it share ownership or prolong its lifetime. If the callee creates shared from the original pointer_ PTR, the new shared_ptr is independent of the original pointer and does not control the underlying resources. When the caller and the callee explicitly specify the caller, retain shared_ The ownership of the PTR lifecycle is passed to the base class object using a base class pointer or reference.
void check_shared_ptr_pass_base (const shared_ptr<Person> &p)
{
   cout << "base reference pass" << endl;
   cout << "   Person" << endl;
   cout << "      point = " << p.get() << endl;
   cout << "      count = " << p.use_count() << endl;
}

Output results
base reference pass
   Person
      point = 0x7ffff21f4e80
      count = 2

When deciding how to pass shared_ During PTR, it is necessary to determine whether the callee must share the ownership of the basic resources. An owner is an object or function that maintains its lifetime as needed.

  • If the caller must ensure that the callee can extend the life of the pointer beyond its (function's) life, use the first option.
  • If you don't care whether the callee extends the life cycle, pass it by reference and let the callee copy or not.
  • If an auxiliary function must be granted access to the underlying pointer and knows that the auxiliary function will only use the pointer and return before the calling function returns, the function does not have to share ownership of the underlying pointer. It only needs to be in the caller's shared_ptr lifetime access pointer. In this case, you can safely pass the reference shared_ptr, or pass the original pointer or reference to the base class object.

Sometimes, for example, in STD:: vector < shared_ In PTR >, you may have to share each_ PTR is passed to the body of a lambda expression or to a named function object. If the lambda or function does not store a pointer, pass shared by reference_ PTR to avoid calling the copy constructor for each element.

weak_ptr

Except that the reference counter is not maintained, it is similar to shared_ptr is very similar. In this case, the pointer does not have a strong impact on the object. The reason is that if you assume that pointers hold the object and request other objects, they may deadlock.

Sometimes an object must store an access shared_ptr is the method of the base class object without causing the reference count to increase. Usually, when you're in shared_ This happens when there are circular references between PTR instances.

The best design is to avoid sharing pointer ownership as much as possible. However, if you must have shared_ Share ownership of PTR instances, please avoid using circular references between them. If circular references are unavoidable or, for some reason, even preferred, use break_ PTR provides one or more owners with access to another shared_ Weak reference of PTR. By using weak_ptr, you can create a shared_ptr, the shared_ptr connects to an existing set of related instances, but only if the base class memory resources are still valid. soft_ptr itself does not participate in the reference count, so it cannot prevent the reference count from becoming zero. However, you can use weak_ptr to try to get the shared initialized with it_ New copy of PTR. If the memory has been deleted, break_ The bool operator of PTR will return false. If the memory is still valid, the new shared pointer will increase the reference count and ensure that as long as shared_ If the PTR variable remains scoped, the memory will be valid.

Create weak_ptr as shared_ Copy of PTR. It provides access to one or more shared_ Access to objects owned by PTR instances that do not participate in reference counting. weak_ The existence or destruction of PTR_ PTR or its other copies have no effect. In some cases, the shared_ Break the circular reference between PTR instances.
Circular dependency (shared_ptr): let's consider A scenario where we have two classes A and B, both of which have pointers to other classes. Therefore, it is always like A points to B and B points to A. Therefore, use_count will never reach zero and will never be deleted.
Therefore, in shared_ptr is caused by cyclic dependency_ Use break when count will never be zero_ PTR can prevent this by putting A_ptr declared as weak_ptr to solve this problem, so class A does not own it, can only access it, and we also need to check the validity of the object, because it may be out of range. Usually, this is a design problem.

Use weak_ptr can cause errors

  1 #include <iostream>
  2 #include <memory>
  3
  4 using namespace std;
  5
  6 class Controller
  7 {
  8 public:
  9    int Num;
 10    string Status;
 11    weak_ptr<Controller> other;
 12
 13    explicit Controller(int i) : Num(i), Status("On")
 14    {
 15       cout << "Constructor" << Num << endl;
 16    }
 17
 18    ~Controller()
 19    {
 20       cout << "De-constructor" << Num << endl;
 21    }
 22
 23    void CheckStatuses() const
 24    {
 25        auto p = other.lock();
 26
 27        cout << "Status of " << p->Num << " = " << p->Status << endl;
 28    }
 29 };
 30
 31 void RunTest()
 32 {
 33
 34    shared_ptr<Controller> ctrl_1 = make_shared<Controller>(0);
 35    shared_ptr<Controller> ctrl_2 = make_shared<Controller>(1);
 36
 37    ctrl_1->other = weak_ptr<Controller>(ctrl_2);
 38    ctrl_2->other = weak_ptr<Controller>(ctrl_1);
 39
 40    ctrl_1->CheckStatuses();
 41    ctrl_2->CheckStatuses();
 42
 43    cout << "ctrl_1 use_count=" << ctrl_1.use_count() << endl;
 44    cout << "ctrl_2 use_count=" << ctrl_2.use_count() << endl;
 45 }
 46
 47 int main()
 48 {
 49    RunTest();
 50 }
 
Output results
Constructor0
Constructor1
Status of 1 = On
Status of 0 = On
ctrl_1 use_count=1
ctrl_2 use_count=1
De-constructor1
De-constructor0

Use weak correctly_ ptr

  1 #include <iostream>
  2 #include <memory>
  3
  4 using namespace std;
  5
  6 class Controller
  7 {
  8 public:
  9    int Num;
 10    string Status;
 11    shared_ptr<Controller> other;
 12
 13    explicit Controller(int i) : Num(i), Status("On")
 14    {
 15       cout << "Constructor" << Num << endl;
 16    }
 17
 18    ~Controller()
 19    {
 20        cout << "De-constructor" << Num << endl;
 21    }
 22
 23    void CheckStatuses() const
 24    {
 25        cout << "Status of " << other->Num << " = " << other->Status << endl;
 26    }
 27 };
 28
 29 void RunTest()
 30 {
 31
 32    shared_ptr<Controller> ctrl_1 = make_shared<Controller>(0);
 33    shared_ptr<Controller> ctrl_2 = make_shared<Controller>(1);
 34
 35    ctrl_1->other = shared_ptr<Controller>(ctrl_2);
 36    ctrl_2->other = shared_ptr<Controller>(ctrl_1);
 37
 38    ctrl_1->CheckStatuses();
 39    ctrl_2->CheckStatuses();
 40
 41    cout<<"ctrl_1 use_count=" << ctrl_1.use_count() << endl;
 42    cout<<"ctrl_2 use_count=" << ctrl_2.use_count() << endl;
 43 }
 44
 45 int main()
 46 {
 47    RunTest();
 48 }

Output results
Constructor0
Constructor1
Status of 1 = On
Status of 0 = On
ctrl_1 use_count=2
ctrl_2 use_count=2

nullptr

Nullptr is a keyword that can be used wherever NULL is expected. Like NULL, nullptr is implicitly convertible and can be compared with any pointer type.
Unlike NULL, it cannot be implicitly converted or comparable to integer types.

When comparing two simple pointers, there is some uncertainty. The two types are nullptr_ The comparison between the values of T is defined as:

  • Return true through < = and > = comparison
  • Returns false by comparing < and >
  • Compare any pointer type with nullptr = = and=
    If it is null or non null respectively, it returns true or false respectively.

Here are some inline snippets.

  1
  2 #include <iostream>
  3
  4 using namespace std;
  5
  6 void f(nullptr_t  p)
  7 {
  8     if (p)
  9        cout << "pointer is non-empty" << endl;
 10     else
 11        cout << "pointer is empty" << endl;
 12 }
 13
 14 void f(int val)
 15 {
 16     if (val == 0)
 17         cout << "val == 0" << endl;
 18     else
 19         cout << "val != 0" << endl;
 20 }
 21
 22 int main()
 23 {
 24     nullptr_t np1, np2;
 25
 26     if (np1 >= np2)
 27         cout << "can compare" << endl;
 28     else
 29         cout << "can not compare" << endl;
 30
 31     char *x = np1;
 32
 33     if (x == nullptr)
 34         cout << "x is null" << endl;
 35     else
 36         cout << "x is not null" << endl;
 37
 38     if (np1)
 39         cout << "bool is true" << endl;
 40     else
 41         cout << "bool is false" << endl;
 42
 43 //    f(NULL);     // overloaded 'f(NULL)' is ambiguous
 44
 45     f(nullptr);
 46     f(0);
 47
 48     return 0;
 49 }

Output results
can compare
x is null
bool is false
pointer is empty
val == 0

Pointers and references in C + +

On the surface, references and pointers are very similar and are used to make one variable provide access to another variable. Both provide many of the same functions, so the difference between these different mechanisms is usually unclear.

Pointer: a pointer is a variable that holds the memory address of another variable. You need to use the * operator to dereference the pointer to access the memory location it points to.

Reference: a reference variable is an alias, that is, another name of an existing variable. References, such as pointers, are also implemented by storing the address of the object.
You can think of a reference as a constant pointer with automatic indirection.

1. Initialization:
Pointers can be initialized in the following ways:

 int  val = 10;
 int *p =&val;

perhaps

 int val=10;
 int *p;
 p =&val;

That is, we can be in the same line; Or multiple lines, describing and initializing pointers.

References can be initialized with the following code

int a=10;
int &p=a;  //it is correct

However, the following code is wrong

int &p;
p=a;

Pointers can be directly assigned as nullptr, while references have no nullptr. The reference has no nullptr and cannot be re assigned to ensure that the basic operation will not encounter exceptions.

Various arithmetic operations can be performed on pointers, while there is no so-called arithmetic operation on references.

Pointers have exactly the same performance as references, because references are implemented internally as pointers.

Topics: C++ Programming pointer