1. 程式人生 > >“虎牙直播”實習生面試 c++中的智慧指標

“虎牙直播”實習生面試 c++中的智慧指標

剛剛接到了“虎牙直播”實習生的電話面試,說實在我都忘了當初什麼時候投的的了,是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+,不加班,雙休,還有外帶餐補,很不錯了!!!

但是最後給導師說,導師不放去實習,,,,,,,,,,,,,,很遺憾!!!