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)當函數的返回值是類的對象或者引用的時候
舉例:
-
-
-
-
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 的方式實現,需要通過傳回地址的形式進行,即地址/指針傳遞。
- 傳值:傳值,實際是把實參的值賦值給行參,相當於copy。那么對行參的修改,不會影響實參的值 。
- 傳址: 實際是傳值的一種特殊方式,只是他傳遞的是地址,不是普通的賦值,那么傳地址以后,實參和行參都指向同一個對象,因此對形參的修改會影響到實參。
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循環體中。例如
-
pthread_mutex_lock(mutex);//加互斥鎖
-
while(條件不成立)//當前線程中條件變量不成立
-
{
-
pthread_cond_wait(cond, mutex);//解鎖,其他線程使條件成立發送信號,加鎖。
-
}
-
...//對進程之間的共享資源進行操作
-
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)讀寫鎖
說到讀寫鎖我們可以借助於“讀者-寫者”問題進行理解。首先我們簡單說下“讀者-寫者”問題。
計算機中某些數據被多個進程共享,對數據庫的操作有兩種:一種是讀操作,就是從數據庫中讀取數據不會修改數據庫中內容;另一種就是寫操作,寫操作會修改數據庫中存放的數據。因此可以得到我們允許在數據庫上同時執行多個“讀”操作,但是某一時刻只能在數據庫上有一個“寫”操作來更新數據。這就是一個簡單的讀者-寫者模型。