What you must learn from C language to C++ --- dynamic memory and smart pointers

Whether you are a C++ beginner or want to change from C language to C++, you should understand C++ dynamic memory and smart pointers. Today we will look at these two aspects.

The content of this article is long and full of dry goods. If you are interested, you can bookmark + like it and watch it slowly in the future!

This article is suitable for students who switch from C language to C++ or learn C++. The codeword is not easy. If you like it, I hope you can come to a three-linked blogger to support it

Article Directory

One, C language dynamic memory

For the C language, the application for dynamic memory is achieved through the keyword malloc . Using malloc for dynamic memory application is to allocate a piece of memory for the current program in the heap area. For the convenience of us, we are not aware of the memory usage of some fragments in the program. When the size is large, it gives the user of the program more flexibility, and can decide how much memory is used from the outside. This function returns a pointer to void*. Let's first look at how malloc's declaration is:

void* __cdecl malloc(
    _In_ _CRT_GUARDOVERFLOW size_t _Size
    );

It can be seen from the above code that malloc uses the calling convention of __cdecl, the return value is void*, and there is only one formal parameter, which is named _Size, which is a variable of type size_t.
When using malloc to apply for memory, you need to pay attention to the following aspects:

1. Parameter transfer:

The parameter transfer can directly pass a variable, a value calculated by sizeof, or a direct value, as shown in the following figure:

Insert picture description here


2. Return value:

We have already said at the beginning that the return value of malloc is a pointer of type void , and it points to the first address of this piece of memory. If we use it for memory application, if the pointer type is not void , then we definitely need to perform the return value Forced type conversion, the type of general conversion is closely related to the type of our variable, as shown in the following figure:

Insert picture description here


3. Judging the validity of memory application:

The example we gave above must be problematic. For the application of memory, the application of two words is very important. Since as a program, you are applying, then the system will not give you memory, then this is not necessarily Therefore, the application may succeed or fail. Then we must make a validity judgment, but how to judge? This is related to the return value. If the application is successful, it returns a pointer to the first address of the memory, and if it fails, it returns NULL. So the judgment is as follows:

Insert picture description here


4. Memory initialization:

What is in the memory that we apply for using malloc? Has it initialized the requested memory? We can check it during debugging:

Insert picture description here


use vs2017 for debugging. The 4 bytes of memory requested are not initialized, and each byte is'cd', if We don’t use it for the time being, or we can’t use all the allocated memory, so we’d better initialize it. No matter what value we will assign to it when we use it later, I usually initialize it to 0.

	int *ptr1 = (int*)malloc(4);
	if (ptr1 == NULL)
	{
		//进行对应处理
		printf("malloc false\n");
		return -1;
	}
	//初始化申请到的4个字节为0
	memset(ptr1,0,4);

5. Memory is out of bounds:

When we use dynamically allocated memory, we must pay attention to the problem of memory out of bounds. This is a hidden error, because the compiler will not report errors when the memory is out of bounds during the compilation phase, and you read and write the memory after the out of bounds. Unpredictable errors may occur ! Below we deliberately write a small code that crosses the boundary of memory to see what happens:

int main()
{
	int *ptr1 = (int*)malloc(4);
	if (ptr1 == NULL)
	{
		//进行对应处理
		printf("malloc false\n");
		return -1;
	}
	//初始化申请到的4个字节为0
	memset(ptr1,0,4);
	for (int i = 0; i < 3; i++, ptr1++)
	{
		printf("%d\n", *ptr1);
	}
	return 0;
}
Insert picture description here


The result analysis shows that after our int* pointer is dereferenced, it is the number 0 we initialized, but after our for loop makes the pointer out of range, we print out other numbers as shown in the figure above. And such an array out-of-bounds access will not cause the compiler to report an error, which is still quite dangerous.

6. Memory release and wild pointer

The biggest problem with memory release is that the programmer forgets to release it , so you must develop good programming habits. The free function is used to release the memory. The parameter that needs to be passed in is the first address of the requested memory. The reason for the memory release is malloc. The requested memory is in the heap area of ​​the current program. If this memory is used up and the programmer does not manually release it, then this memory will not be able to be used again during the life of the program. This is definitely the case for memory utilization. It’s not good, so develop a good habit. After using it, release the requested memory:

Insert picture description here


wild pointer, as the name suggests, is wild, the pointer of "no home", that is, the address pointed to may be invalid , Especially commonly occurs in a situation, that is, after the pointer is released, we still visit the address it points to, unexpected results may occur, and this kind of wild pointer may cause harm to our program, and It is impossible to find an error in the initial compilation stage, and even no abnormality occurs during the running process, but we cannot get the expected result:

	if (ptr1 != NULL)
	{
		//如果指针变量不为NULL
		free(ptr1);
	}
	//我们故意去解这个指针的引用
	printf("%d\n",*ptr1);
Insert picture description here


Printed out such a negative number, obviously not the 0 we initialized it initially. So how can we avoid such operations? After the pointer is released, set it to NULL. In this case, if the pointer is dereferenced and accessed, an exception will occur:

	//初始化申请到的4个字节为0
	memset(ptr1,0,4);
	if (ptr1 != NULL)
	{
		//如果指针变量不为NULL
		free(ptr1); 
		ptr1 = NULL;//让指针指向NULL地址
	}
	//我们故意去解这个指针的引用  这时候就会发生异常报错,因为对空指针解引用了
	printf("%d\n",*ptr1);

Regarding the dynamic memory application in C language, we generally need to pay attention to the above points. What is the difference between C++ and C language memory application, or what improvements have been made? Let's take a look together next.

Two, C++ dynamic memory

For C++, we use the new keyword to apply for a block of memory for the program. As for the reason for dynamically applying for memory, the reason for dynamically applying for memory was mentioned in the C language malloc before, to make the use of memory more reasonable and flexible. The lifetime of the memory space occupied by the requested object is from the beginning of the application to until the call to delete to show that it is released, it will not be automatically released.

1. Parameter transfer

When we use new to dynamically apply for memory, we can only apply for pointers to a single object, or we can apply for pointers to multiple objects, just use a different way of writing:

	int n = 10;
	int *ptr1 = new int;//申请指向一个int类型的指针
	int *ptr2 = new int[10];//申请指向10个int类型的指针
	int *ptr3 = new int[n];//申请指向n个int类型的指针

2. Return value

For new, if we successfully apply for memory for the object, then the pointer to the object will also be returned. If the application is for multiple objects, then the first address of the first object will be returned, and the current pointer variable will naturally point to The corresponding address. Also, when new applies for memory, it has already specified a fixed type . As shown in the code above, all we apply for are pointers of int type.

3. Memory validity judgment

If we use new to apply for a piece of dynamic memory in C++, it may succeed or fail. After malloc fails, NULL is returned. What is returned after new fails? How should we judge? There are two ways we can use: The
first is to use try catch to catch the occurrence of an exception, because after the new application for memory fails, a bad_alloc exception will be returned. If the catch catches this exception, it will be processed.

	try
	{
		int *ptr1 = new int;//申请指向一个int类型的指针
	}
	catch (bad_alloc)
	{
		//当new分配内存失败之后,会抛出bad_alloc的异常代码
		cout << "bad alloc" << endl;
		//接下来做其他处理
	}

The second is to use the expression new (place_address) type that locates new to process:


	int *ptr1 = new (nothrow) int;//申请指向一个int类型的指针,如果失败,返回nullptr指针
	if (ptr1 == nullptr)
	{
		//申请失败进行处理
		cout << "new false" << endl;
	}

We use this method to apply for memory. If it fails, no exception will be thrown and a nullptr pointer will be returned. Nothrow is an object defined by the standard library.

4. Memory initialization

After we allocate the memory, how is the initialization operation initialized? For malloc, you must first apply for memory and then initialize, but this is not the case with new. Of course, for new, if you apply for memory first and then initialize, it is also feasible, and before initialization, for int type In terms of pointers, the content inside is also'cd', which we introduced before in the malloc section. Now let's talk about the difference, that is, the object is initialized while applying for memory for the object.

	int *ptr1 = new int(39);//ptr1指向的对象的值为39
	int *ptr2 = new int();//ptr2指向的int对象被初始化为0
	int *ptr3 = new int[5]();//ptr3指向5个int对象初始化为0

In the new standard of C++11, a method of initializing elements of a curly brace list is given. We can use curly braces to initialize all or part of the elements of the object. If only some elements are initialized, then the rest is based on 0. Assignment, if the number of initialization exceeds the number of elements that we apply for memory, the compiler will report an error of E0146, reminding that there are too many initial value settings, so using this initialization operation can also effectively prevent the element from being initialized. The problem of pointer out of bounds:

	int *ptr4 = new int[5]{ 1,2,3,4,5 };//初始化5个int值为1,2,3,4,5
	int *ptr5 = new int[5]{ 1,2 };//初始化前两个int值为1,2  其余全是0
	
	//编译器会报错 错误(活动)	E0146	初始值设定项值太多
	//int *ptr6 = new int[5]{ 1,2,3,4,5,6,7 };

5. Memory is out of bounds

Memory out-of-bounds is a very noteworthy problem of pointers. The initialization of objects when we apply for memory can prevent the initialization of pointers from out-of-bounds to a certain extent, but it does not prevent pointer out-of-bounds operations during access. For pointers out of bounds, it is already in malloc. For example, what we have to do is to mainly prevent the occurrence of pointer out-of-bounds, because once it happens, there may be some unexpected code errors.

6. Memory release and wild pointer

For C++ memory release, the delete keyword is used. There are two forms, one is to release a single object, the other is to release an array:

	int *ptr1 = new int(39);//ptr1指向的对象的值为39
	
	int *ptr3 = new int[5]();//ptr3指向5个int对象初始化为0

	delete ptr1;//释放单个对象的内存
	delete[] ptr3;//释放数组的内存空间

delete[] ptr3 [] indicates that the pointer points to the first element of an object array . When we write such a little test code, maybe we will not make a mistake on this issue, but when the project becomes larger and the amount of code becomes larger, we may appear: forget to use delete to release the memory, which will cause the memory space to be occupied all the time , Naturally cause memory leaks; using the memory that has been released , such as our example in malloc above, the pointer at this time is a stray pointer , in official terms, it is called a dangling pointer; The requested memory space is repeatedly released, delete has been used, but forgotten, and used again in the program, then the heap may be destroyed:

	int *ptr1 = new int(39);//ptr1指向的对象的值为39
	int *ptr3 = new int[5]();//ptr3指向5个int对象初始化为0
	
	delete[] ptr3;//释放数组的内存空间
	delete ptr1;//释放单个对象的内存
	cout << *ptr1 << endl;//野指针
	delete ptr1;//重复释放  引发异常

So we must pay attention to setting the pointer to nullptr after the memory is released to prevent the occurrence of wild pointers.

7. Use new to apply for memory for class objects

When we use new to apply for dynamic memory of a class object, the constructor of this class will be executed, and when delete is used to release the memory, the destructor will be executed.

class MyClass
{
public:
	MyClass();
	~MyClass();
private:
};

MyClass::MyClass()
{
	cout << "Here is constructor" << endl;
}
MyClass::~MyClass()
{
	cout << "Here is destructor" << endl;
}
int main()
{
	MyClass *ptr1 = new MyClass;//申请内存 执行无参构造函数
	
	delete ptr1;//释放内存,执行MyClass的析构
	return 0;
}
Insert picture description here


The introduction to the dynamic memory application of C/C++ comes to an end here. There are advantages that can make memory usage more flexible, and can apply for memory in the heap area for use, etc., but there are also disadvantages, that is, it may cause some Problems with pointers and memory. Can we optimize these problems?

Three, C++ smart pointer

The use of dynamic memory will bring us convenience and flexibility in program development, but prevent memory leaks, prevent heap damage caused by secondary releases, and prevent wild pointers from wandering around. These big problems still plague us. Too many developers. C++11 new features , smart pointers came into being! ! ! The emergence of smart pointers is mainly to make it safer and easier for developers to use dynamic memory. As a new C++ player, smart pointers must be studied and mastered. In future development, use smart pointers instead of A pointer in the usual sense. Next, let's introduce these pointers and simple usage examples in turn! I think smart pointers are the most used. They are practically no different from ordinary pointers, but they use template classes, and then some encapsulated internal interfaces can be called. The use and operation of ordinary pointers is in smart pointers. It can still be used on the body. There are also some precautions when using it.

1. shared_ptr

Shared_ptr needs to remember one sentence, it can allow multiple pointers to point to the same object.

1.1 Examples of use

We can declare a smart pointer in the following way. This pointer points to a pointer of type int. I named it ptr1. At present, this pointer is initialized by default, and it stores a null pointer.

shared_ptr<int> ptr1;

Of course, we need to point to a dynamically allocated memory for this pointer. In what way should we allocate memory? Call a standard library function called make_shared. Next, we will introduce the memory application and initialization for pointers.

	//动态分配一个新对象,初始化值为39
	shared_ptr<int> ptr1 = make_shared<int>(39);
	//	//动态分配一个新对象,初始化值为0
	shared_ptr<int> ptr2 = make_shared<int>();
	//动态分配一个新对象,初始化值为39
	shared_ptr<int> ptr3(new int(39));

In the above code, we have seen three ways to apply for memory and initialization, which are also the three most commonly used. You can use make_shared to apply for memory and initialize objects, or you can use new to initialize directly. Pay attention to the way new is directly initialized, if It is wrong to write the following:

	//不允许隐式转换  从int*到shared_ptr
	shared_ptr<int> ptr3 = new int(39);

The most important thing about shared_ptr is the function of reference counting, which is why it is named shared, because it can realize multiple pointers to the same object. When a shared_ptr of the same type is copied or assigned, the reference count in the original pointer will be increased. When the reference count in a shared_ptr becomes 0, the object will be released.

	//动态分配一个新对象,初始化值为39  引用计数为1,因为只有当前使用
	shared_ptr<int> ptr1 = make_shared<int>(39);
	long usecount = ptr1.use_count();//计算当前有多少指针在共享对象
	cout << usecount << endl;

	//动态分配一个新对象,初始化值为1  引用计数为1
	shared_ptr<int> ptr2 = make_shared<int>(1);

	//ptr2指向ptr1指向的对象,ptr1中的对象引用计数+1
	//ptr2原本指向的对象引用计数-1,变为0,所以被销毁,内存释放
	ptr2 = ptr1;
	usecount = ptr1.use_count();//计算当前有多少指针在共享对象
	cout << usecount << endl;

	//ptr3指向ptr1指向的对象,引用计数+1
	shared_ptr<int> ptr3(ptr1);
	usecount = ptr1.use_count();//计算当前有多少指针在共享对象
	cout << usecount << endl;
Insert picture description here


The above figure is after applying for memory and initializing ptr1, the reference count is 1.

Insert picture description here


After ptr2 is modified to a pointer to ptr1, the reference count is increased to 2. Regarding ptr3, due to the length of the article, no screenshots are taken. After the copy is copied, the reference count will be increased to 3.

Let's talk about reference counting. Reference counting is designed to control whether memory should be released. If our ordinary pointer is assigned, after the source pointer is deleted, the pointer still points to that address space, but it is The address space that has been released, if an operation is performed at this time, a wild pointer phenomenon will occur. The execution result is very random, so with the reference count, you can know when the object needs to be destroyed. When it is destroyed , it must No pointers refer to it anymore . This concept of reference counting is also widely used in windows kernel objects, and the principles are the same.

1.2 Precautions for use

Ordinary pointers cannot be directly and implicitly converted to shared_ptr:

	//不允许隐式转换  从int*到shared_ptr
	shared_ptr<int> ptr3 = new int(39);

If we bind an ordinary pointer to shared_ptr, then it is best not to use ordinary pointers, but to use smart pointers, because its memory is released when the smart pointer is destroyed, it is very dangerous to use. Let’s give an example:


	//注意事项  内存泄露
	int *ptr5 = new int(22);
	cout << *ptr5 << endl;
	//绑定普通指针,内存管理交给shared_ptr,不能再使用普通指针了
	shared_ptr<int> ptr6(ptr5);
	
	//此时ptr6指向了ptr1的对象,那么原本ptr6指向的对象没有shared_ptr在引用了
	//所以原本的内存就会被销毁,如果我们再使用,可能就很危险
	ptr6 = ptr1;
	
	//我们试着去打印下这块被shared_ptr销毁的内存ptr5中看看是什么
	cout << *ptr5 << endl;

Let’s take a look at the difference between printing the contents of ptr5 twice: as

Insert picture description here


we expected, the memory in ptr5 is released, so it cannot be directly accessed, because its survival has been handed over to shared_ptr, and ptr5 will not be able to know the memory pointed to by it. When will it be destroyed.
Do not use the same ordinary pointer to initialize multiple smart pointers, because it may cause the memory to be released multiple times, causing heap damage and memory collapse:


	//注意事项  内存奔溃
	int *ptr5 = new int(22);

	//多个智能指针绑定同一个普通指针
	shared_ptr<int> ptr6(ptr5);
	shared_ptr<int> ptr7(ptr5);
	//此时ptr6指向了ptr1的对象,那么原本ptr6指向的对象没有shared_ptr在引用了
	//所以原本的内存就会被销毁,如果我们再使用,可能就很危险
	ptr6 = ptr1;

	//此时ptr7指向了ptr1的对象,那么原本ptr7指向的对象没有shared_ptr在引用了
	//所以原本的内存 再次被销毁  导致内存奔溃
	ptr7 = ptr1;
	

2. weak_ptr

Weak_ptr is a weakly referenced smart pointer. It points to the object managed by shared_ptr and does not change the reference count of this object. So no matter whether weak_ptr exists or not, as long as the shared_ptr reference becomes 0, the object memory will still be released.

2.1 Examples of use

In the following code, we create two weak_ptr pointers. When we create such a pointer, we need to initialize it with a pointer of the shared_ptr type, and this process will not increase the reference count of the shared_ptr. However, it will increase the count of the Weaks variable, as we can clearly see in the following running diagram:

	//动态分配一个新对象,初始化值为39  引用计数为1,因为只有当前使用
	shared_ptr<int> ptr1 = make_shared<int>(39);
	//long usecount = ptr1.use_count();//计算当前有多少指针在共享对象
	
	//不会改变ptr1的引用计数
	weak_ptr<int> wptr = ptr1;//不改变引用计数
	weak_ptr<int> wptr2(ptr1);//不改变引用计数
Insert picture description here


For weak_ptr, it will not increase the reference count, so it cannot control the destruction of shared_ptr, so we cannot directly dereference this pointer:

	//不可以对weak_ptr解引用
	cout << *wptr << endl;//错误
	cout << *wptr2 << endl;//错误

So how do we access the objects in a weak_ptr pointer? ? Use an interface function to return a pointer of type shared_ptr:

	//动态分配一个新对象,初始化值为39  引用计数为1,因为只有当前使用
	shared_ptr<int> ptr1 = make_shared<int>(39);
	//long usecount = ptr1.use_count();//计算当前有多少指针在共享对象
	
	//不会改变ptr1的引用计数
	weak_ptr<int> wptr = ptr1;//不改变引用计数
	weak_ptr<int> wptr2(ptr1);//不改变引用计数

	shared_ptr<int> ptr2 = wptr.lock();
	if (ptr2)
	{
		//如果不为空,则访问weak_ptr指向的对象
		//否则,说明对象已经被销毁,不存在了
		cout << *ptr2 << endl;
	}

At this point, the object pointed to by shared_ptr has not been destroyed, so we can print out the result we want:

Insert picture description here


Next, we will reduce the reference count of the object pointed to by shared_ptr to 0:

	//动态分配一个新对象,初始化值为39  引用计数为1,因为只有当前使用
	shared_ptr<int> ptr1 = make_shared<int>(39);
	//long usecount = ptr1.use_count();//计算当前有多少指针在共享对象
	
	//不会改变ptr1的引用计数
	weak_ptr<int> wptr = ptr1;//不改变引用计数
	weak_ptr<int> wptr2(ptr1);//不改变引用计数

	//我让ptr1重新指向一个对象  也就是让weak_ptr中指向的对象引用计数变为0 被销毁
	ptr1 = make_shared<int>(8);

	shared_ptr<int> ptr2 = wptr.lock();
	if (ptr2)
	{
		//如果不为空,则访问weak_ptr指向的对象
		//否则,说明对象已经被销毁,不存在了
		cout << *ptr2 << endl;
	}

If we release the memory of an object pointed to by weak_ptr, the lock function will return empty, and the content in the if statement cannot be executed. Look at the running result:

Insert picture description here


nothing is printed because it did not enter the if statement. It shows that the original object has been destroyed at this time.

2.2 Precautions for use

The next thing we want to talk about is that we will often be asked during interviews . Questions about shared_ptr and weak_ptr, that is, must shared_ptr be safe ? Answer: No, because the shared_ptr cyclic reference will cause the destructor of the class to be unable to execute and cause memory leaks! So how to avoid this problem? Just use weak_ptr and shared_ptr together, look at the error demonstration:

class MyClass;//类的前置声明  为了在YourClass中识别

class YourClass
{
public:
	YourClass();
	~YourClass();
	shared_ptr<MyClass> ptr;//用来指向MyClass的对象
private:

};

YourClass::YourClass()
{
	cout << "Here is yourClass constructor" << endl;
}

YourClass::~YourClass()
{
	cout << "Here is yourClass destructor" << endl;
}


class MyClass
{
public:
	MyClass();
	~MyClass();
	shared_ptr<YourClass> ptr;//用来指向YourClass的对象
private:

};

MyClass::MyClass()
{
	cout << "Here is MyClass constructor" << endl;
}

MyClass::~MyClass()
{
	cout << "Here is MyClass destructor" << endl;
}
void Sub_1()
{
	//pa和pb在这里是两个局部变量,应该在sub_1函数中new申请内存执行构造
	shared_ptr<MyClass> pa(new MyClass());
	shared_ptr<YourClass> pb(new YourClass());
	pa->ptr = pb;//shared_ptr赋值
	pb->ptr = pa;//shared_ptr赋值
	//函数退出,执行析构  是我们希望的结果
}

int main()
{
	Sub_1();
	return 0;
}

The final execution result:

Insert picture description here


only the constructor is executed, and the destructor is not executed, so the problem lies in the use of smart pointers. Because the two share_ptr points to each other's class object, the other class object is always referenced before the other party is destructed, so the reference count cannot be reduced to 0, and it will never be released, which causes a memory leak. The current solution The solution is to change one or two of the share_ptr to weak_ptr, which is ok:

Insert picture description here

3. unique_ptr

3.1 Examples of use

We listen to unique_ptr from the name, unique means unique, and in the process of use, it is indeed like this, unique_ptr exclusively uses the object it points to, and does not allow sharing. When unique_ptr is destroyed, the object it points to must be It ended with it. Let's take a look at its declaration and initialization:

unique_ptr<int> ptr1;

Such a wording is an empty unique_ptr pointer, which can point to an int unique_ptr. We can also initialize him with new:

unique_ptr<int> ptr2(new int(39));//初始化指针指向值为39的对象

Since it is unique, it naturally does not support everyone pointing to the same object, so the following operations are not allowed:

	//这些都是错误的做法,不被允许
	ptr1 = ptr2;
	unique_ptr<int> ptr3(ptr2);

So what can we do with unique_ptr? The external interface provided by unique_ptr can be used to transfer object ownership, or directly give up object ownership and release memory.

	unique_ptr<int> ptr1;
	unique_ptr<int> ptr2(new int(39));//初始化指针指向值为39的对象
	
	//release函数将当前指针的控制权放弃,返回指针,并且将自身置为nullptr
	//reset函数将自己本身指针的所有权放弃,然后重新掌控传进来的参数
	ptr1.reset(ptr2.release());

3.2 Matters needing attention

When using unique_ptr, pay attention to using the get() interface function, because it will return a pointer to the current object, but the object may be released later and the pointer will become invalid, but if you access the return pointer of get(), then Some errors will occur:

	unique_ptr<int> ptr1;
	unique_ptr<int> ptr2(new int(39));//初始化指针指向值为39的对象
	
	//注意事项
	int *ptr3 = ptr2.get();
	ptr2 = nullptr;  //释放了之前ptr2中的内存
	
	cout << *ptr3 << endl;

The wrong result was printed because the freed memory was accessed.

Insert picture description here


Another point to note is the auto_ptr smart pointer. This smart pointer was proposed before, but after the new smart pointer came out in C++11, it was almost enabled later because it will be directly used in the assignment operation. Hand over the access control of the object, not through the interface function or something:

Insert picture description here


The disadvantage of this is that if we call the interface function, then we can also know that we have explicitly given up control of the current object, and auto_ptr is in a simple After the assignment operation, the control will be handed over, which is actually more dangerous.

The knowledge points about smart pointers and dynamic memory are updated here first. There is too much knowledge, and you still need to slowly explore in the follow-up practice!

In a poem: "If you want to be a thousand miles away, go to the next level"

If this article can help you, come to support the blogger Ou with one click and three consecutive times🤞