1、內(nèi)存四區(qū)
在C++程序執(zhí)行時(shí),內(nèi)存可以被劃分為以下四個(gè)區(qū)域:
(1)代碼區(qū)(Code Segment):代碼區(qū)的聲明周期從程序加載到內(nèi)存開始,一直持續(xù)到程序結(jié)束。代碼區(qū)中存儲(chǔ)的是程序的機(jī)器指令代碼,這些指令在程序執(zhí)行過程中被逐條執(zhí)行。代碼區(qū)的內(nèi)容是只讀的,不會(huì)被修改。
(2)全局?jǐn)?shù)據(jù)區(qū)(Global Data):全局?jǐn)?shù)據(jù)區(qū)是用于存儲(chǔ)全局變量和靜態(tài)變量的區(qū)域。全局變量和靜態(tài)變量在程序運(yùn)行期間一直存在,它們的內(nèi)存分配是在程序啟動(dòng)時(shí)完成的。全局?jǐn)?shù)據(jù)區(qū)包括靜態(tài)存儲(chǔ)區(qū)和常量存儲(chǔ)區(qū)。
- 靜態(tài)存儲(chǔ)區(qū)(Static Storage):靜態(tài)變量和局部靜態(tài)變量在靜態(tài)存儲(chǔ)區(qū)分配內(nèi)存,它們的生命周期與程序的運(yùn)行周期相同,即從程序啟動(dòng)到程序結(jié)束。
- 常量存儲(chǔ)區(qū)(Constant Storage):常量數(shù)據(jù)(如字符串常量)在常量存儲(chǔ)區(qū)分配內(nèi)存,它們的值在程序運(yùn)行期間保持不變。
(3)棧區(qū)(Stack Segment):棧區(qū)的聲明周期與函數(shù)的調(diào)用和返回相關(guān)。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),它的局部變量、函數(shù)參數(shù)和一些與函數(shù)調(diào)用相關(guān)的上下文信息會(huì)被分配到棧區(qū)。當(dāng)函數(shù)執(zhí)行完畢并返回時(shí),棧上的這些數(shù)據(jù)會(huì)被釋放。棧區(qū)的大小是動(dòng)態(tài)變化的,隨著函數(shù)的嵌套調(diào)用和返回而動(dòng)態(tài)分配和釋放。
問題:局部變量為什么放到棧里面?
答:局部變量的生命周期相對(duì)較短,當(dāng)調(diào)用一個(gè)函數(shù)時(shí),函數(shù)的局部變量被推入棧,當(dāng)函數(shù)返回時(shí),局部變量被彈出棧。
(4)堆區(qū)(Heap Segment):堆區(qū)的聲明周期由程序員手動(dòng)管理。程序在運(yùn)行時(shí)可以通過動(dòng)態(tài)內(nèi)存分配函數(shù)(如malloc或new)從堆區(qū)申請(qǐng)一塊內(nèi)存,并在不需要時(shí)手動(dòng)釋放(使用free或delete)。堆區(qū)的內(nèi)存分配和釋放不受函數(shù)調(diào)用的影響,可以在程序的任意階段進(jìn)行操作。
2、struct和class的區(qū)別
struct | class | |
---|---|---|
相同點(diǎn) |
|
|
不同點(diǎn) | struct默認(rèn)是public繼承 | class默認(rèn)是private繼承 |
3、final和override關(guān)鍵字
當(dāng)不希望某個(gè)類被繼承,或不希望某個(gè)虛函數(shù)被重寫,可以在類名和虛函數(shù)后添加final關(guān)鍵字,添加final關(guān)鍵字后被繼承或重寫,編譯器會(huì)報(bào)錯(cuò)。例子如下:
class Base
{
virtual void foo();
};
class A : public Base
{
void foo() final; // foo 被override并且是最后一個(gè)override,在其子類中不可以重寫
};
class B final : A // 指明B是不可以被繼承的
{
void foo() override; // Error: 在A中已經(jīng)被final了
};
class C : B // Error: B is final
{
};
當(dāng)在父類中使用了虛函數(shù)時(shí)候,你可能需要在某個(gè)子類中對(duì)這個(gè)虛函數(shù)進(jìn)行重寫(override),以下方法都可以:
class A
{
virtual void foo();
}
class B : public A
{
void foo(); //OK
virtual void foo(); // OK
void foo() override; //OK
}
4、淺拷貝和深拷貝
(1)淺拷貝
????????淺拷貝只是拷貝一個(gè)指針,并沒有新開辟一個(gè)地址,拷貝的指針和原來的指針指向同一塊地址,如果原來的指針?biāo)赶虻馁Y源釋放了,那么再釋放淺拷貝的指針的資源就會(huì)出現(xiàn)錯(cuò)誤。
(2)深拷貝
深拷貝不僅拷貝值,還開辟出一塊新的空間用來存放新的值,即使原先的對(duì)象被析構(gòu)掉,釋放內(nèi)存了也不會(huì)影響到深拷貝得到的值。在自己實(shí)現(xiàn)拷貝賦值的時(shí)候,如果有指針變量的話是需要自己實(shí)現(xiàn)深拷貝的。
示例如下:
#include <iostream>
#include <string.h>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student(){
name = new char(20);
cout << "Student" << endl;
};
~Student(){
cout << "~Student " << &name << endl;
delete name;
name = NULL;
};
Student(const Student &s){//拷貝構(gòu)造函數(shù)
//淺拷貝,當(dāng)對(duì)象的name和傳入對(duì)象的name指向相同的地址
name = s.name;
//深拷貝
//name = new char(20);
//memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
};
};
int main()
{
{// 花括號(hào)讓s1和s2變成局部對(duì)象,方便測(cè)試
Student s1;
Student s2(s1);// 復(fù)制對(duì)象
}
system("pause");
return 0;
}
//淺拷貝執(zhí)行結(jié)果:
//Student
//copy Student
//~Student 0x7fffed0c3ec0
//~Student 0x7fffed0c3ed0
//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 ***
//深拷貝執(zhí)行結(jié)果:
//Student
//copy Student
//~Student 0x7fffebca9fb0
//~Student 0x7fffebca9fc0
5、內(nèi)聯(lián)函數(shù)和宏定義
內(nèi)聯(lián)函數(shù):是一種特殊的函數(shù),編譯器會(huì)嘗試將其內(nèi)聯(lián)展開,而不是通過函數(shù)調(diào)用的方式執(zhí)行。內(nèi)聯(lián)函數(shù)通常用于執(zhí)行簡(jiǎn)單的操作或者頻繁調(diào)用的函數(shù),以減少函數(shù)調(diào)用的開銷和提高性能。
- 在使用時(shí),宏只做簡(jiǎn)單字符串替換(編譯前)。而內(nèi)聯(lián)函數(shù)可以進(jìn)行參數(shù)類型檢查(編譯時(shí)),且具有返回值。
- 內(nèi)聯(lián)函數(shù)在編譯時(shí)直接將函數(shù)代碼嵌入到目標(biāo)代碼中,省去函數(shù)調(diào)用的開銷來提高執(zhí)行效率,并且進(jìn)行參數(shù)類型檢查,具有返回值,可以實(shí)現(xiàn)重載。
- 宏定義時(shí)要注意書寫(參數(shù)要括起來)否則容易出現(xiàn)歧義,內(nèi)聯(lián)函數(shù)不會(huì)產(chǎn)生歧義。
- 內(nèi)聯(lián)函數(shù)有類型檢測(cè)、語法判斷等功能,而宏沒有。
內(nèi)聯(lián)函數(shù)適用場(chǎng)景:
- 使用宏定義的地方都可以使用 inline 函數(shù)。
- 作為類成員接口函數(shù)來讀寫類的私有成員或者保護(hù)成員,會(huì)提高效率。
內(nèi)聯(lián)函數(shù)的示例:
#include <iostream>
// 內(nèi)聯(lián)函數(shù)示例
inline int addNumbers(int a, int b) {
return a + b;
}
int main() {
int result = addNumbers(5, 3); // 內(nèi)聯(lián)函數(shù)調(diào)用
std::cout << "Result: " << result << std::endl;
return 0;
}
6、 new和delete
new的實(shí)現(xiàn)原理
- 當(dāng)使用new運(yùn)算符分配內(nèi)存時(shí),編譯器會(huì)首先計(jì)算所需的內(nèi)存大小,包括對(duì)象本身的大小和可能的額外內(nèi)存(如虛函數(shù)表等)。
- 編譯器會(huì)生成相應(yīng)的代碼,調(diào)用運(yùn)行時(shí)庫中的分配函數(shù)(如operator new)來分配所需大小的內(nèi)存塊。
- 運(yùn)行時(shí)庫會(huì)使用底層的內(nèi)存分配機(jī)制(如操作系統(tǒng)提供的malloc()函數(shù))來獲取一塊連續(xù)的內(nèi)存空間。
- 運(yùn)行時(shí)庫會(huì)將分配到的內(nèi)存進(jìn)行適當(dāng)?shù)膶?duì)齊,并返回一個(gè)指向該內(nèi)存塊的指針。
delete 的實(shí)現(xiàn)原理:
- 當(dāng)使用 delete 運(yùn)算符釋放內(nèi)存時(shí),編譯器會(huì)生成相應(yīng)的代碼,調(diào)用運(yùn)行時(shí)庫中的釋放函數(shù)(如 operator delete)。
- 運(yùn)行時(shí)庫會(huì)接收到要釋放的內(nèi)存指針,并執(zhí)行必要的清理操作(如調(diào)用對(duì)象的析構(gòu)函數(shù))。
- 運(yùn)行時(shí)庫會(huì)使用底層的內(nèi)存釋放機(jī)制(如操作系統(tǒng)提供的 free() 函數(shù))來釋放內(nèi)存空間。
7、malloc與free的實(shí)現(xiàn)原理
- 在標(biāo)準(zhǔn)C庫中,提供了malloc/free函數(shù)分配釋放內(nèi)存,這兩個(gè)函數(shù)底層是由brk、mmap、munmap這些系統(tǒng)調(diào)用實(shí)現(xiàn)的。
- brk是將數(shù)據(jù)段(.data)的最高地址指針_edata往高地址推,mmap是在進(jìn)程的虛擬地址空間中(堆和棧中間,稱為文件映射區(qū)域的地方)找一塊空閑的虛擬內(nèi)存。這兩種方式分配的都是虛擬內(nèi)存,沒有分配物理內(nèi)存。在第一次訪問已分配的虛擬地址空間的時(shí)候,發(fā)生缺頁中斷,操作系統(tǒng)負(fù)責(zé)分配物理內(nèi)存,然后建立虛擬內(nèi)存和物理內(nèi)存之間的映射關(guān)系。
- malloc小于128k的內(nèi)存,使用brk分配內(nèi)存,將_edata往高地址推;malloc大于128k的內(nèi)存,使用mmap分配內(nèi)存,在堆和棧之間找一塊空閑內(nèi)存分配;brk分配的內(nèi)存需要等到高地址內(nèi)存釋放以后才能釋放,而mmap分配的內(nèi)存可以單獨(dú)釋放。當(dāng)最高地址空間的空閑內(nèi)存超過128K(可由M_TRIM_THRESHOLD選項(xiàng)調(diào)節(jié))時(shí),執(zhí)行內(nèi)存緊縮操作(trim)。在上一個(gè)步驟free的時(shí)候,發(fā)現(xiàn)最高地址空閑內(nèi)存超過128K,于是內(nèi)存緊縮。
- malloc是從堆里面申請(qǐng)內(nèi)存,也就是說函數(shù)返回的指針是指向堆里面的一塊內(nèi)存。操作系統(tǒng)中有一個(gè)記錄空閑內(nèi)存地址的鏈表。當(dāng)操作系統(tǒng)收到程序的申請(qǐng)時(shí),就會(huì)遍歷該鏈表,然后就尋找第一個(gè)空間大于所申請(qǐng)空間的堆結(jié)點(diǎn),然后就將該結(jié)點(diǎn)從空閑結(jié)點(diǎn)鏈表中刪除,并將該結(jié)點(diǎn)的空間分配給程序。
8、類成員初始化方式?構(gòu)造函數(shù)的執(zhí)行順序 ?為什么用成員初始化列表會(huì)快一些?
初始化方式有兩種:
(1)、賦值初始化,通過在函數(shù)體內(nèi)進(jìn)行賦值初始化;
class MyClass {
private:
int number;
std::string name;
public:
MyClass() {
number = 0;
name = "Default";
}
void printData() {
std::cout << "Number: " << number << std::endl;
std::cout << "Name: " << name << std::endl;
}
};
(2)、列表初始化,在冒號(hào)后使用初始化列表進(jìn)行初始化。
class MyClass {
private:
int number;
std::string name;
public:
MyClass(int num, const std::string& n) : number(num), name(n) {
// 構(gòu)造函數(shù)體
}
void printData() {
std::cout << "Number: " << number << std::endl;
std::cout << "Name: " << name << std::endl;
}
};
這兩種方式的主要區(qū)別在于:
函數(shù)體中初始化,是在所有的數(shù)據(jù)成員被分配內(nèi)存空間后才進(jìn)行的。
列表初始化是給數(shù)據(jù)成員分配內(nèi)存空間時(shí)就進(jìn)行初始化,就是說分配一個(gè)數(shù)據(jù)成員只要冒號(hào)后有此數(shù)據(jù)成員的賦值表達(dá)式(此表達(dá)式必須是括號(hào)賦值表達(dá)式),那么分配了內(nèi)存空間后在進(jìn)入函數(shù)體之前給數(shù)據(jù)成員賦值,就是說初始化這個(gè)數(shù)據(jù)成員此時(shí)函數(shù)體還未執(zhí)行。
一個(gè)派生類構(gòu)造函數(shù)的執(zhí)行順序如下:
① 虛擬基類的構(gòu)造函數(shù)(多個(gè)虛擬基類則按照繼承的順序執(zhí)行構(gòu)造函數(shù))。
② 基類的構(gòu)造函數(shù)(多個(gè)普通基類也按照繼承的順序執(zhí)行構(gòu)造函數(shù))。
③ 類類型的成員對(duì)象的構(gòu)造函數(shù)(按照成員對(duì)象在類中的定義順序)
④ 派生類自己的構(gòu)造函數(shù)。
為什么用成員初始化列表會(huì)快一些?
????????賦值初始化是在構(gòu)造函數(shù)當(dāng)中做賦值的操作,而列表初始化是做純粹的初始化操作。我們都知道,C++的賦值操作是會(huì)產(chǎn)生臨時(shí)對(duì)象的。臨時(shí)對(duì)象的出現(xiàn)會(huì)降低程序的效率。
????????通過構(gòu)造函數(shù)初始化列表,編譯器可以生成更高效的代碼,避免了臨時(shí)對(duì)象的創(chuàng)建和額外的賦值操作。這種直接初始化的方式可以提高代碼的性能,并且在某些情況下,還可以優(yōu)化構(gòu)造函數(shù)的調(diào)用。
9、STL
*max_element用法
最大值*max_element,最小值*min_element,求和accumulate。
*min_element 和 *max_element頭文件是algorithm
,返回值是一個(gè)迭代器。
accumulate 頭文件是numeric
,第三個(gè)參數(shù)是初始值,返回值是一個(gè)數(shù)。
#include <algorithm>
#include <iostream>
#include <vector>
#include <numeric>
int a[] = {1, 2, 3, 4, 5};
vector<int> v({1, 2, 3, 4, 5});
// 普通數(shù)組
int minValue = *min_element(a, a + 5);
int maxValue = *max_element(a, a + 5); int sumValue = accumulate(a, a + 5, 0);
// Vector數(shù)組
int minValue2 = *min_element(v.begin(), v.end());
int maxValue2 = *max_element(v.begin(), v.end());
int sumValue2 = accumulate(v.begin(), v.end(), 0);
10、c++類的默認(rèn)六個(gè)成員函數(shù)詳解
-
構(gòu)造函數(shù)
-
析構(gòu)函數(shù)
-
拷貝構(gòu)造
-
賦值運(yùn)算符重載
-
取地址運(yùn)算符重載
-
const修飾的取地址運(yùn)算符重載
1、構(gòu)造函數(shù)
構(gòu)造函數(shù)就是在創(chuàng)建類對(duì)象的時(shí)候,由編譯器自動(dòng)調(diào)用,為對(duì)象進(jìn)行初始化的一個(gè)特殊成員函數(shù)。它的名稱和類名相同,并且在對(duì)象的聲明周期內(nèi)只調(diào)用一次。
構(gòu)造函數(shù)的特性:
1)函數(shù)名與類名相同
2)無返回值
3)對(duì)象實(shí)例化時(shí)編譯器自動(dòng)調(diào)用對(duì)應(yīng)的構(gòu)造函數(shù)
4)構(gòu)造函數(shù)可以重載
5)如果用戶沒有自己定義構(gòu)造函數(shù),那么編譯器會(huì)自動(dòng)生成一個(gè)無參的默認(rèn)構(gòu)造,若用戶定義了,則編譯器不再自動(dòng)生成。
6)無參構(gòu)造和全缺省的構(gòu)造統(tǒng)稱為“默認(rèn)構(gòu)造函數(shù)”,并且默認(rèn)構(gòu)造函數(shù)只能有一個(gè)。(無參構(gòu)造函數(shù)、全缺省構(gòu)造函數(shù)、我們沒寫編譯器默認(rèn)生成的構(gòu)造函數(shù),都可以認(rèn)為是默認(rèn)成員函數(shù)。若無參的和全缺省的同時(shí)存在,要構(gòu)造一個(gè)無參對(duì)象時(shí)會(huì)出錯(cuò))
無參構(gòu)造和全缺省構(gòu)造構(gòu)成函數(shù)重載,但是不可以同時(shí)存在。7)編譯器生成無參默認(rèn)構(gòu)造,什么也沒有實(shí)現(xiàn),有什么用?
解:不僅是編譯器生成的默認(rèn)構(gòu)造,還有自己寫的無參默認(rèn)構(gòu)造,構(gòu)造后打印其成員變量的值都為亂碼。但是,因?yàn)镃++把類型分成內(nèi)置類型(基本類型)和自定義類型。內(nèi)置類型就是語法已經(jīng)定義好的類型:如int/char…,自定義類型就是我們使用class/struct/union自己定義的類型,**編譯器會(huì)自動(dòng)調(diào)用自定義類型成員的默認(rèn)構(gòu)造函數(shù)。**而不對(duì)內(nèi)置類型做任何處理。
2、析構(gòu)函數(shù)
析構(gòu)函數(shù)是一個(gè)對(duì)象在銷毀時(shí)會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù),完成對(duì)象銷毀前,類的一些資源清理工作。(不是完成對(duì)象的銷毀,局部對(duì)象銷毀工作是由編譯器完成的)
析構(gòu)函數(shù)的特性:
1)析構(gòu)函數(shù),名是在類名前加上字符~。
2)無參數(shù),無返回值
3)一個(gè)類只有一個(gè)析構(gòu)函數(shù),如果用戶自己沒有定義,則系統(tǒng)會(huì)自動(dòng)生成一個(gè)。
4)對(duì)象生命周期結(jié)束時(shí),C++編譯系統(tǒng)會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)。5)編譯器自動(dòng)生成的析構(gòu)函數(shù),會(huì)對(duì)自定義類型成員調(diào)用它的析構(gòu)函數(shù)
3、拷貝構(gòu)造函數(shù)?
只有單個(gè)形參,并且該形參是對(duì)本類類型對(duì)象的引用(一般用const修飾),在用已存在類類型對(duì)象創(chuàng)建新對(duì)象時(shí),編譯器會(huì)自動(dòng)調(diào)用。
拷貝構(gòu)造函數(shù)的特性:
1)拷貝構(gòu)造只是構(gòu)造函數(shù)的一個(gè)重載
2)拷貝構(gòu)造的參數(shù)只有一個(gè),且必須引用傳參,使用傳值方式會(huì)引發(fā)無窮遞歸調(diào)用。3)若用戶沒有定義拷貝構(gòu)造,編譯器會(huì)自動(dòng)生成默認(rèn)拷貝構(gòu)造,但是只是按對(duì)象字節(jié)序進(jìn)行拷貝,是淺拷貝。
備注:(淺拷貝在拷貝指針的時(shí)候沒有重新開辟一塊空間將指針?biāo)竷?nèi)存單元的內(nèi)容拷貝到新空間里,再讓拷貝的指針指向它。所以淺拷貝只做了表面功夫,**拷貝的指針仍然和之前的指針指向同一片空間,**所以一旦任何一個(gè)指針銷毀,釋放空間,那么另一個(gè)的指針也會(huì)出錯(cuò)。)
4、賦值運(yùn)算符重載
運(yùn)算符的重載
運(yùn)算符重載是具有特殊函數(shù)名的函數(shù),也具有其返回值類型,函數(shù)名字以及參數(shù)列表,其返回值類型與參數(shù)列表與普通的函數(shù)類似。
函數(shù)名為:關(guān)鍵字operator后面接需要重載的符號(hào)
函數(shù)原型:返回值類型 operator操作符(參數(shù)列表)
賦值運(yùn)算符的重載
a)一個(gè)類如果沒有顯式定義賦值運(yùn)算符的重載,那么編譯器會(huì)自動(dòng)生成一個(gè),完成對(duì)象按字節(jié)序的值拷貝(淺拷貝)
b)如果類里面有指針,若同樣賦值運(yùn)算符利用淺拷貝進(jìn)行,那么程序會(huì)崩潰。
5、const成員
1、const 修飾的類成員函數(shù)
const修飾的類成員函數(shù)稱為const成員函數(shù),實(shí)際修飾的是該成員函數(shù)隱含的this指針,表明在該成員函數(shù)中不能對(duì)類的任何成員進(jìn)行修改。
2、const修飾的變量
1)const修飾常量,表示其值無法修改
const int i=6; int const i=6;
2)const指向常量的指針
a)
int a=1; int b=2; const int *p1=&a; p1=&b;//可以修改指針的指向 //*p1=3不可以通過指針修改指針指向內(nèi)存單元的值
指針p1不可修改它指向的內(nèi)存單元的值,但是p1可修改其指向的內(nèi)存單元。
b)
int a=1; int *const p1=&a;//初始化后不能指向其他內(nèi)存單元,但是可以通過指針改變其指向內(nèi)存單元的值
左定值,右定向
(*號(hào)在const的左邊,則指針?biāo)傅膬?nèi)存單元的值不能通過指針改變,*號(hào)在const的右邊,表示指針?biāo)赶虻膬?nèi)存單元不可以改變。)
3)指向常量的指針常量
int a=1; int const * const p1 =&a; //指針指向的值和指向的內(nèi)存單元都不可以改變
備注:
- const對(duì)象可以調(diào)用非const成員函數(shù)嗎?
解:不可以- 非const對(duì)象可以調(diào)用const成員函數(shù)嗎?
解:可以- const成員函數(shù)內(nèi)可以調(diào)用其它的非const成員函數(shù)嗎?
解:不可以- 非const成員函數(shù)內(nèi)可以調(diào)用其它的const成員函數(shù)嗎?
解:可以
6、取地址運(yùn)算符重載及const取地址運(yùn)算符重載
一般情況下不用重新定義,編譯器會(huì)默認(rèn)生成。只有特殊情況下(想讓別人獲取到指定內(nèi)容時(shí))會(huì)重新定義。
11、單例模式
????????在一個(gè)項(xiàng)目中,全局范圍內(nèi),某個(gè)類的實(shí)例有且僅有一個(gè),通過這個(gè)唯一實(shí)例向其他模塊提供數(shù)據(jù)的全局訪問,這種模式就叫單例模式。單例模式的典型應(yīng)用就是任務(wù)隊(duì)列。
防護(hù)措施
????????如果使用單例模式,首先要保證這個(gè)類的實(shí)例有且僅有一個(gè),也就是說這個(gè)對(duì)象是獨(dú)生子女,如果我們實(shí)施計(jì)劃生育只生一個(gè)孩子,不需要也不能給再他增加兄弟姐妹。因此,就必須采取一系列的防護(hù)措施。對(duì)于類來說以上描述同樣適用。涉及一個(gè)類多對(duì)象操作的函數(shù)有以下幾個(gè):
構(gòu)造函數(shù):創(chuàng)建一個(gè)新的對(duì)象
拷貝構(gòu)造函數(shù):根據(jù)已有對(duì)象拷貝出一個(gè)新的對(duì)象
拷貝賦值操作符重載函數(shù):兩個(gè)對(duì)象之間的賦值
為了把一個(gè)類可以實(shí)例化多個(gè)對(duì)象的路堵死,可以做如下處理:
(1)、構(gòu)造函數(shù)私有化,在類內(nèi)部只調(diào)用一次,這個(gè)是可控的。
- 由于使用者在類外部不能使用構(gòu)造函數(shù),所以在類內(nèi)部創(chuàng)建的這個(gè)唯一的對(duì)象必須是靜態(tài)的,這樣就可以通過類名來訪問了,為了不破壞類的封裝,我們都會(huì)把這個(gè)靜態(tài)對(duì)象的訪問權(quán)限設(shè)置為私有的。
- 在類中只有它的靜態(tài)成員函數(shù)才能訪問其靜態(tài)成員變量,所以可以給這個(gè)單例類提供一個(gè)靜態(tài)函數(shù)用于得到這個(gè)靜態(tài)的單例對(duì)象。
(2)、拷貝構(gòu)造函數(shù)私有化或者禁用(使用 = delete)
(3)、拷貝賦值操作符重載函數(shù)私有化或者禁用(從單例的語義上講這個(gè)函數(shù)已經(jīng)毫無意義,所以在類中不再提供這樣一個(gè)函數(shù),故將它也一并處理一下。)
由于單例模式就是給類創(chuàng)建一個(gè)唯一的實(shí)例對(duì)象,所以它的 UML 類圖是很簡(jiǎn)單的:
因此,定義一個(gè)單例模式的類的示例代碼如下:?
// 定義一個(gè)單例模式的類
class Singleton
{
public:
? ? // = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
? ? Singleton(const Singleton& obj) = delete;
? ? Singleton& operator=(const Singleton& obj) = delete;
? ? static Singleton* getInstance();
private:
? ? Singleton() = default;
? ? static Singleton* m_obj;
};
在實(shí)現(xiàn)一個(gè)單例模式的類的時(shí)候,有兩種處理模式:
- 餓漢模式
- 懶漢模式
餓漢模式
????????餓漢模式就是在類加載的時(shí)候立刻進(jìn)行實(shí)例化,這樣就得到了一個(gè)唯一的可用對(duì)象。關(guān)于這個(gè)餓漢模式的類的定義如下:
// 餓漢模式
class TaskQueue
{
public:
// = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
return m_taskQ;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
};
// 靜態(tài)成員初始化放到類外部處理
TaskQueue* TaskQueue::m_taskQ = new TaskQueue;
int main()
{
TaskQueue* obj = TaskQueue::getInstance();
}
定義這個(gè)單例類的時(shí)候,就把這個(gè)靜態(tài)的單例對(duì)象創(chuàng)建出來了。當(dāng)使用者通過 getInstance() 獲取這個(gè)單例對(duì)象的時(shí)候,它已經(jīng)被準(zhǔn)備好了。
注意事項(xiàng):類的靜態(tài)成員變量在使用之前必須在類的外部進(jìn)行初始化才能使用。
懶漢模式
懶漢模式是在類加載的時(shí)候不去創(chuàng)建這個(gè)唯一的實(shí)例,而是在需要使用的時(shí)候再進(jìn)行實(shí)例化。
(1)懶漢模式類的定義
// 懶漢模式
class TaskQueue
{
public:
// = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
if(m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
return m_taskQ;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
????????在調(diào)用 getInstance() 函數(shù)獲取單例對(duì)象的時(shí)候,如果在單線程情況下是沒有什么問題的,如果是多個(gè)線程,調(diào)用這個(gè)函數(shù)去訪問單例對(duì)象就有問題了。假設(shè)有三個(gè)線程同時(shí)執(zhí)行了getInstance() 函數(shù),在這個(gè)函數(shù)內(nèi)部每個(gè)線程都會(huì) new 出一個(gè)實(shí)例對(duì)象。此時(shí),這個(gè)任務(wù)隊(duì)列類的實(shí)例對(duì)象不是一個(gè)而是 3 個(gè),很顯然這與單例模式的定義是相悖的。
(2) 線程安全問題
雙重檢查鎖定
對(duì)于餓漢模式是沒有線程安全問題的,在這種模式下訪問單例對(duì)象的時(shí)候,這個(gè)對(duì)象已經(jīng)被創(chuàng)建出來了。要解決懶漢模式的線程安全問題,最常用的解決方案就是使用互斥鎖??梢詫?chuàng)建單例對(duì)象的代碼使用互斥鎖鎖住,處理代碼如下:
class TaskQueue
{
public:
// = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
m_mutex.lock();
10 if (m_taskQ == nullptr)
11 {
12 m_taskQ = new TaskQueue;
13 }
m_mutex.unlock();
return m_taskQ;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
static mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;
在上面代碼的 10~13 行這個(gè)代碼塊被互斥鎖鎖住了,也就意味著不論有多少個(gè)線程,同時(shí)執(zhí)行這個(gè)代碼塊的線程只能是一個(gè)(相當(dāng)于是嚴(yán)重限行了,在重負(fù)載情況下,可能導(dǎo)致響應(yīng)緩慢)。我們可以將代碼再優(yōu)化一下:
class TaskQueue
{
public:
// = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
9 if (m_taskQ == nullptr)
{
m_mutex.lock();
if (m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
private:
TaskQueue() = default;
static TaskQueue* m_taskQ;
static mutex m_mutex;
};
TaskQueue* TaskQueue::m_taskQ = nullptr;
mutex TaskQueue::m_mutex;
????????改進(jìn)的思路就是在加鎖、解鎖的代碼塊外層有添加了一個(gè) if判斷(第 9 行),這樣當(dāng)任務(wù)隊(duì)列的實(shí)例被創(chuàng)建出來之后,訪問這個(gè)對(duì)象的線程就不會(huì)再執(zhí)行加鎖和解鎖操作了(只要有了單例類的實(shí)例對(duì)象,限行就解除了),對(duì)于第一次創(chuàng)建單例對(duì)象的時(shí)候線程之間還是具有競(jìng)爭(zhēng)關(guān)系,被互斥鎖阻塞。上面這種通過兩個(gè)嵌套的 if 來判斷單例對(duì)象是否為空的操作就叫做雙重檢查鎖定。
雙重檢查鎖定的問題
假設(shè)有兩個(gè)線程 A、B,當(dāng)線程 A 執(zhí)行到第 8 行時(shí)在線程 A 中 TaskQueue 實(shí)例對(duì)象 被創(chuàng)建,并賦值給 m_taskQ。
static TaskQueue* getInstance()
{
if (m_taskQ == nullptr)
{
m_mutex.lock();
if (m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
但是實(shí)際上 m_taskQ = new TaskQueue; 在執(zhí)行過程中對(duì)應(yīng)的機(jī)器指令可能會(huì)被重新排序。正常過程如下:
- 第一步:分配內(nèi)存用于保存 TaskQueue 對(duì)象。
- 第二步:在分配的內(nèi)存中構(gòu)造一個(gè) TaskQueue 對(duì)象(初始化內(nèi)存)。
- 第三步:使用 m_taskQ 指針指向分配的內(nèi)存。
但是被重新排序以后執(zhí)行順序可能會(huì)變成這樣:
- 第一步:分配內(nèi)存用于保存 TaskQueue 對(duì)象。
- 第二步:使用 m_taskQ 指針指向分配的內(nèi)存。
- 第三步:在分配的內(nèi)存中構(gòu)造一個(gè) TaskQueue 對(duì)象(初始化內(nèi)存)。
這樣重排序并不影響單線程的執(zhí)行結(jié)果,但是在多線程中就會(huì)出問題。如果線程 A 按照第二種順序執(zhí)行機(jī)器指令,執(zhí)行完前兩步之后失去 CPU 時(shí)間片被掛起了,此時(shí)線程 B 在第 3 行處進(jìn)行指針判斷的時(shí)候 m_taskQ 指針是不為空的,但這個(gè)指針指向的內(nèi)存卻沒有被初始化,最后線程 B 使用了一個(gè)沒有被初始化的隊(duì)列對(duì)象就出問題了(出現(xiàn)這種情況是概率問題,需要反復(fù)的大量測(cè)試問題才可能會(huì)出現(xiàn))。
在 C++11 中引入了原子變量 atomic,通過原子變量可以實(shí)現(xiàn)一種更安全的懶漢模式的單例,代碼如下:
class TaskQueue
{
public:
// = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
TaskQueue* queue = m_taskQ.load();
if (queue == nullptr)
{
// m_mutex.lock(); // 加鎖: 方式1
lock_guard<mutex> locker(m_mutex); // 加鎖: 方式2
queue = m_taskQ.load();
if (queue == nullptr)
{
queue = new TaskQueue;
m_taskQ.store(queue);
}
// m_mutex.unlock();
}
return queue;
}
void print()
{
cout << "hello, world!!!" << endl;
}
private:
TaskQueue() = default;
static atomic<TaskQueue*> m_taskQ;
static mutex m_mutex;
};
atomic<TaskQueue*> TaskQueue::m_taskQ;
mutex TaskQueue::m_mutex;
int main()
{
TaskQueue* queue = TaskQueue::getInstance();
queue->print();
return 0;
}
上面代碼中使用原子變量 atomic 的 store() 方法來存儲(chǔ)單例對(duì)象,使用 load() 方法來加載單例對(duì)象。在原子變量中這兩個(gè)函數(shù)在處理指令的時(shí)候默認(rèn)的原子順序是 memory_order_seq_cst(順序原子操作 - sequentially consistent),使用順序約束原子操作庫,整個(gè)函數(shù)執(zhí)行都將保證順序執(zhí)行,并且不會(huì)出現(xiàn)數(shù)據(jù)競(jìng)態(tài)(data races),不足之處就是使用這種方法實(shí)現(xiàn)的懶漢模式的單例執(zhí)行效率更低一些。
靜態(tài)局部對(duì)象
在實(shí)現(xiàn)懶漢模式的單例的時(shí)候,相較于雙重檢查鎖定模式有一種更簡(jiǎn)單的實(shí)現(xiàn)方法并且不會(huì)出現(xiàn)線程安全問題,那就是使用靜態(tài)局部局部對(duì)象,對(duì)應(yīng)的代碼實(shí)現(xiàn)如下:
class TaskQueue
{
public:
// = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
9 static TaskQueue taskQ;
10 return &taskQ;
}
void print()
{
cout << "hello, world!!!" << endl;
}
private:
TaskQueue() = default;
};
int main()
{
TaskQueue* queue = TaskQueue::getInstance();
queue->print();
return 0;
}
在程序的第 9、10 行定義了一個(gè)靜態(tài)局部隊(duì)列對(duì)象,并且將這個(gè)對(duì)象作為了唯一的單例實(shí)例。使用這種方式之所以是線程安全的,是因?yàn)樵?C++11 標(biāo)準(zhǔn)中有如下規(guī)定,并且這個(gè)操作是在編譯時(shí)由編譯器保證的:
如果指令邏輯進(jìn)入一個(gè)未被初始化的聲明變量,所有并發(fā)執(zhí)行應(yīng)當(dāng)?shù)却撟兞客瓿沙跏蓟?/p>
最后總結(jié)一下懶漢模式和餓漢模式的區(qū)別:
懶漢模式的缺點(diǎn)是在創(chuàng)建實(shí)例對(duì)象的時(shí)候有安全問題,但這樣可以減少內(nèi)存的浪費(fèi)(如果用不到就不去申請(qǐng)內(nèi)存了)。餓漢模式則相反,在我們不需要這個(gè)實(shí)例對(duì)象的時(shí)候,它已經(jīng)被創(chuàng)建出來,占用了一塊內(nèi)存。對(duì)于現(xiàn)在的計(jì)算機(jī)而言,內(nèi)存容量都是足夠大的,這個(gè)缺陷可以被無視。
寫一個(gè)任務(wù)隊(duì)列?
首要任務(wù)就是設(shè)計(jì)一個(gè)單例模式的任務(wù)隊(duì)列,那么就需要賦予這個(gè)類一些屬性和方法:
屬性:
- 存儲(chǔ)任務(wù)的容器,這個(gè)容器可以選擇使用 STL中的隊(duì)列(queue)
- 互斥鎖,多線程訪問的時(shí)候用于保護(hù)任務(wù)隊(duì)列中的數(shù)據(jù)
方法:主要是對(duì)任務(wù)隊(duì)列中的任務(wù)進(jìn)行操作
- 任務(wù)隊(duì)列中任務(wù)是否為空
- 往任務(wù)隊(duì)列中添加一個(gè)任務(wù)
- 從任務(wù)隊(duì)列中取出一個(gè)任務(wù)
- 從任務(wù)隊(duì)列中刪除一個(gè)任務(wù)
根據(jù)分析,就可以把這個(gè)餓漢模式的任務(wù)隊(duì)列的單例類定義出來了:
#include <iostream>
#include <queue>
#include <mutex>
#include <thread>
using namespace std;
class TaskQueue
{
public:
// = delete 代表函數(shù)禁用, 也可以將其訪問權(quán)限設(shè)置為私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
return &m_obj;
}
// 任務(wù)隊(duì)列是否為空
bool isEmpty()
{
lock_guard<mutex> locker(m_mutex);
bool flag = m_taskQ.empty();
return flag;
}
// 添加任務(wù)
void addTask(int data)
{
lock_guard<mutex> locker(m_mutex);
m_taskQ.push(data);
}
// 取出一個(gè)任務(wù)
int takeTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
return m_taskQ.front();
}
return -1;
}
// 刪除一個(gè)任務(wù)
bool popTask()
{
lock_guard<mutex> locker(m_mutex);
if (!m_taskQ.empty())
{
m_taskQ.pop();
return true;
}
return false;
}
private:
TaskQueue() = default;
static TaskQueue m_obj;
queue<int> m_taskQ;
mutex m_mutex;
};
TaskQueue TaskQueue::m_obj;
int main()
{
thread t1([]() {
TaskQueue* taskQ = TaskQueue::getInstance();
for (int i = 0; i < 100; ++i)
{
taskQ->addTask(i + 100);
cout << "+++push task: " << i + 100 << ", threadID: "
<< this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
});
thread t2([]() {
TaskQueue* taskQ = TaskQueue::getInstance();
this_thread::sleep_for(chrono::milliseconds(100));
while (!taskQ->isEmpty())
{
int data = taskQ->takeTask();
cout << "---take task: " << data << ", threadID: "
<< this_thread::get_id() << endl;
taskQ->popTask();
this_thread::sleep_for(chrono::seconds(1));
}
});
t1.join();
t2.join();
}
在上面的程序中有以下幾點(diǎn)需要說明一下:
正常情況下,任務(wù)隊(duì)列中的任務(wù)應(yīng)該是一個(gè)函數(shù)指針(這個(gè)指針指向的函數(shù)中有需要執(zhí)行的任務(wù)動(dòng)作),此處進(jìn)行了簡(jiǎn)化,用一個(gè)整形數(shù)代替了任務(wù)隊(duì)列中的任務(wù)。
任務(wù)隊(duì)列中的互斥鎖保護(hù)的是單例對(duì)象的中的數(shù)據(jù)也就是任務(wù)隊(duì)列中的數(shù)據(jù),上面所說的線程安全指的是在創(chuàng)建單例對(duì)象的時(shí)候要保證這個(gè)對(duì)象只被創(chuàng)建一次,和此處完全是兩碼事兒,需要區(qū)別看待。
12、公有成員,私有成員,保護(hù)成員
成員權(quán)限
- 公有成員:public表明該數(shù)據(jù)成員、成員函數(shù)是對(duì)所有用戶開放的,所有用戶都可以直接進(jìn)行調(diào)用。
- 私有成員:private為類的內(nèi)部實(shí)現(xiàn)細(xì)節(jié),只能被本類的成員函數(shù)訪問,或者是友元訪問。
- 保護(hù)成員:protected對(duì)于子女(繼承)、朋友(友元)來說,就是public的,可以自由使用,沒有任何限制,而對(duì)于其他的外部class,protected就變成private。
繼承
- 公有繼承:繼承自父類的成員保持不變。
- 私有繼承:繼承自父類的成員全部變?yōu)樗接谐蓡T。
- 保護(hù)繼承:繼承自父類的公有成員變?yōu)楸Wo(hù)成員,其余不變。
13、友元函數(shù)和友元類
????????對(duì)象中的 protected 成員和private 成員不允許被非成員函數(shù)直接訪問,這稱為類的封裝性。為了外部訪問類內(nèi)的私有和保護(hù)數(shù)據(jù),我們可以定義友元。這么做事為了實(shí)現(xiàn)數(shù)據(jù)共享,提高執(zhí)行效率,同時(shí),又保有類的一定的封裝性和數(shù)據(jù)隱藏性。
友元可以是一個(gè)函數(shù),該函數(shù)稱為友元函數(shù)。友元也可以是一個(gè)類,該類被稱為友元類。
友元函數(shù)
類的友元函數(shù)是指在類定義的一個(gè)普通函數(shù),不是類的成員函數(shù),但是它可以自由地訪問類中的私有數(shù)據(jù)成員。它訪問對(duì)象中的成員必須通過對(duì)象名。
友元類
若一個(gè)類為另一個(gè)類的友元,則此類的所有成員都能訪問對(duì)方類的私有成員。
也就是,我認(rèn)你做朋友了,那么我的東西你隨便看隨便用,哪怕是私有的東西。認(rèn)你做朋友的方式,就是在我底下,把你聲明成是我的朋友。舉個(gè)例子如下:
在 A 底下將 B 聲明為了友元類,那么 B 中實(shí)例化了一個(gè) A 類型的 a,a 中的私有變量就可以被訪問了。文章來源:http://www.zghlxwxcb.cn/news/detail-490884.html
主要注意的幾點(diǎn):友元關(guān)系不可以被繼承;友元關(guān)系是單向的;友元關(guān)系不能傳遞。文章來源地址http://www.zghlxwxcb.cn/news/detail-490884.html
到了這里,關(guān)于C++:基礎(chǔ)知識(shí)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!