1. 起因
某天,一個同事跟我反饋說在windows上調試公司產品的一個交易核心時出現了使用未初始化的指針導致后台服務崩潰的情況。示例代碼如下所示:
1 struct sample 2 { 3 int* ptr_table[4][4]; 4 //... other members 5 }; 6 7 void test() 8 { 9 sample* sample_ptr = new sample[10]; 10 for (int i = 0; i < 4; i++) 11 sample_ptr[0].ptr_table[0][i] = new int(i); 12 13 // 實際系統中是根據初始化數據對sample_ptr數組中的對象進行賦值,但不是所有的對象都有初始化數據; 14 int* int_ptr = sample_ptr[0].ptr_table[0][0]; 15 if (int_ptr != NULL) 16 { 17 printf("ptr1 = 0x%x\n", int_ptr); 18 *int_ptr = 100; 19 } 20 21 int_ptr = sample_ptr[1].ptr_table[0][0]; 22 if (int_ptr != NULL) 23 { 24 printf("ptr2 = 0x%x\n", int_ptr); 25 *int_ptr = 100; // crashed here! 26 } 27 }
使用未初始化的指針是c++的大忌,但是該代碼在產品發布2年左右的時間一直沒有出現過問題。唯一的區別是發布運行環境是linux,而調試環境是最近才配置好的windows環境。分別在linux下和windows下對代碼進行debug,發現linux下新申請的內存被初始化為0,而windows下新申請的內存卻並未初始化。
將sample* sample_ptr = new sample[10]這行改為sample* sample_ptr = new sample[10]()后兩個系統執行的結果變一樣了,都是被初始化的內存。
那么問題來了:
(1) 為什么相同的代碼(new sample[10])在兩個系統下表現形式不一樣呢?是兩個系統的內存分配機制的原因還是類庫的原因?
(2) new sample[10]和new sample[10]()的區別到底是什么?
2. 研究
在c++中,一般都是以new/delete來申請和釋放內存。對於以下幾種new的用法,各自的區別是什么呢?
1 int* p1 = new int; 2 int* p2 = new int(); 3 int* p3 = new int(1); 4 // define class A; 5 A* p4 = new A; 6 A* p5 = new A(); 7 A* p6 = new A[10]; 8 A* p7 = new A[10]();
在平時寫代碼的時候,對於一塊新申請的內存我都會要對它進行初始化后才會去用它,一般是例如p3的直接初始化或者memset,因此對於new A和new A()兩種用法的結果還真是不了解之間的區別。
根據《C++ Primer, Fourth Edition》中5.11節[1] The new and delete Expressions中關於new的描述,new A屬於Default Initializing of Dynamically Allocated Objects(動態創建對象的默認初始化),而new A()則屬於Value Initializing of Dynamically Allocated Objects(動態創建對象的值初始化)。
當為默認初始化操作時,若被創建的對象沒有顯式定義默認構造函數,則按照2.3.4節[2]Variable Initialization Rules的規則進行初始化:
- 對象為內置類型時,任何在函數體外定義的變量都會被初始化0(全局變量或者靜態變量),在函數體內定義的變量都不會進行初始化
- 對象為類類型時,調用對象的默認構造函數
當為值初始化操作時,若被創建的對象沒有顯式定義默認構造函數,則認為對該對象進行初始化操作。
不同new的用法對應的初始化的邏輯總結如下:
new A | new A() | new A(parameters) | |
A為內置類型 | 無初始化動作 | 進行值初始化 例A為int類型,則初始化為0 |
進行值初始化,A被初始化為parameters |
A為calss/struct | 調用默認構造函數,A中成員是否 初始化依賴於默認構造函數的實現 |
若自定義了默認構造函數,則調用自定義的默認構造函數。 否則調用系統默認構造函數,並對A中的成員進行值初始化 |
調用A的自定義構造函數 |
因此上面代碼的執行結果分別為:
- p1指向了一個未被初始化的int空間
- p2指向了一個被初始化的int空間,其值為0
- p3指向了一個被初始化的int空間,其值為1
- p4指向了一個調用了默認構造函數的實例A,除非A的默認構造函數對A的成員進行初始化,否則A的成員全為未初始化變量
- p5指向了一個調用了默認構造函數的實例A,若A自定義了默認構造函數,A成員變量的初始化依賴於自定義的默認構造函數,反之A的成員變量全為初始化后的變量
- p6、p7指向了調用了默認構造函數的實例A的數組,其執行結果同p4、p5
回到之前的問題,結構體sample是沒有自定義默認構造函數的,按照c++標准,new sample的執行結果是sample中的ptr_table是不會被初始化的。這個在windows上是一致的,可是在linux上為什么被初始化成了0了呢?在stackoverflow上找到了一個類似的問題[3],其答案為:“Memory coming from the OS will be zeroed for security reasons....the C standard says nothing about this. This is strictly an OS behavior. So this zeroing may or may not be present on systems where security is not a concern”。操作系統的安全機制會在用戶程序申請內存時對分配的內存進行初始化,以防止別有用心的人從新申請到的內存中讀取到敏感數據,例如密碼等,正是由於這個linux的特性使程序在首次申請內存時總是能得到一塊被初始化的內存。
關於這個安全機制我在網上並沒有搜到比較官方的說明文檔,也許是我的搜索方式有誤或者沒有找對關鍵詞,而且發現在程序運行過程中申請的內存也總並不是被初始化了的內存,例如下面的代碼:
1 int size = 10; 2 { 3 int * p1 = new int[size]; 4 for (int i = 0; i < size; i++) 5 { 6 p1[i] = i + 200; 7 printf("%d\t", p1[i]); 8 } 9 printf("\n"); 10 delete []p1; 11 } 12 13 { 14 int * p1 = new int[size]; 15 for (int i = 0; i < size; i++) 16 printf("%d\t", p1[i]); 17 delete []p1; 18 } 19 printf("\n");
執行結果:
希望對這方面有了解的大神能提供一下相關的資料。
3. 總結
(1) 項目中的代碼是明顯不符合c++代碼規范的,在邏輯上會存在使用未經初始化的指針的現象。個人認為變量的初始化不應依賴於編譯器或者系統的實現,而是盡量遵照c++標准或者手工初始化。
(2) 針對class/struct類型,如果沒有自定義默認構造函數,不同的new的用法會產生不同的結果,這個在以后寫代碼的時候要注意。
1:http://shouce.jb51.net/c++/0201721481/ch05lev1sec11.html
2. http://shouce.jb51.net/c++/0201721481/ch02lev1sec3.html#ch02lev2sec13
3:http://stackoverflow.com/questions/8029584/why-does-malloc-initialize-the-values-to-0-in-gcc
本文為原創內容,若有錯誤的地方煩請指正
本文地址:http://www.cnblogs.com/morebread/p/4936441.html