深度探索C++模型----除第七章筆記
第三章:Data語意學
3.1 Data Member的繫結
主要講Alignment,之前做筆試題遇到過挺多的,大概就是不滿計算機一個位元組的會加入padding填充滿這一個位元組。
3.2 Data Member的佈局
3.3 Data Member的存取
靜態資料&非靜態資料
Point3d origin;
origin.x=0.0;//x的存取成本分x是否為靜態資料以及Point3d是否是獨立的class。
重點:
Point3d origin,*pt=&origin;
origin.x=0.0;
pt->x=0.0;
問:這兩種取法有什麼重大差異。
.和->的問題,一個是針對物件,一個是針對指標。
比如你有這個結構體:
struct xx
{
int a;
int b;
}yy, *kk;
yy.a=3, yy.b=5;
kk=new xx;
kk->a=4, kk->b=6;
class A
{
public :
int a;
}
A ma;
A *p=&ma;
那麼指標p應使用->來訪問成員a,比如p->a,而ma應使用.來訪問,比如ma.a區別就在這裡,凡是指標就使用->,物件就使用.運算子。
只有當Point3d是一個derived class,而在其繼承結構中有一個virtual base class,並且存取的member是一個從該virtual base class繼承而來的member時就會有重大的差異。因為我們無法在編譯時期知道這個member的真正的offset的位置,因此這個存取操作需要被延遲到執行期,經由一個額外的間接導引才能夠解決。但如果使用引用就不會有這個問題,由於其型別無疑是Point3d型別,即使它繼承自virtual base class,members的offset位置在編譯時期也就固定了。一個積極進取的編譯器甚至可以靜態的經由origin就解決掉對x的存取。
非靜態資料會以明喻的或者暗喻的方式去呼叫。
明喻(explicit)
暗喻(implicit)也就是使用隱含的this指標。
對nonstatic data member進行存取操作編譯器需要把class object的起始地址加上data member的偏移量。對於data member來說:
//下面兩種形式等價
&origin._y
&origin+(&Point3d::_y-1);//這個-1是對於data member來說的。
編譯器會對data member的offset值進行加一使得編譯器去區分"一個指向data member的指標,用以指出class的第一個member"和"一個指向data member的指標,沒有指出任何member"的情況。相當於只要指出了member就需要-1還原。
3.4 繼承與Data Member
a.單一繼承而且沒有virtual function時
這樣做是沒有virtual的成本的,也就是跟普通C結構沒兩樣,沒有額外的成本。
inline定義的函式會被放入符號表中,在使用時像巨集一樣展開進行替換,效率很高,並且會有引數檢查,在類中定義還可以保護類的保護成員和私有成員。缺點在於程式碼過大過繁會造成很大開銷。
b.加上多型,也就是引入了virtual function
c.多重繼承
d.虛擬繼承
3.5 物件成員的效率
實際上具體繼承(非virtual繼承)並不會增加空間或者存取時間上的額外負擔,但是虛擬繼承的間接性壓抑了"把所有運算都移往快取器執行"的優化能力,即使通過類物件訪問編譯器也會像對待指標一樣,效率令人擔心,一般來說virtual base class最有效地運用形式就是一個抽象的virtual base class,沒有任何的data member。
虛擬函式到底有多慢:https://blog.csdn.net/hengyunabc/article/details/7461919
呼叫虛擬函式其實相當於多了3條彙編指令:根據指標或引用取虛表,取虛表的函式地址,對函式進行call呼叫。
call指令主要會影響CPU的流水線。
在獲得引用和指標前無法進行內聯優化。
3.6 指向Data Member的指標
&Point3d::z將得到z座標在class object中的偏移量(offset)
上文提到過的-1操作是為了區分:
float Point3d::*p1=0;//Point3d::*的意思是:"指向Point3d data member"的指標型別。
float Point3d::*p2=&Point3d::x;
每次存取Point::x,也就是pB.*bx會被轉化為&pB->__vbcPoint+(bx-1),而不是轉化為最直接的&pB+(bx-1),因此會降低"把所有處理都搬移到快取器中執行"的優化能力。
Base2* pb1 = new Derived; // 調整指標指向base2 clss子物件
Base2* pb2 = pb1->clone(); // pb1被調整至Derived物件的地址,產生新的物件,再次調整物件指標指向base2基類子物件,賦值給pb2。
Derived* pDerived = new Derived;
Base2* pBase2 = pDerived; // Base2為Derived的第二個基類
pBase2 != pDerived; // 兩者不等,進行了this指標轉換
class Point3d
{
public:
virtual ~Point3d();
protected:
static Point3d origin;
float x, y, z;
};
&Point3d::z;//返回的是float Point3d::*
&origin.z;//返回的是float*
//問:這兩個之間的差異是什麼?
//第一個會得到其在類中的offset,第二個會得到member在記憶體中的真正地址,因為它是綁定於真正classobject身上的data member地址。
第四章:Function語意學
name managling//過載時候的名字改編
extern "C"//實現C與C++混合程式設計
4.1 Member的各種呼叫方式
a.Nonstatic Member Function
C++設計的準則之一是nonstatic member function至少必須和nonmember function有相同的效率。也就是說在要有一樣的訪問速度。
float magnitude3d(const Point *_this){...}
float Point3d::magnitude3d() const{...}
實際上對於member function會被內化為nonmember的形式。
步驟一:給類成員函式新增一個this指標
Point3d Point3d::magnitude(Point3d *const this)
Point3d Point3d::magnitude(const Point3d *const this)
步驟二:對資料成員的呼叫改為this,如x改為this->x
步驟三:將成員函式名稱進行mangling處理,使它在程式中成為獨一無二的語彙
extern magnitude__7Point3dFv(
register Point3d *const this);
此時函式轉化操作已完成,而為每一個呼叫操作也都必須轉化:
obj.magnitude();轉為magnitude__7Point3dFv(&obj);
ptr->magnitude();轉為magnitude__7Point3dFv(ptr);
關於named return value的介紹:
https://www.cnblogs.com/zzj3/articles/3728902.html
https://blog.csdn.net/luofengmacheng/article/details/21072255
https://www.cnblogs.com/autosar/archive/2011/10/09/2204181.html
NRV操作主要就是在返回一個物件時直接用返回值去取代函式內部的區域性物件,此時函式只產生一個物件。而普通的編譯器會在返回一個物件時會再建立一個臨時物件用於獲取返回值,因此函式會產生兩個物件。
CTest(const CTest& rcTest)//所謂的拷貝建構函式
{
cout << "CTest(CTest)" << this << endl;
}
void bar(X& __result)
{
// 呼叫__result的預設建構函式
__result.X::X();
// 處理__result
return;
}
// 函式實現
void bar(X& __result) // 加上一個額外引數
{
// 預留x1的記憶體空間
X x1;
// 編譯器產生的預設建構函式的呼叫,
x1.X::X();
// 處理 x1..
// 編譯器產生的拷貝操作
__result.X::X(x1);
return;
}
// 函式呼叫
X x2; // 這裡只是預留記憶體,並未呼叫初始化函式
bar(x2);
NRV這裡涉及了一個拷貝建構函式的問題,預設的拷貝建構函式是淺拷貝,也是位拷貝,沒有進行過記憶體的開闢而是直接給變數添加了一個引用,如果原始變數被刪除就會產生一個野指標。位拷貝的效率也不見得高效,因此自定義拷貝建構函式很重要,舉兩個例子。
class A
{
public:
A()
{
data = new char;
cout << data << endl;
cout << "預設建構函式" << endl;
}
A(const A& a)//深拷貝
{
data = new char;
memcpy(data, a.data, sizeof(a.data));
}
//淺拷貝
/*A(const A& a)
{
data=a.data;
}*/
~A()
{
cout << "解構函式" << endl;
delete data;
cout << data << endl;
data = NULL;
}
private:
char* data;
};
class CA
{
public:
CA(int b,char* cstr)
{
a=b;
str=new char[b];
strcpy(str,cstr);
}
CA(const CA& C)
{
a=C.a;
str=new char[a]; //深拷貝
if(str!=0)
strcpy(str,C.str);
}
void Show()
{
cout<<str<<endl;
}
~CA()
{
delete str;
}
private:
int a;
char *str;
};
呼叫方式:
A a;
A b(a);//A b=a;
b.Virtual Member Function
virtual Point3d::normalize();
ptr->normalize()=>(*ptr->vptr[1])(ptr);//第二個ptr表示this指標
obj.normalize()=>(*obj.vptr[1])(&obj)=>normalize__7Point3dFv(&obj)
c.Static Member Function
((Point3d*)0)->object_count();
4.2 Virtual Member Function
什麼是RTTI:其儲存著類執行的相關資訊,比如類的名字,以及類的基類。可以在執行時識別物件的資訊。C++裡面只記錄類的名字和類的繼承關係鏈,是的編譯成二進位制的程式碼中物件可以知道自己的名字,以及在繼承鏈中的位置。
const type_info& typeinfo = typeid(pbase);
cout << typeinfo.name()<<endl;//輸出pbase的型別class Base*
if(NULL != dynamic_cast<Derive*>(pbase))
{
cout<<"type: Derive"<<endl;//type:Derive
}
else if(NULL != dynamic_cast<Derive2*>(pbase))
{
cout<<"type: Derive2"<<endl;//type:Derive2
}
else
{
//ASSERT(0);
}
C++中有4中強制型別轉換符:dynamic_cast、const_cast、static_cast、reinterpret_cast。
其中dynamic_cast與執行時型別轉換密切相關。該轉換符用於將一個指向派生類的基類指標或引用轉換為派生類的指標或引用,也就是dynamic_cast只能作用於含有虛擬函式的類。
B *pb; D *pd, md; pb=&md; pd=dynamic<D*>(pb);//含有虛指標的基類B和從基類B派生出的派生類D。
比如派生類D中含有特有的成員函式g(),這時可以這樣來訪問該成員dynamic_cast<D*>(pb)->g();但這個轉換並不總是成功的。
dynamic_cast轉換操作符在執行型別轉換時首先將檢查能否成功轉換,如果能成功轉換則轉換之,如果轉換失敗,如果是指標則返回一個0值。因此pd=dynamic_cast<D*>(pb); if(pd){…}else{…},或者這樣測試if(dynamic_cast<D*>(pb)){…}else{…}。
已知dynamic_cast依賴於RTTI,那麼是如何繫結的?
原來虛表上面的地址是指向一個結構 Derive::`RTTI Complete Object Locator , 這個結構指向該類的名字,和其物件繼承鏈。
實現一個程式碼用來從RTTI中讀取類的名字:
https://www.cnblogs.com/zhyg6516/archive/2011/03/07/1971898.html
4.3 函式的效能
編譯器將視為不變的表示式提到迴圈之外,因此只計算一次。因此inline函式不僅可以節省一般函式呼叫帶來
的額外負擔也提供程式優化的額外機會。
4.4 指向Member Functions的指標
獲取地址:https://blog.csdn.net/microsues/article/details/6452249
Derived d;
cout<<"&d="<<&d<<endl;
int *vptr1=(int*)*((int*)&d+0);//vptr1為virtual table[0]的地址
int *pf1=(int*)*((int*)*((int*)&d+0)+0);//pf1為virtual table[0]裡的第一個虛擬函式
Derived::~Derived的地址
int sz=sizeof(Base1)/4;
int *vptr2=(int*)*((int*)&d+sz);//vptr2為virtual table[1]的地址
cout<<"vptr2="<<vptr2<<endl;
pf1=(int*)*((int*)*((int*)&d+sz)+0);//pf1為virtual table[1]裡的第一個虛擬函式
cout<<"&vptr2[0]="<<&vptr2[0]<<endl;//這裡是virtual table[1][0]的地址
cout<<"pf1="<<pf1<<endl;//這裡是virtual table[1][0]裡儲存的地址,也就是真正函式的地址
一個問題:開C語言深度剖析那本書的時候再細講:
#include<iostream>
using namespace std;
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int *ptr1 = (int*)(&a+1);
int *ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);//ptr1[-1]值為5,ptr2會根據大小端來辨別。&a是一整個陣列,而a也能代表一個數組但只存有a[0]一個。
system("pause");
}
4.5 Inline Functions
由於呼叫函式的開銷比求解等價表示式要慢得多,在大多數機器上呼叫函式需要在呼叫前先儲存暫存器,並在返回時恢復,複製實參,程式還必須轉向一個新位置執行。內聯展開也就是省去了寫成函式的開銷。如下程式碼:
inline int max(int a, int b)
{
return a > b ? a : b;
}
//會被內聯展開為:cout<<max(a, b)<<endl;
inline函式不能包含迴圈、switch、if語句。
在一個c檔案中定義的inline不能在其他c檔案中使用,最好將inline函式定義在一個**-inl.h標頭檔案中。
不要過度的使用inline。
第五章:構造、解構、拷貝 語意學
什麼是POD:
1.所有的標量型別(基本型別和指標型別)、POD結構型別、POD聯合型別、以及這幾種型別的陣列、const/volatile修飾的版本都是POD型別。
2.不能有使用者自定義的建構函式、解構函式、拷貝建構函式。
POD的本質是與c相容的資料型別。也就是不受虛擬函式、C++特性等的影響,可以在轉化成二進位制時不發生變化。
Point *heap=new Point會轉換為對new運算子的呼叫:
Point *heap=__new(sizeof(Point));
delete heap會被轉化為__delete(heap);
建構函式入棧,析構函數出棧
並且構造是從基類開始的,每一個繼承的類都是從基類建構函式一層層構造上去的。
拷貝建構函式
Point::Point(Point &p)
{
X=p.x;
Y=p.y;
cout<<"這裡是拷貝建構函式"<<endl;
}
拷貝建構函式使用的三個地方:
a.用類的一個物件去初始化該類的另一個物件
Point A(1,2);
Point B(A);
b.函式的形參為類的物件
void fun1(Point p)
{
cout<<p.GetX()<<endl;
}
Point A(1,2);
fun1(A);
c.函式的返回值是類的物件
Point fun2()
{
Point A(1,2);
return A;
}
拷貝賦值函式
class Foo
{
public:
Foo& operator=(const Foo&);//返回自引用,拷貝新內容。都是同類型引用。
}
過載是在一個類中有兩個及以上的方法,要求方法名相同但引數卻不相同。
動態過載與靜態過載
動態過載是指繼承多型,靜態就是單純的程式碼函式過載
int Max (int,int);//返回兩個整數的最大值;
int Max (const vector <int> &);//返回vector容器中的最大值;
int Max (const matrix &);//返回matrix引用的最大值;
int Add (int a,int b);
float Add(int a,float b);
float Add(float a,int b);
float Add(float a,float b);
第六章 執行期語意學
全域性物件:class object會在編譯時期放置在data segment並且內容為0,但constructor一直到程式及或是才會實施。cfront的實現策略是munch策略,會產生__sti()和__std()函式,以及一組執行時庫,一個_main()函式,一個_exit()函式。
區域性靜態物件:
const Matrix & identity(){
static Matrix mat_identity;//mat_identity的constructor和destructor必須只能執行一次,就算函式被呼叫很多次,會有一個臨時變數的匯入來判斷constructor是否已經被呼叫過。
return mat_identity;
}
物件陣列:
#include<iostream>
using namespace std;
class CSample{
public:
CSample(){ //建構函式 1
cout<<"Constructor 1 Called"<<endl;
}
CSample(int n){ //建構函式 2
cout<<"Constructor 2 Called"<<endl;
}
}
int main(){
CSample arrayl[2];
cout<<"stepl"<<endl;
CSample array2[2] = {4, 5};
cout<<"step2"<<endl;
CSample array3[2] = {3};
cout<<"step3"<<endl;
CSample* array4 = new CSample[2];
delete [] array4;
return 0;
}
輸出結果:
Constructor 1 Called
Constructor 1 Called
stepl
Constructor 2 Called
Constructor 2 Called
step2
Constructor 2 Called
Constructor 1 Called
step3
Constructor 1 Called
Constructor 1 Called
class CTest{
public:
CTest(int n){ } //建構函式(1)
CTest(int n, int m){ } //建構函式(2)
CTest(){ } //建構函式(3)
};
int main(){
//三個元素分別用建構函式(1)、(2)、(3) 初始化
CTest arrayl [3] = { 1, CTest (1, 2) };
//三個元素分別用建構函式(2)、(2)、(1)初始化
CTest array2[3] = { CTest(2,3), CTest (1,2), 1};
//兩個元素指向的物件分別用建構函式(1)、(2)初始化
CTest* pArray[3] = { new CTest(4) , new CTest(1,2) };
return 0;
}
只要物件裡聲明瞭建構函式&解構函式,物件數組裡的每個元素都需要進行構造&析構操作。
物件的new和delete
Point3d *origin=new Point3d;
//被轉化為:
Point3d *origin;
if(origin=__new(sizeof(Point3d)))
Point3d::Point3d(origin);
delete originl;
//被轉化為
if(orgin!=0)
{
Point3d::~Point3d(origin);
__delete(orgin);
}
針對陣列的new的情況:
如果class中定義有一個default constructor,某些版本的vec_new()就會被呼叫配置並構造class objects組成的陣列。
Point3d *p_array=new Point3d[10];//通常會被編譯為Point3d *p_array;
p_array=vec_new(0,sizeof(Point3d),10,&Point3d::Point3d,&Point3d::~Point3d);