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的虛函式可以呼叫嗎?可以使用特殊寫法進行呼叫)

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/255255.html
標籤:其他
上一篇:ensp三層架構實驗
