[New Features] Introduction to C++STL containers and C++11 new features (continuous updates)

References

Introduction to C++STL containers and algorithms

1. Introduction to STL in C++

STL provides a total of six components, including containers, algorithms, iterators, functors, adapters and configurators, which can be combined and applied with each other. The container obtains data storage space through the configurator, and the algorithm accesses the container content through iterators. Functors can assist the algorithm in completing different strategic changes. Adapters can be applied to containers, functors, and iterators.

  • Container: Various data structures, such as vector, list, deque, set, and map, are used to store data. From an implementation perspective, they are a type of template.

  • Algorithm: Various commonly used algorithms, such as sort (insertion, quick sort, heap sort), search (binary search), are a method template from an implementation perspective.

  • Iterator: From an implementation perspective, an iterator is a class template that overloads pointer-related operations such as operator*, operator->, operator++, operator–, etc. All STL containers have their own iterators. .

  • Functor: From an implementation perspective, a functor is a class or class template that overloads operator(). Can help algorithms implement different strategies.

  • Adapter: A thing used to decorate a container or functor or iterator interface.

  • Configurator: Responsible for space configuration and management. From an implementation perspective, the configurator is a class template that implements dynamic space configuration, space management, and space release.

2. Implementation of serial containers in STL

1. vector

vector concept

  • Vector is a dynamic space . As elements are added, its internal mechanism will expand the space by itself to accommodate new elements. Vector maintains a continuous linear space , and ordinary pointers can meet the requirements as the iterator (RandomAccessIterator) of vector.

  • The data structure of vector is actually composed of three iterators, an iterator pointing to the head of the currently used space, an iterator pointing to the tail of the currently used space, and an iterator pointing to the tail of the currently available space. If the capacity is not enough, the capacity will be doubled.

  • The implementation of vector is based on the idea of ​​doubling: if the actual length of the vector is n, m is the current maximum length of the vector, when a new element is inserted, if the current capacity is sufficient, it will be inserted directly; if the current n =m-1, then dynamically apply for a 2m-sized memory during the next storage, copy the original data to the new space, and then release the original space to complete an expansion. It should be noted that each expansion is a newly opened space, so after expansion, the original iterator will become invalid. On the contrary, when deleting, if n ≥ m/2, half of the memory will be released.

Declaration of vector container

The vector container is stored in the template library:. #include<vector>This library needs to be added before use.

#include<vector>
vector<int> vec;
vector<char> vec;
vector<pair<int,int> > vec;
vector<node> vec;
struct node{
    
    ...};

How to use vector container

The vector container supports random access, that is, it can be []used to .

2. list

Compared with vector, the advantage of list is that every time an element is inserted or deleted, a space is allocated or released, and the original iterator will not become invalid. STL list is a doubly linked list . Ordinary pointers can no longer meet the needs of list iterators because the storage space of list is discontinuous. The iterator of list must have forward and backward functions, so list provides BidirectionalIterator. The data structure of the list only needs a pointer to the node node.

3. therefore

Vector is a continuous linear space with one-way opening, while deque is a continuous linear space with two-way opening. The so-called bidirectional opening means that deque supports the insertion and deletion of elements from both ends. Compared with vector's way of expanding space, deque actually implements the concept of dynamic space more closely. Deque has no concept of capacity because it is dynamically composed of segmented continuous spaces, and a new space can be added and connected at any time .

Since it is necessary to maintain the illusion of overall continuity and provide a random access interface (that is, it also provides RandomAccessIterator), it avoids the cycle of "reconfiguration, copying, and releasing" at the expense of a complex iterator structure. In other words, unless necessary, we should use vector instead of deque as much as possible.

Deque templates are stored in C++STL #include<deque>.

How to use deque container

In addition to these usages, one of the better properties of deque than queue is that it supports random access , that is, it can take out an element like an array subscript. That is: q[i].

4. stack

It is a first-in, last-out data structure with only one outlet. Stack allows adding elements from the top, removing the top elements, and obtaining the top elements. Deque is a data structure with two-way openings, so using deque as the bottom structure and closing its head opening forms a stack.

declaration of stack container

The stack container is stored in the template library:. #include<stack>You need to open this library before using it.

#include<stack>
stack<int> st;
stack<char> st;
stack<pair<int,int> > st;
stack<node> st;
struct node{
    
    ...};

How to use stack container

5. queue

It is a first-in, first-out data structure with two exits, allowing elements to be added from the bottom, get the top element, add elements from the bottom, and remove elements from the top. Deque is a data structure with two-way openings. If you use deque as the bottom structure and close the exit at the bottom and the entry at the head, a queue is formed. (In fact, list can also implement deque)

Declaration of queue container

The queue container is stored in the template library:, #include<queue>and this library needs to be added before use.


#include<queue>

queue<int> q;

queue<char> q;

queue<pair<int,int> > q;

queue<node> q;

struct node{
    
    ...};


How to use queue container

In terms of usage:

  • Queue does not support random access, that is, it cannot take any value like an array.
  • The queue cannot be cleared using the clear() function. To clear the queue, you must pop it one by one.
  • Queue also does not support traversal, whether it is array type traversal or iterator type traversal, so there is no begin(),end();function, so you must be clear about the similarities and differences when using it!

6. priority_queue

The bottom layer is a vector, and the internal implementation is a binary heap. By maintaining this vector, the purpose is to allow users to insert any elements into the container in any order, but when taking them out, they must start from the element with the highest priority (highest value).

A big root heap is a binary tree in which the key value of each node is greater than or equal to the key value of its child node (the specific implementation is a vector, a continuous space, and this is achieved by maintaining a certain order. Binary tree), when a new element is added, the newly added element should be placed at the bottom level as a leaf node, that is, the specific implementation is to fill in the first space from left to right (that is, insert the new element in end() of the underlying vector), and then executes a so-called backtracking program: compare the new node with the parent node, if its key value is larger than the parent node, swap the positions of the parent node, and so on, all the way back up. Until no swap is needed or until the root node. When taking out an element, the maximum value is at the root node. To take the root node, you need to remove the rightmost right node at the bottom and re-insert its value into the maximum heap. After the last node is placed into the root node, proceed Perform a descending procedure: swap the spatial node with its larger node, and continue descending until the leaf node is reached.

Declaration of priority_queue container

priority_queueThe container is stored in the template library:. #include<queue>You need to open this library before using it.

It should be noted here that the declaration of priority queues is different from the declaration of general STL templates.

Big root heap declaration method :
A big root heap is a heap that puts large elements on the top of the heap. The priority queue implements a large root heap by default .

#include<queue>
priority_queue<int> q;
priority_queue<string> q;
priority_queue<pair<int,int> > q;

Another way to build a large top stack

priority_queue<int> q;
//等价于
priority_queue<int,vector<int>,less<int> >  q; 

Int, string and other types in C++ can be directly compared in size, so we don’t need to worry too much, the priority queue will naturally help us implement it.

  • But if it is a structure we define ourselves, we need to overload operators .

    struct Edge{
          
          
        int v1,v2,cost;
        Edge(int tv1,int tv2,int c):v1(tv1),v2(tv2),cost(c){
          
          }
        bool operator <(const Edge& e)const{
          
          
            return this-> cost > e.cost;
        }
    };
    priority_queue<Node> A;   //默认  大根堆
    priority_queue<Edge, vector<Edge>, less<Edge>>B;    //大根堆
    priority_queue<Edge, vector<Edge>, greater<Edge> > C;    //小根堆
    //或者
    struct cmp {
          
          
        bool operator()(Edge a,Edge b) {
          
          
            return  a.v1 > b.v1;  //小顶堆
        }
    };
    
    priority_queue<Edge, vector<Edge>, cmp > C;    //小根堆
    
    
  • If there is no structure, you can define it yourself as needed.

    struct cmp{
          
          
        bool operator ()(const pair<int,char>& a,const pair<int,char>  &b)
        {
          
          
            if(a.first==b.first)return a.second<b.second;
            return a.first>b.first;
        }
    };
    
    int main() {
          
          
        //小根堆
        priority_queue<pair<int,char>,vector<pair<int,char>>,cmp > tmp;
        tmp.push({
          
          3,'a'});
        tmp.push({
          
          3,'b'});
        tmp.push({
          
          2,'a'});
        cout<<tmp.top().first<<' '<<tmp.top().second;
    }
    

Small root heap declaration method :

The big root heap puts the big elements on the top of the heap, and the small root heap puts the small elements on the top of the heap.

There are two ways to implement a small root heap:

  • The first one is more clever, because the priority queue implements a large root heap by default, so we can invert the elements and put them in. Because the smaller the absolute value of a negative number, the larger it is, then the element with the smaller absolute value will be placed in Earlier, when we took it out, we took the opposite step and used the large root heap to implement the small root heap.

  • The second type:

    priority_queue<int,vector<int>,greater<int> >q;
    

    <Note that when we encounter two " " or " " put together when declaring >, we must remember to add a space in the middle . In this way, the compiler will not judge two connected symbols as a left shift/right shift of bit operations.

How to use priority_queue container

Note: priority_queuetaking out the first element is using top, not front, you must pay attention to this! !

7. string

string is not a container of STL. In fact, the string container is just a string.

How to use string containers and comparison with traditional character reading

8. set

The elements in the set are sorted (in ascending order) by default. The automatic ordering and rapid addition and deletion of the set container are realized internally: a red-black tree (a type of balanced tree).
he canCustom sorting rules

#include <bits/stdc++.h>
struct cmp {
    
    
    bool operator()(int x, int y) {
    
     return x > y; }
};
struct cmp1 {
    
    
    bool operator()(const int &x, const int &y) const {
    
     return x > y; }
};
int main() {
    
    
    set<int, cmp> st;
    st.insert(5);
    st.insert(3);
    cout << *st.begin() << '\n';
    return 0;
}

set container declaration

The declaration of the set container is the same as that of most C++STL containers, which is: 容器名<变量类型> 名称structure.

#include<set>
set<int> s;
set<char> s;
set<pair<int,int> > s;
set<node> s;
struct node{
    
    ...};

Use of set container

s.empty();//empty()函数返回当前集合是否为空,是返回1,否则返回0.
s.size();//size()函数返回当前集合的元素个数
s.clear();//clear()函数清空当前集合。
s.insert(k);//insert(k)函数表示向集合中加入元素k。
s.begin(),s.end();

begin()Functions and end()functions return iterators to the beginning and end of a collection. Note that it is an iterator. We can think of iterators as subscripts of arrays. But actually an iterator is a pointer . What needs to be noted here is that due to the " front closed and then open " structure of the computer interval, the pointer returned by the begin() function does point to the first element of the set. But the pointer returned by end() points to the element after the last element of the collection.

s.erase(k);

erase(k)The function represents deleting element k from the set. This also reflects the power of the set container, which completely omits cumbersome operations such as traversing, searching, copying, and restoring. Directly use a function O(logn)to solve the problem with minimal complexity.

s.find(k);

find(k)The function returns an iterator pointing to element k in the collection. If the element does not exist, it will be returned s.end(). This property can be used to determine whether the element exists in the collection.

9. multiset

multisetThe conceptual setdifference between a container and a container is that setthe elements of are different from each other, while multisetthe elements of are allowed to be the same.

s.erase(k);

erase(k)The function in the set container represents deleting element k in the set. But in multisetthe container, it means to delete all elements equal to k.

The time complexity becomes O(tot+logn), where totrepresents the number of elements to be deleted. So, there will be a situation where I only want to delete one of these elements. What should I do? Here’s a great use for it:

if((it=s.find(a))!=s.end())
	s.erase(it);

ifThe conditional statement in means that an iterator pointing to an element a is defined. If the iterator is not equal s.end(), it means that the element does exist, and the element pointed to by the iterator can be deleted directly.

s.count(k);

count(k)The function returns the number of elements k in the set. This operation does not exist in the set container. This is multisetunique.

10. map

Map is a "mapping container", and the two variables it stores form a mapping relationship from key values ​​to elements .

We can quickly find this mapped data based on the key value. The internal implementation of the map container is a red-black tree (a type of balanced tree).

How to use map container

Because both map containers and set containers are implemented using red-black trees as internal structures, their usage is relatively similar.

insert operation
There are about two methods for inserting into a map container. The first one is similar to the array type. You can use the key value as an array subscript to directly assign a value to the map:

mp[1]='a';

Of course, you can also use insert()functions for insertion:

mp.insert(map<int,char>::value_type(5,'d'));

Delete operation
You can directly use erase()functions to delete, such as:

mp.erase('b');

Traversal operation
Like other containers, map also uses iterators to implement traversal. If we want to query the key value (that is, the previous one) during traversal, we can it->firstuse query. Then, of course, we can also it->secondquery the corresponding value (the latter one).

Find operation
The search operation is analogous to the search operation of set. But what is searched in the map are key values .
for example:

mp.find(1);

That is, find the element with the key value 1.

The relationship between map and pair

First of all, the relationship built by map is a mapping, that is, if we want to query a key value, only one corresponding value will be returned. However, if you use pair, not only does it not support O(log)level search, it also does not support finding one by one, because the first dimension of a pair can have many the same ones, which means that one key value may correspond to n multiple corresponding values. Condition. This obviously does not conform to the concept of mapping.

3. Points to note when using vector

Notes on use:

  • Pay attention to the problem of iterator invalidation after inserting and deleting elements;
  • When clearing vector data, if the saved data items are pointer types, they need to be deleted item by item, otherwise memory leaks will occur.

4. vector’s push_back() and emplace_back()

  • Impact of frequent calls to push_back():

    Adding elements to the end of the vector is likely to cause the reallocation of the entire object storage space, reallocation of larger memory, copying the original data to the new space, and then releasing the original memory. This process is time-consuming and labor-intensive. Yes, frequent calls to push_back() on vector will cause performance degradation.

  • After C++11, a new method was added to the vector container: emplace_back(). Like push_back(), a new element is added to the end of the container. The difference is that emplace_back() is more efficient. Compared with push_back(), there is a certain improvement.

  • The emplace_back() function has certain improvements in principle compared to push_back(), including in terms of memory optimization and operating efficiency. Memory optimization is mainly reflected in the use of in-place construction (constructing objects directly in the container, without copying a copy and then using it) + forced type conversion. In terms of operating efficiency, due to saving The copy construction process is removed, so there is a certain improvement.

5. What is the difference between map and set, and how are they implemented?

Both map and set are associative containers of C++, and their underlying implementations arered black tree(RB-Tree)。

Since the various operation interfaces opened by map and set are also provided by RB-tree, almost all the operation behaviors of map and set are just the operation behaviors of RB-tree.

The difference between map and set is:

  • The elements in map are key-value (keyword-value) pairs: the key plays the role of an index, and the value represents the data associated with the index; in contrast, Set is a simple collection of keywords, and each set The element contains only one keyword.

  • The iterator of set is const, and the value of the element is not allowed to be modified; map allows the value to be modified, but the key is not allowed to be modified. ThatreasonThis is because map and set are sorted according to keywords to ensure their orderliness. If the key is allowed to be modified, then the key needs to be deleted first, then the balance is adjusted, then the modified key value is inserted, and the balance is adjusted, and so on. Later, the structure of map and set was seriously damaged, causing the iterator to fail. It was not known whether it should point to the position before the change or the position after the change. Therefore, in STL, the set iterator is set to const, and the iterator value is not allowed to be modified; while the map iterator does not allow the key value to be modified, but the value value is allowed to be modified .

  • Map supports subscripting operations, but set does not support subscripting operations . Map can use key as a subscript. The subscript operator of map [ ]uses the key as a subscript to perform a search. If the key does not exist, an element with the key and the default value of the mapped_type type is inserted into the map. , so the subscript operator [ ]needs to be used with caution in map applications . const_map cannot be used. It should not be used when you only want to determine whether a certain key value exists but do not want to insert elements. The mapped_type type does not have a default value and should not be used. Use. If find can solve the need, use find as much as possible .

6. STL iterator deletes elements: erase function

  • forpecking order containerFor vector and deque, after using erase(itertor), the iterator of each subsequent element will become invalid, but each subsequent element will move forward by one position, and erase will return the next valid iterator ;
  • forassociated containerFor map set, after using erase(iterator), the iterator of the current element becomes invalid, but its structure is a red tree. Deleting the current element will not affect the iterator of the next element, so when calling Before erasing, just record the iterator of an element.
  • For list, it uses non-contiguously allocated memory, and its erase method also returns the next valid iterator, so the above two correct methods can be used.

7. The role of iterators in STL

  • The Iterator mode is a mode used for aggregate objects. By using this mode, we can access each item in the aggregate object in a certain order (method provided by iterator) without knowing the internal representation of the object. element.
  • Due to the above characteristics of the Iterator pattern: coupling with aggregate objects, its widespread use is limited to a certain extent. It is generally only used for underlying aggregate support classes, such as STL's list, vector, stack and other container classes and ostream_iterator Wait for extended iterator.
  • The access method of the Iterator class is to abstract the access logic of different collection classes, so that the effect of looping through the collection can be achieved without exposing the internal structure of the collection.

8. The difference between iterators and pointers

Iterators are not pointers, they are class templates that behave like pointers. It just simulates some functions of pointers, by overloading some operators of pointers, ->、 *、 ++、 --etc. The iterator encapsulates a pointer and is an object that "can traverse all or part of the elements in the STL (StandardTemplate Library) container". It essentially encapsulates the raw pointer. It is a lift of the pointer concept and provides a comparison pointer. More advanced behavior is equivalent to a smart pointer, which can implement different ++, --operations based on different types of data structures.The iterator returns an object reference rather than the object's value, so cout can only output *the value obtained by using the iterator but cannot directly output itself .

9. The difference between resize and reserve in STL

  • resize(): Change the number of elements (size()) contained in the current container. For example: vector v; v.resize(len);the size of v changes to len. If the original size of v is less than len, then the container adds (len-size) elements, and the value of the element is the default . is 0 . After v.push_back(3);that, 3 is placed at the end of v, that is, the subscript is len. At this time, the size of the container is len+1;
  • reserve(): Change the maximum capacity of the current container (capacity). It will not generate elements, but only determines how many objects the container allows. If the value is greater reserve(len)than the current capacity(), then a block that can store len will be reallocated. The space of the object, then v.size()copy the previous object through copy construtor, and destroy the previous memory;

C++11 new features

The features of C++11 mainly include the following aspects:

  • Language features that improve operating efficiency: rvalue references, generalized constant expressions
  • The usability of the original syntax is enhanced: initialization list, unified initialization syntax, type derivation, range for loop,
  • Lambda expressions, final and override, constructor delegation
  • Improvement of language capabilities: null pointer nullptr, default and delete, long integer, static assert
  • Updates to the C++ standard library: smart pointers, regular expressions, hash tables, and more

1. Null pointer nullptr

  • nullptrThe purpose of appearing is to replace NULL.
    In a sense, traditional C++ will NULLregard 0 and 0 as the same thing. It depends on how the compiler defines NULL. Some compilers will define NULL as 0 ((void*)0), and some will directly define it as 0. C++ does not allow direct implicit conversion of void * to other types, but if NULL is defined ((void*)0), then when compiled char *ch= NULL;, NULL will have to be defined as 0. However, this will still cause problems and will lead to confusion in the overloading feature in C++. Consider:

    void func(int);
    void func(char *);
    
  • For these two functions, if NULL < is defined as 0, then the statement func(NULL) will call func(int), causing the code to be counterintuitive.

  • In order to solve this problem, C++11 introduced the nullptr keyword, which is specifically used to distinguish null pointers and 0s. nullptris of type nullptr_tthat can be implicitly converted to any pointer or member pointer type, and can also be compared with them for equality or inequality.

  • When you need to use NULL, develop nullptrthe habit of using it directly.

2. Lambda expression

  • Lambda expressions actually provide a feature similar to anonymous functions, and anonymous functions are used when you need a function but don't want to bother naming a function.

  • Lambda expressions can be used to write embedded anonymous functions to replace independent functions or function objects and make the code more readable.

  • In essence, lambda expression is just a kind of syntactic sugar, because all the work it can accomplish can be achieved with other slightly more complex codes, but its simple syntax has brought far-reaching impact to C++.

  • In a broad sense, lamdba expressions produce function objects. In a class, the function call operator () can be overloaded. At this time, the objects of the class can have function-like behavior. We call these objects function objects (Function Object) or functors (Functor). Compared with lambda expressions, function objects have their own unique advantages.

  • lambda A more important application of lambda expressions is that they can be used as parameters of functions . In this way, callback functions can be implemented.

  • Lambda expressions generally []start with square brackets and end with curly braces {}. The curly braces contain the body of the lambda expression just like defining a function. If parameters are required, they must be placed in parentheses like a function. If there is a return value, the return type must be placed at the ->end, that is, the trailing return type. Of course, you can also ignore the return type. Lambda will help you Automatically infer the return type:

    // 指明返回类型,托尾返回类型
    auto add = [](int a, int b) -> int {
          
           return a + b; };
    // ⾃动推断返回类型
    auto multiply = [](int a, int b) {
          
           return a * b; };
    int sum = add(2, 5); // 输出: 7
    int product = multiply(2, 5); // 输出: 10
    
  • The first one []is a very important function of lambda expression, which isClosure

  • First, let’s explain the general principle of lambda expression: whenever you define a lambda expression, the compiler will automatically generate an anonymous class (of course this class overloads the () operator), which we call closure type. Then at runtime, this lambda expression will return an anonymous closure instance, which is actually an rvalue. Therefore, the result of our lambda expression above is each closure instance.

  • One of the powerful features of closures is that they can capture variables within their encapsulated scope by passing values ​​or references. The square brackets in front are used to define capture modes and variables. We call them lambda capture block . .

    int main() {
          
          
    	int x = 10;
    	auto add_x = [x](int a) {
          
           return a + x; }; // 复制捕捉x,lambda表达式⽆法修改此变量
    	auto multiply_x = [&x](int a) {
          
           return a * x; }; // 引⽤捕捉x, lambda表达式可以修改此变量
    	cout << add_x(10) << " " << multiply_x(10) << endl;
    	// 输出: 20 100
    	return 0;
    }
    

The capture method can be by reference or copying, but specifically there are the following situations to capture variables in the scope where they are located:

  • []: No variables are captured by default;
  • [=]: By default, all variables are captured by value;
  • [&]: By default, all variables are captured by reference;
  • [x]: Capture x only by value, other variables are not captured;
  • [&x]: Capture x only by reference, other variables are not captured;
  • [=, &x]: By default, all variables are captured by value, but x is an exception, which is captured by reference;
  • [&, x]: By default, all variables are captured by reference, but x is an exception, which is captured by value;
  • [this]: Capture the current object by reference (actually copying the pointer);
  • [*this]: Capture the current object by passing value;

3. Unified initialization syntax

  • Different data types have different initialization syntax. How to initialize a string? How to initialize array? How to initialize multidimensional array? How to initialize an object?
  • C++11 provides a unified initialization syntax: " {}-初始化变量列表" can be used:
X x1 = X{
    
    1,2};
X x2 = {
    
    1,2}; // 此处的'='可有可⽆
X x3{
    
    1,2};
X* p = new X{
    
    1,2};
struct D : X {
    
    
	D(int x, int y) :X{
    
    x,y} {
    
    };
};
struct S {
    
    
	int a[3];
	// 对于旧有问题的解决⽅案
	S(int x, int y, int z) :a{
    
    x,y,z} {
    
    };
};

4. Constructor delegation

Constructors can call another constructor in the same class to simplify the code:

class myBase {
    
    
	int number; string name;
	myBase( int i, string& s ) : number(i), name(s){
    
    }
public:
	myBase( ) : myBase( 0, "invalid" ){
    
    }
	myBase( int i ) : myBase( i, "guest" ){
    
    }
	myBase( string& s ) : myBase( 1, s ){
    
     PostInit(); }
};

5. final 和 override

  • C++ uses virtual functions to achieve runtime polymorphism, but C++'s virtual functions have many vulnerabilities: subclasses cannot be prevented from overriding them. Maybe when we reach a certain level, we don't want subclasses to continue to override the current virtual function. It is easy to accidentally hide virtual functions of parent classes. For example, when rewriting, you accidentally declare a new function with an inconsistent signature but the same name.
  • C++11 provides final to prevent virtual functions from being overridden/prohibiting classes from being inherited, and override to explicitly override virtual functions. This allows the compiler to provide more useful errors and warnings for behaviors we don't care about.
struct Base1 final {
    
     };
struct Derived1 : Base1 {
    
    }; // 编译错: Base1不允许被继承
struct Base2 {
    
    
	virtual void f1() final;
	virtual void f2();
};
struct Derived2 : Base2 {
    
    
	virtual void f1(); // 编译错: f1不允许重写
	virtual void f2(int) override; // 编译错:⽗类中没有 void f2(int)
};

6. Hash table

  • C++'s map, multimap, set, and multiset are implemented using red trees. Both insertion and query have a complexity of O(lgn).
  • But C++11 provides (underlying hash implementation) for these four template classes to achieve O(1) complexity:
    Insert image description here

7. Smart pointers

https://blog.csdn.net/qq_34170700/article/details/107493939

There are three main types of smart pointers provided in C++11:

shared_ptr 、 unique_ptr 、 weak_ptr

auto_ptrIt is a solution of c++98, which has been abandoned in c++11

Why use smart pointers: solving memory leaks

  • The function of a smart pointer is to manage a pointer because of the following situations: 申请的空间在函数结束时忘记释放,造成内存泄漏.
  • Using smart pointers can avoid this problem to a large extent, because a smart pointer is a class. When the scope of the class is exceeded, the class will automatically call the destructor, and the destructor will automatically release resources.
  • The working principle of smart pointers isAutomatically release memory space at the end of the function, no need to manually release memory space
  • Smart pointers are included in header files <memory>.

1. shared_ptr

  • shared_ptr implements the concept of shared ownership . Multiple smart pointers can point to the same object , and the object and its related resources will be released when " the last reference is destroyed ". As can be seen from the name share, the resource can be shared by multiple pointers. It uses a reference counting mechanism to indicate that the resource is shared by several pointers. Each copy of shared_ptr points to the same memory.
  • Every time shared_ptr is used, the internal reference count increases by 1. Every time it is destructed, the internal reference count decreases by 1. When it decreases to 0, the heap memory pointed to is automatically deleted. The reference counting inside shared_ptr is thread-safe, but reading the object requires locking.
  • shared_ptr is to solve the limitations of auto_ptr in object ownership (auto_ptr is exclusive), and provides smart pointers that can share ownership using a reference counting mechanism.
  • Shared_ptr will have the problem of mutual reference deadlock : if two shared_ptr refer to each other, then the reference count of the two pointers can never drop to 0, and the resource will never be released.
  • When initializing shared_ptr, you cannot directly assign a normal pointer to a smart pointer, because one is a pointer and the other is a class . Ordinary pointers can be passed in through make_sharedfunctions or through constructors . And you can get ordinary pointers through the get function.
  • You can check the number of resource owners through the member function use_count().
  • In addition to being constructed through new, it can also be constructed by passing in auto_ptr, unique_ptr, weak_ptr.
  • When we call release(), the current pointer releases the resource ownership and the count is decremented by one. When the count equals 0, the resource will be released.

Citation example

#include <iostream>
#include <memory>

class CB;
class CA {
    
    
  public:
    CA() {
    
    
      std::cout << "CA()" << std::endl;
    }
    ~CA() {
    
    
      std::cout << "~CA()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CB>& ptr) {
    
    
      m_ptr_b = ptr;
    }
  private:
    std::shared_ptr<CB> m_ptr_b;
};

class CB {
    
    
  public:
    CB() {
    
    
      std::cout << "CB()" << std::endl;
    }
    ~CB() {
    
    
      std::cout << "~CB()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CA>& ptr) {
    
    
      m_ptr_a = ptr;
    }
  private:
    std::shared_ptr<CA> m_ptr_a;
};

int main()
{
    
    
  std::shared_ptr<CA> ptr_a(new CA());
  std::shared_ptr<CB> ptr_b(new CB());
  ptr_a->set_ptr(ptr_b);
  ptr_b->set_ptr(ptr_a);
  std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;

  return 0;
}

member function

  • use_countReturns the number of reference counts.
  • uniqueReturns whether it is exclusive ownership (use_count is 1).
  • swapSwap two shared_ptr objects (that is, swap owned objects).
  • resetGiving up ownership of internal objects or changing owned objects will cause the reference count of the original object to be reduced.
  • getReturns the internal object (pointer). Since the () method has been overloaded, it is the same as using the object directly. For example, shared_ptr<int> sp(new int(1));sp is equivalent to sp.get().

The difference between make_shared and new :

  • new will cause memory fragmentation, make_shared will not.

  • new: The method of new first and then assignment is to first allocate a piece of memory on the heap, and then build a smart pointer control block on the heap. These two things are discontinuous and will cause memory fragmentation;

  • make_shared: The method of make_shared is to create a large enough memory directly on the heap, which contains two parts. The upper part is the memory (for use), and the lower part is the control block (including reference counting), and then uses the constructor of T to initialize the allocation. of memory.

    shared_ptr<int> p1 = make_shared<int>(42);//安全的内存分配返回指向此对象的shared_ptr
    

2. weak_ptr

https://blog.csdn.net/qq_38410730/article/details/105903979

  • weak_ptr is a smart pointer that does not affect the life cycle of the object . It points to an object managed by shared_ptr . The memory management of the object is the shared_ptr of the strong reference. weak_ptr only provides a means of access to the managed object .

  • Binding a weak_ptr to a shared_ptr does not change the shared_ptr's reference count .

  • The purpose of weak_ptr design is to introduce a smart pointer to cooperate with shared_ptr to assist shared_ptr work. It can only be constructed from a shared_ptr or another weak_ptr object. Its construction and destruction will not cause the reference count to increase or decrease .

  • weak_ptr is used to solve the deadlock problem when shared_ptr refers to each other (reference loop) . It is a weak reference to the object . It does not modify the reference count of the object. It can only be used to detect whether the object has been released . It can be converted to and from shared_ptr. Shared_ptr can be directly assigned to it, and it can be done by calling the lock function. Get shared_ptr.

    Weak references can detect whether the managed object has been released, thereby avoiding access to illegal memory.

Reference loop solution: Taking the example above, the solution is to change one of the member variables in the two classes to a weak_ptr object. For example, change the member variable in CB to a weak_ptr object, that is, the code of the CB class is as follows:

class CB {
    
    
  public:
    CB() {
    
    
      std::cout << "CB()" << std::endl;
    }
    ~CB() {
    
    
      std::cout << "~CB()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CA>& ptr) {
    
    
      m_ptr_a = ptr;
    }
  private:
    std::weak_ptr<CA> m_ptr_a;
};

3. auto_ptr

  • Before C++11, the role of auto_ptr was similar to that of unique_ptr: exclusive ownership of the pointed object , using ownership mode .

  • After C++11, auto_ptr has been deprecated.

  • The constructor of auto_ptr explicitprevents the implicit conversion of general pointers into the construction of auto_ptr, so you cannot directly assign a general type pointer to an object of auto_ptr type. You must use the constructor of auto_ptr to create an object;

  • Since the auto_ptr object will delete the pointer it owns when it is destructed, avoid multiple auto_ptr objects managing the same pointer when using it;

  • Auto_ptr is implemented internally. In the destructor, objects are deleted using deleteinstead delete[], so auto_ptr cannot manage arrays;

  • The difference between auto_ptr and unique_ptr is:

    If you try to copy or assign a value to auto_ptr/unique_ptr, unique_ptr will report an error directly in the compilation phase ; but auto_ptr can be compiled and passed, only in the running phase , and after accessing the object control due to being copied, it becomes a null pointer. After auto_ptr, the program will report an error.

    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
    auto_ptr<string> p2;
    p2 = p1; //auto_ptr不会报错.
    

    No error will be reported at this time. p2 has deprived p1 of its ownership and the ownership has been transferred. p1 no longer points to the object and points to nothing. Accessing p1 while the program is running will result in an error.
    So auto_ptrshortcomingYes: There is a potential memory corruption problem !

    • Only one pointer can be owned for a particular object, so the destructor of the smart pointer that owns the object deletes the object, and the assignment transfers ownership.

4. unique_ptr

  • unique_ptr exclusively owns the object it refers to, ensuring that only one smart pointer can point to the object at the same time.

  • Belongs to tracking reference and ownership patterns .

  • There is no function similar to make_shared for initializing unique_ptr. Unique_ptr can only be bound to a pointer returned by new.

    unique_ptr<string> p3 (new string ("auto"));   //#4
    unique_ptr<string> p4;                       //#5
    p4 = p3;//此时会报错!!
    

    The compiler considers p4=p3 illegal, which avoids the problem that p3 no longer points to valid data. therefore,unique_ptr is safer than auto_ptr

  • In addition, unique_ptr is smarter: when the program tries to assign one unique_ptr to another, if the source unique_ptr is a temporary rvalue, the compiler allows it; if the source unique_ptr will exist for a period of time, the compiler will prohibit it.

    unique_ptr<string> pu1(new string ("hello world"));
    unique_ptr<string> pu2;
    pu2 = pu1;                                      // #1 not allowed
    unique_ptr<string> pu3;
    pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed
    
  • If you really want to perform an operation like #1 and safely reuse this pointer, you can assign a new value to it. C++ has a standard library function std::move()that allows you to assign one unique_ptr to another. For example:

    unique_ptr<string> ps1, ps2;
    ps1 = demo("hello");
    ps2 = move(ps1);
    ps1 = demo("alexia");
    cout << *ps2 << *ps1 << endl;
    
  • The life cycle of the unique_ptr pointer itself : starts when the unique_ptr pointer is created until it leaves the scope. When leaving the scope, if it points to an object, the object it points to will be destroyed (the delete operator is used by default, and the user can specify other operations).

8. C++ type conversion

On the surface, C's forced conversion seems powerful and can convert anything, but the conversion is not clear enough, error checking cannot be performed, and it is prone to errors.

C++ type conversion is mainly divided into two types: implicit type conversion and explicit type conversion (forced type conversion).

The so-called implicit type conversion refers to the type conversion behavior performed by the compiler by default without user intervention. C++ provides explicitkeywords. Adding keywords when declaring the constructor explicitcan prohibit implicit conversion.

There are four types of cast operators: static_cast, const_cast, reinterpret_cast,dynamic_cast

1. static_cast static type conversion

The so-called static means that the type of conversion can be determined during compilation , and it is the most commonly used one. It mainly has the following uses:

  • Used for conversion of pointers or references between parent and child classes in a class hierarchy. Upcasting ( converting a pointer or reference from a subclass to a parent class representation) isSafetyof;
  • When performing down conversion (converting a parent class pointer or reference to a subclass pointer or reference), since there is no dynamic type checking, it isnot safeof;
  • Used for conversion between basic data types. The security of this conversion also needs to be ensured by developers.
    	int val_int;
    	double val_double = 3.1415;
    	val_int = static<int>(val_double);
    
  • Convert a void pointer to a pointer of the target type :not safe
  • Convert any type of expression to void type.

2. const_cast

  • It is specially used for the conversion of const attributes and is the only conversion operator among the four conversion operators that can operate on constants.

  • const_castOperator is used to remove the const or volatile attributes of a type. However, it is important to note that const_cast is not used to remove the constantness of variables, but to remove the constantness of pointers or references pointing to constant objects. The objects to remove constantness must be pointers or references.

  • For the behavior of converting a constant object into a non-const object, we generally call it "cast away the const". Once we remove the const nature of an object, the compiler no longer prevents us from writing to the object.

    const char* ptr_char;
    char* p = const_cast<char*>(ptr_char); // 正确:但是通过p写值的行为是未定义的行为
    

3. reinterpret_cast reinterprets type conversion

  • Do not use this conversion character unless absolutely necessary, it is a high-risk operation.
  • Usage features: Data is reinterpreted from the bottom layer, depending on the specific platform, and has poor portability; integers can be converted into pointers, and pointers can be converted into arrays; unbridled conversion between pointers and references can be performed.

4. dynamic_cast Polymorphic type conversion between subclass and parent class

  • dynamic_cast is mainly used for safe downward transformation in inheritance systems. It can safely convert a pointer to a base class into a pointer or reference to a subclass and know whether the conversion operation was successful. If the transformation fails, it will return null(when the transformation object is a pointer) or throw an exception bad_cast(when the transformation object is a reference).
  • Unlike static_cast, dynamic_cast involves runtime type checking.If downward transition is safe(That is, if the base class pointer or reference actually points to a derived class object), this operator returns the cast pointer.If downward transition is unsafe(That is, the base class pointer or reference does not point to an object of a derived class), this operator will return a null pointer.

Guess you like

Origin blog.csdn.net/qq_40145095/article/details/126554934