智能指针之unique_ptr

2023-11-06

unique_ptr实现的是专属所有权语义,用于独占它所指向的资源对象的场合。某个时刻只能有一个unique_ptr指向一个动态分配的资源对象,也就是这个资源不会被多个unique_ptr对象同时占有,它所管理的资源只能在unique_ptr对象之间进行移动,不能拷贝,所以它只提供了移动语义。资源对象的生命周期被唯一的一个unique_ptr对象托管着,一旦这个unique_ptr对象被销毁或者变成空对象,或者拥有了另一个资源对象,它所管理的资源对象同时一并销毁。资源对象要么随同unique_ptr对象一起销毁,要么在离开unique_ptr对象的管理范围时被销毁,从而保证了内存不会泄露。

unique_ptr以模板形式提供的,它有两种版本:一个是普通版本,即标量版本,用于管理一个动态分配的资源对象;另一个是数组版本,是一个偏特化版本,用于管理一个动态分配的数组。
下面看一下它的用法。

构造对象
构造一个unique_ptr相当简单,以int资源类型为例:

unique_ptr<int> up(new int(8)); // 创建一个指向int型对象的指针,删除器缺省为delete
unique_ptr<int[]> ar_up(new int[10]); // 指向一个10个元素的int型数组,删除器缺省为delete[]

定义数组版本的unique_ptr对象时,模板参数需要声明为数组形式,注意在模板参数中不要指定数组的大小,写成下面那样的定义是无法编译的。

unique_ptr<int[10]> ar_up(new int[10]); // 无法编译

也不要使用普通版本来分配数组,下面代码编译时没有问题,但是运行时会可能出错,因为在销毁资源时,使用的是delete操作符,不是delete[]操作符。

unique_ptr<int> ar_up(new int[10]); // 编译没有问题,但是运行时可能会出错 

unique_ptr可以不拥有资源对象,使用缺省构造函数创建的是不拥有任何资源的空对象:

unique_ptr<int> up_empty;
unique_ptr<int[]> ar_empty;

空对象可以在需要的时候使用reset()为它分配指针。

up_empty.reset(new int(42));
ar_empty.reset(new int[10]);

或者移动一个unique_ptr对象给它.。

up_empty= move(up);
ar_empty = move(ar_up);

构造unique_ptr对象时,不能直接把裸指针赋值给unique_ptr对象,它没有提供这样的隐式转换,但是可以把nullptr赋值给unique_ptr对象。

//!	std::unique_ptr<int> up_0 = new int(4); // error
	std::unique_ptr<int> up_1(nullptr); // 可以使用nullptr直接构造
	std::unique_ptr<int> up_2 = nullptr; // 可以使用nullptr直接赋值
	up_2.reset(nullptr);

定制删除器
构造unique_ptr对象时除了传递指针外,还可以提供一个删除器。如果没有指定删除器,unique_ptr会使用缺省删除器,即缺省使用delete和delete[]来销毁对象,分别用于普通版本和数组版本。如果动态分配的资源有专门的释放函数,必须在构造时同时提供一个删除器。比如:

unique_ptr<int, void(*)(int*)> up2(new int(4), [](int *ptr){delete ptr;});
unique_ptr<int[], function<void(int*)>> ar_up2(new int[10], [](int *ptr){delete[] ptr;});

删除器要求是可调用对象,它的类型可以是函数指针、函数对象、lambda表达式、function对象等。

当使用提供删除器时,删除器的类型也要作为模板的参数类型声明在unique_ptr中,也就是说删除器类型也是unique_ptr模板实例化后的类型的一部分。因此,如果一个unique_ptr对象在move时,源和目的unique_ptr对象的删除器类型也必须一样,这点与shared_ptr不一样。

一般情况下,unique_ptr释放的资源是在内存中动态分配的,但通过定制删除器,也可以使用unique_ptr来管理一些通过其他方式分配的资源,比如文件句柄、socket等。C++中有一种资源管理的惯例-RAII,可以通过它来实现对资源的自动释放,比如,对于使用fopen打开的文件,返回一个文件指针FILE,在程序结束时,需要调用fclose关闭,为了防止忘记关闭,可以手动编写一个RAII类来负责管理这个FILE的生命周期,不是很方便。现在可以使用unique_ptr来实现,只需定义一个删除器就可以了,非常方便。下面是一个使用unique_ptr来自动关闭打开文件的例子。

FILE *file = fopen("/tmp/tmp.txt", "r"); // 分配FILE资源
unique_ptr<FILE, void(*)(FILE *)> up(file, [](FILE *file) {
	fclose(file);
});
...//其它操作file的代码逻辑

移动语义
unique_ptr是一个独占型的智能指针,它是资源对象的唯一拥有者,不允许其它智能指针共享其内部的资源。unique_ptr没有拷贝语义,它的拷贝构造和拷贝赋值函数都是delete的,它和其它智能指针只能通过move操作来转移内部资源,比如可以转移给另一个unique_ptr对象,或者shared_ptr对象。

extern void foo(unique_ptr<int> up);
unique_ptr<int> up(new int (5));
//!foo(up) ; // 无法编译
foo(move(up));
// 或者
foo(unique_ptr<int>(new int (5)));

移动操作要求两个对象的类型必须完全一样,即删除器的类型也必须一样。下面的代码是无法编译的,原因是unique_ptr<int, function<void(int*)>>和unique_ptr 是不同的类型,它们的内存布局不一样。这点不同于shared_ptr,shared_ptr<int, function<void(int*)>>和shared_ptr之间是可以进行移动或者拷贝操作的。

unique_ptr<int, function<void(int*)>> up1(new int(4), [](int *ptr){delete ptr;});
unique_ptr<int> up(move(up1));

不过,对于返回值为unique_ptr类型的函数,比如函数unique_ptr foo() ,调用时,可以直接赋值:

extern unique_ptr<int> foo();
unique_ptr<int> up = foo();

当然并不是说在此应用场景中支持拷贝语义了,而是因为调用foo()函数时返回的是临时对象,是一个纯右值,编译器使用了移动构造操作(当然,在编译器优化时,如果满足条件,可能使用命名返回值优化(NRVO)技术,直接在foo()中构造了up对象)。

使用
unique_ptr可以通过成员函数get()来获取原始的裸指针,如果unique_ptr对象是个空对象,它里面没有资源时,则返回nullptr。

int *q=up.get();

既然是智能指针,unique_ptr具有指针的特点和操作方式,编程时可以象操作普通裸指针那样进行使用。
1、unique_ptr重载了操作符*,->可以象使用指针那样使用它们:

int x  = *up1;
printf("%d\n", *up2);

2、对于数组版本的unique_ptr,还重载了数组操作符[],可以通过它访问指定位置的数组元素:

int x = ar_up[0];  // unique_ptr重载了[]操作符
//	int x = ar_up[10]; // 同裸指针访问数组一样,不进行越界检查 

操作符[]返回的是引用,可以作为左值使用:

ar_up[1] = 150;

注意unique_ptr<int[]> up2(new int[10])与unique_ptr up2(new int[10])的区别,后者不是一个指向数组的智能指针,而是普通版本的智能指针,并没有提供[]操作符重载,下面的语句编译失败:

int x = up2[0]; 
up2[1] = 150;

但可以:*(up2) = 150;,不过,这样使用,访问的是位置0处的元素。

3、unique_ptr并没有重载+,-,++,–,+=,-=等算术运算操作符,如果访问其它位置的元素,只能通过裸指针,如:

int *q = up2.get();
std::cout << *q << std::endl;
std::cout << *(q+1) << std::endl;
std::cout << q[4] << std::endl;

4、unique_ptr也可以直接判断是否为nullptr,有两种方式,和使用裸指针的形式完全一样。

if(!up)
	std::cout<<"up is nullptr"<<std::endl;

if(up== nullptr)
	std::cout<<"up is nullptr"<<std::endl;

5、unique_ptr也可象普通指针那样,当指向一个类继承体系的基类对象时,也具有多态性质,如同使用裸指针管理基类对象和派生类对象那样。如:

// 基类
class base {
public:
	void foo(); // 非virtual成员函数,没有多态性质
	virtual void bar(); // virtual成员函数
	virtual ~base();
}; 

// 派生类
class derive : public base {
public:
	void foo(); // 覆盖基类的foo
	void bar(); // 重写基类的bar
	~derive(); // 重写基类的析构
};

// 测试一个管理派生类对象的智能指针能否赋值给一个管理基类对象的智能指针 
// 即,智能指针是否有派生类到基类的隐式转换 
void test1() {
	unique_ptr<base> up_base(new base); // up_base指向基类
	up_base->foo(); // 调用基类的成员函数
	up_base->bar(); // 调用基类的成员函数
	
	unique_ptr<derive> up_derive(new derive);
	up_base = move(up_derive); // unique_ptr可以进行隐式转换,基类指针指向派生类对象
	up_base->foo(); // 不是多态,调用基类对象的成员函数 
	up_base->bar(); // 由多态机制调用派生类对象的成员函数 
	up_base.reset(); // 调用派生类的析构函数
}

管理资源对象
当使用裸指针构造unique_ptr对象成功之后,unique_ptr对象就和所指资源的生命周期绑定在一起了。如果中间没有脱离开的话,unique_ptr对象和资源生命周期完全一致,当unique_ptr对象销毁时,资源同时一块销毁。如果在中间过程中对unique_ptr对象调用了移动构造、移动赋值、reset()等函数,涉及到的资源也会被销毁。

up = move(up0); // 释放up对象指向的旧资源对象,隐式释放,up0置空
up.reset(); // 释放up对象指向的资源对象,显式释放
up.reset(new int); // 释放up指向的资源对象,同时指向新的对象,隐式释放
up.reset(nullptr); // 释放up对象指向的资源对象,显式释放

int *p = up.release(); // 返回指针,放弃控制权,并将up置空
up.release(); // 内存泄露

获取裸指针有两种形式: get()和release(),二者对待返回的裸指针不一样。当使用get()返回裸指针时,程序不要自己释放内存,而使用release()返回裸指针时,要由程序负责释放这个裸指针。

值得注意的是,在程序中不能以“up.release();”的形式调用release(),它并不会释放内存,相反会造成内存泄露,一定要把返回值赋值给一个指针变量,并在合适的时机进行释放。

空间和时间开销成本
先看空间开销,下面是一段测试代码,分别为unique_ptr指定了不同的删除器,测试环境是x86-64,一个指针变量的长度是8字节,分别输出不同删除器情况下的unique_ptr对象的大小:

// 下面是不同类型的删除器 
class deleter0 { // 空基类
public:
	deleter0() = default;
	
	void operator()(int *p) {
		delete p;
	}
}; 

class deleter1 : public deleter0 { // 非空类
	int dummy;
public:
	deleter1() = default;
}; 

void delete_ptr(int *p) { // 回调函数
	delete p;
}

void unique_size() {
	const char *msg = "deleter in lambda!";
	
	auto func = [&msg](int *p){ // 捕捉变量的lambda表达式 
		puts(msg);
		delete p;
	};
	auto lambda = [](int*p){delete p;}; // 没有捕捉变量的lambda表达式 
	
	unique_ptr<int> ptr1(new int);
	std::cout << sizeof(ptr1) << std::endl; // 8

	unique_ptr<int, deleter0> ptr2(new int, deleter0());
	std::cout << sizeof(ptr2) << std::endl; // 8
	
	unique_ptr<int, decltype(lambda)> ptr3(new int, lambda);
	std::cout << sizeof(ptr3) << std::endl;  // 8 
	
	unique_ptr<int, void(*)(int *)> ptr4(new int, [](int *p){delete p;});
	std::cout << sizeof(ptr4) << std::endl; // 16

	unique_ptr<int, decltype(func)> ptr5(new int, func);
	std::cout << sizeof(ptr5) << std::endl; // 16
	
	unique_ptr<int, void(*)(int *)> ptr6(new int, delete_ptr);
	std::cout << sizeof(ptr6) << std::endl; // 16
	
	unique_ptr<int, deleter1> ptr7(new int, deleter1());
	std::cout << sizeof(ptr7) << std::endl; // 16
	
	unique_ptr<int, std::function<void(int *)>> ptr8(new int, deleter0());
	std::cout << sizeof(ptr8) << std::endl; // 40
	
	unique_ptr<int, std::function<void(int *)>> ptr9(new int, deleter1());
	std::cout << sizeof(ptr9) << std::endl; // 40
	
	unique_ptr<int, std::function<void(int*)>> ptr11(new int(10), [](int *ptr){delete ptr;});  // function类型
	std::cout << sizeof(ptr11) << std::endl; // 40
	
	unique_ptr<int, std::function<void(int*)>> ptr12(new int(10), lambda);  // function类型
	std::cout << sizeof(ptr12) << std::endl; // 40
	
	unique_ptr<int, std::function<void(int*)>> ptr13(new int(10), func);  // function类型
	std::cout << sizeof(ptr13) << std::endl; // 40
}

从输出的log来看:
1、当使用缺省删除器、没有数据成员的函数对象(即用空类创建的对象)和decltype声明的没有捕获变量的lambda表达式时,unique_ptr占用的内存空间仅为8字节,是一个指针所占用的空间大小,也就是unique_ptr对象除了保存裸指针之外,没有额外的内存消耗了;

2、如果删除器是函数指针,需要额外使用8字节的内存空间,unique_ptr要使用额外空间保存这个函数指针;

3、如果删除器是捕捉了变量的lambda表达式或者有数据成员的函数对象,还要额外使用内存空间,空间大小和所捕捉的变量和数据成员数量及类型有关。

4、当使用std::function声明删除器的类型时,所占用的空间最多,unique_ptr对象要占用40字节的空间。除了这显式的40个字节之外,std::function对象自己还要额外地进行动态分配内存,所分配的内存空间和所包装的可调用对象的大小有关,也就是最终整个unique_ptr对象占用的空间要大于40字节。

因此,一般而言,如果删除器不是缺省的或者空函数对象,unique_ptr对象与裸指针相比要占用额外的内存空间,即unique_ptr要分配空间来存放删除器对象,可见,为了管理动态分配的对象资源,在内存消耗上是要花费代价的。此外,在定义unique_ptr对象时,不要轻易使用std::function来声明删除器的类型,它会使unique_ptr的空间开销变得更大。

从这里我们就理解了,在定义unique_ptr对象时,如果要提供删除器,为什么必须也要提供删除器的模板类型了。是因为编译器在实例化模板类时,可以根据删除器的类型进行优化,可以生成占用内存最小的unique_ptr类。比如,如果删除器没有数据成员,是一个空类,那么编译器在使用模板参数生成具体unique_ptr类时,可以采用继承的方式,这样在unique_ptr中只继承了删除器的成员函数,成员函数是不占用对象内存空间的,数据成员仍然只是一个裸指针,不会增加对象的存储空间;如果是其它形式的删除器,可以把它作为unique_ptr类的一个值类型的数据成员,此情况下unique_ptr要增加额外的存储空间。如果把空类形式的删除器也作为unique_ptr的数据成员的话,也会额外占用一个字节,增加了不必要的内存开销。这种对于空类采用继承而不是组合的方式,是一种内存空间优化方式,称为EBO。

使用没有捕捉外部变量的lambda表达式来定义unique_ptr的删除器,是比较常见的形式,它的使用场景有如下几种:
1、就地声明,模板参数类型使用函数指针类型,创建的unique_ptr对象大小为16字节。因为此时编译器把lambda表达式类型转换为函数指针,前面例子已经说明过,如果删除器是函数指针,需要额外使用8字节的内存空间,因此unique_ptr对象大小为16字节。

unique_ptr<int, void(*)(int *)> up(new int(10), [](int *p){delete p;});  // 函数指针类型

2、先定义lambda表达式,赋值给一个变量,然后在unique_ptr时,传入变量,模板参数类型使用decltype来自动推导,创建的unique_ptr对象大小为8字节。因为此时编译器把lambda表达式编译为一个没有数据成员的函数对象,使用了EBO优化,所以它没有额外占用unique_ptr对象空间。

auto deleter = [](int *ptr){delete ptr;};
unique_ptr<int, decltype(deleter)> up(new int(10), deleter);  //使用decltype声明类型

可见,对于同一个lambda表达式生成的删除器,使用不同的类型声明方式,所创建的unique_ptr对象的大小是不一样的。使用decltype来声明类型比使用函数指针声明类型的空间要小,而且lambda表达式生成的删除器还可以被不同的unique_ptr对象复用,不像“就地声明”的使用方式,每次都要创建一个新对象。不过应当指出,如果编译器在编译过程中进行了优化,有可能会识别出函数指针形式的回调函数,在优化时会把它内联展开(inline),此时也不会额外分配内存空间。

下面看一下运行开销:
1、在对unique_ptr对象使用*操作符进行解引用时,编译器在编译优化过程中会进行内联展开,它包含的指针成员会使用一个寄存器来存放,不额外占用内存空间,与一个裸指针编译后的代码几乎完全一样。解引用一个unique_ptr和解引用一个裸指针的开销完全一样,没有额外的执行时间消耗,而且删除销毁时也没有额外的开销。

extern int *get(int n); // 为了防止编译器把指针优化掉,使用一个函数返回指针
void test0(int arg) {
    unique_ptr<int> up(get(arg));
    printf("%d\n", *up);
}

void test1(int arg) {
    int *p = get(arg);
    printf("%d\n", *p);
    delete p;
}

下面是编译器优化后得汇编指令,可以看出,unique_ptr作为局部变量解引用操作时被优化为和普通指针一样的代码。

 test1(int): // 解引用普通指针类型的汇编代码
        push    rbp
        call    get()
        mov     edi, OFFSET FLAT:.LC0
        mov     esi, DWORD PTR [rax] // rax寄存器为指针p,它所指向的数据赋值给寄存器esi
        mov     rbp, rax
        xor     eax, eax
        call    printf
        mov     rdi, rbp
        pop     rbp
        jmp     operator delete(void*)
test0(int): // 解引用unique_ptr指针类型的汇编代码
        push    r12
        push    rbp
        sub     rsp, 8 // 为unique_ptr分配了空间,但没有使用
        call    get()
        mov     edi, OFFSET FLAT:.LC0
        mov     esi, DWORD PTR [rax] // rax寄存器为up所管理的指针,它指向的数据赋值给寄存器esi
        mov     rbp, rax
        xor     eax, eax
        call    printf
        add     rsp, 8
        mov     rdi, rbp
        pop     rbp
        pop     r12
        jmp     operator delete(void*)

2、如果作为参数传递时,同裸指针相比,会多了一次间接访问。因为会有一次从this指针获取指针变量这个数据成员的操作,这是一个寄存器间接寻址。当然如果函数比较简单,编译器优化时会进行内联优化,此时这个unique_ptr对象优化后可能就是一个局部变量,可以直接使用,避免了从this中获取的过程,开销和直接使用裸指针完全一样:

 void test0(unique_ptr<int> up)
 //下面是获取裸指针的相关汇编代码
 mov     rax, QWORD PTR [rdi] // rdi存放up的this指针,间接访问得到裸指针的值,存放在rax中
 mov     esi, DWORD PTR [rax] // 间接访问rax得到裸指针指向的值,存放的rsi中

void test1(int *p) 
//下面是获取指针的相关汇编代码
mov     esi, DWORD PTR [rdi] // rdi存放是裸指针地址,间接访问得到指向的值,存放在rsi中

总之,如果unique_ptr使用了缺省删除器,无论是空间开销还是时间开销,几乎等同于使用裸指针的开销。使用其它形式的删除器,尤其是使用std::fucntion包装后的可调用对象作为删除器时,unique_ptr对象占用的内存空间要多一些,而且在调用删除器时,使用了动态绑定的函数调用形式,也会增加一定的运行时开销,我们在程序开发时要注意这点。

应用场合

1、作为函数参数
当unique_ptr作为函数参数时,有如下几种形式:
第一种,作为值语义的参数形式:

void foo(unique_ptr<T> up);

因为unique_ptr仅支持移动语义,在调用函数时,只能使用移动语义,要么传入一个纯右值对象,要么对左值使用move()转为将亡值。调用完成后意味着资源对象被转移了,从调用者的实参转移到函数内部的unique_ptr参数中。函数调用完成之后资源对象被释放,原参数对象就成为了一个空对象,它接下来的命运要么在离开作用域后被销毁,要么接收一个新的资源对象焕发新生,总之它和所托管的资源对象没有任何关联了,无法继续使用了。

第二种,使用引用语义的参数形式,重载了两个函数,它们的参数类型分别为常量左值引用和右值引用。

void foo(const unique_ptr<T> &up);
void foo(unique_ptr<T> &&up);

调用函数时,实参是以引用形式传递的,是传址操作,没有涉及到资源移动的操作。如果unique_ptr实参对象是左值,就选择使用常量左值引用形式的函数类型;如果是个纯右值或者move后的将亡值,就使用右值引用形式的函数类型。但无论使用哪一种形式,实参都是引用形式传入的,即传入的是实参对象的别名,这样实参对象和函数内的对象都是指向同一个unique_ptr对象。

常量左值引用参数形式的函数,在函数内部只能以只读的形式访问up参数,不能进行移动操作,也不能调用reset()、release()等函数。因此,在函数内部,不会对实参对象的生命周期造成影响,当函数调用完成之后,原参数对象仍然占有资源对象。

而右值引用参数形式的函数,在函数内部可以使用move操作,把资源转移到别的地方去,或者调用reset()、release()等函数释放资源,造成函数外部的unique_ptr对象是一个空对象。因此在调用完成之后,实参的资源有可能被释放了,这点应当注意。此外,因为调用者传递的和foo函数使用的是同一个unique_ptr对象,假设在foo()函数中启动了一个异步线程,并以引用形式把unique_ptr对象传递给线程,如果在线程函数中对unique_ptr对象进行了修改操作,如移动操作,可能会造成线程不安全。

第三种,使用非常量左值引用的参数形式

void foo(unique_ptr<T> &up);

这种是按照一般引用方式作为参数传递的,它和右值引用形式的参数类似,也有类似的不安全问题,使用时要注意资源是否被转移了。

总之,如果调用函数之后,调用者不再使用所管理的内部资源了,可以把它转移到函数内部,就使用第一种方式;如果调用者自己还要占有资源,那就使用参数为常量左值引用的函数形式;右值引用和非常量左值引用的参数形式尽量不要使用,稍有不慎,就会造成错误。

2、函数返回值
使用智能指针最常见的场景是在一个函数中动态分配资源,并且在别的函数中释放,比如使用下面的形式来定义一个通过unique_ptr返回资源的函数。

unique_ptr<T> get_resource();

get_resource函数内部动态分配一个资源,使用unique_ptr进行包装并返回,当调用auto up = get_resource()之后,资源就从函数内部转移到up对象中了,释放资源也就完全交给up来负责了。前面说过,此处赋值是采用了移动语义,因为调用函数返回的临时对象是个纯右值。

3、数据成员
一个类中,如果数据成员是指针类型的,而这个成员是这个类的对象独有的,也可以考虑使用unique_ptr。如果对象中只有一个指针成员,也可以不使用unique,因为类自己的构造和析构函数,完全可以用它们实现一个RAII语义的资源管理模式:在构造函数中初始化指针,在析构函数中销毁指针。如果有一个以上的指针成员时,这样实现时有可能不安全,最好考虑使用unique_ptr来管理它们。下面有一个例子。

使用裸指针类型定义数据成员:

class C {
	A *a;
	B *b;
public:
	C(): a(new A), b(new B) { // 构造函数分配资源
	}
	
	~C() { // 需要提供析构函数来释放a和b
		delete a;
		delete b;
	} 
};

使用unique_ptr类型定义指针成员:

class D {
	unique_ptr<A> a;
	unique_ptr<B> b;
public:
	D() : a(new A), b(new B) {
	}
	
	~D() = default; // 析构函数使用缺省方式就可以了,会自动析构a和b对象
};

当使用C来定义一个对象时,如果在构造函数时,a已经初始化成功了,当初始化b时抛出了异常,此时不会调用析构函数(C++语义规定,没有构造成功的对象不会调用它的析构函数),a所分配的内存就不会释放,造成内存泄漏。但是如果使用D来定义对象,发生同样的问题时,a所分配的内存会被释放。可见,如果类中有指针类型的数据成员,要在构造过程中,想使部分成员在发生异常时进行析构,最好使用智能指针。

其次,如果一个类要提供移动语义,当它有指针类型的数据成员时,使用unique_ptr来管理这个指针成员的好处是,不用手动编写移动构造函数和移动赋值函数了,可以使用=default让编译器在需要时自动生成相关的函数就行了。当然,因为unique_ptr没有拷贝语义,如果类同时也要提供复制语义的话,就不能使用=default让编译器自动实现了,可以考虑使用shared_ptr。

4、局部变量
在一个函数中,如果有动态资源的分配和释放,如果不要求资源共享的话,最好也使用unique_ptr,因为如果使用裸指针的话,需要在函数退出的所有路径处,包括发生异常时,都要加上释放内存的语句,否则就会产生内存泄露。而且,unique_ptr对象作为局部变量,是一个在栈上创建的对象,前面说过,在此场景下,编译器在编译时完全可以把unique_ptr对象优化成普通指针进行使用,无论是内存空间占用还是执行时间的开销,同裸指针完全一样,但使用shared_ptr时没有这样高的效率,无论是内存占用还是执行时间。这是unique_ptr最为常见的应用场景,不举例多说了。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

智能指针之unique_ptr 的相关文章

  • 如何获取正在访问 ASP.NET 应用程序的当前用户?

    为了获取系统中当前登录的用户 我使用以下代码 string opl System Security Principal WindowsIdentity GetCurrent Name ToString 我正在开发一个 ASP NET 应用程
  • 以文化中立的方式将字符串拆分为单词

    我提出了下面的方法 旨在将可变长度的文本拆分为单词数组 以进行进一步的全文索引处理 删除停止词 然后进行词干分析 结果似乎不错 但我想听听关于这种实现对于不同语言的文本的可靠性的意见 您会建议使用正则表达式来代替吗 请注意 我选择不使用 S
  • GLKit的GLKMatrix“列专业”如何?

    前提A 当谈论线性存储器中的 列主 矩阵时 列被一个接一个地指定 使得存储器中的前 4 个条目对应于矩阵中的第一列 另一方面 行主 矩阵被理解为依次指定行 以便内存中的前 4 个条目指定矩阵的第一行 A GLKMatrix4看起来像这样 u
  • 按成员序列化

    我已经实现了template
  • 为什么当实例化新的游戏对象时,它没有向它们添加标签? [复制]

    这个问题在这里已经有答案了 using System Collections using System Collections Generic using UnityEngine public class Test MonoBehaviou
  • 如何使用 ICU 解析汉字数字字符?

    我正在编写一个使用 ICU 来解析由汉字数字字符组成的 Unicode 字符串的函数 并希望返回该字符串的整数值 五 gt 5 三十一 gt 31 五千九百七十二 gt 5972 我将区域设置设置为 Locale getJapan 并使用
  • 用于登录 .NET 的堆栈跟踪

    我编写了一个 logger exceptionfactory 模块 它使用 System Diagnostics StackTrace 从调用方法及其声明类型中获取属性 但我注意到 如果我在 Visual Studio 之外以发布模式运行代
  • 如何从 appsettings.json 文件中的对象数组读取值

    我的 appsettings json 文件 StudentBirthdays Anne 01 11 2000 Peter 29 07 2001 Jane 15 10 2001 John Not Mentioned 我有一个单独的配置类 p
  • 不同枚举类型的范围和可转换性

    在什么条件下可以从一种枚举类型转换为另一种枚举类型 让我们考虑以下代码 include
  • 创建链表而不将节点声明为指针

    我已经在谷歌和一些教科书上搜索了很长一段时间 我似乎无法理解为什么在构建链表时 节点需要是指针 例如 如果我有一个节点定义为 typedef struct Node int value struct Node next Node 为什么为了
  • 将多个表映射到实体框架中的单个实体类

    我正在开发一个旧数据库 该数据库有 2 个具有 1 1 关系的表 目前 我为每个定义的表定义了一种类型 1Test 1Result 我想将这些特定的表合并到一个类中 当前的类型如下所示 public class Result public
  • 转发声明和包含

    在使用库时 无论是我自己的还是外部的 都有很多带有前向声明的类 根据情况 相同的类也包含在内 当我使用某个类时 我需要知道该类使用的某些对象是前向声明的还是 include d 原因是我想知道是否应该包含两个标题还是只包含一个标题 现在我知
  • 控件的命名约定[重复]

    这个问题在这里已经有答案了 Microsoft 在其网站上提供了命名指南 here http msdn microsoft com en us library xzf533w0 VS 71 aspx 我还有 框架设计指南 一书 我找不到有关
  • 什么时候虚拟继承是一个好的设计? [复制]

    这个问题在这里已经有答案了 EDIT3 请务必在回答之前清楚地了解我要问的内容 有 EDIT2 和很多评论 有 或曾经 有很多答案清楚地表明了对问题的误解 我知道这也是我的错 对此感到抱歉 嗨 我查看了有关虚拟继承的问题 class B p
  • 如何使用 C# / .Net 将文件列表从 AWS S3 下载到我的设备?

    我希望下载存储在 S3 中的多个图像 但目前如果我只能下载一个就足够了 我有对象路径的信息 当我运行以下代码时 出现此错误 遇到错误 消息 读取对象时 访问被拒绝 我首先做一个亚马逊S3客户端基于我的密钥和访问配置的对象连接到服务器 然后创
  • WPF/C# 将自定义对象列表数据绑定到列表框?

    我在将自定义对象列表的数据绑定到ListBox in WPF 这是自定义对象 public class FileItem public string Name get set public string Path get set 这是列表
  • cmake 将标头包含到每个源文件中

    其实我有一个简单的问题 但找不到答案 也许你可以给我指一个副本 所以 问题是 是否可以告诉 cmake 指示编译器在每个源文件的开头自动包含一些头文件 这样就不需要放置 include foo h 了 谢谢 CMake 没有针对此特定用例的
  • C# 成员变量继承

    我对 C 有点陌生 但我在编程方面有相当广泛的背景 我想做的事情 为游戏定义不同的 MapTiles 我已经像这样定义了 MapTile 基类 public class MapTile public Texture2D texture pu
  • 如何防止用户控件表单在 C# 中处理键盘输入(箭头键)

    我的用户控件包含其他可以选择的控件 我想实现使用箭头键导航子控件的方法 问题是家长控制拦截箭头键并使用它来滚动其视图什么是我想避免的事情 我想自己解决控制内容的导航问题 我如何控制由箭头键引起的标准行为 提前致谢 MTH 这通常是通过重写
  • 使用.NET技术录制屏幕视频[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 有没有一种方法可以使用 NET 技术来录制屏幕 无论是桌面还是窗口 我的目标是免费的 我喜欢小型 低

随机推荐

  • ffmpeg windows编译及调试完整版

    目录 编译 基础环境准备 依赖环境安装 依赖库安装 X264 fdk aac X265 ffmpeg 4 3 6 调试 基础项目环境搭建 VS2019项目创建 VS2019项目代码 vs2019配置 VS2019调试 编译 基础环境准备 1
  • 多行字符串无法匹配问题处理

    关于多行字符串无法匹配的问题 处理当前问题 需要加上下面的东西 Pattern MULTILINE Pattern DOTALL 这样才能处理多行问题 防止拿不到匹配符中的效果 这里简单的处理方式入如下所示 Pattern patternC
  • 《MySQL实战45讲》读后感 21

    在上一篇文章中 我和你介绍了间隙锁和 next key lock 的概念 但是并没有说明加锁规则 间隙锁的概念理解起来确实有点儿难 尤其在配合上行锁以后 很容易在判断是否会出现锁等待的问题上犯错 所以今天 我们就先从这个加锁规则开始吧 首先
  • aspose java api_NetBeans中如何下载并使用Aspose Java API和示例

    为了提升文件格式应用程序界面 API 的用户体验 Java NetBeans是Java开发人员在管理Java项目 开发组件或应用程序时使用最多的集成开发环境之一 为了让他们在NetBeans项目中通过一个简单的点击就能下载和使用多个Aspo
  • NITIRE 2023官方的PSNR及SSIM计算代码

    NITIRE 2023官方的PSNR及SSIM计算代码 问题描述 做图像复原任务时 总避免不了计算PSNR和SSIM等图像质量评估指标 但是网上实在是太多计算这类指标的代码了 不同代码计算的结果还可能存在差异 有使用matlab计算SSIM
  • 提高C++性能的编程技术笔记:多线程内存池+测试代码

    为了使多个线程并发地分配和释放内存 必须在分配器方法中添加互斥锁 全局内存管理器 通过new 和delete 实现 是通用的 因此它的开销也非常大 因为单线程内存管理器要比多线程内存管理器快的多 所以如果要分配的大多数内存块限于单线程中使用
  • MySQL多表操作:建表与多表查询

    目录 多表间的建表原则 一对多 多对多 一对一 实例演示多表查询 数据准备 多表查询 笛卡尔积 内连接 隐式内连接 显式内连接 外连接 左外连接 右外连接 子查询 子查询的多种情况 多表间的建表原则 一对多 如分类与商品是一对多的关系 一个
  • 花了 44 大洋,我实现 Java 发送短信功能!

    本篇为从 0 到 1 的一个 用 Java 对接腾讯短信发送功能的一篇流程文章 而标题中的 44 大洋则是用来购买短信套餐 当然如果你是首次使用腾讯云平台则可以享受 免费使用 这一功能 我是老用户 那 废话不多说 我们往下看对接步骤 1 短
  • java设计模式之代理模式

    什么是代理模式 为什么要使用代理模式 代理模式属于结构型 通过目标对象类完成其实现 静态代理 静态代理要求目标对象和代理对象实现同一个业务接口 由目标类去创建和实现 代理类负责实现增强 静态代理的缺点 代理类和目标类实现了相同的接口 每个代
  • CUDA编程一天入门

    目录 0 环境准备 1 套路 2 并行执行内核设置 3 示例代码simpleTexture3D 4 参考链接 0 环境准备 1 套路 CUDA 编程模型是一个异构模型 其中同时使用 CPU 和 GPU 在 CUDA 中 主机是指 CPU 及
  • MySQL-删除命令的区别

    MySQL 删除命令的区别 一 drop命令 1 删除数据库 DROP DATABASE IF EXISTS 数据库名 例 drop database if exists aa 2 删除数据表 DROP TABLE IF EXISTS 表名
  • Ts中函数的使用

    函数 基本示例 和 JavaScript 一样 TypeScript 函数可以创建有名字的函数和匿名函数 命名函数 function add x y return x y 匿名函数 let myAdd function x y return
  • Qt基础:六、标准对话框

    1 颜色对话框 添加 include
  • 软件包没有可安装候选

    前文 新人一枚 通过资料以及他人的经验进行学习 学习了以下博主的文章 链接是 https blog csdn net metheir article details 85040796 写下自己的学习记录 有不对之处请指出 有侵权处请联系本人
  • 在Python中通过OpenCV自己训练分类器 进行特定物体实时识别

    在Python中通过OpenCV自己训练分类器进行特定物体实时识别 0 前言 OpenCV中提供了一些训练好的分类器供我们调用 从而实现物体识别和分类 如人脸检测 年龄和性别预测 猫脸检测 汽车 船 猫 狗 沙发等 物体识别等 本篇文章的目
  • OPC新标准

    今天收到 Automation World 的邮件 OPC基金会在11月16日投票通过增加一个新标准 OPC Express Interface Xi OPC Xi 基于微软的WCF 能使 Net客户端和已有的老式OPC客户端同时访问老式O
  • HashMap 和 Hashtable 的区别

    HashMap 和 Hashtable 的区别 线程是否安全 HashMap 是非线程安全的 HashTable 是线程安全的 因为 HashTable 内部的方法基本都经过synchronized 修饰 如果你要保证线程安全的话就使用 C
  • Python轻量级Web框架:Bottle库!

    Bottle是一个超轻量级的python库 说是库 其本身只由一个4000行左右的文件构成 并且不需要任何依赖 只靠python标准库即可运作 和它本身的轻便一样 Bottle库的使用也十分简单 相信在看到本文前 读者对python也已经有
  • RAM、ROM、内存、存储、外存、硬盘的理解

    内存在电脑中起着举足轻重的作用 内存一般采用半导体存储单元 包括随机存储器 RAM 只读存储器 ROM 以及高速缓存 CACHE 只不过因为RAM是其中最重要的存储器 所以通常所说的内存即指电脑系统中的RAM RAM 随机存取存储器 ran
  • 智能指针之unique_ptr

    unique ptr实现的是专属所有权语义 用于独占它所指向的资源对象的场合 某个时刻只能有一个unique ptr指向一个动态分配的资源对象 也就是这个资源不会被多个unique ptr对象同时占有 它所管理的资源只能在unique pt