【c++面试之智能指针】【万字长文】【超级详细】最详细解析四种智能指针,面试题汇总及解答,妈妈再也不担心面试官考我智能指针了。

前言

  本篇文章收集了近一年来所有关于智能指针的面试相关内容。以智能指针的面试题线索,穿插讲解完最常用的四种智能指针的各个方面。本文讲解4个智能指针的基本概念和特性,以及其他设计到的知识点。讲解的过程中,如果此部分内容涉及到面试题目,会在旁边以这个题目的序号来标注出,方便大家对着题目序号来寻找问题的答案。文章整理了近两年来10个关于智能指针的问题,并且将这些面试问题编号序号,**所有的面试题都在文章里面有答案,在文章中以上标的形式标出了对应的地方,方便大家查阅和重新阅读。**在文章的最后,会根据文章讲解的内容和答案,对着10个问题进行解答。
  对于不需要面试的同学,看完此篇文章,相信大家会对智能指针有更深入的了解和感受。并且结合着面试的题目来看,在阅读的过程中带着自己的思考,相信你会有更大的收获。
  对于需要面试C++相关岗位的文章,看完这篇文章,如果面试官再问你智能指针,我敢肯定,你一定可以侃侃而谈了~智能指针的面试题,只看这一篇就够了!相信看完这篇文章,妈妈再也不用担心面试官考我智能指针的问题啦

引入智能指针的原因[3][4][7]

   因为裸指针是C++的一把双刃剑,他可以让程序员精确地控制堆上每一块内存,也让程序发生内存泄漏,因此,从C++98开始便推出了智能指针(auto_ptr),对裸指针进行封装,初衷是让程序员无需手动释放内存,来避免内存泄漏。
   相比于裸指针,智能指针是对裸指针的一种封装。智能指针也带来了双面性:虽然它可以阻止内存泄漏,但是智能指针种类多,用法复杂,各个指针有各个指针的特点和适用场景。我们需要搞清楚它们的特性,才能够正确地使用它。
  有的同学就要问了,那智能指针这么复杂,我还不如自己注意一下保证不会发生内存泄漏,干嘛要用这么复杂的指针呢?原因就是:**正确使用智能指针带来的花销比使用裸指针,来确保程序不会产生内存开销的代价要小!**使用智能指针是一种良好的编程习惯,这是我们的前辈所推荐的。
  举一个很简单的例子吧,这个例子很简单,但是说明了:有时候使用裸指针,是很难保证程序不会产生内存泄露的!

class A;        
A *p = new A;
......
/* do something */
......
delete p;

  要保证程序不会内存泄漏, 必须保证自new之后一直到delete之间, 代码是异常安全的, 即使异常出现p所持资源也会被释放。然而随着程序的开发,这个过程中可能经过无数人共同开发完成,在这个简单的例子中都很难保证程序未来永远异常安全!更何况更复杂的场景了!因此使用智能指针虽然更复杂,但他可以规避更为晦涩难以发现的内存泄漏的情况!
  简而言之,智能指针的引入就是为了规避内存泄漏!

智能指针的分类

  C++ 标准模板库 STL提供了四种智能指针,也是面试最常问的四种:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr,其中 auto_ptr在C++11 已将其摒弃(deprecated),并提出了 unique_ptr 作为 auto_ptr 替代方案。一般情况下都不建议使用auto_ptr,建议使用较新的 unique_ptr,为了大家更深入的理解,还是会对auto_ptr进行讲解。另外,Boost 库还提出了 boost::scoped_ptr、boost::scoped_array、boost::intrusive_ptr 等智能指针,但并没有归入STL中。

auto_ptr

  auto_ptr是一种对资源独占的指针(和unique_ptr一样),但是它已经被unique_ptr取代,我们不应该使用它。

auto_ptr被取代的原因

  auto_ptr已经被废弃,取而代之的是unique_ptr。原因有以下几点:

  1. 安全性低
      当使用一个auto_ptr赋值给另一个auto_ptr时,旧的指针失去所有权,会变成空指针。此时再操作这个空指针,程序就会crash。
auto_ptr<int> ps1(new int(3));
auto_ptr ps2(ps1);//ps1失去所有权,变成空指针。
cout<<*ps1<<endl;//操作空指针,程序crash

  通过上面的例子,使用auto_ptr程序在运行时crash。使用unique_ptr,程序在编译时报错。使用shared_ptr,因为它采用引用计数的策略,程序不会crash。从上面可以看出,auto_ptr是不安全的!
2. unique_ptr更灵活,功能更强大。
  这里主要有两点:
(1) 一个是auto_ptr不可以放在STL容器中,而unique_ptr可以。

vector<unique_ptr<String>> test{
    
     new string{
    
    "12"}, new string{
    
    "23"}};

(2) auto_ptr不可以用于数组的变体,只可以和new一起使用。unique_ptr有new[]和delete[]的版本,功能更强大。
  总结:处于功能的扩展性和安全性的问题,auto_ptr被unique_ptr取代,auto_ptr已经被废弃,我们在任何场景下都不应该使用。

unique_ptr

实现原理[1][5][7]

  unique_ptr的实现原理核心就是:它对于资源是独占的,对于同一块内存只能有一个持有者,也就是不能放在等号的右边。Unique_ptr会在栈上分配,当离开作用域后,删除里面持有的资源对象。

具体操作(具体使用更需关注)

转移所有权

  unique_ptr删除了拷贝构造和赋值构造(上面讲了),如果在执行上述操作的时候会直接报错,那么使用unique_ptr时如何转移所有权呢?可以通过move语句来转移。

#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource
{
    
    
public:
	Resource() {
    
     std::cout << "Resource constructed\n"; }
	~Resource() {
    
     std::cout << "Resource deconstructed\n"; }
};

int main()
{
    
    
	std::unique_ptr<Resource> unique_ptr1{
    
     new Resource{
    
    } }; 
	std::unique_ptr<Resource> unique_ptr2{
    
    }; // unique_ptr2此时为空
	
	std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
	std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");
	// res2 = res1; // 编译会报错
	res2 = std::move(res1); // 资源的所有权从ptr1转移到ptr2
	std::cout << "res1 is " << (res1 ? "not null\n" : "null\n");
	std::cout << "res2 is " << (res2 ? "not null\n" : "null\n");
	return 0;
} // Resource destroyed here when res2 goes out of scope

输出:
Resource constructed
res1 is not null
res2 is null
Ownership transferred
res1 is null
res2 is not null
Resource deconstructed

拿回所有权

  通过Release函数可以拿回所有权。

int main() {
    
     
    std::unique_ptr<int> uptr(new int(10));  // 
    std::unique_ptr<int> uptr2 = std::move(uptr); //uptr此时为空了
	if(uptr == nullptr)
        cout<<"uptr give up *int"<<endl; 
    int * p = uptr2.release(); //uptr2释放对指针的控制权,返回指针,并将uptr2置为空

    if(uptr2 == nullptr)
        cout<<"uptr2 give up *int"<<endl; 
    cout<< *p <<endl; 
    delete p; 
    return 0;
}

输出:
uptr give up *int
uptr2 give up *int

unique_ptr作为参数

  unique_ptr作为参数时,如果使用move函数,那么资源的所有权会被转移到函数中。如果不想丢弃函数的所有权,建议使用get函数把指针指向的资源丢进去。

#include <iostream>
#include <memory> // for std::unique_ptr
#include <utility> // for std::move

class Resource
{
    
    
public:
	Resource() {
    
     std::cout << "Resource acquired\n"; }
	~Resource() {
    
     std::cout << "Resource destroyed\n"; }
	friend std::ostream& operator<<(std::ostream& out, const Resource &res)
	{
    
    
		out << "I am a resource";
		return out;
	}
};

void takeOwnership(std::unique_ptr<Resource> res) //转移所有权的方式
{
    
    
     if (res)
          std::cout << *res << '\n';
} // the Resource is destroyed here

void useResource(Resource* res)  //不转移所有权的方式
{
    
    
	if (res)
		std::cout << *res << '\n';
	else
		std::cout << "No resource\n";
}

int main()
{
    
    
    auto ptr1{
    
     std::make_unique<Resource>() };
    auto ptr2{
    
     std::make_unique<Resource>() };
//    takeOwnership(ptr); // This doesn't work, need to use move semantics
    std::cout << "takeOwnership called\n";
    takeOwnership(std::move(ptr)); // ok: use move semantics
    std::cout << "useResource called\n"";
    useResource(ptr2.get());
    std::cout << "Ending program\n";
    return 0;
}

输出:
Resource acquired (ptr1指向的资源初始化)
Resource acquired (ptr2指向的资源初始化)
takeOwnership called
I am a resource (进入到了takeOwnership中,打印资源)
Resource destroyed (由于资源的所有权转移到了函数中,所以函数调用结束后,没人持有这个Ptr1之前指向的资源,析构函数调用了)
useResource called
Ending program
Resource destroyed (注意这个顺序,这个是ptr2指向的资源被析构了,它是在程序结束后才析构的,而不是函数结束后,这就是因为它并没有转移所有权给到函数内部)

不要多个unique_ptr持有同一对象

  当unique_ptr持有同一个对象时,程序会试图多次释放同一个资源,导致程序crash。

int main()
{
    
    
    //此程序会因为多次释放的原因导致crash
	Resource* res{
    
     new Resource() };
	std::unique_ptr<Resource> ptr1{
    
     res };
	std::unique_ptr<Resource> res2{
    
     res };
}

不要手动delete unique_ptr持有的

   操作系统会帮你释放对象的,不要自己手动再去释放,也会导致多次释放,程序crash.。

Resource* res{
    
     new Resource() };
std::unique_ptr<Resource> ptr1{
    
     res };
delete res; //不要自己Delete,多此一举

注意异常安全问题

  unique_ptr不能保证异常安全。

fun(std::unique_ptr<T>(new T), function_that_can_throw_exception());

   c++编译器没有规定编译器对函数参数的求值次序,如果按照这样的顺序进行:

  1. 调用new T分配内存
  2. 调用function_that_can_throw_exception()
  3. 调用unique_ptr的构造函数
       此时,执行到第二步时,程序抛出异常,但是对于unique_ptr来说,只分配了内存,构造函数还没调用,那它当然析构不了了,内存泄漏由此发生。
      解决以上问题,要使用Make_unique函数。因为这个函数保证了对象T的创建和unique_ptr的创建都在这个函数中进行,不会出现上述的顺序而导致异常。

std::shared_ptr

实现原理[1][5][7]

  相较于unique_ptr的独占式持有资源的特点而言,shared_ptr则是一种共享式的智能指针:多个shared_ptr可以共享同一个对象的所有权。在shared_ptr的内部有一个辅助类,辅助类采用引用计数的方式来持有资源(这也就是为何shared_ptr所占用的内存更多的原因)。每次指针指向某一个资源时,内部的引用计数会加1,当每次指针不再指向对象时,内部的引用计数会减1。当内部的引用计数减到0的时候,就会释放指向对象的堆内存空间。
  它的具体的实现原理和规则可以简单的描述为:
  (a)当进行构造函数操作时,创建一个智能指针的新的对象的时候,指针被初始化,内部的引用计数的值设置为1。
  (b)当进行拷贝构造操作时,指针会指向被赋值的指针,并且会对它们指向的资源的引用计数加1。
  (c)当进行赋值构造操作时,会先使赋值前资源的引用计数减1,如果减1后计数为0了,那么就要去释放这个资源。然后进行指针的赋值操作,然后对赋值后的新的资源的引用计数加1。
  (d)当进行析构操作时,会先使引用计数的值减1,如果计数等于0了,那么就去释放资源。
  了解了前面这4个操作原理后,加上对=,->,&三个操作符的重载,就可以手撕出shared_ptr了。这也是面试常考察的内容。

手撕shared_ptr[2]

  注意上一小节讲的几个原理,手撕代码其实也就是对上述原理的一个代码实现。

辅助类的实现

  根据上节所讲,一个shared_ptr需要一个辅助类,来辅助完成引用计数的功能。因为这个类本身就是一个辅助MySharedPtr来实现功能得类,所以在它的实现里要将MySharedPtr设置成它的友元类,这样MySharedPtr类才能更方便地使用这个类中的资源。

class RefPtr
{
    
    
private:
    friend class MySharedPtr;
    RefPtr(int *ptr) :p(ptr), count(1) {
    
     }
    ~RefPtr() {
    
     delete p; }
    //引用计数
    int count;
    int *p;
};

MySharedPtr的实现

  这块就是真正的对Shared_ptr类的重写,我们将我们重写的类的名字起做MySharedPtr。

class MySharedPtr
{
    
    
public:
    //构造函数,需要对一个辅助类RefPtr初始化
    MySharedPtr(int *ptr) :rp(new RefPtr(ptr)) {
    
    }
    //拷贝构造函数,参考上面的原理第(b)条,将指针指向用来赋值的指针,并将资源的引用计数加1.
    MySharedPtr(const MySharedPtr &sp) :rp(sp.rp) {
    
     ++rp->count; }

    //重载赋值构造函数,参考上述原理第(c)条。首先将之前指向的资源的引用计数减1,如果计数为0了,那么就要释放资源。然后将指针指向新的指针,并对这个资源的引用计数加1。
    MySharedPtr& operator=(const MySharedPtr& rhs)
    {
    
    
        if (--rp->count == 0)
            delete rp;
        ++rhs.rp->count;
        rp = rhs.rp;
        return *this;
    }
    //重载->操作符:原理很简单,因为内部真正实现资源的指针实际上是rp中的裸指针,我们只用找到这个裸指针即可。
    int* operator->()
    {
    
    
        return rp->p;
    }
    //重载*操作符:同理,我们只要对这这个实际的裸指针执行取值操作即可。
    int& operator*()
    {
    
    
        return *(rp->p);
    }
    //析构函数:原理即上一小节说的第(d)点。
    ~MySharedPtr()
    {
    
    
        if (--rp->count == 0) {
    
    
            cout << "资源被释放啦" << endl;
            delete rp;
        }
        else
            cout << "还有" << rp->count << "个指针指向基础对象" << endl;
    }

private:
    RefPtr *rp;
};

测试MySharedPtr

   测试程序重点关注的就是它的引用计数,首先我们对sptr1进行了构造,它的计数为1。然后通过sptr2对sptr1进行拷贝构造,资源的计数为2,接着使用sptr2对sptr1进行了赋值构造,资源的计数为3。最后,随着花括号作用域的结束,三个指针生命周期结束,引用计数逐渐减为0,当为0时,自动对resource资源进行了释放。然后我们再对resouse输出,它的值不在是10,因为它已经被释放了。通过上面的例子,我们体会了shared_ptr是如何对资源进行共同持有,计数,并且自动释放的。

int main()
{
    
    
    //定义三个智能指针类对象,对象都指向基础类对象resource
    int* resource = new int(10);
    //使用花括号控制三个智能指针的生命周期,观察计数的变化
    {
    
    
        MySharedPtr sptr1(resource);//此时计数count=1
        cout << "sptr1:" << *sptr1<< "," << endl;
        {
    
    
            MySharedPtr sptr2(sptr1); //调用拷贝构造函数,此时计数为count=2
            cout << "sptr2:" << *sptr2 << "," << endl;
            {
    
    
                MySharedPtr sptr3(nullptr);
                sptr3 = sptr1; //调用赋值操作符,此时计数为conut=3
                cout << "sptr3:" << *sptr3 << "," << endl;
            }
            //此时count=2
        }
        //此时count=1;
    }
    //此时count=0;resource对象被delete掉
    cout << *resource << endl;
    return 0;
}

输出:
在这里插入图片描述

改进MySharedPtr(使用模板)[2]

   注意上面的例子肯定是不能作为面试中的答案的!因为上面我们的MySharedPtr只能对int类型的资源进行持有,这只是为了大家更容易读懂代码。真正的STL中的实现肯定是以模板的方式来实现通用性的。因此,真正的答案还需要将上述涉及到资源类型int的地方改成模板T的形式,当然这一部分与智能指针无关,只涉及到泛型编程,我就不展开讲解了!贴出最后的答案!

template <typename T> class RefPtr
{
    
    
private:
    friend class MySharedPtr<T>;
    RefPtr(T *ptr) :p(ptr), count(1)
    {
    
    }
    ~RefPtr()
    {
    
    
        delete p;
    }
    int count;
    T *p;
};

template <typename T> class MySharedPtr
{
    
    
public:
    MySharedPtr(T *ptr) :rp(new RefPtr<T>(ptr))
    {
    
    }
    MySharedPtr(const SmartPtr<T> &sp) :rp(sp.rp)
    {
    
    
        ++rp->count;
    }
   
    SmartPtr& operator=(const SmartPtr<T>& rhs)
    {
    
    
        ++rhs.rp->count;        //然后将引用计数减1,可以应对自赋值
            delete rp;
        rp = rhs.rp;
        return *this;
    }

    T & operator *()
    {
    
    
        return *(rp->p);
    }

    T* operator ->()
    {
    
    
        return rp->p;
    }
   
    ~MySharedPtr()
    {
    
    
        if (--rp->count == 0)   
        {
    
    
            delete rp;
        }
        else
        {
    
    
            cout << "还有" << rp->count << "个指针指向基础对象" << endl;
        }
    }
private:
    RefPtr<T> *rp; 
};

循环引用[6]

   正是因为shared_ptr的这种共享资源的特性,所以可能会产生循环引用:A中引用B,B中引用A。这样即使在程序结束后,两个资源的引用计数仍然大于等于1,导致无法释放资源,还是会产生内存泄漏。具体的例子如下:

class A {
    
    
public:
    shared_ptr<B> b;
};
class B {
    
    
public:
    shared_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    
    
    shared_ptr<A> spa = make_shared<A>(); //资源A引用计数为1
    shared_ptr<B> spb = make_shared<B>();//资源B引用计数为1
    spa->b = spb;//资源B引用计数为2
    spb->a = spa;//资源A引用计数为2
    return 0; 
} //main函数退出后,资源B和资源A强引用计数依然为1,无法释放

   解决内存泄漏的方法是使用weak_ptr,我们在下一章会讲解。

shared_ptr线程安全问题[9]

   boost官方文档中有对此问题给出结论。我对这些结论做一下解析:
  (1) 同一个shared_ptr被多个线程“读”是安全的.
   这个应该很容易理解吧,因为是进行读操作,没有修改,肯定是线程安全的了。
  (2)同一个shared_ptr被多个线程“写”是不安全的
   假如说有两个线程同时访问一个shared_ptr对象,一个进行释放(reset),另一个读取裸指针的值,那么最后的结果就不确定了,很可能会因为访问野指针的问题导致crash。
  (3)共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的;
   c++ 11官方文档有说,share_ptr的计数操作具有原子性。也就是说多个线程通过多个shaped_ptr(虽然这多个shared_ptr指向的是同一个对象)进行操作,是线程安全的。

weak_ptr

weak_ptr的基本原理[1]

   weak_ptr设计的目的就是为了和shared_ptr共同工作。它也不具备普通指针的行为,无法使用operator*和operator->。因此它的定位更倾向于一个辅助使用的工具而非一个真正的智能指针。weak_ptr不会托管对象,它指向一个由shared_ptr管理的对象,但是它不影响它指向对象的生命周期,因此也不会增加这个对象的引用计数。通常weak_ptr有两个作用:解决引用计数避免悬垂指针

解决循环引用

  解决引用计数的方法,就接着上面shared_ptr产生循环引用的代码继续讲解。其实可能我们的类B内部只是想要引用一下A中的对象,B中根本无需管理它的生命周期,它不在B中创建,也不在B中销毁。这个时候我们只用将B中的对A的引用使用weak_ptr来引用,这样会更加合适,也避免了循环引用。

class A {
    
    
public:
    shared_ptr<B> b;
};
class B {
    
    
public:
    weak_ptr<A> a;
};
int main(int argc, const char * argv[]) {
    
    
    shared_ptr<A> spa = make_shared<A>();//资源A引用计数为1
    shared_ptr<B> spb = make_shared<B>();//资源B引用计数为1
    spa->b = spb; //B引用计数为2,
    spb->a = spa; //A引用计数为1,
    return 0; 
} //main函数退出后,A先释放,B再释放,循环解开了

如何选择智能指针

  在实际过程中,我们应该如何使用智能指针呢?首先,auto_ptr只是一个过渡的产品,已经淘汰了,我们不应该使用。其次,weak_ptr更多的是一个辅助类,无法直接当作智能指针来用,它是用来配合shared_ptr来用的。因此我们的问题就是确定如何取选择使用unique_ptr还是shared_ptr呢?
(1) 如果我们要使用多个指向同一个对象的指针,那么就应该选择shared_ptr。这种情况具体来讲,包含以下场景:
  (a)将指针作为参数或者返回值传递的话,应该使用shared_ptr。
  (b)两个对象都包含指向第三个对象的指针,应该用shared_ptr来管理第三个对象。
  (c) STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可以用于shared_ptr。如果用于unique_ptr,编译器会产生警告。
(2)如果不需要多个指向同一对象的指针,那么就应该使用unique_ptr。这种情况具体来讲,包含以下场景:
  (a)使用new来分配返还指向该内存的指针。这样,所有权转让给接收返回值的unique_ptr,而该智能指针将负责调用delete
  (b)在STL容器中使用到unique_ptr,注意不能使用一些具有赋值或复制的操作(eg. sort())。
(3)需要解决shared_ptr的循环引用问题时,要根据实际情况使用weak_ptr。
(4) 在局部作用域(函数内部或类内部),并且不需要指针作为参数来传递的情况下,使用 scope_ptr 的开销较 shared_ptr 会小一些。

常见面试题汇总和解答

  所有的面试题都在文章里面有答案,在文章中以上标的形式标出了对应的地方,方便大家查阅和重新阅读。
[1] 智能指针的实现原理
  (每一章的开头对每一种智能指针的大概原理进行了讲解。这个题目重点需要讲出每一个智能指针是如何设计的来实现可以自动进行内存管理的。)

  答:
   对于Unique_ptr和auto_ptr来说,它对于资源是独占的,对于同一块内存只能有一个持有者,也就是不能放在等号的右边。Unique_ptr会在栈上分配,当离开作用域后,删除里面持有的资源对象,从而达到自动管理内存的的功能。
   对于shared_ptr来说,它是一种共享式的智能指针:多个shared_ptr可以共享同一个对象的所有权。在shared_ptr的内部有一个辅助类,辅助类采用引用计数的方式来持有资源(这也就是为何shared_ptr所占用的内存更多的原因)。每次指针指向某一个资源时,内部的引用计数会加1,当每次指针不再指向对象时,内部的引用计数会减1。当内部的引用计数减到0的时候,就会释放指向对象的堆内存空间。
   对于weak_ptr来说,weak_ptr设计的目的就是为了和shared_ptr共同工作。它也不具备普通指针的行为,无法使用operator*和operator->。它的定位更倾向于一个辅助使用的工具而非一个真正的智能指针。weak_ptr不会托管对象,它指向一个由shared_ptr管理的对象,但是它不影响它指向对象的生命周期,因此也不会增加这个对象的引用计数。

[2]手写shared_ptr
  (介绍shared_ptr的内容中专门有手写shared_ptr的内容,掌握清楚原理后写出文中所给代码并讲清楚即可)
  答:

template <typename T> class RefPtr
{
    
    
private:
    friend class MySharedPtr<T>;
    RefPtr(T *ptr) :p(ptr), count(1)
    {
    
    }
    ~RefPtr()
    {
    
    
        delete p;
    }
    int count;
    T *p;
};

template <typename T> class MySharedPtr
{
    
    
public:
    MySharedPtr(T *ptr) :rp(new RefPtr<T>(ptr))
    {
    
    }
    MySharedPtr(const SmartPtr<T> &sp) :rp(sp.rp)
    {
    
    
        ++rp->count;
    }
   
    SmartPtr& operator=(const SmartPtr<T>& rhs)
    {
    
    
        ++rhs.rp->count;        //然后将引用计数减1,可以应对自赋值
            delete rp;
        rp = rhs.rp;
        return *this;
    }

    T & operator *()
    {
    
    
        return *(rp->p);
    }

    T* operator ->()
    {
    
    
        return rp->p;
    }
   
    ~MySharedPtr()
    {
    
    
        if (--rp->count == 0)   
        {
    
    
            delete rp;
        }
        else
        {
    
    
            cout << "还有" << rp->count << "个指针指向基础对象" << endl;
        }
    }
private:
    RefPtr<T> *rp; 
};

[3] 智能指针的特性和用途
  (偏向于对于整体的理解,自己围绕这个概念讲解就好)。

答:
   相比于裸指针,智能指针是对裸指针的一种封装。从C++98开始便推出了智能指针(auto_ptr),对裸指针进行封装,目的是让程序员无需手动释放内存,来避免内存泄漏。STL中引入的智能指针有四种:auto_ptr, unique_ptr, shared_ptr, weak_ptr。其中auto_ptr已经被弃用,取而代之的是unique_ptr。unique_ptr更适用于指针对资源独占的场景,shared_ptr适用于多个指针要指向同一资源的场景,而weak_ptr是一个辅助类型的指针,无法单独使用,一般是配合shared_ptr来解决循环引用的。(答到这里就可以了,面试官想想深入的话再根据他新的问题继续深入)

[4] 智能指针和指针的区别
  (偏向于对于整体的理解,自己围绕这个概念讲解就好。)

答:
   智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,这样的一层封装机制的目的是为了使得智能指针可以方便且自动地管理一个对象的生命期。可以有效防止内存泄漏。

[5] 智能指针怎么知道自己的生命周期结束的
   这个问题和第一个问题一样嘛!第一个问题问的原理是什么,其实翻译一下啊,不就是问的是怎么知道自己的生命周期结束的吗?

答:
   不同的智能指针原理不一样,以STL中引入的智能指针shared_ptr和unique_ptr为例。(继续使用问题1的回答即可)对于Unique_ptr,它对于资源是独占的,对于同一块内存只能有一个持有者,也就是不能放在等号的右边。Unique_ptr会在栈上分配,当离开作用域后,删除里面持有的资源对象,从而达到自动管理内存的的功能。
   对于shared_ptr来说,它是一种共享式的智能指针:多个shared_ptr可以共享同一个对象的所有权。在shared_ptr的内部有一个辅助类,辅助类采用引用计数的方式来持有资源(这也就是为何shared_ptr所占用的内存更多的原因)。每次指针指向某一个资源时,内部的引用计数会加1,当每次指针不再指向对象时,内部的引用计数会减1。当内部的引用计数减到0的时候,就会释放指向对象的堆内存空间。

[6] 智能指针一定不会造成内存泄漏吗?使用的时候要注意什么?
   shared_ptr的循环引用的例子,就是使用了智能指针仍然内存泄漏的例子。
答:
  并不是这样,使用智能指针也会造成内存泄漏。举一个例子就是使用了shared_ptr,然后导致了资源的循环引用:A中引用B,B中引用A。这样即使在程序结束后,两个资源的引用计数仍然大于等于1,导致无法释放资源,还是会产生内存泄漏。
  使用的时候我们需要注意每一个智能指针的特性,适用场景和用法。我们需要遵循它们的使用规则,正确使用它们,比如unique_ptr更适用于资源独占的场景,而在共享资源的时候,我们要用shared_ptr。比如使用unique_ptr结合STL的时候,要避免使用STL容器中具有复制和赋值操作的算法,只有我们正确的使用它们,才能够有效使用智能指针从而防止内存泄漏。

[7] 智能指针你是如何理解的?底层设计呢?你觉得智能指针设计的核心思想是什么?
  这个问题很多啊,分开解答:首先问你如何理解的,我们可以讲一下就可以回答智能指针是为何引入的,解决什么问题。
  第二个问题,底层的设计,就是我们我们实现的原理,回答第一个问题即可。
  第三个问题,核心思想,偏重于整体的理解。我认为智能指针的核心思想就是利用RAII(资源获取即初始化)的技术对普通的指针进行封装,从而使得智能指针的行为看上去像一个普通指针,从而方便取代普通指针,并且规避了使用普通指针很容易造成潜在的内存泄漏的问题。

[8] 如果我返回智能指针,有什么优缺点?
(这题感觉还挺难的,需要对智能指针和c++有较深的理解,结合自己的使用经验才能回答,
答:
  好处的话,是可以让提供给用户一个智能指针,而让他无需关心在函数内部如何处理和管理这个指针。
  坏处的话是:如果通过引用返回智能指针,可能的存在的风险是返回了这个指针,可能没有对这个指针的引用计数增加,那么就有可能这个指针指向的对象已经销毁了,但是它的引用计数还不为1,通过指针使用访问这个资源就会造成野指针访问crash的问题。
  如果通过值返回智能指针,那么问题就是通过值传递的开销较大。不过现在编译器有return value optimization(RVO)机制,这个成本开销会较低,并且最少是安全的。如果要进行智能指针的返回,也建议用值传递的方式进行返回。

[9] shared_ptr是线程安全的吗?
答:(文章中有专门讲解,直接答就可以)
   boost官方文档中有对此问题给出结论。
  (1) 同一个shared_ptr被多个线程“读”是安全的.
  (2)同一个shared_ptr被多个线程“写”是不安全的
   假如说有两个线程同时访问一个shared_ptr对象,一个进行释放(reset),另一个读取裸指针的值,那么最后的结果就不确定了,很可能会因为访问野指针的问题导致crash。
  (3)共享引用计数的不同的shared_ptr被多个线程”写“ 是安全的;
   c++ 11官方文档有说,share_ptr的计数操作具有原子性。也就是说多个线程通过多个shaped_ptr(虽然这多个shared_ptr指向的是同一个对象)进行操作,是线程安全的。

猜你喜欢

转载自blog.csdn.net/weixin_41937380/article/details/129151613