天天看點

《深度探索C++對象模型》讀書筆記-第六章第六章 執行期語意學三、臨時性對象

第六章 執行期語意學

一、對象的構造與解構

1.全局對象

Matrix identity;
main()
{
    //identity必須在此處被初始化
    Matrix m1=identity;
    ...
    return 0;
}
           

C++保證,一定會在main()函數中第一次用到identity之前,把identity構造出來,而在main()函數結束之前把identity摧毀掉。像identity這樣的所謂全局對象,如果有構造函數和析構函數的話,就說它需要靜态的初始化操作和記憶體釋放操作。

C++程式中所有的全局對象都被放置在程式的data segment中,如果明确指定給它一個值,object将以該值為初值。否則object所配置到的記憶體内容為0。

2.局部靜态對象 

const Matrix&
identity(){
    static Matrix mat_identity;
    //...
    return mat_identity;
}
           

以上代碼中,無論函數identity()被調用多少次,局部靜态變量mat_identity的構造函數和析構函數都隻施行一次。

1)靜态局部變量存放在記憶體的全局資料區。函數結束時,靜态局部變量不會消失,每次該函數調用時,也不會為其重新配置設定空間。它始終駐留在全局資料區,直到程式運作結束。

2)靜态局部變量的初始化與全局變量類似.如果不為其顯式初始化,則C++自動為其 初始化為0。

4)靜态局部變量與全局變量共享全局資料區,但靜态局部變量隻在定義它的函數中可見。

5)靜态局部變量與局部變量在存儲位置上不同,使得其存在的時限也不同,導緻對這兩者操作的運作結果也不同

3.對象數組

數組其實也可以容納更複雜的資料類型,比如程式員定義的結構或對象。這一切所需的條件就是,每個元素都擁有相同類型的結構或同一類的對象。

比如數組

Point knot[10];
           

  關于對象數組的7個要點:

1)數組的元素可以是對象。

2)如果在建立對象數組時未使用初始化清單,則會為數組中的每個對象調用預設構造函數。

3)沒有必要讓數組中的所有對象都使用相同的構造函數。

4)如果在建立對象數組時使用初始化清單,則将根據所使用參數的數量和類型為每個對象調用正确的構造函數。

5)如果構造函數需要多個參數,則初始化項必須釆用構造函數調用的形式。

6)如果清單中的初始化項調用少于數組中的對象,則将為所有剩餘的對象調用預設構造函數。

7)最好總是提供一個預設的構造函數。如果沒有,則必須確定為數組中的每個對象提供一個初始化項。

二、new和delete

此部分參考https://blog.csdn.net/passion_wu128/article/details/38966581

new

new操作針對資料類型的處理,分為兩種情況:

int *p=new int;
int *p=new int(4);//指定初值
           

1.簡單資料類型(包括基本資料類型和不需要構造函數的類型) 

 簡單類型直接調用operator new配置設定記憶體;

可以通過new_handler來處理new失敗的情況;

new分類失敗的時候不像malloc那樣傳回NULL,它直接抛出異常。要判斷是否配置設定成功應該用異常捕獲的機制

2.複雜資料類型(需要由構造函數初始化對象)

class Object
{
  public:
     Object()
     {
          _val=1;
      }
      ~Object()
      {
      }
private:
    int _val;
}
void main()
{
    Object *p= new Object();
}
           

new複雜資料類型的時候先調用operator new,然後在配置設定的記憶體上調用構造函數。

3.new數組

new[]也分為兩種情況

簡單資料類型(包括基本資料類型和不需要析構函數的類型)

new[]調用的是operator new[],計算出數組總大小之後調用operator new.

可以通過()初始化數組為零值,例如

char *p = new char[32];
等同于
char *p = new char[32];
memset(p,32,0);
           

針對簡單類型,new[]計算好大小後調用operator new.

複雜資料類型(需要由析構函數銷毀對象)

class Object
{
public:
	Object()
	{
		_val = 1;
	}
 
	~Object()
	{
		cout << "destroy object" << endl;
	}
private:
	int _val;
};
 
void main()
{
	Object* p = new Object[3];
}
           

ew[]先調用operator new[]配置設定記憶體,然後在p的前四個位元組寫入數組大小,最後調用三次構造函數。

實際配置設定的記憶體塊如下:

《深度探索C++對象模型》讀書筆記-第六章第六章 執行期語意學三、臨時性對象

這裡為什麼要寫入數組大小呢?因為對象析構時不得不用這個值,舉個例子:

當我們在main()函數最後中加上

delete[] p;
           

釋放記憶體之前會調用每個對象的析構函數。但是編譯器并不知道p實際所指對象的大小。如果沒有儲存數組大小,編譯器如何知道該把p所指的記憶體分為幾次來調用析構函數呢?

總結:

針對複雜類型,new[]會額外存儲數組大小。

delete

delete也分為兩種情況:

簡單資料類型( 包括基本資料類型和不需要析構函數的類型)。

int *p = new int(1);
delete p;
           

delete簡單資料類型預設隻是調用free函數。

複雜資料類型

class Object
{
public:
	Object()
	{
		_val = 1;
	}
 
	~Object()
	{
		cout << "destroy object" << endl;
	}
private:
	int _val;
};
 
void main()
{
	Object* p = new Object;
	delete p;
}
           

delete複雜資料類型先調用析構函數再調用operator delete

delete[]也分為兩種情況:

簡單資料類型( 包括基本資料類型和不需要析構函數的類型)。

delete和delete[]效果一樣

比如下面的代碼:

int* pint = new int[32];
delete pint;
 
char* pch = new char[32];
delete pch;
           
運作後不會有什麼問題,記憶體也能完成的被釋放。看下彙編碼就知道operator delete[]就是簡單的調用operator delete。
           

總結:

針對簡單類型,delete和delete[]等同。

複雜資料類型(需要由析構函數銷毀對象)

釋放記憶體之前會先調用每個對象的析構函數。

new[]配置設定的記憶體隻能由delete[]釋放。如果由delete釋放會崩潰,為什麼會崩潰呢?

假設指針p指向new[]配置設定的記憶體。因為要4位元組存儲數組大小,實際配置設定的記憶體位址為[p-4],系統記錄的也是這個位址。delete[]實際釋放的就是p-4指向的記憶體。而delete會直接釋放p指向的記憶體,這個記憶體根本沒有被系統記錄,是以會崩潰。

總結:

針對複雜類型,new[]出來的記憶體隻能由delete[]釋放。

三、臨時性對象

有三種常見的臨時對象建立的情況

  • 以值的方式給函數傳參
  • 類型轉換
  • 函數需要傳回對象時

以下内容來自:http://blog.leanote.com/post/gaunthan/C-%E5%B0%BD%E9%87%8F%E9%81%BF%E5%85%8D%E4%B8%B4%E6%97%B6%E5%AF%B9%E8%B1%A1

從一個例子出發

下面的代碼你能找出幾個不必要的臨時對象?

string FindAddr(list<Employee> emps, string name)
{
for(list<Employee>::iteraotr i = emps.begin();
i != emps.end(); i++) {
if(*i == name)
return i->addr;
}
return "";
}
           

無論你是否相信,在上面這個短短的函數中存在着三個明顯的,以及兩個不太明顯的不必要的臨時對象,還有兩處可能會迷惑你的地方。

以 const 引用傳遞對象參數

在函數的聲明語句中有兩個明顯的臨時對象:

string FindAddr(list<Employee> emps, string name)
           

這些參數應該通過 const & 的方式來傳遞,而不應該通過傳值方式。傳值方式将會使編譯器建立這兩個參數對象的完全副本,而這種做法非常昂貴,而且完全沒有必要。

在傳遞對象參數時,選擇 const & 方式而不是傳值方式。

緩存不變量而不是重新構造

第三個臨時對象是在 for 循環的條件判斷語句中,這個臨時對象比前面兩個更明顯,而且同樣是可以避免的:

for(/*..*/; i != emps.end(); /*..*/)
           

對于大部分的容器(包括連結清單)而言,調用容器的 

end()

 函數将傳回一個臨時對象,這個對象需要被構造和析構。由于這個臨時對象的值在循環中是不會改變的,是以如果在每次循環疊代中都重新進行計算(包括重新構造和重新析構),都将會導緻不必要的低效,而且代碼也不夠幹淨利落。實際上,這個臨時對象的值隻需計算一次,将其儲存在一個局部對象中,之後重複使用即可。

對于程式運作中不會改變的值,應該預先計算并儲存起來備用,而不是重複地建立對象,這是沒有必要的。

優先選擇字首遞增

接下來再考慮一下在

for

循環中

i

的遞增方式:

for(/*...*/; i++)
           

這個臨時對象并不是很明顯。通常,後置遞增的運算效率要低于前置遞增,因為字尾遞增必須記錄和傳回操作數的初始值。通常為了保持一緻性,使用前置遞增來實作後置遞增,看起來像下面這樣:

const T T::operator++(int)
{
T old(*this); // 記錄初始值
++*this; // 使用前置遞增
return old; // 傳回記錄的初始值
}
           

現在,就很容易了解為什麼後置遞增的運算效率要低于前置遞增了。在後置遞增運算中除了必須完成與前置遞增相同的所有工作外,還必須構造和傳回一個包含初始值的臨時對象。

通常,為了保持一緻性,應該使用前置遞增來實作後置遞增。

優先選擇使用前置遞增。隻有在需要初始值時,才使用後置遞增。

在上述問題的代碼中,初始值永遠都用不到,是以也就沒有必要使用後置遞增,而應該使用前置遞增。

編譯器何時優化後置遞增

也許你會認為編譯器會對上面那種沒有使用初始值的後置遞增進行優化。然而編譯器通常都不會這樣做。隻有當操作數的類型是内置類型或者标準類型,比如 int 和 complex,編譯器才會将後置遞增改寫為前置遞增以進行優化,因為編譯器知道這些标準類型的語義。

而對于我們自己建立的類型,編譯器不可能知道前置遞增和後置遞增的實際語義——實際上,這兩個運算所執行的操作可能确實不同。不過,如果這兩個運算的語義不同,那将是一件非常可怕的事情。

有一種方法可以讓編譯器知道在一個類中前置遞增和後置遞增之間的關系:用标準的形式來實作後置遞增,即在後置遞增函數中調用前置遞增,并使用 

inline

 來聲明後置遞增函數,這樣編譯器就能跨越函數邊界來檢測未被使用的臨時對象(這要求編譯器支援 

inline

 指令)。然而 

inline

 并不是萬能的,它有可能會被編譯器忽略,并在其他一些情況下帶來更為緊密的耦合。

更好的解決方案就是養成一種習慣:如果不需要初始值,那麼就使用前置遞增,這樣就不需要使用上面的優化措施了。

注意隐式轉換中的臨時對象

再看看 if 條件語句:

if(*i == name)
...
           

雖然我們沒有給出 Employee 類的定義,但還是可以推斷出這個類的一些資訊。為了使上面的代碼能夠運作,在 Employee 類中很可能有一個将 Employee 轉換為 string 的函數,或者有一個帶有 string 參數的類型轉換構造函數。即要麼轉換 

*i

 為 string 臨時對象,要麼以name為實參構造一個 Employee 臨時對象。

在這兩種情況中都會建立一個臨時對象,并在這個臨時對象上調用 string 的 

operator==()

,或者調用 Employee 的 

operator==()

。隻有當存在一個同時帶有 Employee 參數和 string 參數的 

operator==

,或者 Employee 能夠被轉換為引用類型,即 string & 時,才不會生成臨時對象。

在進行隐式轉換時,要注意在轉換過程中建立的臨時對象。要避免這個問題,一個好辦法就是盡可能地通過顯式的方式來構造對象,并避免編寫類型轉換運算符。

單入/單出更好嗎

代碼中有兩處傳回語句:

return i->addr;
...
return "";
           

這是第一個可能迷糊你的地方。這兩條語句确實都建立了臨時的 string 對象,但是這些臨時對象是無法避免的。你也許會想使用單入/單出(single-entry/single-exit)的程式設計方式,即在函數中聲明一個局部的 string 對象來儲存傳回值,這樣隻需一句 return 語句:

string ret;
...
ret = i->addr;
break;
...
return ret;
           

這種單入/單出的方式通常可以提高代碼的可讀性(而且有時也能使程式運作得更快),但這種做法的表現很大程度上取決于實際的代碼和編譯器。因為還附加了 string 的指派運算符函數的調用開銷。具體的表現得在你使用的編譯器上做測試。但是一般來說,“兩條 return 語句”的函數表現得更加良好。

絕不傳回局部對象的句柄

确實有一個方法能夠避免傳回語句使用的臨時對象,那就是聲明一個靜态局部對象,并傳回這個對象的句柄(引用或指針)。但是,将局部對象定義為靜态的,将導緻函數不可重入,這意味着在多線程環境中它幾乎肯定是一個大問題。此外,傳回局部對象的引用,不用多說,調用者總是會在該對象已經銷毀後還使用它(通過這個函數傳回的句柄),而這一般都會引起記憶體故障,更壞的情況是程式“正常”運作下去······

記住對象的生存期。永遠都不要傳回指向局部對象的指針或引用,它們沒有任何用處,因為主調代碼無法跟蹤它們的有效性,但卻可能會試圖這麼做。

繼續閱讀