1. 程式人生 > 其它 >深入探索C++之執行期語義學

深入探索C++之執行期語義學

技術標籤:c++c++

  主要談論程式執行期全部,區域性,堆,臨時物件的構造和銷燬時機。

1 物件構造和析構

1.1 全域性變數

  一般來說,區域性物件的構造和銷燬時機都和一般的認為相同,在定義處進行構造呼叫constructor,在離開當前作用域時銷燬呼叫destructor。但是有時候會帶來效能上額外的負擔,一般建議類的定義儘可能放置於需要使用該類的作用域附近,可以節省一些不必要的物件構造和銷燬操作的效能消耗。
  對於全域性變數,編譯器一定能夠保證在程式中第一次使用該全域性變數之前初始化或者構造該變數,並在程式結束前銷燬。其初始化方式無非兩種:一種是編譯器預設初始化為常量表達式0;二是使用者預設提供初始化方式,提供的初始化方式一般為常量表達式。對於基本型別完全可以,但是對於類來說因為建構函式根本就不是常量表達式,因此無法對其進行靜態初始化(靜態初始化指物件擁有自定義的構造和解構函式時)。因此編譯器一般的處理方式是在編譯期將該全域性變數的記憶體設定為0,然後在程式開始之後在呼叫相應的建構函式構造物件。

  對於以上策略cfront早期版本的一個實現方式是:

  1. 為每一個需要靜態初始化的檔案中產生一個_sti()函式,內帶必要的建構函式呼叫或inline expansion;
  2. 為每一個需要靜態的記憶體釋放操作的檔案中,產生一個__std()函式,內帶必要的析構呼叫操作,或是inline expansion;
  3. 提供一組runtime library ”munch“函式,一個__main()函式,用來呼叫可執行檔案中的__sti()函式,還有一個exit()函式,呼叫所有的__std()函式。

  System V的ELF被擴充以支援.init.fini兩個section,該section中包含物件所需要的資訊,分別對應於靜態初始化和釋放操作,編譯器特定的startup

函式會在特定的平臺支援完成靜態初始化和釋放操作。
  對於虛繼承同樣麻煩,因為虛繼承中的一些資訊並無法在編譯期確定需要在執行期才能確定。
  使用需要靜態初始化的物件存在很多缺點:

  1. 如果exception handling被支援,則那些物件無法放置於try中,可能無法觸發本來觸發的異常;
  2. 為了控制需要跨模組做靜態初始化物件的相依順序而帶來的複雜度。

  因此,一般建議儘量不要使用需要靜態初始化的全部變數。

1.2 區域性靜態變數

  區域性靜態變數的特點是在物件定義處,僅僅呼叫一次構造,在程式結束時只析構一次。
  一個可能的實現方式是在函式第一次被呼叫時維護一個flag,如果物件被構造和設為true

,則之後不再構造物件。

1.3 物件陣列

  對於一個物件的陣列,一般需要對每一個物件呼叫建構函式對物件進行初始化。一般分為兩種情況:

  1. 如果物件沒有定義建構函式或者解構函式,則物件只需要分配固定大小的記憶體即可;
  2. 如果物件擁有預設建構函式和預設解構函式,一種可能的實現是建構函式和解構函式的呼叫分別有編譯器新增的執行庫函式vec_newvec_delete呼叫,該函式傳入物件的記憶體地址,建構函式地址或者解構函式地址,對物件進行構造或者析構。但是因為建構函式往往是可以有引數的,因此:
    • 使用vec_new無法傳入建構函式的預設引數值;
    • 一個可能的實現是為類新增一個sub_constructor,在該建構函式中呼叫預設引數的建構函式。

2 new和delete

2.1 普通物件的new和delete

  一般情況下,使用new構造物件和陣列類似,實際的初始化方式被拆分為兩步:

  1. 首相分配物件的記憶體;
  2. 在該記憶體上呼叫建構函式構造物件。

  下面為可能的轉換:

//轉換前
Point *point = new Point;
//轉換後,虛擬碼
if(point == __new(sizeof(Point)))
{
    point = Point::Point(point);
}

//轉換前
delete point;
//轉換後
//雖然這裡顯示編譯器在對記憶體釋放前可能檢查指標是否為0,但是並無法完全保證,因此實際使用中還是需要檢查指標
if(0 != point)
{
    Point::Point(point);
    __delete(point);
}

2.2 陣列物件的new和delete

  陣列的newdelete和普通物件類似,唯一不同的是需要對多個物件進行初始化構造,銷燬:

  1. 如果物件呈現出bitwise的語義並且未定義構造和析構,則只會簡單的申請記憶體;
  2. 如果類包含建構函式,則特定版本的vec_new會被呼叫來負責構造物件。
Point3d *p_array=new Point3d[10];
//通常會被編譯為
Point3d *p_array;
p_array=vec_new(0,sizeof(Point3d),10,&Point3d::Point3d,&Point3d::~Point3d);

  另外需要注意的是,現代C++中銷燬記憶體時,直接使用delete [] ptr,那麼編譯器如何知道使用者希望刪除的記憶體大小,實際上編譯器根據不同版本實現內部維護了相關的長度資訊。
  另外更需要注意的是,陣列中不能以多型的思想考慮物件的構造和銷燬,即不要讓一個基類的指標指向派生類的陣列。

#include <iostream>
#include <iomanip>
#include <cstdlib>

using namespace std;

class base
{
public:
	base()
	{
		cout<<"呼叫基類建構函式"<<endl;
	}

	virtual ~base()
	{
		cout<<"呼叫基類解構函式"<<endl;
	}
};

class derived : public base
{
public:

	derived()
	{
		cout<<"呼叫派生類建構函式"<<endl;
	}

	virtual ~derived()
	{
		cout<<"呼叫派生類析解構函式"<<endl;
	}
};

int main()
{
	base *ptr = new derived[5];
	cout<<endl;
	delete [] ptr;

	return 0;
}

  下面的程式碼的執行結果:上面的程式碼在g++下執行無多型特性,但是在vc下正常,可參考C++通過基類指標delete派生類陣列,解構函式是虛擬函式,程式為什麼會崩潰?

呼叫基類建構函式
呼叫派生類建構函式
呼叫基類建構函式
呼叫派生類建構函式
呼叫基類建構函式
呼叫派生類建構函式
呼叫基類建構函式
呼叫派生類建構函式
呼叫基類建構函式
呼叫派生類建構函式

呼叫基類解構函式
呼叫基類解構函式
呼叫基類解構函式
呼叫基類解構函式
呼叫基類解構函式

2.3 placement operator new

  placement operator new的需要注意的問題是:

  1. 申請記憶體的一端也要執行析構的責任,明確責任鏈;
  2. placement operator new並不支援多型性質。
class base
{
public:
	base()
	{
		cout<<"呼叫基類建構函式"<<endl;
	}

	virtual void func()
	{
		cout<<"base::func"<<endl;
	}

	virtual ~base()
	{
		cout<<"呼叫基類解構函式"<<endl;
	}
};

class derived : public base
{
public:

	derived()
	{
		cout<<"呼叫派生類建構函式"<<endl;
	}

	virtual void func()
	{
		cout<<"derived::func"<<endl;
	}

	virtual ~derived()
	{
		cout<<"呼叫派生類析解構函式"<<endl;
	}
};

int main()
{
	base b;
	b.func();
	b.~base();

	new (&b) derived;
	b.func();

	return 0;
}

  

呼叫基類建構函式
base::func
呼叫基類解構函式
呼叫基類建構函式
呼叫派生類建構函式
base::func
呼叫基類解構函式

3 臨時物件

  程式中的表示式是否導致一個臨時物件昂,視編譯器的進取性以及上述操作發生時的程式上下關係而定。C++標準對是否產生臨時物件有完全的自由度。

  • 臨時物件被銷燬應該是完整表示式求值過程中的最後一個步驟,該表示式造成臨時物件的產生;
  • 凡是含有表示式執行結果的臨時性物件,應該存留到物件的初始化操作完成為止;
  • 如果一個臨時物件被綁定於一個引用,物件將殘留,直到被初始化的引用的生命結束,或者直到臨時物件的生命範疇結束。