1. 程式人生 > >常見C++筆試面試題整理

常見C++筆試面試題整理

1、C和C++的區別

1)C是面向過程的語言,是一個結構化的語言,考慮如何通過一個過程對輸入進行處理得到輸出;C++是面向物件的語言,主要特徵是“封裝、繼承和多型”。封裝隱藏了實現細節,使得程式碼模組化;派生類可以繼承父類的資料和方法,擴充套件了已經存在的模組,實現了程式碼重用;多型則是“一個介面,多種實現”,通過派生類重寫父類的虛擬函式,實現了介面的重用。

2)C和C++動態管理記憶體的方法不一樣,C是使用malloc/free,而C++除此之外還有new/delete關鍵字。

3)C++支援函式過載,C不支援函式過載

4)C++中有引用,C中不存在引用的概念

2、C++中指標和引用的區別

1)指標是一個新的變數,儲存了另一個變數的地址,我們可以通過訪問這個地址來修改另一個變數;

引用只是一個別名,還是變數本身,對引用的任何操作就是對變數本身進行操作,以達到修改變數的目的

2)引用只有一級,而指標可以有多級

3)指標傳參的時候,還是值傳遞,指標本身的值不可以修改,需要通過解引用才能對指向的物件進行操作

引用傳參的時候,傳進來的就是變數本身,因此變數可以被修改

3、結構體struct和共同體union(聯合)的區別

結構體:將不同型別的資料組合成一個整體,是自定義型別

共同體:不同型別的幾個變數共同佔用一段記憶體

1)結構體中的每個成員都有自己獨立的地址,它們是同時存在的;

共同體中的所有成員佔用同一段記憶體,它們不能同時存在;

2)sizeof(struct)是記憶體對齊後所有成員長度的總和,sizeof(union)是記憶體對齊後最長資料成員的長度、

結構體為什麼要記憶體對齊呢?

4、#define和const的區別

1)#define定義的常量沒有型別,所給出的是一個立即數;const定義的常量有型別名字,存放在靜態區域

2)處理階段不同,#define定義的巨集變數在預處理時進行替換,可能有多個拷貝,const所定義的變數在編譯時確定其值,只有一個拷貝。

3)#define定義的常量是不可以用指標去指向,const定義的常量可以用指標去指向該常量的地址

4)#define可以定義簡單的函式,const不可以定義函式

5、過載overload,覆蓋override,重寫overwrite,這三者之間的區別

1)overload,將語義相近的幾個函式用同一個名字表示,但是引數和返回值不同,這就是函式過載

特徵:相同範圍(同一個類中)、函式名字相同、引數不同、virtual關鍵字可有可無

2)override,派生類覆蓋基類的虛擬函式,實現介面的重用

特徵:不同範圍(基類和派生類)、函式名字相同、引數相同、基類中必須有virtual關鍵字(必須是虛擬函式)

3)overwrite,派生類遮蔽了其同名的基類函式

特徵:不同範圍(基類和派生類)、函式名字相同、引數不同或者引數相同且無virtual關鍵字

6、new、delete、malloc、free之間的關係

new/delete,malloc/free都是動態分配記憶體的方式

1)malloc對開闢的空間大小嚴格指定,而new只需要物件名

2)new為物件分配空間時,呼叫物件的建構函式,delete呼叫物件的解構函式

既然有了malloc/free,C++中為什麼還需要new/delete呢?

因為malloc/free是庫函式而不是運算子,不能把執行建構函式和解構函式的功能強加於malloc/free

7、delete和delete[]的區別

delete只會呼叫一次解構函式,而delete[]會呼叫每個成員的解構函式

用new分配的記憶體用delete釋放,用new[]分配的記憶體用delete[]釋放

8、STL庫用過嗎?常見的STL容器有哪些?演算法用過幾個?

STL包括兩部分內容:容器和演算法

容器即存放資料的地方,比如array, vector,分為兩類,序列式容器和關聯式容器

序列式容器,其中的元素不一定有序,但是都可以被排序,比如vector,list,queue,stack,heap, priority-queue, slist

關聯式容器,內部結構是一個平衡二叉樹,每個元素都有一個鍵值和一個實值,比如map, set, hashtable, hash_set

演算法有排序,複製等,以及各個容器特定的演算法

迭代器是STL的精髓,迭代器提供了一種方法,使得它能夠按照順序訪問某個容器所含的各個元素,但無需暴露該容器的內部結構,它將容器和演算法分開,讓二者獨立設計。

9、const知道嗎?解釋一下其作用

const修飾類的成員變數,表示常量不可能被修改

const修飾類的成員函式,表示該函式不會修改類中的資料成員,不會呼叫其他非const的成員函式

10、虛擬函式是怎麼實現的

每一個含有虛擬函式的類都至少有有一個與之對應的虛擬函式表,其中存放著該類所有虛擬函式對應的函式指標(地址),

類的示例物件不包含虛擬函式表,只有虛指標;

派生類會生成一個相容基類的虛擬函式表。

11、堆和棧的區別

1)棧 stack 存放函式的引數值、區域性變數,由編譯器自動分配釋放

堆heap,是由new分配的記憶體塊,由應用程式控制,需要程式設計師手動利用delete釋放,如果沒有,程式結束後,作業系統自動回收

2)因為堆的分配需要使用頻繁的new/delete,造成記憶體空間的不連續,會有大量的碎片

3)堆的生長空間向上,地址越大,棧的生長空間向下,地址越小

12、關鍵字static的作用

1)函式體內: static 修飾的區域性變數作用範圍為該函式體,不同於auto變數,其記憶體只被分配一次,因此其值在下次呼叫的時候維持了上次的值

2)模組內:static修飾全域性變數或全域性函式,可以被模組內的所有函式訪問,但是不能被模組外的其他函式訪問,使用範圍限制在宣告它的模組內

3)類中:修飾成員變數,表示該變數屬於整個類所有,對類的所有物件只有一份拷貝

4)類中:修飾成員函式,表示該函式屬於整個類所有,不接受this指標,只能訪問類中的static成員變數

注意和const的區別!!!const強調值不能被修改,而static強調唯一的拷貝,對所有類的物件

13、STL中map和set的原理(關聯式容器)

map和set的底層實現主要通過紅黑樹來實現

紅黑樹是一種特殊的二叉查詢樹

1)每個節點或者是黑色,或者是紅色 

2)根節點是黑色

3) 每個葉子節點(NIL)是黑色。 [注意:這裡葉子節點,是指為空(NIL或NULL)的葉子節點!]

4)如果一個節點是紅色的,則它的子節點必須是黑色的

5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。

特性4)5)決定了沒有一條路徑會比其他路徑長出2倍,因此紅黑樹是接近平衡的二叉樹。

14、#include<file.h> #include "file.h" 的區別
  前者是從標準庫路徑尋找
  後者是從當前工作路徑

15、什麼是記憶體洩漏?面對記憶體洩漏和指標越界,你有哪些方法?

動態分配記憶體所開闢的空間,在使用完畢後未手動釋放,導致一直佔據該記憶體,即為記憶體洩漏。

方法:malloc/free要配套,對指標賦值的時候應該注意被賦值的指標是否需要釋放;使用的時候記得指標的長度,防止越界

16、定義和宣告的區別

宣告是告訴編譯器變數的型別和名字,不會為變數分配空間

定義需要分配空間,同一個變數可以被宣告多次,但是隻能被定義一次

17、C++檔案編譯與執行的四個階段

1)預處理:根據檔案中的預處理指令來修改原始檔的內容

2)編譯:編譯成彙編程式碼

3)彙編:把彙編程式碼翻譯成目標機器指令

4)連結:連結目的碼生成可執行程式

18、STL中的vector的實現,是怎麼擴容的?

vector使用的注意點及其原因,頻繁對vector呼叫push_back()對效能的影響和原因。 
vector就是一個動態增長的陣列,裡面有一個指標指向一片連續的空間,當空間裝不下的時候,會申請一片更大的空間,將原來的資料拷貝過去,並釋放原來的舊空間。當刪除的時候空間並不會被釋放,只是清空了裡面的資料。對比array是靜態空間一旦配置了就不能改變大小。

vector的動態增加大小的時候,並不是在原有的空間上持續新的空間(無法保證原空間的後面還有可供配置的空間),而是以原大小的兩倍另外配置一塊較大的空間,然後將原內容拷貝過來,並釋放原空間。在VS下是1.5倍擴容,在GCC下是2倍擴容。

在原來空間不夠儲存新值時,每次呼叫push_back方法都會重新分配新的空間以滿足新資料的新增操作。如果在程式中頻繁進行這種操作,還是比較消耗效能的。

19、STL中unordered_map和map的區別

map是STL中的一個關聯容器,提供鍵值對的資料管理。底層通過紅黑樹來實現,實際上是二叉排序樹和非嚴格意義上的二叉平衡樹。所以在map內部所有的資料都是有序的,且map的查詢、插入、刪除操作的時間複雜度都是O(logN)。

unordered_map和map類似,都是儲存key-value對,可以通過key快速索引到value,不同的是unordered_map不會根據key進行排序。unordered_map底層是一個防冗餘的雜湊表,儲存時根據key的hash值判斷元素是否相同,即unoredered_map內部是無序的。

20、C++的記憶體管理

在C++中,記憶體被分成五個區:棧、堆、自由儲存區、靜態儲存區、常量區

棧:存放函式的引數和區域性變數,編譯器自動分配和釋放

堆:new關鍵字動態分配的記憶體,由程式設計師手動進行釋放,否則程式結束後,由作業系統自動進行回收

自由儲存區:由malloc分配的記憶體,和堆十分相似,由對應的free進行釋放

全域性/靜態儲存區:存放全域性變數和靜態變數

常量區:存放常量,不允許被修改

21、 建構函式為什麼一般不定義為虛擬函式?而解構函式一般寫成虛擬函式的原因 ?

1、建構函式不能宣告為虛擬函式

1)因為建立一個物件時需要確定物件的型別,而虛擬函式是在執行時確定其型別的。而在構造一個物件時,由於物件還未建立成功,編譯器無法知道物件的實際型別,是類本身還是類的派生類等等

2)虛擬函式的呼叫需要虛擬函式表指標,而該指標存放在物件的記憶體空間中;若建構函式宣告為虛擬函式,那麼由於物件還未建立,還沒有記憶體空間,更沒有虛擬函式表地址用來呼叫虛擬函式即構造函數了

2、解構函式最好宣告為虛擬函式

首先解構函式可以為虛擬函式,當析構一個指向派生類的基類指標時,最好將基類的解構函式宣告為虛擬函式,否則可以存在記憶體洩露的問題。

如果解構函式不被宣告成虛擬函式,則編譯器實施靜態繫結,在刪除指向派生類的基類指標時,只會呼叫基類的解構函式而不呼叫派生類解構函式,這樣就會造成派生類物件析構不完全。

22、靜態繫結和動態繫結的介紹

靜態繫結和動態繫結是C++多型性的一種特性

1)物件的靜態型別和動態型別

靜態型別:物件在宣告時採用的型別,在編譯時確定

動態型別:當前物件所指的型別,在執行期決定,物件的動態型別可變,靜態型別無法更改

2)靜態繫結和動態繫結

靜態繫結:繫結的是物件的靜態型別,函式依賴於物件的靜態型別,在編譯期確定

動態繫結:繫結的是物件的動態型別,函式依賴於物件的動態型別,在執行期確定

只有虛擬函式才使用的是動態繫結,其他的全部是靜態繫結

23、 引用是否能實現動態繫結,為什麼引用可以實現

可以。因為引用(或指標)既可以指向基類物件也可以指向派生類物件,這一事實是動態繫結的關鍵。用引用(或指標)呼叫的虛擬函式在執行時確定,被呼叫的函式是引用(或指標)所指的物件的實際型別所定義的。

24、深拷貝和淺拷貝的區別

深拷貝和淺拷貝可以簡單的理解為:如果一個類擁有資源,當這個類的物件發生複製過程的時候,如果資源重新分配了就是深拷貝;反之沒有重新分配資源,就是淺拷貝。

25、 什麼情況下會呼叫拷貝建構函式(三種情況) 

系統自動生成的建構函式:普通建構函式和拷貝建構函式 (在沒有定義對應的建構函式的時候)

生成一個例項化的物件會呼叫一次普通建構函式,而用一個物件去例項化一個新的物件所呼叫的就是拷貝建構函式

呼叫拷貝建構函式的情形:

1)用類的一個物件去初始化另一個物件的時候

2)當函式的引數是類的物件時,就是值傳遞的時候,如果是引用傳遞則不會呼叫

3)當函式的返回值是類的物件或者引用的時候

舉例:

#include <iostream>
#include <string>

using namespace std;

class A{
	private:
		int data;
	public:
		A(int i){ data = i;} 	//自定義的建構函式
		A(A && a);  			//拷貝建構函式 
		int getdata(){return data;} 
};
//拷貝建構函式 
A::A(A && a){
	data = a.data;
	cout <<"拷貝建構函式執行完畢"<<endl;
}
//引數是物件,值傳遞,呼叫拷貝建構函式
int getdata1(A a){
	return a.getdata();
}
//引數是引用,引用傳遞,不呼叫拷貝建構函式 
int getdata2(A &a){
	return a.getdata();
} 
//返回值是物件型別,會呼叫拷貝建構函式
 A getA1(){
 	A a(0);
 	return a;
 } 
 //返回值是引用型別,會呼叫拷貝建構函式,因為函式體內生成的物件是臨時的,離開函式就消失
 A& getA2(){
 	A a(0);
 	return a;
 } 
 
 int main(){
    A a1(1);  
    A b1(a1);           		//用a1初始化b1,呼叫拷貝建構函式  
    A c1=a1;            		//用a1初始化c1,呼叫拷貝建構函式  
  
    int i=getdata1(a1);        	//函式形參是類的物件,呼叫拷貝建構函式  
    int j=getdata2(a1);      	//函式形參型別是引用,不呼叫拷貝建構函式  
  
    A d1=getA1();       		//呼叫拷貝建構函式  
    A e1=getA2();     			//呼叫拷貝建構函式  
  
    return 0;  
}  

26、 C++的四種強制轉換 

型別轉化機制可以分為隱式型別轉換和顯示型別轉化(強制型別轉換)

  • (new-type) expression
  • new-type (expression)

隱式型別轉換比較常見,在混合型別表示式中經常發生;四種強制型別轉換操作符:

static_cast、dynamic_cast、const_cast、reinterpret_cast

1)static_cast :編譯時期的靜態型別檢查

static_cast < type-id > ( expression )

該運算子把expression轉換成type-id型別,在編譯時使用型別資訊執行轉換,在轉換時執行必要的檢測(指標越界、型別檢查),其運算元相對是安全的

2)dynamic_cast:執行時的檢查

用於在整合體系中進行安全的向下轉換downcast,即基類指標/引用->派生類指標/引用

dynamic_cast是4個轉換中唯一的RTTI操作符,提供執行時型別檢查。

dynamic_cast如果不能轉換返回NULL

源類中必須要有虛擬函式,保證多型,才能使用dynamic_cast<source>(expression)

3)const_cast

去除const常量屬性,使其可以修改 ; volatile屬性的轉換

4)reinterpret_cast

通常為了將一種資料型別轉換成另一種資料型別

27、除錯程式的方法 

windows下直接使用vs的debug功能

linux下直接使用gdb,我們可以在其過程中給程式新增斷點,監視等輔助手段,監控其行為是否與我們設計相符

28、extern“C”作用

extern "C"的主要作用就是為了能夠正確實現C++程式碼呼叫其他C語言程式碼。加上extern "C"後,會指示編譯器這部分程式碼按C語言的進行編譯,而不是C++的。

29、typdef和define區別

#define是預處理命令,在預處理是執行簡單的替換,不做正確性的檢查

typedef是在編譯時處理的,它是在自己的作用域內給已經存在的型別一個別名

typedef    (int*)      pINT;

#define    pINT2   int*

效果相同?實則不同!實踐中見差別:pINT a,b;的效果同int *a; int *b;表示定義了兩個整型指標變數。而pINT2 a,b;的效果同int *a, b;表示定義了一個整型指標變數a和整型變數b。

30、volatile關鍵字在程式設計中有什麼作用

volatile是“易變的”、“不穩定”的意思。volatile是C的一個較為少用的關鍵字,它用來解決變數在“共享”環境下容易出現讀取錯誤的問題。

31、引用作為函式引數以及返回值的好處

對比值傳遞,引用傳參的好處:

1)在函式內部可以對此引數進行修改

2)提高函式呼叫和執行的效率(所以沒有了傳值和生成副本的時間和空間消耗)

如果函式的引數實質就是形參,不過這個形參的作用域只是在函式體內部,也就是說實參和形參是兩個不同的東西,要想形參代替實參,肯定有一個值的傳遞。函式呼叫時,值的傳遞機制是通過“形參=實參”來對形參賦值達到傳值目的,產生了一個實參的副本。即使函式內部有對引數的修改,也只是針對形參,也就是那個副本,實參不會有任何更改。函式一旦結束,形參生命也宣告終結,做出的修改一樣沒對任何變數產生影響。

用引用作為返回值最大的好處就是在記憶體中不產生被返回值的副本。

但是有以下的限制:

1)不能返回區域性變數的引用。因為函式返回以後區域性變數就會被銷燬

2)不能返回函式內部new分配的記憶體的引用。雖然不存在區域性變數的被動銷燬問題,可對於這種情況(返回函式內部new分配記憶體的引用),又面臨其它尷尬局面。例如,被函式返回的引用只是作為一 個臨時變量出現,而沒有被賦予一個實際的變數,那麼這個引用所指向的空間(由new分配)就無法釋放,造成memory leak

3)可以返回類成員的引用,但是最好是const。因為如果其他物件可以獲得該屬性的非常量的引用,那麼對該屬性的單純賦值就會破壞業務規則的完整性。 

32、純虛擬函式

純虛擬函式是隻有宣告沒有實現的虛擬函式,是對子類的約束,是介面繼承

包含純虛擬函式的類是抽象類,它不能被例項化,只有實現了這個純虛擬函式的子類才能生成物件

普通函式是靜態編譯的,沒有執行時多型

33、什麼是野指標

野指標不是NULL指標,是未初始化或者未清零的指標,它指向的記憶體地址不是程式設計師所期望的,可能指向了受限的記憶體

成因:

1)指標變數沒有被初始化

2)指標指向的記憶體被釋放了,但是指標沒有置NULL 

3)指標超過了變量了的作用範圍,比如b[10],指標b+11

33、執行緒安全和執行緒不安全

執行緒安全就是多執行緒訪問時,採用了加鎖機制,當一個執行緒訪問該類的某個資料時,進行保護,其他執行緒不能進行訪問直到該執行緒讀取完,其他執行緒才可以使用,不會出現資料不一致或者資料汙染。

執行緒不安全就是不提供資料訪問保護,有可能多個執行緒先後更改資料所得到的資料就是髒資料。

34、C++中記憶體洩漏的幾種情況

記憶體洩漏是指己動態分配的堆記憶體由於某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式執行速度減慢甚至系統崩潰等嚴重後果。

1)類的建構函式和解構函式中new和delete沒有配套

2)在釋放物件陣列時沒有使用delete[],使用了delete

3)沒有將基類的解構函式定義為虛擬函式,當基類指標指向子類物件時,如果基類的解構函式不是virtual,那麼子類的解構函式將不會被呼叫,子類的資源沒有正確釋放,因此造成記憶體洩露

4)沒有正確的清楚巢狀的物件指標

35、棧溢位的原因以及解決方法

1)函式呼叫層次過深,每呼叫一次,函式的引數、區域性變數等資訊就壓一次棧

2)區域性變數體積太大。

解決辦法大致說來也有兩種:

1> 增加棧記憶體的數目;增加棧記憶體方法如下,在vc6種依次選擇Project->Setting->Link,在Category中選擇output,在Reserve中輸入16進位制的棧記憶體大小如:0x10000000

2> 使用堆記憶體;具體實現由很多種方法可以直接把陣列定義改成指標,然後動態申請記憶體;也可以把區域性變數變成全域性變數,一個偷懶的辦法是直接在定義前邊加個static,呵呵,直接變成靜態變數(實質就是全域性變數)

36、C++標準庫vector以及迭代器

每種容器型別都定義了自己的迭代器型別,每種容器都定義了一隊命名為begin和end的函式,用於返回迭代器。

迭代器是容器的精髓,它提供了一種方法使得它能夠按照順序訪問某個容器所含的各個元素,但無需暴露該容器的內部結構,它將容器和演算法分開,讓二者獨立設計。

37、C++ 11有哪些新特性

C++11不僅包含核心語言的新機能,而且擴充套件了C++的標準程式庫(STL),併入了大部分的C++ Technical Report 1(TR1)程式庫。C++11包括大量的新特性:包括lambda表示式,型別推導關鍵字auto、decltype,和模板的大量改進。

auto

C++11中引入auto第一種作用是為了自動型別推導

auto的自動型別推導,用於從初始化表示式中推斷出變數的資料型別。通過auto的自動型別推導,可以大大簡化我們的程式設計工作

decltype

decltype實際上有點像auto的反函式,auto可以讓你宣告一個變數,而decltype則可以從一個變數或表示式中得到型別,有例項如下:

nullptr

nullptr是為了解決原來C++中NULL的二義性問題而引進的一種新的型別,因為NULL實際上代表的是0,

lambda表示式類似Javascript中的閉包,它可以用於建立並定義匿名的函式物件,以簡化程式設計工作。Lambda的語法如下:

[函式物件引數](操作符過載函式引數)mutable或exception宣告->返回值型別{函式體}

38、C++中vector和list的區別

vector和陣列類似,擁有一段連續的記憶體空間。vector申請的是一段連續的記憶體,當插入新的元素記憶體不夠時,通常以2倍重新申請更大的一塊記憶體,將原來的元素拷貝過去,釋放舊空間。因為記憶體空間是連續的,所以在進行插入和刪除操作時,會造成記憶體塊的拷貝,時間複雜度為o(n)。

list是由雙向連結串列實現的,因此記憶體空間是不連續的。只能通過指標訪問資料,所以list的隨機存取非常沒有效率,時間複雜度為o(n); 但由於連結串列的特點,能高效地進行插入和刪除。

vector擁有一段連續的記憶體空間,能很好的支援隨機存取,因此vector<int>::iterator支援“+”,“+=”,“<”等操作符。

list的記憶體空間可以是不連續,它不支援隨機訪問,因此list<int>::iterator則不支援“+”、“+=”、“<”等

vector<int>::iterator和list<int>::iterator都過載了“++”運算子。

總之,如果需要高效的隨機存取,而不在乎插入和刪除的效率,使用vector;
如果需要大量的插入和刪除,而不關心隨機存取,則應使用list。

39、C語言的函式呼叫過程

函式的呼叫過程:

1)從棧空間分配儲存空間

2)從實參的儲存空間複製值到形參棧空間

3)進行運算

形參在函式未呼叫之前都是沒有分配儲存空間的,在函式呼叫結束之後,形參彈出棧空間,清除形參空間。

陣列作為引數的函式呼叫方式是地址傳遞,形參和實參都指向相同的記憶體空間,呼叫完成後,形參指標被銷燬,但是所指向的記憶體空間依然存在,不能也不會被銷燬。

當函式有多個返回值的時候,不能用普通的 return 的方式實現,需要通過傳回地址的形式進行,即地址/指標傳遞。

  1. 傳值:傳值,實際是把實參的值賦值給行參,相當於copy。那麼對行參的修改,不會影響實參的值 。
  2. 傳址: 實際是傳值的一種特殊方式,只是他傳遞的是地址,不是普通的賦值,那麼傳地址以後,實參和行參都指向同一個物件,因此對形參的修改會影響到實參。

40、C++中的基本資料型別及派生型別

1)整型 int

2)浮點型  單精度float,雙精度double

3)字元型 char

4)邏輯型 bool

5)控制型 void

基本型別的字長及其取值範圍可以放大和縮小,改變後的型別就叫做基本型別的派生型別。派生型別宣告符由基本型別關鍵字char、int、float、double前面加上型別修飾符組成。

 型別修飾符包括:

>short     短型別,縮短字長

>long      長型別,加長字長

>signed    有符號型別,取值範圍包括正負值

>unsigned   無符號型別,取值範圍只包括正值

41、友元函式和友元類

友元提供了不同類的成員函式之間、類的成員函式和一般函式之間進行資料共享的機制。

通過友元,一個不同函式或者另一個類中的成員函式可以訪問類中的私有成員和保護成員。

友元的正確使用能提高程式的執行效率,但同時也破壞了類的封裝性和資料的隱藏性,導致程式可維護性變差。

1)友元函式

有元函式是可以訪問類的私有成員的非成員函式。它是定義在類外的普通函式,不屬於任何類,但是需要在類的定義中加以宣告。

friend 型別 函式名(形式引數);

一個函式可以是多個類的友元函式,只需要在各個類中分別宣告。

2)友元類

友元類的所有成員函式都是另一個類的友元函式,都可以訪問另一個類中的隱藏資訊(包括私有成員和保護成員)。        

 friend class 類名; 

使用友元類時注意: 

(1) 友元關係不能被繼承。 
(2) 友元關係是單向的,不具有交換性。若類B是類A的友元,類A不一定是類B的友元,要看在類中是否有相應的宣告。 
(3) 友元關係不具有傳遞性。若類B是類A的友元,類C是B的友元,類C不一定是類A的友元,同樣要看類中是否有相應的申明

42、C++執行緒中的幾種鎖機制

執行緒之間的鎖有:互斥鎖、條件鎖、自旋鎖、讀寫鎖、遞迴鎖。一般而言,鎖的功能越強大,效能就會越低。

1)互斥鎖

互斥鎖用於控制多個執行緒對他們之間共享資源互斥訪問的一個訊號量。也就是說是為了避免多個執行緒在某一時刻同時操作一個共享資源。例如執行緒池中的有多個空閒執行緒和一個任務佇列。任何是一個執行緒都要使用互斥鎖互斥訪問任務佇列,以避免多個執行緒同時訪問任務佇列以發生錯亂。

在某一時刻,只有一個執行緒可以獲取互斥鎖,在釋放互斥鎖之前其他執行緒都不能獲取該互斥鎖。如果其他執行緒想要獲取這個互斥鎖,那麼這個執行緒只能以阻塞方式進行等待。

標頭檔案:<pthread.h>

型別:pthread_mutex_t,

函式:pthread_mutex_init(pthread_mutex_t * mutex, const phtread_mutexattr_t * mutexattr);//動態方式建立鎖,相當於new動態建立一個物件

            pthread_mutex_destory(pthread_mutex_t *mutex)//釋放互斥鎖,相當於delete

            pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//以靜態方式建立鎖

            pthread_mutex_lock(pthread_mutex_t *mutex)//以阻塞方式執行的。如果之前mutex被加鎖了,那麼程式會阻塞在這裡。

            pthread_mutex_unlock(pthread_mutex_t *mutex)

            int pthread_mutex_trylock(pthread_mutex_t * mutex);//會嘗試對mutex加鎖。如果mutex之前已經被鎖定,返回非0,;如果mutex沒有被鎖定,則函式返回並鎖定mutex

                                                                                                           //該函式是以非阻塞方式運行了。也就是說如果mutex之前已經被鎖定,函式會返回非0,程式繼續往下執行。

2)條件鎖

條件鎖就是所謂的條件變數,某一個執行緒因為某個條件為滿足時可以使用條件變數使改程式處於阻塞狀態。一旦條件滿足以“訊號量”的方式喚醒一個因為該條件而被阻塞的執行緒。最為常見就是線上程池中,起初沒有任務時任務佇列為空,此時執行緒池中的執行緒因為“任務佇列為空”這個條件處於阻塞狀態。一旦有任務進來,就會以訊號量的方式喚醒一個執行緒來處理這個任務。這個過程中就使用到了條件變數pthread_cond_t。

標頭檔案:<pthread.h>

型別:pthread_cond_t

函式:pthread_cond_init(pthread_cond_t * condtion, const phtread_condattr_t * condattr);//對條件變數進行動態初始化,相當於new建立物件

            pthread_cond_destory(pthread_cond_t * condition);//釋放動態申請的條件變數,相當於delete釋放物件

            pthread_cond_t condition = PTHREAD_COND_INITIALIZER;//靜態初始化條件變數

            pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);//該函式以阻塞方式執行。如果某個執行緒中的程式執行了該函式,那麼這個執行緒就會以阻塞方式等待,直到收到pthread_cond_signal或者pthread_cond_broadcast函式發來的訊號而被喚醒。

注意:pthread_cond_wait函式的語義相當於:首先解鎖互斥鎖,然後以阻塞方式等待條件變數的訊號,收到訊號後又會對互斥鎖加鎖。

           為了防止“虛假喚醒”,該函式一般放在while迴圈體中。例如

  1. pthread_mutex_lock(mutex);//加互斥鎖

  2. while(條件不成立)//當前執行緒中條件變數不成立

  3. {

  4. pthread_cond_wait(cond, mutex);//解鎖,其他執行緒使條件成立傳送訊號,加鎖。

  5. }

  6. ...//對程序之間的共享資源進行操作

  7. pthread_mutex_unlock(mutex);//釋放互斥鎖

            pthread_cond_signal(pthread_cond_t * cond);//在另外一個執行緒中改變執行緒,條件滿足傳送訊號。喚醒一個等待的執行緒(可能有多個執行緒處於阻塞狀態),喚醒哪個執行緒由具體的執行緒排程策略決定

            pthread_cond_broadcast(pthread_cond_t * cond);//以廣播形式喚醒所有因為該條件變數而阻塞的所有執行緒,喚醒哪個執行緒由具體的執行緒排程策略決定

            pthread_cond_timedwait(pthread_cond_t * cond, pthread_mutex_t * mutex, struct timespec * time);//以阻塞方式等待,如果時間time到了條件還沒有滿足還是會結束

3)自旋鎖

前面的兩種鎖是比較常見的鎖,也比較容易理解。下面通過比較互斥鎖和自旋鎖原理的不同,這對於真正理解自旋鎖有很大幫助。

假設我們有一個兩個處理器core1和core2計算機,現在在這臺計算機上執行的程式中有兩個執行緒:T1和T2分別在處理器core1和core2上執行,兩個執行緒之間共享著一個資源。

首先我們說明互斥鎖的工作原理,互斥鎖是是一種sleep-waiting的鎖。假設執行緒T1獲取互斥鎖並且正在core1上執行時,此時執行緒T2也想要獲取互斥鎖(pthread_mutex_lock),但是由於T1正在使用互斥鎖使得T2被阻塞。當T2處於阻塞狀態時,T2被放入到等待佇列中去,處理器core2會去處理其他任務而不必一直等待(忙等)。也就是說處理器不會因為執行緒阻塞而空閒著,它去處理其他事務去了。

而自旋鎖就不同了,自旋鎖是一種busy-waiting的鎖。也就是說,如果T1正在使用自旋鎖,而T2也去申請這個自旋鎖,此時T2肯定得不到這個自旋鎖。與互斥鎖相反的是,此時執行T2的處理器core2會一直不斷地迴圈檢查鎖是否可用(自旋鎖請求),直到獲取到這個自旋鎖為止。

從“自旋鎖”的名字也可以看出來,如果一個執行緒想要獲取一個被使用的自旋鎖,那麼它會一致佔用CPU請求這個自旋鎖使得CPU不能去做其他的事情,直到獲取這個鎖為止,這就是“自旋”的含義。

當發生阻塞時,互斥鎖可以讓CPU去處理其他的任務;而自旋鎖讓CPU一直不斷迴圈請求獲取這個鎖。通過兩個含義的對比可以我們知道“自旋鎖”是比較耗費CPU的

標頭檔案:<linux\spinlock.h>

自旋鎖的型別:spinlock_t

相關函式:初始化:spin_lock_init(spinlock_t *x);

             spin_lock(x);   //只有在獲得鎖的情況下才返回,否則一直“自旋”
             spin_trylock(x);  //如立即獲得鎖則返回真,否則立即返回假
             釋放鎖:spin_unlock(x);

                 spin_is_locked(x)//  該巨集用於判斷自旋鎖x是否已經被某執行單元保持(即被鎖),如果是,   返回真,否則返回假。

注意:自旋鎖適合於短時間的的輕量級的加鎖機制。

4)讀寫鎖

說到讀寫鎖我們可以藉助於“讀者-寫者”問題進行理解。首先我們簡單說下“讀者-寫者”問題。

計算機中某些資料被多個程序共享,對資料庫的操作有兩種:一種是讀操作,就是從資料庫中讀取資料不會修改資料庫中內容;另一種就是寫操作,寫操作會修改資料庫中存放的資料。因此可以得到我們允許在資料庫上同時執行多個“讀”操作,但是某一時刻只能在資料庫上有一個“寫”操作來更新資料。這就是一個簡單的讀者-寫者模型。