c++中關於數組的構造、析構,以及a-1(a是數組名)的意義,a-1數組
昨天群裡有人問到關於數組的構造、析構的順序問題,這裡就我的理解範圍解釋一下,當然我對編譯器原理並非是否熟悉,這些也是一個精通C++編譯器的大神教我的,這裡分享出來。
OK,先定義一個類,方便起見,類中增加了一個成員變數,並在構造時進行自增,建構函式和析構都進行了列印操作。類的定義如下:
#include <iostream>#include <cstdio>using namespace std;int total = 0;class A{public : A(int idx){ this->index = idx;cout<<"I am the construct function of idx " << this->index << "."<<endl;};A(){ this->index = total++;cout<<"I am the construct function of idx " << this->index << "."<<endl;};~A(){ cout<<"I am the destrcut function of idx " << this->index << "." << endl;} int index;};int main(){ //normal var //it will be contructed and destructed automaticly A a[17]; for(int i=0 ; i<17 ; i++) cout << "a[" <<i<<"].index = " << a[i].index<<endl; return 0;}
上面定義了一個數組(為求特殊性,個數用了17個。),這樣系統會自動對數組中每個元素進行構造和析構,所以構造和析構的順序就顯而易見了。如下:
不管在看到上面結果之前你是如何猜測的,也不論你對錯與否,上面就是數組常規變數的構造與析構的過程。很顯然,a[i].index = i,所以構造的順序是從數組0開始,逐位構造;而析構的時候,則是從a[16]開始,也就是最後一個開始逐次析構。
其實上面的結論也很容易猜測到,熟悉變數構造和析構順序的同學知道,任何一個函數體的構造,都是從第一個變數開始的,而先構造的變數在函數體結束後會更靠後的被析構。
當然變數是儲存在棧區的,這些變數的空間一般是由系統分配。而數組的線性連續特點,使得數組裡的內容必須滿足有序性,所以構造時需要從小到大逐次構造。而析構時,則需要滿足棧區的操作方式,當然這也與普通變數的析構順序相同。總而言之,普通的變數,析構時總體會滿足“FILO”,也就是先構造的會更靠後被析構。
當然,上面的是對變數的構造、析構順序的一個測試和簡單分析,對指標型的變數呢?
在測試之前,首先要明確幾點就是,指標型的變數也是需要空間的,起分配規則與普通變數無異,但指標變數所指向的內容則是在執行時才確定的,也就是執行到new或malloc才會進行構造或分配,同時只有執行到delete或者free才會進行析構或回收。所以基本可以確定普通的變數的構造與析構順序是完全獨立隨意的。
當然new的內容存在堆區,所以也不存在FILO的操作制約等等。
然後問題就是,對數組的構造是怎樣呢?
帶著疑惑,對上面的程式稍作修改可以容易得出結論:
#include <iostream>#include <cstdio>using namespace std;int total = 0;class A{public : A(int idx){ this->index = idx;cout<<"I am the construct function of idx " << this->index << "."<<endl;};A(){ this->index = total++;cout<<"I am the construct function of idx " << this->index << "."<<endl;};~A(){ cout<<"I am the destrcut function of idx " << this->index << "." << endl;} int index;};int main(){ //normal var //it will be contructed and destructed automaticly //A a[17]; A *a = new A[17]; for(int i=0 ; i<17 ; i++) cout << "a[" <<i<<"].index = " << a[i].index<<endl; delete [] a; return 0;}而運行結果與完全一致。
所以你問系統或者問編譯器,它會如何構造和析構,它會告訴你,它會和上面的流程保持一致。
此時可能你覺得不會有什麼問題,但這裡其實有一個很大的問題,就是指標與數組不同的是,對數組而言,編譯器在編譯時間就可以知道數組長度,從而從最後一個元素進行析構,但是指標如何做到?如果沒有一個變數儲存數組的長度是否還能完成delete操作?如果有,這個變數儲存在哪?
那麼實際上答案是很確定的,有這樣一個變數,這就是傳說中的a-1。首先在構造時儲存該數值,在析構時,讀出該值,並指向數組最後位置,並逐次進行析構操作。
當然這裡多提1句,就是對於單個變數A a = new A();delete [] a;這樣的操作,實際上是合法的,同樣的析構時會讀出a-1,當然在一般情況下該值為0,所以在正常情況下,delete[]與delete完成的都是對a的析構。但若該位置的值被修改,析構時並不會進行驗證,有可能會導致析構時讀取了非法指標等情況造成崩潰。
update 9/9:
上面的解釋實際上是有問題的,之前沒有細看。其實在一般情況下一個變數的-1位置是不為0的,也表現了該變數是記憶體空間的一個隨機位置。所以一般情況下如果執行delete [] a;程式是會crash的。如果遇到為0的情況,由於擷取到的數組長度為0,實際上也不會做任何析構操作。
只有當a-1的值為1時才會完成對a的析構,可以通過手動修改a-1的值達到預期效果,測試代碼和運行如下:
#include <iostream>#include <cstdio>using namespace std;int total = 0;class A{public : A(int idx){ this->index = idx;cout<<"I am the construct function of idx " << this->index << "."<<endl;};A(){ this->index = total++;cout<<"I am the construct function of idx " << this->index << "."<<endl;};~A(){ cout<<"I am the destrcut function of idx " << this->index << "." << endl;} int index;};int main(){ A *a = new A[231]; A *b = new A(); /* for(int i=0 ; i<17 ; i++) cout << "a[" <<i<<"].index = " << a[i].index<<endl; */ unsigned char *point = (unsigned char *)a; //printf("%d %d\n",point , point -1); int num = *(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4); //*(point - 4) = 11; cout << num << endl; delete [] a; point = (unsigned char *)b; num = *(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4); cout << num << endl; *(point - 4) = 1; *(point - 3) = 0; *(point - 2) = 0; *(point - 1) = 0; delete [] b; return 0;}
首先列印的是變數a-1的值(逆序後的),然後強制賦值為1,delete[]a,完成析構。
如果不進行強制賦值,則可能會crash,當然如果是剛好分配到一塊比較空的地區a-1為0,則不會做任何操作,可以自行測試,我這邊是測試過的,結論與我分析的一樣。
-----------------------------------------------------------------------
喜歡測試的人一定會很有興趣的列印出a-1的值,然後說“不對啊,*(a-1)並不是a的長度17啊”,實際上,這種編譯器層級(且叫他這個名字因為我也不知道這個過程在哪裡完成的,因為C++是沒有所謂的源碼的,他的實現都靠彙編或者機器語言)的操作,對變數的伸展方向與它的定址方向是一致的,因為該操作是“向後”定址,所以這個a-1的數值也與一般的變數的高低位構成是相反的。
一般一個4位元組變數會由從低到高4個位元組完成,假設是abcd01,abcd02,abcd03,abcd04,這樣的一個變數最終表示的數值就是
*(abcd01)<<12 | *(abcd02)<<8 | *(abcd03)<<4 | *(abcd04)
對於上面的a-1的逆向伸展方向而言,他在寄存器裡會反向,所以表示的實際的數值為(假設a的地址為point,a-1對於的4個位元組為point-1~point-4)
*(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4);
具體可以參照如下代碼:
#include <iostream>#include <cstdio>using namespace std;int total = 0;class A{public : A(int idx){ this->index = idx;cout<<"I am the construct function of idx " << this->index << "."<<endl;};A(){ this->index = total++;cout<<"I am the construct function of idx " << this->index << "."<<endl;};~A(){ cout<<"I am the destrcut function of idx " << this->index << "." << endl;} int index;};int main(){ A *a = new A[17]; for(int i=0 ; i<17 ; i++) cout << "a[" <<i<<"].index = " << a[i].index<<endl; unsigned char *point = (unsigned char *)a; //printf("%d %d\n",point , point -1); int num = *(point -1)<<12 | (*(point - 2) << 8) | (*(point -3)<< 4) | *(point - 4); *(point - 4) = 11; cout << num << endl; delete [] a; return 0;}
上面的程式我做了幾個測試,首先是列印出a-1的值,當然是和構造時的17是一致的。同時為了驗證析構時的順序,我嘗試通過強制行為對a-1的值進行修改。我這裡講原來的17修改為11,然後再來看此時析構的結果:
如,首先正常列印出了數組長度17,然後仔細看析構的內容,是否發現析構異常了,只析構了從a[10]~a[0],因為我手動將a-1的內容修改為了11,而在delete[]的時候,系統也無法驗證該值的正確性,所以“信任”了該值,認為該"數組"的長度就是11,析構了11個元素。就產生了上面的輸出。
相信看了這些代碼應該對數組構造和析構的順序有了比較鮮明的理解。
這裡簡單總結一下
1、對於變數型的數組,構造與析構嚴格遵守棧區的操作流程,先構造的後析構;
2、指標型的變數new出的數組,也會從0開始逐次構造,從最後一個元素開始逐次析構;
3、new出的數組,會將數組長度儲存到數組-1的位置中;
4、只有new出的數組才會有數組-1的長度這個值的存在,如果是變數定義的數組,該位置的值無意義。
當然如果有其他人有更多補充,歡迎指正!
在C語言中,一維數組的定義方式為:類型說明符數組名——
在C語言中,一維數組的定義方式:
類型說明符 數組名[元素個數]
其中,類型名確定所有元素的資料類型,元素個數給定數組要包含的變數個數,它可以使用運算式形式,但該運算式中只能出現變數常量和運算子。
常用的類型:char ,int ,long .float,double.
數組元素的一般表示形式是:
數組名[下標]
其中,下標可以使用運算式形式,但必須是整型而且有確定的值,取值範圍是0~元素個數-1.
注意:引用數組元素時不應使用超範圍的下標,因為對這種情況編譯時間系統並不報錯,所以編寫程式時要格外注意。
C語言中數組名就是數組的首地址,怎解釋?哥們菜鳥
比如a[3][4]
C語言對二維數組的處理方式是將其分解成多個一維數組。如對二維數組a的處理方式是把a看成是一個一維數組,數組a包含a[0],a[1],a[2]這3個元素。而每一個元素又是一個一維數組,各包含4個元素,如a[0]所代表的一維數組又包含a[0][0],a[0][1],a[0][2],a[0][3],這4個元素。
由於系統並不為數組名分配記憶體,所以由a[0],a[1],a[2]組成的一維數組在記憶體中並不存在,他們只是表示相應的行的首地址。