编程八股文——C/C++中智能指针性质和使用

发布时间:2023年12月21日

编程八股文——C/C++中智能指针性质和使用

1 智能指针

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏

C++ 11中最常用的智能指针类型为shared_ptr,它采用引用计数的方法,记录当前内存资源被多少个智能指针引用。

  • 该引用计数的内存在堆上分配。当新增一个时引用计数加 1 ,当过期时引用计数减一。只有引用计数为 0 时,智能指针才会自动释放引用的内存资源。
  • 对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类,可以通过构造函数传入普通指针。( 从auto_ptr也带一点,虽然C++ 11已经遗弃)

std::auto_ptr<string> ptr(new string);

std::shared_ptr<string> p1;

std::shared_ptr<list<int>> p2;

auto_ptr<char*> ap(new char*);
*ap = "ap1";
*ap = "ap2";
char **bp = new char*;
*bp = "bp1";
cout << *ap << endl;	// "ap2"
cout << *bp << endl;	// "bp1"
#include <memory>
double *p = new double;
shared_ptr<double> pshared(p);		// 合法,显示转换,explicit conversion
//shared_ptr<double> pshared = p;  // 不合法,隐式转换,implicit conversion
int main(int argc, char* argv[]) {
	string str("hello world!");
	// 程序能运行,但是在要释放 pshared 指向的内存时会出错
	// 因为 str 不是存在堆中,当 pshared 过期时,delete 运算符会用于非堆内存,造成错误
	shared_ptr<string> pshared(&str);
	cout << *pshared << endl;
	getchar();
}
  • auto_ptr 是C++98提供的解决方案,C++11已经摒弃,并提供了以下几种方案

  • shared_ptr 被称为共享指针,用于管理多个智能指针共同拥有的动态分配对象,

  • unique_ptr 唯一拥有指定的对象,相比普通指针,拥有 RAII 的特性使得程序出现异常时,动态资源可以得到释放。

    RAII,Resource Acquisition Is Initialization,资源获取即初始化: 其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源

  • weak_ptr 是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。

2 智能指针的内存泄露以及解决方法

当两个对象相互使用一个 shared_ptr 成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏

为了解决循环引用导致的内存泄漏,引入了 weak_ptr 弱指针,weak_ptr 的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存,但是其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

3 为什么摒弃 auto_ptr

auto_ptr<string> p1(new string("hello"));
auto_ptr<string> p2;
p2 = p1;		
// 当 p1, p2 过期时,将删除同一个对象两次

解决之道:

  • 定义复制运算符,使之执行深复制
  • 建立所有权概念,使同时只有一个智能指针可拥有它。这样,只有拥有对象的智能指针有权析构该对象,这是auto_ptr 的策略,unique_ptr 的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。
int main(int argc, char* argv[]) {
	auto_ptr<string> p1(new string("hello"));
	auto_ptr<string> p2;
	cout << *p1 << endl;  // 正常打印
	p2 = p1;			  // p1 丧失了对 string 对象的所有权,p1 此时是空指针
	cout << *p1 << endl;  // 编译通过,但运行时报错,因为试图提领空指针
	getchar();
}
//	将 auto_ptr 换成 unique_ptr,编译器认为语句 p2 = p1; 非法,在编译阶段报错(因为 p1 不是临时右值)。 
//	将 auto_ptr 换成 shared_ptr,编译运行阶段都没问题,正常打印。
//	shared_ptr 采用的策略是引用计数,赋值时,计数加一,过期时,计数减一。仅当最后一个指针过期时,才调用 delete。
shared_ptr<string> p1(new string("hello"));
shared_ptr<string> p2(p1);		// 合法,将右值 p1 赋给 p2
unique_ptr<string> p1(new string("hello"));
//shared_ptr<string> p2(p1);		// 不合法,右值 p1 是 unique_ptr,若能赋给 p2,则 p1,p2 指向同一个对象,导致 p1 不合法,此语句编译不通过
//unique_ptr<string> p2(p1);		// 不合法,p1 不是临时右值,注意临时。
unique_ptr<string> foo() {
	unique_ptr<string> p1(new string("hello"));
	return p1;
}
// 函数返回的 unique_ptr<string> 为临时右值,此时可赋给另一个 unique_ptr 型指针
int main(int argc, char* argv[]) {
	unique_ptr<string> p2(foo());
}

4 C++ 11 智能指针更多用法

shared_ptrunique_ptr都支持的操作:

在这里插入图片描述

shared_ptr独有的操作:

在这里插入图片描述

make_shared函数(定义在头文件memory中)在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr

5 智能指针与普通指针的转换

void show(string s)
{
    cout << s << endl;
}

int main()
{
    std::shared_ptr<std::string> s = std::make_shared<std::string>("hello\n");
    show(*s.get()); // s.get() 获得内置指针 string *,需要解引用传到show()中
    // 特别注意,不要使用get()初始化另一个智能指针或为智能指针赋值。
    return 0;
}
shared_ptr<int> p(new int(42));    // reference count is 1
int *q = p.get();   // ok: but don't use q in any way that might delete its pointer
{   // new block
    // undefined: two independent shared_ptrs point to the same memory
    shared_ptr<int>(q);
} // block ends, q is destroyed, and the memory to which q points is freed
int foo = *p;   // undefined; the memory to which p points was freed

智能指针的get函数返回一个内置指针,指向智能指针管理的对象。主要用于向不能使用智能指针的代码传递内置指针。使用get返回指针的代码不能delete此指针。

不要使用get初始化另一个智能指针或为智能指针赋值。

6 shared_ptr

shared_ptr是否线程安全

在这里插入图片描述

指针和引用计数是线程安全的,但指针所指对象中的操作就需要自己做控制,并不是线程安全的。因为shared_ptr 有两个数据成员(指向被管理对象的指针,和指向控制块的指针),读写操作不能原子化。使得多线程读写同一个 std::shared_ptr 对象需要加锁

  1. 同一个shared_ptr被多个线程“读”是安全的。
  2. 同一个shared_ptr被多个线程“写”是不安全的。
  3. 共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的。

使用 std::shared_ptr管理buffer或者数组

class O
{
  public:
    O() : mValue(0){};
    ~O() { printf("call ~O(), mValue = %d\n", mValue); };

    int mValue;
};

int main()
{
    int bufferSize = 10;
    auto pData     = std::shared_ptr<uint8_t>(new uint8_t[bufferSize], [](uint8_t *ptr) {
        std::cout << "delete[] buffer pData" << std::endl;
        delete[] ptr;
    });

    auto sp = std::shared_ptr<O>(new O[5], [](O *ptr) { delete[] ptr; });
    O *p    = sp.get();
    for (int i = 0; i < 5; ++i)
    {
        (p + i)->mValue = i;
    }

}  
/*
call ~O(), mValue = 4
call ~O(), mValue = 3
call ~O(), mValue = 2
call ~O(), mValue = 1
call ~O(), mValue = 0
delete[] buffer pData
*/

7 unique_ptr

shared_ptr不同,同一时刻只能有一个unique_ptr指向给定的对象。当unique_ptr被销毁时,它指向的对象也会被销毁。

make_unique函数(C++14新增,定义在头文件memory中)在动态内存中分配一个对象并初始化它,返回指向此对象的unique_ptr

unique_ptr<int> p1(new int(42));
// C++14
unique_ptr<int> p2 = make_unique<int>(42);

由于unique_ptr独占其指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。

unique_ptr操作:

在这里插入图片描述

release函数返回unique_ptr当前保存的指针并将其置为空。

reset函数成员接受一个可选的指针参数,重新设置unique_ptr保存的指针。如果unique_ptr不为空,则它原来指向的对象会被释放。

// 将所有权从 p1 (which points to the string Stegosaurus) 转移给 p2
unique_ptr<string> p2(p1.release());    // release makes p1 null
unique_ptr<string> p3(new string("Trex"));
// transfers ownership from p3 to p2
p2.reset(p3.release()); // reset deletes the memory to which p2 had pointed

调用release会切断unique_ptr和它原来管理的对象之间的联系。release返回的指针通常被用来初始化另一个智能指针或给智能指针赋值。如果没有用另一个智能指针保存release返回的指针,程序就要负责资源的释放。

p2.release();   // WRONG: p2 won't free the memory and we've lost the pointer
auto p = p2.release();   // ok, but we must remember to delete(p)

不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个即将被销毁的unique_ptr(移动构造、移动赋值)。

unique_ptr<int> clone(int p)
{
    unique_ptr<int> ret(new int (p));
    // . . .
    return ret;
}

老版本的标准库包含了一个名为auto_ptr的类,

类似shared_ptr,默认情况下unique_ptrdelete释放其指向的对象。unique_ptr的删除器同样可以重载,但unique_ptr管理删除器的方式与shared_ptr不同。定义unique_ptr时必须在尖括号中提供删除器类型。创建或reset这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)。

// p points to an object of type objT and uses an object of type delT to free that object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);

void f(destination &d /* other needed parameters */)
{
    connection c = connect(&d);  // open the connection
    // when p is destroyed, the connection will be closed
    unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);
    // use the connection
    // when f exits, even if by an exception, the connection will be properly closed
} // 当 p 被摧毁时,自动调用 end_connection 函数

8 weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象。将weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。如果shared_ptr被销毁,即使有weak_ptr指向对象,对象仍然有可能被释放。

在这里插入图片描述

创建一个weak_ptr时,需要使用shared_ptr来初始化它。weak_ptr只能配合std::shared_ptr使用,不能单独使用。

auto p = make_shared<int>(42);
weak_ptr<int> wp(p);    // wp weakly shares with p; use count in p is unchanged

使用weak_ptr访问对象时,必须先调用lock函数。该函数检查weak_ptr指向的对象是否仍然存在。如果存在,则返回指向共享对象的shared_ptr,否则返回空指针。

if (shared_ptr<int> np = wp.lock())
{
    // true if np is not null
    // inside the if, np shares its object with p
}

使用weak_ptr防止循环引用

#include <memory>
#include <iostream>

class Foo : public std::enable_shared_from_this<Foo>
{
public:
    Foo() { std::cout << __PRETTY_FUNCTION__ << std::endl; }
    ~Foo() { std::cout << __PRETTY_FUNCTION__ << std::endl; }

    void self()
    {
        mPtr = shared_from_this();
    }

private:
    // std::shared_ptr<Foo> mPtr; // 【1】由于循环引用,不会调用析构函数,改 mPtr 为 std::weak_ptr 类型即可
    std::weak_ptr<Foo> mPtr; //【2】
};

int main()
{
    {
        std::shared_ptr<Foo> c = std::make_shared<Foo>();
        c->self();
    }
    return 0;
}
/**
注释【1】打开【2】,打印
Foo::Foo()
Foo::~Foo()
注释【2】打开【1】,打印
Foo::Foo()
由于循环引用,不会调用析构函数
*/

std::enable_shared_from_this<T>::shared_from_this 是个侵入式设计。为的解决传入this导致对象被析构两次的问题。

什么情况下需要使用 shared_from_this()? 用于返回当前对象 thisstd::shared_ptr类型指针时:

#include <memory>
#include <iostream>

class Foo : public std::enable_shared_from_this<Foo>
{
public:
    Foo() { std::cout << "Foo()\n"; }
    ~Foo() { std::cout << "~Foo()\n"; }

    std::shared_ptr<Foo> getSelf()
    {
        return shared_from_this();
    }
};

int main()
{
    Foo *foo = new Foo;
    std::shared_ptr<Foo> sp1(foo);
    std::shared_ptr<Foo> sp2 = sp1->getSelf(); // 【1】为了对 foo对象进行共享
    //std::shared_ptr<Foo> sp2(foo); // 【2】

    // std::boolalpha 的作用是使 bool 型变量按照 false、true 的格式输出。如不使用该标识符,那么结果会按照 1、0 的格式输出
    std::cout << std::boolalpha << (sp2.get() == foo) << std::endl;
    std::cout << sp1.use_count() << "    " << sp2.use_count() << std::endl;
}
/* 打印
Foo()
true
2    2
~Foo()
*/

如果注释【1】打开【2】,则会析构两次,产生未定义的行为,打印如下

Foo()
true
1    1
~Foo()
~Foo()
free(): double free detected in tcache 2
已放弃 (核心已转储)

尽管sp1sp2都指向了foo,但是却不共享计数,当析构的时候就会被析构两次,产生未定义行为。
std::weak_ptr可以接受std::shared_ptr参数来构造自己,std::shared_ptr也具有接受std::weak_ptr参数来构造自己。

enable_shared_from_this 函数原型

    template<typename _Tp>
    class enable_shared_from_this {
    protected:
        ...
    public:
        shared_ptr<_Tp>
        shared_from_this() { 
            return shared_ptr<_Tp>(this->_M_weak_this); 
        }

        shared_ptr<const _Tp>
        shared_from_this() const { 
            return shared_ptr<const _Tp>(this->_M_weak_this); 
        }
    private:
        ...
        mutable weak_ptr<_Tp>  _M_weak_this;
    }

enable_shared_from_this的子类需要返回自身的std::shared_ptr指针,那么就需要继承这个类。

成员变量为什么是weak_ptr类型
因为如果是std::shared_ptr类型,那么就永远无法析构对象自身。

这个_M_weak_this不是这个类中初始化,而是在shared_ptr中初始化,初始化的值就是this。因此如果智能指针类型是std::shared_ptr,那么这个类对象一旦创建,引用计数就是1,那么永远也无法析构。

为什么不直接传回this
std::shared_ptr的引用计数增加是需要用operator=实现的。

文章来源:https://blog.csdn.net/qq_44814825/article/details/135123446
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。