• C++智能指针(一)——shared_ptr初探



    1. 普通指针存在的问题

    智能指针的引入,是为了解决普通指针在使用过程中存在的一些问题:其中内存泄漏以及空悬指针是最主要的问题。

    正常使用普通指针,我们需要 new 分配内存,使用 delete 释放资源,一旦项目很庞大,尤其是在多个地方共享同一个指针时,产生内存泄漏的风险很大,且需要更多的代码来管理指针。

    下面举一个具体实例,比如两个对象共享同一个指针,此时对于该指针在什么时候释放需要更多地代码来判断,以防止内存泄漏与访问空悬指针。

    #include 
    #include 
    class Person
    {
    public:
    	string name;
    	Person* child;
    	
    	Person(const string& n, Person* c = nullptr) : name(n), child(c) {
    	}
        
        ~Person() {
    		std::cout << "delete" << name << std::endl;
    	}
    }
    
    int main()
    {
    	Person* son = new Person("hhhcbw");
    	Person father("c", son);
    	Person mother("z", son);
    	delete son;
    	std::cout << father.child->name << std::endl; // ERROR: ask hanging pointer
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    为了解决普通指针的痛点,引入智能指针。


    2. Class shared_ptr

    shared_ptr 从字面就可以看出,该智能指针类主要用于共享资源,其能保证当最后一个对对象的引用被删除后,对象本身被删除(包括一些内存与资源的释放)。

    2.1 使用 shared_ptr

    使用 shared_ptr 与使用普通指针差不多。可以赋值,拷贝以及比较 shared_ptr,也可以使用操作符 *-> 来访问指针指向的对象。举一个例子:

    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
    	// two shared pointers representing two persons by their name
    	shared_ptr<string> pNico(new string("nico"));
    	shared_ptr<string> pJutta(new string("jutta"));
    	// capitalize person names
    	(*pNico)[0] = ’N’;
    	pJutta->replace(0,1,"J");
    	// put them multiple times in a container
    	vector<shared_ptr<string>> whoMadeCoffee;
    	whoMadeCoffee.push_back(pJutta);
    	whoMadeCoffee.push_back(pJutta);
    	whoMadeCoffee.push_back(pNico);
    	whoMadeCoffee.push_back(pJutta);
    	whoMadeCoffee.push_back(pNico);
    	// print all elements
    	for (auto ptr : whoMadeCoffee) {
    	cout << *ptr << " ";
    	}
    	cout << endl;
    	// overwrite a name again
    	*pNico = "Nicolai";
    	// print all elements again
    	for (auto ptr : whoMadeCoffee) {
    	cout << *ptr << " ";
    	}
    	cout << endl;
    	// print some internal data
    	cout << "use_count: " << whoMadeCoffee[0].use_count() << endl;
    }
    
    • 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

    上面的代码,具体表现如下图所示
    输出如下

    Jutta Jutta Nico Jutta Nico
    Jutta Jutta Nicolai Jutta Nicolai
    use_count: 4
    
    • 1
    • 2
    • 3

    2.1.1 初始化 shared_ptr

    shared_ptr 类定义在 里,需要注意的是,shared_ptr 的使用一个指针作为单独参数的构造函数是显式的(explicit),因此不能使用赋值符号,来将普通指针赋值给 shared_ptr

    shared_ptr<string> pNico = new string("nico"); // ERROR
    shared_ptr<string> pNico{new string("nico")}; // OK
    shared_ptr<string> pNico = make_shared<string>("nico"); // Better
    
    • 1
    • 2
    • 3

    也可以使用函数 make_shared() 来创建 shared_ptr,且这样更快且更安全:更快是因为相比于前面的初始化的两次分配内存(一次给对象,一次给共享指针的共享数据),使用函数 make_shared() 只需要一次分配内存,完成两个步骤;更安全也是因为不会出现对象分配成功,控制块分配失败的情况。

    注意,尽量不要对一个已有普通指针,创建共享指针,如:

    string* pNico = new string("nico");
    shared_ptr<string> spNico(pNico);
    
    • 1
    • 2

    如果pNico被设为nullptr,spNico.use_count()=1 且字符串未被释放,正常输出 nico
    但如果spNico被设为nullptr,此时字符串被释放,但pNico还保存该地址!!

    2.1.2 reset

    可以先声明一个共享指针,然后给该共享指针分配一个新的指针。当然,不能使用赋值操作,要使用 reset() 方法:

    shared_ptr<string> pNico4;
    pNico4 = new string("nico"); // ERROR: no assignment for ordinary pointers
    pNico4.reset(new string("nico")); // OK
    
    • 1
    • 2
    • 3

    2.1.3 访问数据

    与普通指针类似,使用 *->

    (*pNico)[0] = ’N’;
    pJutta->replace(0,1,"J");
    
    • 1
    • 2

    2.1.4 use_count()

    use_count() 表示当前拥有该对象的所有共享指针的数量,当一个共享指针被删除后,use_count()-1,反之,use_count()+1

    上面例子中,pJutta 本身算一个,容器 vector 里还有三个,所有 use_count() = 4

    2.1.5 类型转换

    可以声明一个 void 类型的共享指针,其与 void* 功能一样,表示未定义类型的指针。
    shared_ptr 有专门的语法转换指针的类型,但我们不能使用普通的指针类型转换操作以初始化共享指针,结果是未定义的:

    shared_ptr<void> sp(new int); // shared pointer holds a void* internally
    ...
    shared_ptr<int>(static_cast<int*>(sp.get())) // ERROR: undefined behavior
    static_pointer_cast<int*>(sp) // OK
    
    • 1
    • 2
    • 3
    • 4

    3. Deleter

    当最后一个拥有者被删除后,共享指针为对象调用 delete 进行内存和资源的释放。这不一定在作用域结束处发生,比如上面的例子中,当给 pNico 赋值 nullptr 且在将 vector resize 为 2, 也会导致最后一个拥有者被删除,以至调用 delete

    3.1 定义一个 Deleter

    我们甚至可以自定义 Deleter,例如在删除引用对象前输出一条信息:

    shared_ptr<string> pNico(new string("nico"),
    [](string* p) {
    cout << "delete " << *p << endl;
    delete p;
    });
    ...
    pNico = nullptr; // pNico does not refer to the string any longer
    whoMadeCoffee.resize(2); // all copies of the string in pNico are destroyed
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里传入一个lambda表达式,作为 shared_ptr 构造函数的第二个参数,当然对于任何可调用的对象都是可以的,比如函数与重载了()运算符的类与std::function,比如下面的代码就是重载了 () 运算符的类:

    #include 
    #include  // for ofstream
    #include  // for shared_ptr
    #include  // for remove()
    class FileDeleter
    {
    private:
    std::string filename;
    public:
    FileDeleter (const std::string& fn)
    : filename(fn) {
    }
    void operator () (std::ofstream* fp) {
    fp->close(); // close.file
    std::remove(filename.c_str()); // delete file
    }
    };
    
    int main()
    {
    // create and open temporary file:
    std::shared_ptr<std::ofstream> fp(new std::ofstream("tmpfile.txt"),
    FileDeleter("tmpfile.txt"));
    ...
    }
    
    • 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

    3.2 处理数组

    shared_ptr 提供的默认deleter调用 delete 而不是 delete[]。这意味着默认 deleter只有在共享指针拥有的是一个单独由new创建的对象才有效。需要注意的是,给一个数组创建共享指针是可能的,但是是错误的:

    std::shared_ptr<int> p(new int[10]); // ERROR, but compiles
    
    • 1

    所以,如果要使用 new[] 来创建一个对象数组,需要定义自己的 deleter。可以传入一个函数、function object或lambda,在内部调用 delete[],例如:

    std::shared_ptr<int> p(new int[10],
    [](int* p) {
    delete[] p;
    });
    
    • 1
    • 2
    • 3
    • 4

    也可以使用提供给 unique_ptr 的官方helper,其内部调用 delele[]

    std::shared_ptr<int> p(new int[10],
    std::default_delete<int[]>());
    
    • 1
    • 2

    当然,unique_ptrshared_ptr 在数组的处理上有一定的区别,更详细地会在 unique_ptr 讲解

    std::unique_ptr<int[]> p(new int[10]); // OK
    std::shared_ptr<int[]> p(new int[10]); // ERROR: does not compile
    std::unique_ptr<int,void(*)(int*)> p(new int[10],
    [](int* p) {
    delete[] p;
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    shared_pr 不提供操作符 []。因此如果我们想要访问数组元素,可以先获取到原指针,然后再使用 [] 操作符访问数组元素,以下两种方法是等价的,第一种使用 get() 方法获取到 shared_ptr 封装的内部指针。

    • p.get()[i] = i * 42;
    • (&*p)[i] = i * 42;

    3.3 get_deleter()

    get_deleter() 得到一个指向删除器函数的指针,有可能是nullptr。为了获取deleter,必须传入它的类型,作为模板参数,比如:

    auto del = [] (int* p) {
    delete p;
    };
    std::shared_ptr<int> p(new int, del);
    decltype(del)* pd = std::get_deleter<decltype(del)>(p);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4. 共享指针误用的情况

    首先,不能同时有多组共享指针拥有一个对象,比如下面的代码就是错误的:

    int* p = new int;
    shared_ptr<int> sp1(p);
    shared_ptr<int> sp2(p); // ERROR: two shared pointers manage allocated int
    
    • 1
    • 2
    • 3

    因为,这两个共享指针在失去p的所有权之后,都会释放资源,那么就会被释放两次。所以,应该第二个共享指针应该使用第一个共享指针进行初始化:

    shared_ptr<int> sp1(new int);
    shared_ptr<int> sp2(sp1); // OK
    
    • 1
    • 2

    这个问题也可能会间接产生。比如下面的例子中,想要使用this指针初始化共享指针是不被允许的:

    class Person {
    	public:
    	...
    	void setParentsAndTheirKids (shared_ptr<Person> m = nullptr,
    	shared_ptr<Person> f = nullptr) {
    		mother = m;
    		father = f;
    		if (m != nullptr) {
    		m->kids.push_back(shared_ptr<Person>(this)); // ERROR
    		}
    		if (f != nullptr) {
    		f->kids.push_back(shared_ptr<Person>(this)); // ERROR
    		}
    	}
    	...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    上面的例子,和两个共享指针都使用同一个普通指针初始化是一样的问题,所以C++不允许直接使用this指针初始化 shared_ptr

    但是,C++提供了 std::enable_shared_from_this<> 类,我们可以让Person继承自该类,然后在类内部使用 shared_from_this() 以提供一个由this创建的共享指针,以初始化我们的共享指针:

    class Person : public std::enable_shared_from_this<Person> {
    public:
    ...
    void setParentsAndTheirKids (shared_ptr<Person> m = nullptr,
    shared_ptr<Person> f = nullptr) {
    	mother = m;
    	father = f;
    	if (m != nullptr) {
    	m->kids.push_back(shared_from_this()); // OK
    	}
    	if (f != nullptr) {
    	f->kids.push_back(shared_from_this()); // OK
    	}
    	}
    	...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注意,我们不能在构造函数内部调用 shared_from_this()(其实可以,不过会报运行时错误>_<)。

    class Person : public std::enable_shared_from_this<Person> {
    public:
    ...
    Person (const string& n,
    shared_ptr<Person> m = nullptr,
    shared_ptr<Person> f = nullptr)
    : name(n), mother(m), father(f) {
    	if (m != nullptr) {
    	m->kids.push_back(shared_from_this()); // ERROR
    	}
    	if (f != nullptr) {
    	f->kids.push_back(shared_from_this()); // ERROR
    	}
    	}
    	...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这是因为,将this指针存储为共享指针(Person父类enable_shared_from_this<>私有成员),是在Person构造函数的某尾进行的。


    5. 附录

    A. shared_ptr 的操作列表一

    B. shared_ptr 的操作列表二


    6. 参考文献

    《The C++ Standard Library》A Tutorial and Reference, Second Edition, Nicolai M. Josuttis.

  • 相关阅读:
    基于邻接矩阵的克鲁斯卡尔算法和普利姆算法
    第三天:配置+运行代码+改个保存键
    手写实现call() apply() bind()函数,附有详细注释,包含this指向、arguments讲解
    Docker换国内源和简单操作
    初识链表(7.25)
    PBKDF2
    ubuntu 安装 mariadb,如何创建用户,并远程连接
    后台管理---用户页搜索框的封装
    【算法】【递归与动态规划模块】跳跃游戏
    【LLM】Prompt tuning大模型微调实战
  • 原文地址:https://blog.csdn.net/weixin_44491423/article/details/133776589