## 引言 > 類(class)的使用分為兩種——基於對象(object Based)和麵向對象(object oriented) > > 基於對象是指,程式設計中單一的類,和其他類沒有任何關係 > > 單一的類又分為:不帶指針的類(class without pointer members)和帶指 ...
引言
類(class)的使用分為兩種——基於對象(object Based)和麵向對象(object oriented)
基於對象是指,程式設計中單一的類,和其他類沒有任何關係
單一的類又分為:不帶指針的類(class without pointer members)和帶指針的類(class with pointer members)
面向對象則是類(class)中涉及了類之間的關係:複合(composition)、委托(delegation)、繼承(inheritance)
1.頭文件的防禦式聲明
#ifndef xxx
#define xxx
...
#endif
在編寫頭文件時應該有這樣的一種習慣
目的是避免多次重覆包含同一個頭文件,否則會引起變數及類的重覆定義
2.使用初始化列表的好處
- 只有構造函數這類函數具有“初始化列表”這一特性
- 從結果上來看,構造函數時使用初始化列表和在類內賦值是一樣的,但我們都知道一個變數必須先初始化然後才被賦值,而初始化列表顧名思義是只執行初始化這一步,在類內賦值時就要先初始化再被賦值,所以從執行效率上講,使用初始化列表會更快,也更簡潔
3.設計模式:singleton(單例類)
- demo:
class A {
public:
static A & getInstance();
setup() {...}
private:
A();
A(const A & rhs);
...
}
A & A::getInstance()
{
static A a;
return a;
}
...
//外部介面
A::getInstance().setup();
-
原理:將構造函數設置為私有屬性,同時設置一個靜態函數介面返回一個該類對象
-
作用:保證每一個類僅有一個實例,併為它提供一個全局訪問點
-
單例模式(Singleton)的主要特點不是根據用戶程式調用生成一個新的實例,而是控制某個類型的實例唯一性。它擁有一個私有構造函數,這確保用戶無法通過new直接實例它。除此之外,該模式中包含一個靜態私有成員變數instance與靜態公有方法Instance()。Instance()方法負責檢驗並實例化自己,然後存儲在靜態成員變數中,以確保只有一個實例被創建。
這種模式主要有以下特征或條件:
- 有一個私有的無參構造函數,這可以防止其他類實例化它,而且單例類也不應該被繼承,如果單例類允許繼承那麼每個子類都可以創建實例,這就違背了Singleton模式“唯一實例”的初衷。
- 單例類被定義為sealed,就像前面提到的該類不應該被繼承,所以為了保險起見可以把該類定義成不允許派生,但沒有要求一定要這樣定義。
- 一個靜態的變數用來保存單實例的引用。
- 一個公有的靜態方法用來獲取單實例的引用,如果實例為null即創建一個。
-
參考:
4.常成員函數的重要性
-
如果一個成員函數不改變類的數據成員時,就把它聲明為常函數,這是一個好的習慣
-
當實例化一個常對象時,常對象要求不能改變數據成員,如果成員函數不加const,將無法調用此成員函數,編譯器不會通過,即使此函數確實沒有改變數據成員;同時,即使成員函數被聲明為了常函數,實例化一個普通對象時依然可以調用。
-
簡單來說,不聲明為常成員函數可能不會有問題,但聲明為常成員函數能確保一定不出問題
5.如何解釋成員函數接收同類對象參數...
-
問:如何解釋一個類的成員函數在接收同類對象的參數(比如拷貝構造函數)可以直接調用該對象的任何成員,明明既不是友元也不是嵌套?
-
答:相同class的各個objects互為friends(友元)
6.設計一個類要考慮什麼
- 目的:高效、安全、簡潔、嚴密
- 1.數據成員私有
- 2.參數傳遞和返回值優先考慮用引用(傳遞的是地址值,這樣不管傳遞的數據記憶體占用多大,依然固定傳入四個位元組,即使當傳遞字元這樣小於四個位元組時用值傳遞確實比引用或指針傳遞更快一些,但不必考慮這些細枝末節)
- 3.構造函數優先去使用初始化列表
- 4.能聲明為常成員就聲明為常成員
7.返回值加不加引用
- 取決於返回的值是否要改變、是否可以改變,前者由我們決定,後者由語法限制
兩個案例
class A
{
int value;
...
};
...
A& fun1(A* x, const A& y)
{
x.value += y.value; //第一參數會改變,第二參數不會改變
return *x
}
class B
{
int value;
...
};
...
B fun2(const B& x, const B& y)
{
//第一參數和第二參數都不會改變
return B(x.value + y.value);
}
8.運算符重載成員函數的思考
e.x.
inline complex&
_doapl(comlex* ths, const complex& r)
{
...
return *ths;
}
inline complex&
complex::operator += (const comlex& r)
{
return _doapl(this, r);
}
...
comlex c1(2,1), c2(3), c3;
//c2 += c1;
//c3 += c2 += c1;
當重載一個二元運算符為成員函數時,我們知道重載函數除了右操作數是我們傳遞的,函數還會預設用一個this指針,來接收左操作數
那麼可能會有疑問,我們想要改變的是左操作數,而且由於傳遞的是指針,函數內也確實可以改變,那返回值又有什麼用呢,聲明為空不就行了。
當我們使用重載運算符時只是像被註釋的第一行代碼一樣,那麼返回值確實不重要,但是當我們使用的形式像被註釋的第二行代碼時,那麼返回值就很重要,因為c2.+=(c1) 這個函數的返回值就是 c3 += () 函數的參數
9.temp object(臨時對象)
- 語法: typename ();
- 生存期:僅聲明那一行
- 返回值是臨時對象時不難return by reference
10.<<重載的一些註意點
-
只能重載為非成員函數
-
左操作數固定為系統定義的 ostream 類型,且為非常量引用
- 非常量:向流寫入內容其實就是改變了它的狀態
- 引用:無法複製一個 ostream 對象
- 註:ostream 類與 istream 類一樣,它的的拷貝構造函數和賦值函數都是保護類型的,所以 ostream 是不允許拷貝或者賦值的,所以它也不能直接作為返回類型和參數傳遞,很多時候需要使用引用來進行傳遞。
-
最好加返回值且為引用,原因前面已經說明,且我們對於連續調用<<的頻率要大得多
-
連續調用時的調用順序
complex c1, c2; cout << c1 << c2; //先執行 <<(cout, c1) 函數 //返回的 ostream類型的cout的引用 又作為<<(ostream &, c2)的第一參數
-
在語法上我們當然也可以重載為成員函數,只要左操作數為自定義類型即可,但這樣並不符合我們通常的書寫習慣
11.Big Three(三位一體原則)
-
三大件:拷貝構造、拷貝賦值、析構函數
-
解釋:當一個類需要我們去主動設計析構函數時,那它很大概率也需要一個拷貝構造函數和賦值運算符重載成員函數
-
應用:當一個類具有指針成員時(class with point member)或者說當我們設計了一個有動態記憶體管理的類時
-
原因:
-
析構函數角度:預設析構函數會僅刪除指向對象的指針,而刪除一個指針不會釋放指針指向對象占用的記憶體,最終會導致記憶體泄露。
-
拷貝構造角度:預設的構造函數是淺拷貝,複製的只是指針也就是地址值,這樣導致兩個對象共用一個記憶體空間,這是十分危險的,當其中一個對象被刪除後,析構函數將釋放那片共用的記憶體空間,接下來對這片已經釋放了記憶體的任何引用都將會導致不可遇見的後果。
-
賦值運算角度:
賦值相比於拷貝構造要考慮更多
首先是自我賦值判斷,如果不判斷,當左右操作數指向的是同一個地址時,會造成將左操作數對象的元素刪除並釋放其占用的記憶體,同時由於左右操作數指向同一對象,導致右操作數同時被刪除,但接下來還要將右操作對象複製,這會造成不可預知的結果。這也被稱為證同測試。
其次是進行三步必要操作:
- 釋放已有記憶體
- 開闢新的記憶體
- 內容賦值
-
-
代碼示例:
#ifndef __MYSTRING__
#define __MYSTRING__
class String
{
public:
String(const char* cstr=0);//構造函數
String(const String& str);//拷貝構造函數
String& operator=(const String& str);//重載=運算符
~String();//析構函數
char* get_c_str() const { return m_data; }//成員函數,返回指向字元數組首地址的指針
private:
char* m_data;//字元數組指針
};
#include <cstring>
//構造函數
inline
String::String(const char* cstr)
{
//開闢記憶體、計算長度、內容拷貝
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
//析構函數
inline
String::~String()
{
delete[] m_data;//釋放指針指向空間
}
//重載=
inline
String& String::operator=(const String& str)
{
//檢測自我賦值(self assignment)
if (this == &str)
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
//構造函數
inline
String::String(const String& str)
{
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
#include <iostream>
using namespace std;
//重載<<
ostream& operator<<(ostream& os, const String& str)
{
os << str.get_c_str();
return os;
}
#endif
12.new和構造函數,delete和析構函數
-
當我們使用new創建了一個指向類的對象的指針時
這裡的new幹了三件事:
- 調用 operator new 函數,這個函數內部又調用了malloc函數來分配記憶體
- operator new 函數返回的是空指針,顯式轉換為類類型指針後賦值給我們創建的指針
- 調用指針指向對象的構造函數
- 當我們使用delete釋放一個指向對象的指針時
- 首先調用對象的析構函數,這裡該類的析構函數又用delete釋放了類內動態分配的數組指針
- 然後再釋放這個指向對象的指針
- 結合本例來看,就是delete ps,先delete它指向的成員,再delete它自己
13.malloc()動態分配記憶體的結構
-
紅色部分是 cookie ,記錄記憶體分配的總大小,就是圖中的41,其最低位用於表示是否已分配(1表示已分配,0表示已回收),之所以最低位可以變,是因為分配的記憶體總空間一定是16的倍數,其16進位表示時最低位一定為0,也就是說這個位置是空出來的,剛好用來表示記憶體狀態。每一個 new 的對象都會有上下兩個 cookie,來預先申請一塊記憶體池,然後供對象實例化。
-
綠色部分是調用malloc()時向系統申請的記憶體,該函數返回時,也會返回這塊區域開頭的指針。
-
綠色部分上下兩塊 gap 預先被填充為了0xfdfdfdfd,用來分隔客戶可以使用的記憶體區和不可使用的記憶體區,同時,當這塊記憶體被歸還時,編輯器也可以通過下gap的值區判斷當前記憶體塊是否被越界使用了
-
從gap向上連續的7個記憶體空間共同組成了debug header,從上向下標號為1-7
- 1、2兩塊空間保存了兩根指針,目的是使多個記憶體塊連接成鏈表。
- 3空間保存了申請本記憶體塊的文件名
- 4空間保存了申請本記憶體塊的代碼行數
- 5空間記錄了本記憶體塊中實際可以被用戶使用的記憶體空間的大小
- 6空間記錄了當前記憶體塊的流水號,即是鏈表中的第幾個,從1開始
- 7空間記錄了當前記憶體塊被分配的形式
-
填補區pad
參考:
https://zhuanlan.zhihu.com/p/492161361
https://www.cnblogs.com/zyb993963526/p/15682014.html#_label2
https://blog.csdn.net/qq_61500888/article/details/122170203
14.array new 搭配 array delete
動態分配數組時要註意的:
- 其記憶體區域相比上面所提到的多了一個記憶體塊用來記錄數組長度(分配對象數量)
- 當申請記憶體後,返回的指針指向數據開始處,而使用 delete[] 釋放時,指針會指向它的上一塊,也就是記錄數組長度的那一塊,從而可以根據對象的數量調用相應次數的析構函數。如果使用 delete 釋放的話,它不會去獲取對象的長度,而是只調用指針指向的那一個對象的析構函數。
- 如果對象的類型是內置類型或者是無自定義的析構函數的類類型,是可以使用 delete 來釋放 new[] 對象的。但是,如若不然,使用 delete 來釋放對象,對象所分配的記憶體空間雖然會照樣全部釋放,但是只會調用第一個對象的析構函數,這就導致記憶體泄漏。所以,養成良好的習慣,new [] 必 delete []
15.this指針
類的每一個非靜態成員函數(包括構造函數、拷貝構造等)都隱含著一個指針形參名為this,當對象調用成員函數時就會隱含傳遞該對象的地址給它,這也是為什麼一個類的成員函數雖然只有一份但也會根據接收的消息不同產生不同的行為,而靜態成員函數不隱含this指針,所以即使調用它的對象不同維護的依然是同一段代碼
16.namespace
三個案例:
- 使用using引入一個命名空間的全部
- 使用using引入一個命名空間的個體
- 不用using,使用時手動引入