从零开始学C++之对象语义与值语义、资源管理(RAII、资源所有权)、模拟实现auto_ptr<class>、实现Ptr_vector

一、对象语义与值语义

1、值语义是指对象的拷贝与原对象无关。拷贝之后就与原对象脱离关系,彼此独立互不影响(深拷贝)。比如说int,C++中的内置类型都是值语义,前面学过的三个标准库类型string,vector,map也是值语义


2、对象语义指的是面向对象意义下的对象


对象拷贝是禁止的(Noncopyable)

OR

一个对象被系统标准的复制方式复制后,与被复制的对象之间依然共享底层资源,对任何一个的改变都将改变另一个(浅拷贝)


3、值语义对象生命期容易控制


4、对象语义对象生命期不容易控制(通过智能指针来解决,见本文下半部分)。智能指针实际上是将对象语义转化为值语义,利用局部对象(智能指针)的确定性析构,包括auto_ptr, shared_ptr, weak_ptr,  scoped_ptr。


5、值语义与对象语义是分析模型决定的,语言的语法技巧用来匹配模型。


6、值语义对象通常以类对象的方式来使用,对象语义对象通常以指针或引用方式来使用


7、一般将只使用到值语义对象的编程称为基于对象编程,如果使用到了对象意义对象,可以看作是面向对象编程。


8、基于对象与面向对象的区别

  很多人没有区分“面向对象”和“基于对象”两个不同的概念。面向对象的三大特点(封装,继承,多态)缺一不可。通常“基于对

象”是使用对象,但是无法利用现有的对象模板产生新的对象类型,继而产生新的对象,也就是说“基于对象”没有继承的特点。而“多

态”表示为父类类型的子类对象实例,没有了继承的概念也就无从谈论“多态”。现在的很多流行技术都是基于对象的,它们使用一些

封装好的对象,调用对象的方法,设置对象的属性。但是它们无法让程序员派生新对象类型。他们只能使用现有对象的方法和属

性。所以当你判断一个新的技术是否是面向对象的时候,通常可以使用后两个特性来加以判断。“面向对象”和“基于对象”都实现了“封

装”的概念,但是面向对象实现了“继承和多态”,而“基于对象”没有实现这些。



假设现在有这样一个继承体系:



其中Node,BinaryNode 都是抽象类,AddNode 有两个Node* 成员,Node应该实现为对象语义:

(一):禁止拷贝。

比如

AddNode ad1(left, right);

AddNode ad2(ad1);

假设允许拷贝且没有自己实现拷贝构造函数(默认为浅拷贝),则会有两个指针同时指向一个Node对象,容易发生析构两次的运行时错误。


下面看如何禁止拷贝的两种方法:

方法一:将Node 的拷贝构造函数和赋值运算符声明为私有,并不提供实现


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

//抽象类
class Node
{
public:
    Node() { }
     virtual  double Calc()  const =  0;
     virtual ~Node( void) {}
private:
    Node( const Node &);
     const Node & operator=( const Node &);
};

//抽象类
class BinaryNode :  public Node
{
public:
    BinaryNode(Node *left, Node *right)
        : left_(left), right_(right) {}
    ~BinaryNode()
    {
         delete left_;
         delete right_;
    }
protected:
    Node * const left_;
    Node * const right_;
};

class AddNode:  public BinaryNode
{
public:
    AddNode(Node *left, Node *right)
        : BinaryNode(left, right) { }
     double Calc()  const
    {
         return left_->Calc() + right_->Calc();
    }
};

class NumberNode:  public Node
{
public:
    NumberNode( double number): number_(number)
    {

    }
     double Calc()  const
    {
         return number_;
    }

private:
     const  double number_;
};


此时如下的最后一行就会编译出错了:

 C++ Code 
1
2
3
4
5
6
7
NumberNode *left =  new NumberNode( 3);
NumberNode *right =  new NumberNode( 4);

AddNode ad1(left, right);

AddNode ad2(ad1);

即要拷贝构造一个AddNode 对象,最远也得从调用Node类的拷贝构造函数开始(默认拷贝构造函数会调用基类的拷贝构造函数,如果是自己实现的而且没有显式调用,将不会调用基类的拷贝构造函数),因为私有,故不能访问。

需要注意的是,因为声明了Node类的拷贝构造函数,故必须实现一个构造函数,否则没有默认构造函数可用。


方法二:Node类继承自一个不能拷贝的类,如果有很多类似Node类的其他类,此方法比较合适


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class NonCopyable
{
protected//构造函数可以被派生类调用,但不能直接构造对象
    NonCopyable() {}
    ~NonCopyable() {}
private:
    NonCopyable( const NonCopyable &);
     const NonCopyable & operator=( const NonCopyable &);
};

//抽象类,对象语义,禁止拷贝(首先需要拷贝NonCopyable)
class Node :  private NonCopyable
{
public:
     virtual  double Calc()  const =  0;
     virtual ~Node( void) {}
};

注意NonCopyable 类的构造函数声明为protected,则不能直接构造对象,如NonCopyable nc; // error 

但在构造派生类,如最底层的AddNode类时,可以被间接调用。

同样地,NonCopyable类的拷贝构造函数和赋值运算符为私有,故如 AddNode ad2(ad1); 编译出错。



二、资源管理

(一)、资源所有权

1、局部对象

资源的生存期为嵌入实体的生存期。

(1)、一个代码块拥有在其作用域内定义的所有自动对象(局部对象)。释放这些资源的任务是完全自动的(调用析构函数)。

如 void fun()

{

Test t; //局部对象

}

(2)、所有权的另一种形式是嵌入。一个对象拥有所有嵌入其中的对象。释放这些资源的任务也是自动完成(外部对象的析构函数调用内部对象的析构函数)。如

class A

{

private:

B b; //先析构A,再析构b 

};

2、动态对象(new 分配内存)

(1)、对于动态分配对象就不是这样了,它总是通过指针访问。在它们的生存期内,指针可以指向一个资源序列,若干指针可以指向相同的资源。动态分配资源的释放不是自动完成的,需要手动释放,如delete 指针。
(2)、如果对象从一个指针传递到另一个指针,所有权关系就不容易跟踪。容易出现空悬指针、内存泄漏、重复删除等错误。


(二)、RAII 与 auto_ptr

一个对象可以拥有资源。在对象的构造函数中执行资源的获取(指针的初始化),在析构函数中释放(delete 指针)。这种技法把它称之为RAII(Resource Acquisition Is Initialization:资源获取即初始化),如前所述的资源指的是内存,实际上还可以扩展为文件句柄,套接字,互斥量,信号量等资源。

对应于智能指针auto_ptr,可以理解为一个auto_ptr对象拥有资源的裸指针,并负责资源的释放。

下面先来看auto_ptr 的定义:

// TEMPLATE CLASS auto_ptr
template<class _Ty>
class auto_ptr

{

....

private:

_Ty *_Myptr; // the wrapped object pointer

}


实际上auto_ptr 是以模板方式实现的,内部成员变量只有一个,就是具体类的指针,即将这个裸指针包装起来。auto_ptr 的实现里面还封装了很多关于裸指针的操作,这样就能像使用裸指针一样使用智能指针,如->和* 操作;负责裸指针的初始化,以及管理裸指针指向的内存释放。

这样说还是比较难理解,可以自己实现一个模拟 auto_ptr<Node> 类的NodePtr 类,从中体会智能指针是如何管理资源的:

Node.h:


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#ifndef _NODE_H_
#define _NODE_H_

class Node
{
public:
    Node();
    ~Node();
     void Calc()  const;
};

class NodePtr
{
public:
     explicit NodePtr(Node* ptr =  0)
        : ptr_(ptr) {}

    NodePtr(NodePtr& other)
        : ptr_(other.Release()) {}

    NodePtr&  operator=(NodePtr& other)
    {
        Reset(other.Release());
         return * this;
    }

    ~NodePtr() 
    { 
         if (ptr_ !=  0)
             delete ptr_; 
    }

    Node&  operator*()  const {  return *Get(); }

    Node*  operator->()  const {  return Get(); }

    Node* Get()  const {  return ptr_; }

    Node* Release()
    {
        Node* tmp = ptr_;
        ptr_ =  0;
         return tmp;
    }
     void Reset(Node* ptr =  0)
    {
         if (ptr_ != ptr)
        {
             delete ptr_;
        }
        ptr_ = ptr;
    }
private:
    Node* ptr_;
};

#endif  // _NODE_H_

Node.cpp:


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include  "Node.h"

Node::Node()
{
    std::cout <<  "Node ..." << std::endl;
}

Node::~Node()
{
    std::cout <<  "~Node ..." << std::endl;
}

void Node::Calc()  const
{
    std::cout <<  "Node::Calc ..." << std::endl;
}

main.cpp:


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using  namespace std;

#include  "DebugNew.h"
#include  "Node.h"

int main( void)
{
    Node *p1 =  new Node;
    NodePtr np(p1);
    np->Calc();

    NodePtr np2(np);

    Node *p2 =  new Node;
    NodePtr np3(p2);
    np3 = np2;  //np3先delete p2,接着接管p1;

     return  0;
}


从输出可以看出,通过NodePtr 智能指针对象包装了裸指针,NodePtr类通过重载-> 和 * 运算符实现如同裸指针一样的操作,如

np->Calc();  程序中通过智能指针对象的一次拷贝构造和赋值操作之后,现在共有3个局部智能指针对象,但np 和 np2 的成员ptr_ 已经被设置为0;第二次new 的Node对象已经被释放,现在np3.ptr_ 指向第一次new 的Node对象,程序结束,np3局部对象析构,delete ptr_,析构Node对象。

从程序实现可以看出,Node 类是可以拷贝,而且是默认浅拷贝,故是对象语义对象,现在使用智能指针来管理了它的生存期,不容易发生内存泄漏问题。(程序中编译时使用了这里的内存泄漏跟踪器,现在new 没有匹配delete 但没有输出信息,说明没有发生内存泄漏)。


所以简单来说,智能指针的本质思想就是:用栈上对象(智能指针对象)来管理堆上对象的生存期。


在本文最前面的程序中,虽然实现了禁止拷贝,但如上所述,对象语义对象的生存期仍然是不容易控制的,下面将通过智能指针auto_ptr<Node>  来解决这个问题,通过类比上面NodePtr 类的实现可以比较容易地理解auto_ptr<Node>的作用:


 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//抽象类
class Node
{
public:
    Node() { }
     virtual  double Calc()  const =  0;
     virtual ~Node( void) {}
private:
    Node( const Node &);
     const Node & operator=( const Node &);
};

//抽象类
class BinaryNode :  public Node
{
public:
    BinaryNode(std::auto_ptr<Node>& left, std::auto_ptr<Node>& right)
        : left_(left), right_(right) {}
    ~BinaryNode()
    {
//        delete left_;
//        delete right_;
    }
protected:
    std::auto_ptr<Node> left_;
    std::auto_ptr<Node> right_;
};

class AddNode:  public BinaryNode
{
public:
    AddNode(std::auto_ptr<Node>& left, std::auto_ptr<Node>& right)
        : BinaryNode(left, right) { }
     double Calc()  const
    {
         return left_->Calc() + right_->Calc();
    }
};

class NumberNode:  public Node
{
public:
    NumberNode( double number): number_(number)
    {

    }
     double Calc()  const
    {
         return number_;
    }

private:
     const  double number_;
};

需要注意的是,在BinaryNode 中现在裸指针的所有权已经归智能指针所有,由智能指针来管理Node 对象的生存期,故在析构函数中不再需要delete 指针; 的操作。


对auto_ptr 做一点小结:

1、auto_ptr不能作为STL容器的元素
2、STL容器要求存放在容器中的元素是值语义,要求元素能够被拷贝。
3、auto_ptr的拷贝构造或者赋值操作会改变右操作数,因为右操作数的所有权要发生转移。

实际上auto_ptr 是值语义(将对象语义转换为值语义),auto_ptr 之所以不能作为STL容器的元素,关键在于第3点,即
auto_ptr的拷贝构造或者赋值操作会改变右操作数,如下的代码:


 C++ Code 
1
2
3
std::auto_ptr<Node> node( new Node);
vector<std::auto_ptr<Node> > vec;
vec.push_back(node);

在编译到push_back 的时候就出错了,查看push_back 的声明:

void push_back(const _Ty& _Val);

即参数是const 引用,在函数内部拷贝时不能对右操作数进行更改,与第3点冲突,所以编译出错。

其实可以这样来使用:


 C++ Code 
1
2
3
std::auto_ptr node( new Node);
vector<Node *> vec;
vec.push_back(node.release());

也就是先释放所有权成为裸指针,再插入容器,在这里再提一点,就是vector 只负责裸指针本身的内存的释放,并不负责指针指向内存的释放,假设一


MultipleNode 类有成员vector<Node*> vec_; 那么在类的析构函数中需要遍历容器,逐个delete 指针; 才不会造成内存泄漏。


更谨慎地说,如上面的用法还是存在内存泄漏的 可能性。考虑这样一种情形:


vec.push_back(node.release()); 当node.release() 调用完毕,进而调用push_back 时,由这里知道,push_back 会先调用operater


 new 分配指针本身的内存,如果此时内存耗尽,operator new 失败,push_back 抛出异常,此时裸指针既没有被智能指针接管,也


没有插入vector(不能在类的析构函数中遍历vector 进行delete 操作),那么就会造成内存泄漏。


为了解决这个潜在的风险,可以实现一个Ptr_vector 模板类,负责指针指向内存的释放:


Ptr_vector.h:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#ifndef _PTR_VECTOR_H_
#define _PTR_VECTOR_H_

#include <vector>
#include <memory>

template < typename T>
class ptr_vector :  public std::vector<T *>
{
public:
    ~ptr_vector()
    {
        clear();
    }

     void clear()
    {
        std::vector<T *>::iterator it;
         for (it = begin(); it != end(); ++it)
             delete *it;  //释放指针指向的内存

        std::vector<T *>::clear();  //释放指针本身
    }

     void push_back(T * const &val)
    {
        std::auto_ptr<T> ptr(val);               // 用auto_ptr接管val所有权
        std::vector<T *>::push_back(val);        // operator new
        ptr.release();
    }

     void push_back(std::auto_ptr<T> &val)
    {
        std::vector<T *>::push_back(val.get());
        val.release();
    }
};

#endif  // _PTR_VECTOR_H_

Ptr_vector 继承自vector 类,重新实现push_back 函数,插入裸指针时,先用局部智能指针对象接管裸指针所有权,如果


std::vector<T *>::push_back(val);  成功(operator new 成功),那么局部智能指针对象释放裸指针的所有权;如果


std::vector<T *>::push_back(val);  失败(operator new 失败),抛出异常,栈展开的时候要析构局部对象,此时局部智能指针对象的析构函数内会


delete 裸指针。


此外,在Ptr_vector 类中还重载了push_back,能够直接将智能指针作为参数传递,在内部插入裸指针成功后,释放所有权。


当Ptr_vector 对象销毁时调用析构函数,析构函数调用clear(),遍历vector<T*>,delete 裸指针。


此时,我们就可以如下地使用Ptr_vector:

 C++ Code 
1
2
3
4
std::auto_ptr node( new Node);
Ptr_vector<Node> vec;
vec.push_back(node.release());
// vec.push_back(node);

这样就确保一定不会发生内存泄漏,即使push_back 失败也不会。


参考:

C++ primer 第四版
Effective C++ 3rd
C++编程规范



<think> 我们正在讨论的是 `unique_ptr` 的嵌套使用,具体是 `unique_ptr<classa<classb>>` 这种形式。这实际上是一个模板嵌套的情况,其中 `classa` 是一个模板类,而 `classb` 是它的模板参数。然而,从用户的问题描述来看,可能是指 `unique_ptr` 管理一个模板类对象,比如 `ClassA<ClassB>` 类型的对象。我们假设用户的意思是:如何使用 `unique_ptr` 来管理一个模板类(例如 `ClassA<ClassB>`)的对象。 下面我将详细阐述 `unique_ptr` 管理模板类对象的使用方法、原理和注意事项。 ### 一、使用方法 #### 1. 声明初始化 当使用 `unique_ptr` 管理模板类对象时,声明方式如下: ```cpp std::unique_ptr<ClassA<ClassB>> ptr; ``` 初始化可以使用 `new` 直接初始化,或者使用 `std::make_unique`(C++14 及以上): ```cpp // 使用 new std::unique_ptr<ClassA<ClassB>> ptr(new ClassA<ClassB>(args...)); // 使用 make_unique(推荐) auto ptr = std::make_unique<ClassA<ClassB>>(args...); ``` #### 2. 在类成员中使用 引用[1]和[2]展示了在类中使用 `unique_ptr` 替代原始指针以避免资源泄漏[^1][^2]: ```cpp class MyClass { private: std::unique_ptr<ClassA<ClassB>> memberPtr; public: // 构造函数初始化 MyClass(int arg1, int arg2) : memberPtr(std::make_unique<ClassA<ClassB>>(arg1, arg2)) { // 如果在构造过程中发生异常,memberPtr 会自动释放已分配的内存 } // 不需要显式定义析构函数,因为 unique_ptr 会自动释放资源 // 拷贝构造函数和赋值运算符需要特殊处理(见注意事项) }; ``` #### 3. 重置和释放 - **重置**:使用 `reset()` 方法释放当前对象并可选地接管新对象: ```cpp ptr.reset(); // 释放对象并将 ptr 置为空 ptr.reset(new ClassA<ClassB>(...)); // 释放旧对象,管理新对象 ``` - **释放所有权**:使用 `release()` 方法返回原始指针并放弃所有权,之后需要手动管理该指针: ```cpp ClassA<ClassB>* rawPtr = ptr.release(); // 现在必须手动删除 rawPtr,否则内存泄漏 delete rawPtr; ``` ### 二、原理 #### 1. 所有权独占 `unique_ptr` 严格独占所管理对象所有权: - 不能被复制(拷贝构造函数和拷贝赋值运算符被删除)。 - 只能通过移动语义转移所有权(如 `std::move`)[^4]。 #### 2. 自动生命周期管理 - 当 `unique_ptr` 被销毁(例如离开作用域)时,其析构函数会自动调用 `delete` 释放对象。 - 对于数组类型,使用 `std::unique_ptr<T[]>` 会在析构时调用 `delete[]`。 #### 3. 模板类的支持 `unique_ptr` 可以管理任意类型的对象,包括模板类实例。编译器会根据模板参数生成相应的特化版本。 ### 三、注意事项 #### 1. 构造异常安全 引用[2]和[3]强调了使用 `unique_ptr` 在构造函数中的异常安全优势[^2][^3]: - 如果在初始化列表中构造多个 `unique_ptr`,其中一个抛出异常,则已构造的成员会自动销毁,避免资源泄漏。 - 相比之下,使用原始指针需要手动处理异常(如在构造函数体内捕获异常并删除)。 #### 2. 自定义删除器 如果对象需要特殊释放方式(如文件句柄、自定义分配器等),可以指定删除器: ```cpp auto deleter = [](ClassA<ClassB>* p) { customDeleteFunction(p); // 自定义释放逻辑 }; std::unique_ptr<ClassA<ClassB>, decltype(deleter)> ptr(new ClassA<ClassB>, deleter); ``` #### 3. 拷贝和赋值 由于 `unique_ptr` 不可复制,包含它的类需要手动实现拷贝语义: - **深拷贝**:在拷贝构造函数中重新构造对象(如引用[1]所示[^1]): ```cpp MyClass(const MyClass& other) : memberPtr(std::make_unique<ClassA<ClassB>>(*other.memberPtr)) { } ``` - **赋值运算符**:同样需要深拷贝: ```cpp MyClass& operator=(const MyClass& other) { if (this != &other) { *memberPtr = *other.memberPtr; // 假设 ClassA<ClassB> 支持赋值 } return *this; } ``` #### 4. 多态支持 如果 `ClassA<ClassB>` 有派生类,需确保基类析构函数为虚函数: ```cpp class Base { public: virtual ~Base() = default; }; class Derived : public Base { ... }; std::unique_ptr<Base> ptr = std::make_unique<Derived>(); ``` ### 四、示例代码 ```cpp #include <memory> #include <iostream> template<typename T> class ClassA { T data; public: ClassA(T d) : data(d) {} void print() { std::cout << data << "\n"; } }; using MyTemplateType = ClassA<int>; // 别名简化 int main() { // 创建 unique_ptr 管理 ClassA<int> auto ptr = std::make_unique<MyTemplateType>(42); ptr->print(); // 输出: 42 // 重置指针 ptr.reset(new MyTemplateType(100)); // 释放旧对象,管理新对象 ptr->print(); // 输出: 100 // 离开作用域自动释放内存 } ``` §§相关问题§§ 1. `unique_ptr` 管理模板类对象时,如何实现深拷贝? 2. 为什么在构造函数中使用 `unique_ptr` 成员更安全? 3. 如何为 `unique_ptr<ClassA<ClassB>>` 指定自定义删除器? 4. 使用 `unique_ptr` 管理多态对象需要注意什么? [^1]: 可以使用 unique_ptr 替换原生指针来避免资源泄漏。 [^2]: 在 class 内使用 unique_ptr 可避免资源泄漏。 [^3]: 考虑使用 unique_ptr 以避免构造过程中的资源泄漏。 [^4]: unique_ptr 持有对象的独有权,不可复制只能移动。 </think> ### `unique_ptr<ClassA<ClassB>>` 使用详解 `unique_ptr<ClassA<ClassB>>` 表示用独占所有权智能指针管理模板类 `ClassA` 的实例(其模板参数为 `ClassB`)。以下是核心原理和使用方法: --- ### 一、核心原理 1. **所有权独占性** - `unique_ptr` 严格遵循独占所有权原则,禁止拷贝构造和拷贝赋值(引用[4])[^4]。 - 所有权转移需通过 `std::move` 显式进行: ```cpp unique_ptr<ClassA<ClassB>> p1 = make_unique<ClassA<ClassB>>(); unique_ptr<ClassA<ClassB>> p2 = std::move(p1); // p1 失效,所有权转至 p2 ``` 2. **自动资源释放** - 析构时自动调用 `delete` 释放对象,无需手动管理(引用[2])[^2]。 - 完美支持模板类:`ClassA<ClassB>` 的析构函数会被正确调用。 3. **异常安全** - 构造函数中若部分成员初始化失败,已构造的 `unique_ptr` 成员会自动释放资源(引用[2][3])[^2][^3]。 --- ### 二、使用方法 #### 1. 基本操作 ```cpp #include <memory> // 创建对象 auto ptr = std::make_unique<ClassA<ClassB>>(arg1, arg2); // 重置资源(释放旧对象,接管新对象ptr.reset(new ClassA<ClassB>(arg3, arg4)); // 显式释放资源并置空 ptr.reset(); ``` #### 2. 在类成员中使用(推荐场景) ```cpp class Container { private: std::unique_ptr<ClassA<ClassB>> member; // 模板类成员 public: // 构造函数:异常安全! Container(int x, int y) : member(std::make_unique<ClassA<ClassB>>(x, y)) { // 若此处抛出异常,member 会自动释放资源 } // 无需显式析构函数(默认析构自动释放 member) }; ``` > ✓ **优势**:避免构造函数异常导致资源泄漏(引用[2][3])[^2][^3]。 --- ### 三、关键注意事项 1. **禁止拷贝操作** ```cpp unique_ptr<ClassA<ClassB>> p1 = make_unique<...>(); unique_ptr<ClassA<ClassB>> p2 = p1; // 编译错误!违反独占性 ``` 2. **自定义删除器(高级用法)** 若 `ClassA<ClassB>` 需特殊释放逻辑: ```cpp auto deleter = [](ClassA<ClassB>* p) { customCleanup(p); // 自定义清理函数 delete p; }; unique_ptr<ClassA<ClassB>, decltype(deleter)> ptr(new ClassA<ClassB>, deleter); ``` 3. **多态支持** 若 `ClassB` 是基类,需保证其析构函数为 `virtual`: ```cpp class ClassB { public: virtual ~ClassB() = default; // 虚析构确保正确释放派生类 }; ``` 4. **原始指针互操作** - 获取指针:`ClassA<ClassB>* raw = ptr.get();`(不释放所有权) - 释放所有权:`ClassA<ClassB>* raw = ptr.release();`(需手动 `delete`) --- ### 四、典型错误案例 ```cpp // 错误1:多个 unique_ptr 管理同一对象 ClassA<ClassB>* raw = new ClassA<ClassB>(); unique_ptr<ClassA<ClassB>> p1(raw); unique_ptr<ClassA<ClassB>> p2(raw); // 运行时双重释放! // 错误2:忽略移动语义 void process(unique_ptr<ClassA<ClassB>> obj); unique_ptr<ClassA<ClassB>> obj = make_unique<...>(); process(obj); // 错误!尝试拷贝 process(std::move(obj)); // 正确:所有权转移 ``` --- ### 五、最佳实践总结 1. **优先使用 `make_unique`** 避免显式 `new`,提高异常安全性(引用[2])[^2]。 2. **利用 RAII 特性** 在类成员中使用 `unique_ptr` 简化资源管理。 3. **所有权转移用 `std::move`** 严格遵循独占所有权规则(引用[4])[^4]。 4. **避免跨模块传递原生指针** 防止所有权混乱。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值