“虎牙直播”实习生面试 c++中的智能指针

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/bian_qing_quan11/article/details/73333214

刚刚接到了“虎牙直播”实习生的电话面试,说实在我都忘了当初什么时候投的的了,是C++方向的。下面说一说电话面试的过程。

本来中午一个珠海的有人给我打电话,我没接到。后来中午我吃饭时看到信息后,回了条短信,约定下午四点电话面试。四点钟电话准时接通,开始面试:

下面我就直接说下面试中问到的东西吧。


(1)因为我的项目中写到一个项目:简单线程池的实现,那么面试官就问我“如何保证线程池是线程安全的”。

首先给大家普及下线程池的知识。我写的线程池是使用POSIX写的。线程池整个项目中包括两个类:一个是Task类,用于描述执行一个任务的方法(一个函数)和执行这个任务所需要的参数(函数的参数)。另外一个类就是我们的线程池类ThreadPool类,在线程池中主要有两个队列,一个是Task类队列,用于存放要处理的任务。一个是线程池中存放线程的数组。下面我就面试官的问题给出回答:

在线程池类中通过一个互斥锁(pthread_mutex_t类型变量)来实现线程池的线程安全性。每次往任务队列中添加任务、或者从任务队列中取任务前都要使用pthread_mutex_lock函数加一把锁,然后对任务队列进行操作。操作完后再使用pthread_mutex_unlock释放这个锁,从而实现对任务队列的互斥访问。也就是说每次想要对任务队列进行操作都需要:

          pthread_mutex_lock(&mutex);

          增加任务到任务队列或者从任务队列取任务;

          pthread_mutex_unlock(&mutex);

来互斥的访问任务队列。以避免多个线程同时操作任务队列造成死锁。如任务队列只剩下一个空位置,但是多个线程同时向任务队列中添加任务;任务队列中只剩下一个任务,但是多个线程同时到任务队列中取任务;使用互斥锁来实现线程安全的访问任务队列。


(2)第二个问题还是围绕线程问的,“当有一个任务进入任务队列时,其他阻塞线程是如何获取并执行这个任务的?”

前面我们说过,在ThreadPool的构造函数中根据我们指定的个数使用new来动态创建线程数组。然后使用pthread_create为每个线程注册线程启动函数,在线程启动函数中每一个前程启动函数都要对任务队列进行实时监控,使得一旦有任务到来我们的空闲线程就去任务队列获取并执行任务,每个线程函数都是互斥的访问任务队列。

由于刚开始时没有任务到来,我们可以在线程函数中使用条件变量(pthread_cond_t)使得的线程都处于阻塞状态phtread_cond_wait(&cond, &mutex)。一旦有任务到来就使用pthread_cond_signal(&cond)来激活一个因为该条件而阻塞的线程。当然也可以使用pthread_cond_broadcast(&cond)

线程函数中的代码架构如下

void * threadFunc(void *paramter)//线程中的线程启动函数,相当于执行任务的函数

{

    ...

    pthread_mutex_lock(&mutex);//加锁对任务队列互斥访问

    while(任务队列为空)

    {

            pthread_cond_wait(&cond, &mutex);

    }

    ....//获取任务

    pthread_mutex_unlock(&mutex);

    ....//执行任务

}

注意:1)这里我们首先使用了pthread_mutex_lock对任务队列加锁实现多个线程互斥访问任务队列。

          2)判断任务队列为空时,使用的是while循环而不是if,这里可以防止出现“虚假唤醒”的情况。

          3)当程序执行到pthread_cond_wait函数内部时,首先会释放掉互斥锁,线程函数阻塞到这里,不再往下运行。当有任务时会以“信号”的形式唤醒该线程函数,加锁并继续往下执行。所以pthread_cond_wait函数=pthread_mutex_unlock+pthread_mutex_lock这两个函数功能。

向任务队列中添加任务的代码架构如下:

void addTast()

{

     pthread_mutex_lock(&mutex);//因为要互斥的访问任务队列,加锁

     ....//将任务添加到任务队列

     pthread_cond_signal(&cond);或者是pthread_cond_broadcast(&cond);//发送信号给一个因为该条件而阻塞的线程

     pthread_mutex_unlock(&mutex);//解锁

}


(3)C++中有三种智能指针,他们功能分别是什么?

当听到这个问题时,我心里不禁一笑。因为我知道的有4种智能指针分别是shared_ptr、auto_ptr、weak_ptr、scoped_ptr。但是我只清楚前面两种智能指针,并且也看过里边的源码,但是对于后面两个智能指针不太熟,只是听说过。后来我查阅资料发现原来C++11中新加入了shared_ptr、weak_ptr、unique_ptr这三个智能指针,估计他是想让我回答这三个智能指针。

在STL中智能指针它们的本质都是类模板,都是使用了RAII机制。所有的智能指针的构造函数都是explicit的,意思就是必须使用原生指针作为构造函数的参数,不能进行隐式类型装换(shared_ptr<int>p = new int(3)×shared_ptr<int>p(new int(3))

首先我们解释下C++中的RAII机制。(Resource Acquisition Is Initialization资源分配即初始化)这个机制主要使用C++语言的一个特性:创建对象时系统会调用constructor,释放对象时系统会调用destructor。那么这样就可以在构造函数中申请资源,在析构函数中释放资源,智能指针就充分使用了这一特性。

1>shared_ptr(C++11)

从英文意思上看,我们大概知道了它的意思:共享式指针,是一个共享资源所有权的指针。在shared_ptr类内部维护着一个引用计数器,该引用计数器实际上就是指向该资源的指针的个数。

在普通情况使用裸指针的时候,如果有多个指针指向同一个对象时,只要我们使用delete释放了其中一个指针指向的资源,这个资源就不会存在了,其他指向该资源的指针指向的资源就不存在了,造成指针空悬的错误,相当于产生了野指针。

但是如果在shared_ptr中使用引用计数的话,情况会有所改善。有多个指针指向同一个资源时,他们以共享这个资源的方式指向该资源,其中还会维护一个引用计数器(用于标示有多少个指针指向这个资源)。如果拷贝一次,引用计数就会+1,如果某个指针指向了其他资源,引用计数就会-1.当引用计数变为0时,系统就会彻底的释放这个资源。

shared_ptr的常用成员函数:

= , . , *运算符

use_count():返回对应指针资源的引用计数;

reset():用于释放对当前所指资源的指向。当前资源的引用计数-1;

reset(T* ptr):释放对当前所指资源的指向,指向新的内存T*ptr。当前资源额引用计数-1,ptr指向的资源引用计数+1;

#include<iostream>
#include<memory>
using namespace std;
int main()
{
	shared_ptr<int>p1(new int(3));//智能指针的对象该指针指向一块内存a
	cout << *p1 << endl;
	cout << *p1.get() << endl;
	shared_ptr<int>p2(p1.get());//又有一个智能指针指向这个个内存a
	shared_ptr<int>p3 = p1;//又有一个智能指针指向这个内存a
	shared_ptr<int>p4 = p1;//又有一个指针指针指向这个内存a
	//到这里内存a的引用计数为4

	shared_ptr<int>p5(new int(5));//一个智能指针指向内存b
	p2.reset(p5.get());//原先p2指向的是内存a,使用reset(p5.get())后,对p2进行重置,使p2指向内存b。
						//此时指向a内存的引用计数-1变为3,指向b内存的引用计数+1变为2;
	return 0;
}

注意:shared_ptr并不会完全避免内存泄漏的出现,在以下情况下可能会导致内存泄漏:

//case1
//header file  
void func( shared_ptr<T1> ptr1, shared ptr<T2> ptr2 );  


//call func like this  
func( shared_ptr<T1>( new T1() ), shared_ptr<T2>( new T2() ) );  

上面函数调用过程中可能会出现内存泄漏的可能。因为C++中并没有定义一个表达式的求值过程,也就是说case1中除了func函数在最后调用之外,func的两个参数的产生过程是不确定的,其执行过程有可能是:

a.    分配内存给T1

b.   构造T1对象

c.    分配内存给T2

d.   构造T2对象

e.    构造T1的智能指针对象

f.     构造T2的智能指针对象

g.   调用func

或者:

a’. 分配内存给T1

b’. 分配内存给T2

c’. 构造T1对象

d’. 构造T2对象

e’. 构造T1的智能指针对象

f’. 构造T2的智能指针对象

g’. 调用func

这样一旦异常出现在c或者d,c'或者d'那么为T1分配的内存就无法回收,从而造成内存泄漏。

//case2交叉引用时,shared_ptr的引用计数不会变为0,因此对应的资源不会销毁
class CLeader;  
class CMember;  
   
class CLeader  
{  
public:  
      CLeader() { cout << "CLeader::CLeader()" << endl; }  
      ~CLeader() { cout << "CLeader:;~CLeader() " << endl; }  
   
      std::shared_ptr<CMember> member;  
};  
   
class CMember  
{  
public:  
      CMember()  { cout << "CMember::CMember()" << endl; }  
      ~CMember() { cout << "CMember::~CMember() " << endl; }  
   
      std::shared_ptr<CLeader> leader;     
};  
   
void TestSharedPtrCrossReference()  
{  
      cout << "TestCrossReference<<<" << endl;  
      boost::shared_ptr<CLeader> ptrleader( new CLeader );  
      boost::shared_ptr<CMember> ptrmember( new CMember );  
   
      ptrleader->member = ptrmember;  
      ptrmember->leader = ptrleader;  
   
      cout <<"  ptrleader.use_count: " << ptrleader.use_count() << endl;  
      cout <<"  ptrmember.use_count: " << ptrmember.use_count() << endl;  
}  
//output:  
CLeader::CLeader()  
CMember::CMember()  
  ptrleader.use_count: 2  
  ptrmember.use_count: 2  

以后后边说到weak_ptr时,我们会给大家说如何使用weak_ptr解决交叉引用引起的资源泄露问题。

2>weak_ptr(C++11)

weak_ptr功能

weak_ptr的出现是为了解决shared_ptr循环引用未造成内存泄漏的问题。通常情况下,weak_ptr与shared_ptr搭配使用,是shared_ptr的助手。接下来我么就对weak_ptr的详细过程进行解说。

创建一个weak_ptr

我们要通过一个shared_ptr对象来创建一个weak_ptr对象,并且创建后并不会引起原来shared_ptr引用计数的改变。

int main() {
    shared_ptr<int> sp(new int(5));
    cout << "创建前sp的引用计数:" << sp.use_count() << endl;    // use_count = 1

    weak_ptr<int> wp(sp);
    cout << "创建后sp的引用计数:" << sp.use_count() << endl;    // use_count = 1
}
通过代码,我们知道当创建对象sp时,sp所指向的资源的引用计数为1.后边使用shared_ptr创建一个weak_ptr对象,但是创建后sp的引用计数仍为1,没有发生改变。
那么问题来了。首先我们使用已有的shared_ptr对象创建一个weak_ptr对象,但是后面当我们想要使用weak_ptr对象时如何判断weak_ptr指针所指向的对象是否已经被销毁?

在weak_ptr类中有一个成员函数lock(),这个函数可以返回一个weak_ptr指针所指向的资源的shared_ptr。如果weak_ptr所指向的资源已经不存在了,lock函数返回一个“空”shared_ptr。因此我们可以使用lock函数判断weak_ptr所指向的资源是否被回收。

class A
{
public:
    A() : a(3) { cout << "A Constructor..." << endl; }
    ~A() { cout << "A Destructor..." << endl; }

    int a;
};

int main() {
    shared_ptr<A> sp(new A());//指向该资源的引用计数为1
    weak_ptr<A> wp(sp);
    //sp.reset();

    if (shared_ptr<A> pa = wp.lock())//使用weak_ptr中的lock成员函数来判断与weak_ptr关联的shared_ptr是否释放了资源
    {                                //如果释放,返回的一个"空"shared_ptr
        cout << pa->a << endl;
    }
    else
    {
        cout << "wp指向对象为空" << endl;
    }
}
在上述代码中如果将sp.reset()的注释取消掉的话,此时sp指向的资源的引用计数为0,释放掉这个资源。从而weak_ptr指向的资源也不存在了,lock()函数会返回空指针。
注意:

weak_ptr类中没有重载operator *和operator->,不能使用weak_ptr类对象直接访问指针所指向的资源,因此如果想要访问weak_ptr指向的资源的时候,必须首先使用lock成员函数获取到该weak_ptr所指向资源的shared_ptr的对象,然后再去访问。这样做也为了避免我们在写程序时,忘记考虑weak_ptr所指向的资源被释放的情况。


3>unique_ptr(C++11)

资源所有权独占的智能指针,类似于c++99中的auto_ptr,但是要比auto_ptr更加强大,因此取代了auto_ptr。下面说下unique_ptr和auto_ptr的区别。

(1)unique_ptr不具备拷贝语义和赋值语义

在unique_ptr不能通过copy constructor和operator=转移资源所有权。

unqiue_ptr<int> ptr(new int(4));
unique_ptr<int> ptr1(ptr);//错误,因为没有实现拷贝语义
unique_ptr<int> ptr2 = ptr;//错误,因为没有实现赋值语义

在auto_ptr中实现了拷贝语义和赋值语义,因此对auto_ptr对象使用copy constructor和operator=是完全正确的。

那么接下来一个问题来了,unique_ptr没有实现拷贝和赋值语义,那么它如何实现资源所有权的转移呢?

这是因为C++中实现了“move语义”,使用“move语义”可以将资源所有权从一个对象转移到另一个对象。

unqiue_ptr<int> ptr(new int(4));
unique_ptr<int> ptr1(std::move(ptr));//正确
unique_ptr<int> ptr2 = std::move(ptr);//正确
(2)unique_ptr的一些操作

通过reset函数转移资源所有权:

ptr1.reset();ptr1会释放当前他所管理的对象;

ptr1.reset(ptr2);ptr1会释放当前他所管理的对象,获取ptr2的资源管理权限

通过release函数释放资源的所有权:int * p = ptr.release()释放ptr对象所管理的资源,返回一个指向ptr所管理的对象的原生指针。

通过“move语义”转移资源所有权:ptr1 = std::move(ptr);将资源管理权限由ptr移交给ptr1,ptr不在拥有对资源的管理权。

(3)unique_ptr对象可以借助于“move语义”存放在容器中,而auto_ptr不允许存放在容器中

vector<unqiue_ptr<int> >v;
unique_ptr<int>ptr (new int(5));
v.push_back(std::move(ptr));//v.push_back(ptr);是不对的

不允许将auto_ptr对象放入到容器中去。

vector  < auto_ptr <Foo> > vf; // 声明 auto_ptr类型向量元素
//将若干个auto_ptr放入容器 vf中
int g()
{
  auto_ptr 
<Foo> temp = vf[0]; // vf[0] 变成 null
}

当temp 被初始化,成员vf[0]被改变:其指针变成null。任何对该元素的使用企图将导致运行时崩溃。任何时候,只要拷贝容器元素,这种情况都有可能发生。记住,即使代码没有进行显式的拷贝或赋值操作,许多算法如:swap()、random_shuffle()、 sort()……会创建一个或多个容器元素的临时拷贝。此外,某些容器的成员函数可能会创建一个或多个元素的临时拷贝。从而使原来的对象变成无效对象。任何并发的对容器元素的操作企图因此而变成了不明确的或者说未定义的行为。

(4)unique_ptr可以用于管理数组,auto_ptr不支持数组

因为在unique_ptr的构造函数函数中有unique_ptr<int []>,析构函数中有delete[],而在auto_ptr中没有这些实现。

4>auto_ptr(C++99)

auto_ptr是一个与shared_ptr对立的一种智能指针,auto_ptr是资源所有权独占的智能指针。对于某一块资源,在任何时候只会有一个指针指向它。shared_ptr在某个时刻可能多个指针指向底层的同一块资源。

当我们使用赋值或者拷贝的方式使用一个auto_ptr对象产生另一个auto_ptr对象时,以前的auto_ptr对象不再拥有资源(释放对资源的所有权,不能访问资源),资源的拥有权给了新的对象。

auto_ptr对象释放的时候,类的析构函数中调用的delete(不是delete[])。因此,不能使用auto_ptr管理数组;

auto_ptr不能作为容器中的元素;

auto_ptr提供了拷贝语义,但是拷贝完后原来的指针将资源所有权转移给新的指针,原来的指针失;

类中的成员函数

release():首先返回智能指针对应的原生指针,然后释放对资源的所有权;

reset():重新设置auto_ptr所指的对象的所有权;

auto_ptr和unique_ptr相比起来,unique_ptr更有优势:

unqiue_ptr无拷贝赋值语义,但实现了move语义;

auto_ptr对象不能存放在数组中,unique_ptr能够放在数组中;

auto_ptr不能用于管理数组,unique_ptr可用于管理数组;

5>scoped_ptr

根据字面意思我们可以理解到“一个范围内的智能指针”,也就是说这个指针不能再该范围之外被使用。scoped_ptr特点是不能进行拷贝,这样做是为了使其资源的所有权不会被转移到作用域外;

其特点:

  1. 不能转换所有权
    boost::scoped_ptr所管理的对象生命周期仅仅局限于一个区间(该指针所在的"{}"之间),无法传到区间之外,这就意味着boost::scoped_ptr对象是不能作为函数的返回值的(std::auto_ptr可以)。
  2. 不能共享所有权
    这点和std::auto_ptr类似。这个特点一方面使得该指针简单易用。另一方面也造成了功能的薄弱——不能用于stl的容器中。
  3. 不能用于管理数组对象
    由于boost::scoped_ptr是通过delete来删除所管理对象的,而数组对象必须通过deletep[]来删除,因此boost::scoped_ptr是不能管理数组对象的,如果要管理数组对象需要使用boost::scoped_array类。
6>shared_array

其核心思想与shared_ptr相同,当时shared_ptr不能用于管理数组对象,shared_array可用于管理数组对象。

四、STL中map容器底层使用了什么数据结构?

我回答“红黑树”。

五、他又问该数据结构的查找时间复杂度是多少?

红黑树的平均查找、插入、删除时间复杂度均为对数的,即为logn(以2为底的n)。因为红黑树中性质决定最长路径不会超过最短路径的2倍,因此插入、删除、查找最坏的时间为2logn

六、你还有什么问题么?

我大致就问了他所在的部门中用C++语言做些什么?


他巴拉巴拉给我解释了一堆,最后经过几轮面试通过了,实习工资给5k+,不加班,双休,还有外带餐补,很不错了!!!

但是最后给导师说,导师不放去实习,,,,,,,,,,,,,,很遗憾!!!

展开阅读全文

没有更多推荐了,返回首页