📚?博主的專欄
🐧?Linux???|?? 🖥??C++???|?? 📊?數據結構??|?💡C++ 算法?| 🌐?C 語言
上篇文章: C++ 智能指針使用,以及shared_ptr編寫
下篇文章: C++ IO流
目錄
特殊類的設計
請設計一個類,不能被拷貝
請設計一個類,只能在堆上創建對象
請設計一個類,只能在棧上創建對象
請設計一個類,不能被繼承
請設計一個類,只能創建一個對象(單例模式)
設計模式:
單例模式:
餓漢模式
懶漢模式
C++的類型轉換
C語言中的類型轉換
CPP中類型轉換
為什么C++需要四種類型轉換
隱式類型轉換
示例:內置類型 -> 自定義類型
示例:自定義類型 -> 內置類型
自定義類型和自定義類型之間的轉換
隱式類型轉換的坑:
C++強制類型轉換
static_cast(對應之前的隱式類型轉換)
reinterpret_cast(對應的之前的顯式類型轉換)
const_cast
dynamic_cast(專門爭對C++設計的,向下轉換)
RTTI(了解)運行時類型識別
特殊類的設計
請設計一個類,不能被拷貝
拷貝只會放生在兩個場景中:拷貝構造函數以及賦值運算符重載,因此想要讓一個類禁止拷貝,只需讓該類不能調用拷貝構造函數以及賦值運算符重載即可。
線程不能被拷貝,IO流(IO流中的緩沖區不拷貝)不能被拷貝
C++98:
將拷貝構造函數與賦值運算符重載只聲明不定義,并且將其訪問權限設置為私有即可。
C++11:
請設計一個類,只能在堆上創建對象
實現方式:
1. 將類的構造函數私有,拷貝構造聲明成私有。防止別人調用拷貝在棧上生成對象。
2. 提供一個靜態的成員函數,在該靜態成員函數中完成堆對象的創建
class HeapOnly
{
public:static HeapOnly* CreateObject(){return new HeapOnly;//智能在堆上new}
private:HeapOnly() {}// C++98// 1.只聲明,不實現。因為實現可能會很麻煩,而你本身不需要// 2.聲明成私有HeapOnly(const HeapOnly&);// or
// C++11HeapOnly(const HeapOnly&) = delete;HeapOnly& operator=(const HeapOnly&) = delete;};
調用成員函數需要對象,因此將函數指定為static讓其只屬于類,不屬于對象?
//調用成員函數需要對象,因此將函數指定為static讓其只屬于類,不屬于對象HeapOnly* obj4 = HeapOnly::CreateObject();
第一種辦法:
為了防止調用拷貝構造,因此將拷貝構造也設置為私有,或者使用C++11的方法,直接封掉拷貝構造
HeapOnly obj5(*obj4);
一般封了拷貝構造,將賦值也封上,避免發生淺拷貝。
第二種方法:
將析構私有化(讓析構無法自動調用):再自己寫函數顯示調用自己寫的釋放指針函數:
class HeapOnly { public:void Release(){delete this;} private:~HeapOnly(){ } };
請設計一個類,只能在棧上創建對象
同上將構造函數私有化,然后設計靜態方法創建對象返回即可
class StackOnly { public:static StackOnly CreateObj(){return StackOnly();}// 禁掉operator new可以把下面用new 調用拷貝構造申請對象給禁掉// StackOnly obj = StackOnly::CreateObj();// StackOnly* ptr3 = new StackOnly(obj);void* operator new(size_t size) = delete;void operator delete(void* p) = delete; private:StackOnly():_a(0){} private:int _a; };
請設計一個類,不能被繼承
C++98方式
// C++98中構造函數私有化,派生類中調不到基類的構造函數。則無法繼承 class NonInherit { public:static NonInherit GetInstance(){return NonInherit();} private:NonInherit(){} };
C++11方法
final關鍵字,final修飾類,表示該類不能被繼承。
class A final {// .... };
請設計一個類,只能創建一個對象(單例模式)
設計模式:
設計模式(Design Pattern)是一套被反復使用、多數人知曉的、經過分類的、代碼設計經驗的總結。為什么會產生設計模式這樣的東西呢?就像人類歷史發展會產生兵法。最開始部落之間打仗時都是人拼人的對砍。后來春秋戰國時期,七國之間經常打仗,就發現打仗也是有套路的,后來孫子就總結出了《孫子兵法》。孫子兵法也是類似。
使用設計模式的目的:為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。 設計模式使代碼編寫真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。
設計模式舉例:
單例模式
迭代器模式
適配器模式
觀察者
工廠?
單例模式:
一個類只能創建一個對象,即單例(單實例)模式,該模式可以保證系統中該類只有一個實例,并提供一個訪問它的全局訪問點,該實例被所有程序模塊共享。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然后服務進程中的其他對象再通過這個單例對象獲取這些配置信息,這種方式簡化了在復雜環境下的配置管理。
單例模式有兩種實現模式:
餓漢模式
就是說不管你將來用不用,程序啟動時就創建一個唯一的實例對象。
優點:簡單
缺點:可能會導致進程啟動慢,且如果有多個單例類對象實例啟動順序不確定。
餓漢(很饑餓,極早就創建對象)模式 : 一開始就(main函數之前)就創建對象
問題1:很多單例類,都是餓漢模式,有些單例對象初始化資源很多,導致程序啟動慢,遲遲進不了main函數
問題2:如果兩個單例類有初始化依賴關系,餓漢也無法解決。比如A類和B類是單例,A單例要連接數據庫,B單例要用A單例訪問數據庫。
示例代碼:
//餓漢(很饑餓,極早就創建對象)模式 : 一開始就(main函數之前)就創建對象//問題1:很多單例類,都是餓漢模式,有些單例對象初始化資源很多,導致程序啟動慢,遲遲進不了main函數//如果兩個單例類有初始化依賴關系,餓漢也無法解決。比如A類和B類是單例,A單例要連接數據庫,B單例要用A單例訪問數據庫
class ConfigInfo
{
public:static ConfigInfo* GetInstance(){return &_sInfo;//可以用指針也能用引用(看需求)}string GetIp(){return _ip;}void SetIp(const string& ip){_ip = ip;}private://1.私有化構造函數ConfigInfo(){cout << "ConfigInfo()" << endl;}//2.把拷貝構造和賦值封掉ConfigInfo(const ConfigInfo&) = delete;ConfigInfo& operator=(const ConfigInfo&) = delete;
private:string _ip = "127.0.0.1";//存網絡ipint _port = 80;//端口//...// 聲明 static ConfigInfo _sInfo;//受類域的限制,但實際上在靜態區,這里只是一個聲明,需要在類外面定義
};// 定義
ConfigInfo ConfigInfo::_sInfo;
int main()
{//ConfigInfo info;cout << ConfigInfo::GetInstance() << endl;ConfigInfo::GetInstance()->SetIp("192.22.1.13");cout << ConfigInfo::GetInstance()->GetIp() << endl;return 0;
}
如果這個單例對象在多線程高并發環境下頻繁使用,性能要求較高,那么顯然使用餓漢模式來避免資源競爭,提高響應速度更好
?
懶漢模式
優點:第一次使用實例對象時,創建對象。進程啟動無負載。多個單例實例啟動順序自由控制。
如果單例對象構造十分耗時或者占用很多資源,比如加載插件啊, 初始化網絡連接啊,讀取文件啊等等,而有可能該對象程序運行時不會用到,那么也要在程序一開始就進行初始化,就會導致程序啟動時非常的緩慢。 所以這種情況使用懶漢模式(延遲加載)更好。
懶漢就是第一次調用時,創建單例對象
//懶漢完美解決上面餓漢存在的問題
class ConfigInfo
{
public:static ConfigInfo* GetInstance(){static ConfigInfo info;//定義一個局部的靜態對象return &info;}string GetIp(){return _ip;}void SetIp(const string& ip){_ip = ip;}private://1.私有化構造函數ConfigInfo(){cout << "ConfigInfo()" << endl;}//2.把拷貝構造和賦值封掉ConfigInfo(const ConfigInfo&) = delete;ConfigInfo& operator=(const ConfigInfo&) = delete;
private:string _ip = "127.0.0.1";//存網絡ipint _port = 80;//端口//...
};int main()
{//ConfigInfo info;cout << ConfigInfo::GetInstance() << endl;cout << ConfigInfo::GetInstance() << endl;cout << ConfigInfo::GetInstance() << endl;ConfigInfo::GetInstance()->SetIp("192.22.1.13");cout << ConfigInfo::GetInstance()->GetIp() << endl;return 0;
}
缺點是:在C++11前,局部static對象構造,有線程安全的風險
C++11之前的解決辦法:仍然在第一次調用的時候再創建
class ConfigInfo
{
public:static ConfigInfo* GetInstance(){if (_spInfo == nullptr){_spInfo = new ConfigInfo;}return _spInfo;}string GetIp(){return _ip;}void SetIp(const string& ip){_ip = ip;}private://1.私有化構造函數ConfigInfo(){cout << "ConfigInfo()" << endl;}//2.把拷貝構造和賦值封掉ConfigInfo(const ConfigInfo&) = delete;ConfigInfo& operator=(const ConfigInfo&) = delete;
private:string _ip = "127.0.0.1";//存網絡ipint _port = 80;//端口//...// 定義一個靜態指針static ConfigInfo* _spInfo;//受類域的限制,但實際上在靜態區,這里只是一個聲明,需要在類外面定義
};ConfigInfo* ConfigInfo::_spInfo = nullptr;
int main()
{//ConfigInfo info;cout << ConfigInfo::GetInstance() << endl;cout << ConfigInfo::GetInstance() << endl;cout << ConfigInfo::GetInstance() << endl;ConfigInfo::GetInstance()->SetIp("192.22.1.13");cout << ConfigInfo::GetInstance()->GetIp() << endl;return 0;
}
現在還不能避免多線程調用的線程安全的問題:
如果兩個線程都來,t1先來正在new的時候,t2判斷了_spInfo為空,也要開始new,t1已經new完了,賦值給了_spInfo,最怕的是,t2時間片到了,開始在判斷后的地方休眠,t1繼續執行,并且已經SetIp(),t2醒了,又開始new,將t1申請的全給覆蓋,導致內存泄漏。因此需要加鎖?。
注意靜態成員函數,只能訪問靜態的成員變量,因此mutex的對象也要設置成靜態的:
注意看注釋
class ConfigInfo
{
public:static ConfigInfo* GetInstance(){//加鎖t1、t2只能一個進判斷,保證只會new一個對象//單單只這樣寫,會導致,已經創建好對象了,但是每次調用GetInstance的時候都需要再被加鎖/* unique_lock<mutex> lock(_mtx);if (_spInfo == nullptr){_spInfo = new ConfigInfo;}*///因為不能直接將鎖加載if判斷內,這仍然會導致線程安全,要的就是,每個線程都得判斷對象是否創建//解決辦法:多寫一層if (_spInfo == nullptr){unique_lock<mutex> lock(_mtx);if (_spInfo == nullptr){_spInfo = new ConfigInfo;}}return _spInfo;}string GetIp(){return _ip;}void SetIp(const string& ip){_ip = ip;}private://1.私有化構造函數ConfigInfo(){cout << "ConfigInfo()" << endl;}//2.把拷貝構造和賦值封掉ConfigInfo(const ConfigInfo&) = delete;ConfigInfo& operator=(const ConfigInfo&) = delete;
private:string _ip = "127.0.0.1";//存網絡ipint _port = 80;//端口//...// 定義一個靜態指針static ConfigInfo* _spInfo;//受類域的限制,但實際上在靜態區,這里只是一個聲明,需要在類外面定義static mutex _mtx;
};ConfigInfo* ConfigInfo::_spInfo = nullptr;
mutex ConfigInfo::_mtx;
int main()
{//ConfigInfo info;cout << ConfigInfo::GetInstance() << endl;cout << ConfigInfo::GetInstance() << endl;cout << ConfigInfo::GetInstance() << endl;ConfigInfo::GetInstance()->SetIp("192.22.1.13");cout << ConfigInfo::GetInstance()->GetIp() << endl;return 0;
}
C++的類型轉換
C語言中的類型轉換
在C語言中,如果賦值運算符左右兩側類型不同,或者形參與實參類型不匹配,或者返回值類型與接收返回值類型不一致時,就需要發生類型轉化,C語言中總共有兩種形式的類型轉換:隱式類型轉換和顯式類型轉換。
1. 隱式類型轉化:編譯器在編譯階段自動進行,能轉就轉,不能轉就編譯失敗
2. 顯式類型轉化:需要用戶自己處理
隱式類型轉換一般是類型關聯度緊密(像是指針和double不能轉換)的就能轉,整形、浮點型、char(ASCII碼)之間就能互相隱式類型轉。
顯式(強制)類型轉換一般就是任意指針之間,整形(數據大小)和指針(字節編號)之間
示例:
// 隱式類型轉換double d = i;printf("%d, %.2f\n", i, d);int* p = &i;// 顯示的強制類型轉換int address = (int)p;printf("%p, %d\n", p, address);
缺陷:
轉換的可視性比較差,所有的轉換形式都是以一種相同形式書寫,難以跟蹤錯誤的轉換
CPP中類型轉換
為什么C++需要四種類型轉換
C風格的轉換格式很簡單,但是有不少缺點的:
1. 隱式類型轉化有些情況下可能會出問題:比如數據精度丟失
2. 顯式類型轉換將所有情況混合在一起,代碼不夠清晰
因此C++提出了自己的類型轉化風格,注意因為C++要兼容C語言,所以C++中還可以使用C語言的轉化風格。
隱式類型轉換
示例:
仍然支持有一定關聯的類型的類型轉換
1.C++內置類型支持轉換為自定義類型
2.C++自定義類型支持轉換成內置類型
單參數的構造函數支持隱式類型的轉換
示例:內置類型 -> 自定義類型
class A { public:A(int a1):_a1(a1){}A(int a1, int a2):_a1(a1), _a2(a2){} private:int _a1 = 1;int _a2 = 1; };
// 內置類型 -> 自定義類型 A aa1 = 1; // C++11 A aa2 = {2,2};
示例:自定義類型 -> 內置類型
默認是不支持的,我們需要重載operator int
int x = aa1; cout << x << endl;
想轉什么類型就寫什么類型: 這里想轉成int,因此就寫int
operator int(){return _a1 + _a2;}
在OJ當中會遇到這樣的寫法:
while (cin>>x){cout << x << endl; }
原理是:
while (cin>>x)while (cin.operator>>(x))while (cin.operator>>(x).operator bool()){cout << x << endl;}
如果想要輸入string呢?
string str;while (cin>>str){cout << str << endl;}
自定義類型和自定義類型之間的轉換
比如我現在有一個B類型,正常情況下:能否將B類型轉為A類型,不可以,強制類型轉換也不可以
class B { public:private:int _b1; };
解決辦法:用構造函數來支持相關的轉換
class B { public:B(const A& aa):_b1(aa.get()){}private:int _b1; };
B bb = aa1;
就能支持了(實際上,我們模擬寫const iterator和普通iterator時,就用的這個方法)
隱式類型轉換的坑:
// 隱式類型轉換的坑 void Insert(size_t pos) {int end = 10;while (end >= pos){cout << end << endl;--end;} } // int main() {Insert(5);Insert(0);return 0; }
運行結果:由于隱式類型轉換導致的死循環,原因是因為,這里的int被隱式類型轉換為unsize_int
C++強制類型轉換
標準C++為了加強類型轉換的可視性,引入了四種命名的強制類型轉換操作符: static_cast、reinterpret_cast、const_cast、dynamic_cast
static_cast(對應之前的隱式類型轉換)
static_cast用于非多態類型的轉換(靜態轉換),編譯器隱式執行的任何類型轉換都可用static_cast,但它不能用于兩個不相關的類型進行轉換。
int i = 1;// 隱式類型轉換 : static_castdouble d = static_cast<double>(i);printf("%d, %.2f\n", i, d);int* p = &i;// error C2440: “static_cast”: 無法從“int *”轉換為“int”// int address1 = static_cast<int>(p);
reinterpret_cast(對應的之前的顯式類型轉換)
reinterpret_cast操作符通常為操作數的位模式提供較低層次的重新解釋,用于將一種類型轉換為另一種不同的類型
示例:
// 顯示的強制類型轉換 : reinterpret_castint address = reinterpret_cast<int>(p);printf("%p, %d\n", p, address);
之前不支持的還是不會支持?
// 類型強制轉換”: 無法從“int *”轉換為“double”// double x = (double)p;// 類型強制轉換”: 無法從“int *”轉換為“double”//double x = reinterpret_cast<double> p;
const_cast
const_cast最常用的用途就是刪除變量的const屬性,方便賦值
示例:
const int a = 1;//a不是存在常量區的,還是在棧上的(常變量// a++;int x = 0;cout << &a << endl;cout << &x << endl;
是可以修改的,間接修改:
int* ptr = (int*)&a;(*ptr)++;cout << *ptr << endl;cout << a << endl;
為啥用*ptr間接去訪問拿到的是2,直接訪問a又拿到的是1
但是又通過監視窗口,又會發現:a已經被修改了,為什么卻打印1。
這是因為編譯器的優化,第一種是因為:a是const修飾了的,不會被改變,把a存在寄存器中,寄存器中的a值沒有被改變,內存變了,但是取值是直接從寄存器中拿。
第二種是:a直接被替換成 1,類似于宏,在編譯的時候被替換
這個時候就可以在定義a的時候加一個關鍵字:volatile,穩定的。告訴編譯器別優化,每次都到內存當中取
volatile const int a = 1;
運行結果:
因此增加一個const_cast,來顯式告訴自己,a的const屬性已經被去掉了,他已經可以被改變了,提醒要加上volatile:
int* ptr = const_cast<int*>(&a);
示例:這種情況用哪一種隱式類型轉換方式
A aa1 = static_cast<A>(1);
這是單參數的構造函數,還是應該走隱式類型的轉換因此使用 static_cast ,reinterpret_cast是顯式類型轉換。
dynamic_cast(專門爭對C++設計的,向下轉換)
dynamic_cast用于將一個父類對象的指針/引用轉換為子類對象的指針或引用(動態轉換)
向上轉型:子類對象指針/引用->父類指針/引用(是天然的,不會產生臨時對象,切片父類,不需要轉換,賦值兼容規則)
向下轉型:父類對象指針/引用->子類指針/引用(用dynamic_cast轉型是安全的)
注意:
1. dynamic_cast只能用于父類含有虛函數的類
2. dynamic_cast會先檢查是否能轉換成功,能成功則轉換,不能則返回0
示例:
class A
{
public:virtual void f() {}int _a1 = 1;
};class B : public A
{
public:int _b1 = 1;
};void fun(A* pa)
{// 無差別轉換,存在一定風險B* pb = (B*)pa;cout << pb << endl;//pb->_b1++;
}int main()
{A a;B b;fun(&a);fun(&b);return 0;
}
這樣轉換是有風險,我們分別給A、B成員_a1,_b1,pa指針有可能只想A對象也有可能指向B對象,如果原本就指向B對象,將你強轉成B是沒有問題的。轉換的時候,現在就指向整個B。如果原來指向A,轉成B之后,會多看一部分,但是那一部分并不屬于pb,越界。
因此C++設計了dynamic_cast來動態轉換:
不是無差別轉換,他會檢查pa原本指向什么對象,如果指向B對象,轉換成功,如果指向A對象,轉換失敗,返回空。能識別是指向父類還是子類。
void fun(A* pa)
{// pa指向B對象,轉換成功// pa指向A對象,轉換失敗,返回空B* pb = dynamic_cast<B*>(pa);if (pb){cout << pb << endl;pb->_b1++;}else{cout << "轉換失敗" << endl;}
}
RTTI(了解)運行時類型識別
RTTI:Run-time Type identification的簡稱,即:運行時類型識別。C++通過以下方式來支持RTTI:
1. typeid運算符
2. dynamic_cast運算符
3. decltype
復習RAII是什么:把一個資源交給一個對象管理,可能是動態開辟的資源,或者鎖,當生命周期到了自動調用析構函數,解鎖、釋放資源
結語:
? ? ? ?隨著這篇博客接近尾聲,我衷心希望我所分享的內容能為你帶來一些啟發和幫助。學習和理解的過程往往充滿挑戰,但正是這些挑戰讓我們不斷成長和進步。我在準備這篇文章時,也深刻體會到了學習與分享的樂趣。 ? ?
? ? ? ? ?在此,我要特別感謝每一位閱讀到這里的你。是你的關注和支持,給予了我持續寫作和分享的動力。我深知,無論我在某個領域有多少見解,都離不開大家的鼓勵與指正。因此,如果你在閱讀過程中有任何疑問、建議或是發現了文章中的不足之處,都歡迎你慷慨賜教 ? ? 。
? ? ? ? 你的每一條反饋都是我前進路上的寶貴財富。同時,我也非常期待能夠得到你的點贊、收藏,關注這將是對我莫大的支持和鼓勵。當然,我更期待的是能夠持續為你帶來有價值的內容。