1. 程式人生 > >inside the c++ object筆記(一)

inside the c++ object筆記(一)

chapter1

struct

struct宣告,class定義只會警告,實際由定義的關鍵字決定

物件

物件之中的變數在記憶體中的順序為:

同access section中按照宣告順序,不同的access section按照section寫的順序

c++物件模型

物件內:

non-static data members(非靜態資料成員)                      放在類內

static data member(靜態資料成員)                                   放在類外獨立一塊(獨立在任何物件外)

non-static func                                                                       類外獨立一塊

static func                                                                              類外獨立一塊

vptr -> vptbl -> virtual func                                                     vptr在類內

若資料複雜,則可以把資料成員分離出來作為一個struct

在舊的c中,由class繼承資料struct

現在的做法是把struct的指標/引用/物件放在class的private中

三種模型

程式模型:資料函式分離

抽象模型ADT

單一物件加上介面

面向物件模型

基類分配統一介面給所有的派生類

class大小計算

non-static data member的大小

virtual函式的vptr

virtual base class 的 virtual base class pointer

齊位造成的補足空間(可能補在成員間可能補在物件邊界)

void*

可以指向任何物件,但是無法執行任何操作

多型

引用與指標時實現多型機制的根本

在派生類賦給(初始化給)基類的“物件”時,編譯器需要進行裁定這個object的真正型別,並且保證不同object的vptr不會因為這個物件的建立/賦值而改變。故,派生類會被裁切出基類的部分,但是基類無法擴充成派生類物件(因為無法給出相應的vptr)。

程式記憶體

global object的記憶體保證會在每一次程式啟動的時候清零;local object的記憶體儲存在棧,heap object儲存在自由空間(堆),它們的狀態位上一次使用的狀態。

chapter2

預設建構函式default constructor

如果object內含其他object,則default constructor會自動按照“類內宣告這些object的順序“進行default constructor的呼叫(執行這些功能的程式碼會插入在任何使用者寫在建構函式體內的程式碼前)。即使聲明瞭含有引數的constructor或顯示呼叫了一些建構函式(或使用部分初始化列表),編譯器仍然會把沒有呼叫的預設函式插入。這一條對繼承的基類也同樣奏效,呼叫基類的預設建構函式的程式碼也會插入到每個建構函式(按照繼承的順序,而不是初始化列表的順序)

會合成default constructor建構函式的情況

1、擁有object成員,需要呼叫object的default constructor

2、擁有基類,而基類有合成/宣告的default constructor

3、具有virtual function,需要建立vptr,所以基類擁有virtual function在非private時也是(在private的話是不需要去建立的,因為調用不了)

4、具有virtual base class,需要為每個virtual base class建立一個指向基類的指標,放在派生類的每個虛擬繼承的基類之中,也就是每有一個虛擬基類就增加一個虛指標的空間。

其他時候,或許可以使用無參構造,但實際沒有合成建構函式。也就是說並不是沒有default構造,編譯器就會合成一個。另一方面,預設構造並不會初始化全部的成員,只會初始化物件成員,內建型別不會擁有初始化值。

class A {
public:
	//A() :num(1) { cout << "A" << endl; }
	int num;
};

class B {
public:
	//B() :num(2) { cout << "B" << endl; }
	int num;
};

class C {
public:
	C(int n) :num(n) { cout << "C" << endl; }
	int num;
	B b;
	A a;
};

int main()
{
	C c(3);//c中的b,a裡的num都沒有初始化,但它們的預設建構函式是呼叫了的
	cout << c.a.num << endl;
	cout << c.b.num << endl;
	cout << c.num << endl;

	//A a;
	//cout << a.num << endl;//無法通過編譯,因為沒有初始化num,但是不用就不會有問題
}

2.2

拷貝建構函式copy constructor

通常object可以進行copy,但是並不代表其有一個拷貝建構函式,而是其使用了編譯器的memberwise initialization -> 對每個變數進行bitwise copy(逐位複製),如果有物件成員,那麼就對這個物件進行遞迴的memberwise initialization。這個時候其實沒有一個實際合成的拷貝建構函式。

會合成拷貝建構函式的情況

1、物件成員有顯示宣告/合成的拷貝構造

2、基類具有顯示宣告/合成的拷貝構造

3、擁有vptr

4、擁有virtual base class

1與2需要合成拷貝構造來呼叫相應的構造,3與4需要拷貝構造來建立正確的vptr,這一點特別發生在把派生類物件賦值給基類物件進行初始化,編譯器需要決定vptr的類別。

2.3

初始化引數的方法

1、先建立一個臨時物件,呼叫拷貝構造複製傳入的引數,然後把function改為接受一個引用,引用的就是這個臨時物件的地址。(需要一組拷貝/析構)//通常做法

2、直接在引數的位置上構造這個物件(構造/析構)

返回值的初始化

1、返回物件的時候,先為函式增加一個引用引數作為返回結果,然後在return前呼叫copy constructor賦值一個物件,把這個物件地址給第二個引數。、

2、這個返回的物件如果沒有用來初始化另一個物件,那麼就會被析構(即使使用=賦值給另一個物件也不行,使用=只是進行了拷貝),用來初始化另一個物件的時候並不是呼叫拷貝建構函式,而是把返回的物件保留,變成需要建立的物件。

int num;
class A {
public:
	A() { cout << "default" << endl; num++; }
	A(const A &a) { cout << "copy " << a.name << endl; num++; }
	~A() { cout << name << "'s desstructor" << endl; }
	string name;
};

A test(A a) { 
	cout << "-------" << endl;
	A b = a;
	b.name = "b";
	cout << "-------" << endl;
	return b;
}

int main()
{
	A a;
	a.name = "a";
	A b(test(a));
	cout << num << endl;
	cout << b.name << endl;
}

A a;

a = func();//銷燬

A a(func()); 或 A a = func();//保留,變成a

在使用者層面進行返回值以及引數傳遞的優化

引用代替物件作為引數,return 語句呼叫建構函式返回臨時建立物件

X fun(const X&lhs,const X&rhs)

{ return X(lhs,rhs); }

//如此就沒有任何由編譯器建立的臨時物件產生了,不過這樣的return是可能出問題的

引用引數不會進行物件複製,顯示呼叫構造也避免了對返回的物件進行復制。但是要慎用,比如smart_ptr就不可以這麼返回,這樣會導致智慧指標在語句結束後被析構,也會導致其指向的物件被釋放(可能被釋放,如果你返回的這個物件沒有被用來初始化,那麼這個物件就要被銷燬,銷燬智慧指標可能會銷燬其管理的內建指標指向的值)。

在編譯器層面進行優化

X fun()

{

    X xx;

    //進行一次對xx的拷貝,然後析構xx,再把拷貝所得的結果返回

    return xx;

}

優化為

void fun(X &result)

{

    result.X::X();

    return;

}

也就是把接受結果的物件的地址作為引用引數,然後直接呼叫建構函式,並且不返回值,避免了拷貝的進行。

也可以說是使用引用引數代替了return返回結果,成為name return value(NRV優化)

這個優化只有在copy constructor發生了inline的時候發生(可能你手動加上了inline都還沒發生,實際上我就沒試到發生過)

copy constructor的取捨(剔除)

這條並不是說什麼時候不定義copy constructor,而是系統什麼時候不選擇copy constructor

初始化形式

A a(1024)//單一引數構造

A a = A(1024)//拷貝運算子

A a = (A)(1024)//強制型別轉換

以上三種形式,後兩者似乎應該是先呼叫預設構造建立a,然後再進行拷貝構造,但實際上,三者的效果完全一致,剔除了copy constructor的呼叫,使用的一致是單一引數建構函式。

copy costructor定義的取捨

對於一個內部只有非靜態型別成員的物件,也許不要定義copy costructor會更好,即使自定義的copy constructor只是簡單複製數值,它的效率也不會比編譯器的bitwise copy更好,它的inline copy constructor出現的時候應該是能夠確認拷貝操作大量出現的時候才定義,用來啟動NRV優化,否則不定義就好了。

memset與memcopy

memcopy(pointer result,pointer locate,size)//複製pointer locate開始size大小的記憶體塊

memset(pointer result,0/-1,size)//為指定記憶體塊填入0/-1不能是其他值

這兩個函式均無法在有虛指標的時候使用

因為他們會把vptr也一起復制/清零

初始化list

初始化的順序是成員宣告的順序,而不是初始化列表中使用的順序。

初始化程式碼會插入在任何使用者寫在constructor中的程式碼之前。

允許使用類內函式在初始化列表中初始化某個成員,也可以用來當做基類建構函式的引數,但是最好不要這麼做,因為可能無法確定這個函式對自身成員的依賴性。

如果要用一個成員初始化另一個成員,那麼考慮把這個操作放到函式體裡,保證初始化程式碼已經發生

必須要使用初始化list的情況

1、const/reference成員初始化

2、需要呼叫基類除了預設建構函式以外的建構函式

3、需要呼叫成員除了預設建構函式以外的建構函式