C++對象模型
對象
對象模型分類
在C++中,成員數據(class data member)有兩種:static和nonstatic,成員函數(class member function)有三種:static、nonstatic和virtual。
-
簡單對象模型
class Point
{
public:
Point(float xval);
virtual ~Point();
float x() const;
static int PointCount();
protected:
virtual ostream& print(ostream& os) const;
float _x;
static int _point_count;
}; -
表格驅動模型
-
c++對象模型(目前採用的對象模型)
類對象所佔用的空間
-
一個空類佔一個字節
class A
{
public:};
int main()
{
A obj;
cout << sizeof(obj) << endl; //1
cout << sizeof(A) << endl;//1
} -
普通成員函數和靜態成員函數不計算在sizeof內
一:
class A
{
public:
void fun(){}//成員函數,靜態成員函數也不佔用內存空間
};
int main()
{
A obj;
cout << sizeof(obj) << endl;//1
cout << sizeof(A) << endl;//1
}二:
class A
{
public:
void fun(){ int a; }//成員函數};
int main()
{
A obj;
cout << sizeof(obj) << endl;//1
cout << sizeof(A) << endl;//1
} -
靜態成員變量不計算在sizeof內
class A
{
public:
static int a;
static int b;
};
int main()
{
A obj;
cout << sizeof(obj) << endl;//1
cout << sizeof(A) << endl;//1
}結論:靜態成員變量跟着類走,不佔用對象內存空間。
-
虛函數不計算在對象的sizeof內,但是會存在一個虛函數表指針
class A
{
public:virtual void fun1(){ } virtual void fun2(){ }
};
int main()
{
A obj;
cout << sizeof(obj) << endl;//4
cout << sizeof(A) << endl;//4
}
結論:不管虛函數有幾個,都是佔4個字節。
虛函數表:vtbl
虛函數表:跟着類走,用來保存指向類裏面每個虛函數的指針,即如果類裏面有一個虛函數,那保存的指針就有一個,如果有兩個虛函數,那虛函數表裏就就保存有兩個指針。針對於上面的結果進行分析,爲什麼是佔用4個字節呢?
答:這4個字節是一個指針(vptr),這個指針用來指向虛函數表。
這個指針的值,系統會在適當的時機,比如調用構造函數時,給這個指針賦值。也就是虛函數表的首地址。
結論:虛函數不計算在類對象的sizeof裏,但是會額外增加一個虛函數表指針
另外,虛析構函數也是佔用4個字節。
需要說明的是:爲什麼普通成員函數不需要搞虛函數表,而虛函數例外呢?
因爲虛函數的多態性問題,所以虛函數的處理方式與普通的成員函數不一樣。 -
字節對齊問題
字節對齊總的來說是爲了提高訪問速度。
如果類裏有的成員變量是指針,例如,int *p,char *str等等,就佔用4個字節,,當然Linux平臺下可能是8字節。
this指針調整問題(出現在多重繼承中,調用哪個子類的成員函數,這個this指針就會被編譯器自動調整到對象內存佈局中對應改子類對象的起始地址那去
範例一:
class A
{
public:
A()
{
printf(“A():%p\n”, this);
}
void funA()
{
printf(“funA():%p\n”, this);
}
public:
int a;
};
class B
{
public:
B()
{
printf(“B():%p\n”, this);
}
void funB()
{
printf(“funB():%p\n”, this);
}
public:
int b;
};
class C :public A, public B //繼承的順序和類C的內存空間佈局有關
{
public:
C()
{
printf(“C():%p\n”, this);
}
void funC()
{
printf(“funC():%p\n”, this);
}
public:
int c;
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof© << endl;
C obj;
obj.funA();
obj.funB();
obj.funC();
}
結論:如果派生類只繼承一個基類,那麼這個基類的地址和派生類的地址相同。
如果一個類,同時繼承多個基類,那麼這個子類的對象和它繼承順序的第一個基類的地址相同。
範例二:
class A
{
public:
A()
{
printf("A():%p\n", this);
}
void funA()
{
printf("A::funA():%p\n", this);
}
public:
int a;
};
class B
{
public:
B()
{
printf(“B():%p\n”, this);
}
void funB()
{
printf(“B::funB():%p\n”, this);
}
public:
int b;
};
class C :public A, public B
{
public:
C()
{
printf(“C():%p\n”, this);
}
void funB() //覆蓋掉類B中的funb函數,所以調用該函數時,使用的this指針就會調整,即用類C的this指針去調用該函數。
{
printf(“C::funB():%p\n”, this);
}
void funC()
{
printf(“C::funC():%p\n”, this);
}
public:
int c;
};
int main()
{
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
cout << sizeof© << endl;
C obj;
obj.funA();
obj.funB();
obj.funC();
}
總結:該案列只有一些簡單的成員函數,無虛函數,,所以分析起來也較簡單。
這種情況的話,一般時出現在多重繼承(繼承多個父類)中,,後面的話,,調用那個子類或者父類的成員函數,,就用誰的this指針去調用。
比如說,,這裏有3個類,,A和C的this指針是相同的(和繼承順序有關),所以調用A和C的成員函數的this指針相同,,調用B的成員函數,就用B的this指針去調用。
上面,C類覆蓋了B裏的一個成員函數,所以再調用這個成員函數的話,調用這個函數的話就變成調用C裏的這個函數了,也就是用C的this指針去調用。
編譯器合成默認構造函數的5種情況
-
如果一個類沒有任何構造函數,但包含一個類類型的成員變量,而這個類類型的成員變量有一個默認構造函數
class A
{
public:
A()
{
cout << “aaaaa” << endl;
}
};class C
{
public:
int c;
A a;
};
int main()
{
C c;//會調用A()
}
分析:爲什麼會調用A()呢?其實是編譯器爲類C合成了一個默認的構造函數,而這個默認構造函數又去調用A(),來初始化a,所以會調用A() -
父類帶有默認構造函數,子類沒有任何構造函數
class A
{
public:
A()
{
cout << “aaaaa” << endl;
}
};
class B:public A
{
public:};
int main()
{
B b;
}父類有一個默認構造函數,而子類沒有構造函數時,編譯器會爲子類合成一個默認構造函數,從而讓這個合成的默認構造函數去調用父類中的構造函數。
-
一個類有虛函數,但是該類沒有任何構造函數
note:有虛函數,就會存在虛函數表,所以編譯器會合成一個默認構造函數,這個默認構造函數的目的是將虛函數表首地址賦給虛函數表指針。
class A
{
public:virtual void fun() { cout << "aaaaa" << endl; }
};
A a;//只是這裏不會去調用該虛函數,因爲編譯器安插的代碼中沒這麼幹。
-
一個類帶有虛基類(給虛基類表賦值以及調用父類的構造函數)
虛基類(虛繼承)只會出現在三層結構中:
class Grand
{
public:
int a;
};
class A:virtual public Grand//虛繼承
{
public:};
class A2 :virtual public Grand//虛繼承
{
public:
};
class C :public A, public A2
{
public:
};
int main()
{
C c;
} -
定義成員變量時賦初值(c++11)
class A
{
public:
int a =10;
};
編譯器合成拷貝構造函數的4種情況
拷貝構造函數語義:
傳統上,大家認爲:如果我們沒有定義一個自己的拷貝構造函數,編譯器會幫助我們合成 一個拷貝構造函數。
但,這個合成的拷貝構造函數,也是在 必要的時候纔會被編譯器合成出來。 所以 “必要的時候”;是指什麼時候?
那編譯器在什麼情況下會幫助我們合成出拷貝構造函數來呢?那這個編譯器合成出來的拷貝構造函數又要幹什麼事情呢?
(1)如果一個類A沒有拷貝構造函數,但是含有一個類類型CTB的成員變量m_ctb。該類型CTB含有拷貝構造函數,那麼當代碼中有涉及到類A的拷貝構造時,編譯器就會爲類A合成一個拷貝構造函數。
編譯器合成的拷貝構造函數往往都是幹一些特殊的事情。如果只是一些類成員變量值的拷貝這些事,編譯器是不用專門合成出拷貝構造函數來乾的,編譯器內部就幹了;
(2)如果一個類CTBSon沒有拷貝構造函數,但是它有一個父類CTB,父類有拷貝構造函數,
當代碼中有涉及到類CTBSon的拷貝構造時,編譯器會爲CTBSon合成一個拷貝構造函數 ,調用父類的拷貝構造函數。
(3)如果一個類CTBSon沒有拷貝構造函數,但是該類聲明瞭或者繼承了虛函數,
當代碼中有涉及到類CTBSon的拷貝構造時,編譯器會爲CTBSon合成一個拷貝構造函數 ,往這個拷貝構造函數裏插入語句:
(4)如果 一個類沒有拷貝構造函數, 但是該類含有虛基類
當代碼中有涉及到類的拷貝構造時,編譯器會爲該類合成一個拷貝構造函數;
(5)(6)其他編譯器合成拷貝構造函數的情形留給大家探索。
-
一個類A沒有構造函數,但是含有一個類類型B的成員變量m_b,該類型B含有拷貝構造函數,那麼當涉及到類A拷貝構造時,編譯器會爲類A合成一個拷貝構造函數。
class B
{
public:
B(const B&)
{
cout << “B()的拷貝構造函數執行了” << endl;
}
class A
{
public:
B m_b;
};A a1;
A a2 =a1;//實際累A的拷貝構造時纔會合成 -
一個類A沒有拷貝構造函數,但是它有一個父類,父類有拷貝構造函數,當代碼涉及到類A的拷貝構造時,編譯器會類A合成一個拷貝構造函數。
class B
{
public:
B(const B&)
{
cout << “B()的拷貝構造函數執行了” << endl;
}
};
class A:public B
{
public:
};A a1;
A a2 =a1;//實際累A的拷貝構造時纔會合成 -
一個類A沒有拷貝構造函數時,但是該類聲明瞭或者繼承了虛函數,當代碼中涉及到類A的拷貝構造時,編譯器會爲類A合成一個拷貝構造函數。(給虛函數表指針值)
class A
{
public:
virtual void mvirfunc() {}
};
A a1;
A a2 = a1;
合成的原因是,要把a1這個對象的,虛函數表首地址賦值給虛函數表指針,這個動作,,拷貝給a2。 -
如果一個類沒有拷貝構造函數,但是該類含有虛基類時,當代碼涉及到類的拷貝構造時,編譯器會爲該類合成一個拷貝構造函數(涉及虛基類表話題)
虛基類主要解決在多重繼承時,基類可能被多次繼承,虛基類主要提供一個基類給派生類,
#include
using namespace std;
class B0// 聲明爲基類B0
{
int nv;//默認爲私有成員
public://外部接口
B0(int n){ nv = n; cout << “Member of B0” << endl; }//B0類的構造函數
void fun(){ cout << “fun of B0” << endl; }
};
class B1 :virtual public B0
{
int nv1;
public:
B1(int a) :B0(a){ cout << “Member of B1” << endl; }
};
class B2 :virtual public B0
{
int nv2;
public:
B2(int a) :B0(a){ cout << “Member of B2” << endl; }
};
class D1 :public B1, public B2
{
int nvd;
public:
D1(int a) :B0(a), B1(a), B2(a){ cout << “Member of D1” << endl; }// 此行的含義,參考下邊的 “使用注意5”
void fund(){ cout << “fun of D1” << endl; }
};
int main(void)
{
D1 d1(1);
d1.fund();
d1.fun();
return 0;
}
拷貝構造函數的深淺拷貝問題(同一塊內存會釋放兩次的情形)
前言:和默認構造函數類似,在某些情況下,我們只能自己定義自己的拷貝構造函數,而不能使用系統提供的,因爲在某些情況下使用系統提供的拷貝構造函數會帶來一定的影響,例如深淺拷貝。
例一:
class Student
{
public:
int m_age;
int *m_heigh;
public:
Student(int heigh, int age);
~Student();
};
Student::Student(int heigh,int age)
{
m_age = age;
m_heigh = new int;//申請內存空間
*m_heigh = heigh;//往申請的內存空間裏寫值
}
Student::~Student()
{
if (m_heigh != nullptr)
{
delete m_heigh;//在堆上申請的內存需要手動釋放
m_heigh = nullptr;
}
}
Student s1(10,20);
Student s2 = s1;//由於沒有自己定義拷貝構造函數,會造成程序有錯。
當一個類中有指針類的成員時,而我們自己是使用的系統給我們提供的拷貝構造函數,在進行了類似於Student s2 = s1
這種類之間的拷貝動作的時候就會造成程序的錯誤了,爲什麼?
原因在於指針之間的賦值,是把指針指向了一個共同的內存地址,所以在進行析構的時候,這個共同的內存地址就會析構兩次,所以就造成了系統的crash。這就是淺拷貝。
例二:
那麼什麼是深拷貝呢?利用自己定義的拷貝構造函數就可以解決這個問題,這樣一來,s1和s2的指針都會指向不同的內存地址,當然他們各自的內存地址當中的值是一樣的,要達到這樣的目的,就是我們說的深拷貝。
class Student
{
public:
int m_age;
int *m_heigh;
public:
Student(int heigh, int age);
~Student();
Student(const Student &s);//拷貝構造函數,const防止對象被改變
};
Student::Student(int heigh,int age)
{
m_age = age;
m_heigh = new int;
*m_heigh = heigh;
}
Student::~Student()
{
if (m_heigh != nullptr)
{
delete m_heigh;
m_heigh = nullptr;
}
}
//拷貝構造函數裏,當發生拷貝時,重新申請了一塊內存。這樣就避免了同一塊內存地址被釋放兩次。
Student::Student(const Student &s)
{
m_age = s.m_age;
m_heigh = new int;
*m_heigh = *(s.m_heigh);
}
Student s1(10,20);
Student s2 = s1;
cout << s1.m_age << endl;
cout << *(s1.m_heigh) << endl;
cout << s2.m_age << endl;
cout << *(s2.m_heigh) << endl;
移動構造函數語義學
程序轉化語義(我們寫的代碼,編譯器會對代碼進行拆分,拆分成編譯器更容易理解和實現的代碼)
-
定義時初始化
例如:
X X0;
//以下都屬於定義時初始化
X X1 = X0;
X X2 = (X0);
X X3 (X0);對於X X3 = X0;
編譯器如何解析這行代碼?
編譯器會對這行代碼進行拆分,拆分成以下兩行代碼,
X X3_3; //在編譯器看來,這當然不會調用默認構造函數。
X3_3.X::X(X0); -
參數的初始化
-
函數返回值
程序的優化
成員初始化列表
https://blog.csdn.net/qq_38158479/article/details/106888318
-
何時必須使用成員初始化列表
- 類中含有引用類型的成員
- 類中含有const類型成員
- 一個類繼承於另一個類,並且繼承的這個類中有構造函數,且構造函數帶有參數時
- 一個類,含有一個類類型成員,並且這個類類型成員有構造函數(帶參的)
-
使用初始化列表的優勢(對於類中含有類類型成員,可以減少一些構造函數或者賦值運算符的調用以提高程序運行效率)
-
初始化列表細節探究
- 初始化列表中的代碼可以看作是被編譯器安插在構造函數中的
- 初始化列表中的代碼是在構造函數的函數體之前被執行的
- 初始化列表成員變量的初始化順序看的是變量在類中定義的順序,而不是看在初始化列表中出現的順序
虛函數
虛函數表指針位置(對象模型的開頭)
單繼承情況下父類和子類虛函數表指針和虛函數表分析
- 子類中有覆蓋父類虛函數時情況分析
- 子類中沒有覆蓋父類虛函數時情況分析
多繼承情況下父類和子類虛函數表指針和虛函數表分析
- 子類中有覆蓋父類或者多個父類虛函數時情況分析
- 子類中沒有覆蓋父類虛函數時情況分析
分析虛函數表的工具與vptr,vtbl創建時機
- 輔助工具(查看虛函數表指針專用工具)
- vptr與vtbl都是在編譯期間創建起來的,而給vptr賦值是在運行期間,即,生成對象時,會調用構造函數進行賦值。
單純的類不純時引發的虛函數調用問題(memset和memcpy問題)
- 單純的類:只有一些簡單的成員變量
- 不純的類:指類中有一些隱藏的變量,例如虛函數表指針(有虛函數時存在),虛基類表指針
- 涉及靜態聯編和動態聯編概念
數據語義學
數據成員綁定時機
- 成員函數函數體的解析時機
- 成員函數參數類型的確定時機
進程內存空間佈局
數據成員的存取
- 靜態成員變量的存取
- 非靜態成員變量的存取
數據成員的佈局
- 單一繼承關係下的數據成員佈局(父類和子類都不帶虛函數)–父類和子類的內存佈局
- 單一繼承關係下,父類和子類都帶虛函數時,子類對象的內存佈局
- 單一繼承關係下,父類不帶虛函數,子類都帶虛函數時,子類對象的內存佈局
多重繼承數據成員佈局與this指針偏移話題
- 子主題 1
- 子主題 2
- 子主題 3
- 子主題 4
虛基類與虛繼承
- 虛基類/虛繼承的提出(爲了解決3層結構中孫子類重複包含爺爺類成員的問題)
- 虛基類探討
- 兩層結構的虛基類表5-8字節內容分析
- 三層結構的虛基類表1-4字節內容分析
成員變量地址,偏移與指針話題深入探討
- 對象成員變量內存地址及其指針(對象的成員變量是有真正的地址的,這與變量的偏移值不同)
- 成員變量的偏移值及其指針(即:每個數據成員距離對象首地址的距離)
- 沒有指向任何數據成員變量的指針(通過對象名/對象指針接成員變量指針的一種方式訪問成員變量)
函數語義學
普通成員函數調用方式(編譯器在形參上隱藏了一個this指針,性能上和調用全局函數差不多)
虛函數,靜態成員函數調用方式
class A
{
public:
int a;
virtual void fun()
{
printf("%p\n", this);
fun1();//直接調用
A::fun1();//走虛函數表
}
virtual void fun1()
{
printf("%p\n", this);
}
};
int main()
{
A obj;
obj.fun();
A *obj1 = new A();
obj1->fun();
}
-
虛函數調用方式
- 通過對象調用是直接調用,和調用全局函數性能一樣
- 通過指針調用是走虛函數表
- 虛函數內調用另一個虛函數,如果用類名,則是採用全局函數調用方式,自己用函數名則是走虛函數表調用方式
-
靜態成員函數調用方式
虛函數地址問題的vcall引入(爲了解決多重繼承中this指針調整問題)
靜動態類型綁定
- 靜態類型與動態類型
- 靜態綁定與動態綁定
- 繼承的非虛函數坑
- 虛函數的動態綁定
- 重新定義虛函數的缺省參數坑
- c++中的多態性(走虛函數表肯定是多態)
單繼承下的虛函數特殊範例演示
多重繼承虛函數深釋,第二基類與虛析構必加
- 多繼承下的虛函數
- 如何成功刪除用第二基類指針new出來的子類對象
- 父類非虛析構函數時導致的內存泄漏演示
多繼承第二基類虛函數支持與虛繼承帶虛函數
- 多重繼承第二基類對虛函數支持的影響(this指針調整的作用)
- 虛繼承下的虛函數
RTTI運行時類型識別與存儲位置
- 子主題 1
- 子主題 2
- 子主題 3
函數調用,繼承關係性能說
- 函數調用中編譯器的循環代碼優化
- 繼承關係深度增加,開銷也增加
- 繼承關係深度增加,虛函數導致的開銷增加
指向成員函數的指針以及vcall細節談
-
指向成員函數的指針
- 指向成員函數的成員函數指針(這也體現了,爲什麼成員函數指針調用成員函數需要對象的介入,因爲,成員函數的調用需要一個隱藏的this指針)
- 指向靜態成員函數的函數指針(不需要this指針)
-
特殊代碼分享(不通過對象也可以實現成員函數的調用()不需要this指針)
-
指向虛函數的成員函數指針及vcall談
- 指向虛函數的成員函數指針(虛函數的調用也是需要this指針的)
- vcall(有時我們打印虛函數的地址不是真正的虛函數地址,而是vcall的地址,vcall裏放着虛函數在虛函數表裏的偏移值,引入vcall是編譯器的一種做法)
-
vcall在繼承關係中的體現(有虛函數)
inline函數擴展細節(是否真的內聯取決於編譯器)
- 形參被對應的實參取代
- 局部變量的引入(帶來了性能的消耗)
- inline失敗情形(例如:遞歸)
對象構造語義學
繼承體系下的對象構造順序
- 對象的構造順序(從父到子,析構則相反)
- 構造函數裏調用虛函數(直接調用,不是走虛函數表)
對象複製語義學與析構函數語義學
-
對象的默認複製行爲(簡單的按值拷貝)
-
拷貝賦值運算符與拷貝構造函數
-
如何禁止對象的拷貝構造和賦值
- 聲明爲private,只寫聲明,不寫函數體
- c++11提供的delete關鍵字
-
析構函數語言(編譯器默認提供析構函數的幾種情況)
- 在繼承體系中,父類帶析構函數,如果子類不帶析構函數,編譯器會默認合成一個
- 在一個類中,如果帶有一個類類型的成員變量,並且這個成員變量帶有析構函數。
局部對象,全局對象的構造和析構
- 局部對象的構造和析構(建議現用現定義,減少不必要的構造和析構)
- 全局對象的構造和析構(main函數執行前就開始構造了,main函數結束以後,執行析構函數)
局部靜態對象,對象數組的構造析構和內存分配
- 局部靜態對象的構造和析構(一個局部的靜態對象,如果多次使用,則只會構造一次,編譯器採取了標記的方法,以便防止靜態的局部對象構造多次。)
- 對象數組的構造和析構(靜態對象數組到底在編譯時,分配了多少給字節,這並不取決於你的數組有幾個元素,而是取決於你的程序幹了什麼事,這是編譯器的一個智能做法。)
new與delete高級話題
-
new/delete的認識
- malloc0個字節的話題
-
重載new/delete
-
new/delete細節探討
-
new一個類加括號和不加括號的區別
- 類是空時無區別
- 類A中有成員變量則:帶括號的初始化會把一些和成員變量有關的內存清0,但不是整個對象的內存全部清0
- 類有構造函數 得到的結果一樣
-
new幹了什麼
- 調用operator new(malloc)
- 調用了類的構造函數
-
delete幹了什麼
- 調用了·類的析構函數
- 調用了operator delete(free)
-
-
重載operator new/operator delete
-
-
嵌入式指針與內存池
臨時性對象的詳細探討
模板實例化語義學
模板及其實例化詳細分析
-
函數模板
-
類模板的實例化分析
- 類模板中的枚舉類型
- 類模板中的靜態成員變量
- 類模板的實例化
- 成員函數的實例化
-
多個源文件中使用類模板
炫技寫法
-
不能被繼承的類
- c++11的final
- 友元函數+虛繼承
-
類外調用私有虛函數(一個private的虛函數可以調用嗎?可以使用特殊寫法進行調用)