1. 程式人生 > >一些讓人常搞糊塗的名稱, 陣列指標 指標**

一些讓人常搞糊塗的名稱, 陣列指標 指標**

2004年7月31日星期六 第二次更新,增加了物件的堆記憶體分配,細化了指標陣列和陣列指標記憶體的說明 !

昨天給0405班輔導,當他們問我指標陣列的問題的時候,由於好久沒用,感覺有些陌生了,所以今天歸納總結了一下,希望以後的朋友可以少走些彎路:)
一 :關於指標和堆的記憶體分配
先來介紹一下指標 : 指標一種型別,理論上來說它包含其他變數的地址,因此有的書上也叫它:地址變數。既然指標是一個型別,是型別就有大小,在達內的伺服器上或者普通的PC機上,都是4個位元組大小,裡邊只是儲存了一個變數的地址而已。不管什麼型別的指標,char * ,int * ,int (*) ,string * ,float * ,都是說明了本指標所指向的地址空間是什麼型別而已,瞭解了這個基本上所有的問題都好象都變的合理了。

在C++中,申請和釋放堆中分配的存貯空間,分別使用new和delete的兩個運算子來完成:
指標型別 指標變數名=new 指標型別 (初始化);
        delete 指標名;
例如:1、 int *p=new int(0);
     它與下列程式碼序列大體等價:
2、int tmp=0, *p=&tmp;
區別:p所指向的變數是由庫操作符new()分配的,位於記憶體的堆區中,並且該物件未命名。
  
下面是關於new 操作的說明 : 部分引自<<C++面向物件開發>>
1、new運算子返回的是一個指向所分配型別變數(物件)的指標。對所建立的變數或物件,都是通過該指標來間接操作的,而動態建立的物件本身沒有名字。
2、一般定義變數和物件時要用識別符號命名,稱命名物件,而動態的稱無名物件(請注意與棧區中的臨時物件的區別,兩者完全不同:生命期不同,操作方法不同,臨時變數對程式設計師是透明的)。
3、堆區是不會在分配時做自動初始化的(包括清零),所以必須用初始化式(initializer)來顯式初始化。new表示式的操作序列如下:從堆區分配物件,然後用括號中的值初始化該物件。

下面是從堆中申請陣列
1、申請陣列空間:
指標變數名=new 型別名[下標表達式];
注意:“下標表達式”不是常量表達式,即它的值不必在編譯時確定,可以在執行時確定。這就是堆的一個非常顯著的特點,有的時候程式設計師本身都不知道要申請能夠多少記憶體的時候,堆就變的格外有用。
2、釋放陣列空間:
delete [ ]指向該陣列的指標變數名;
注意:方括號非常重要的,如果delete語句中少了方括號,因編譯器認為該指標是指向陣列第一個元素的,會產生回收不徹底的問題(只回收了第一個元素所佔空間),我們通常叫它“記憶體洩露”,加了方括號後就轉化為指向陣列的指標,回收整個陣列。delete [ ]的方括號中不需要填陣列元素數,系統自知。即使寫了,編譯器也忽略。<<Think in c++>>上說過以前的delete []方括號中是必須添加個數的,後來由於很容易出錯,所以後來的版本就改進了這個缺陷。
下面是個例子,VC上編譯通過
#include<iostream>
using namespace std;
//#include <iostream.h>  //for VC
#include <string.h>
void main(){
int n;
char *p;
cout<<"請輸入動態陣列的元素個數"<<endl;
cin>>n; //n在執行時確定,可輸入17
p=new char[n]; //申請17個字元(可裝8個漢字和一個結束符)的記憶體空間strcpy(pc,“堆記憶體的動態分配”);//
cout<<p<<endl;
delete []p;//釋放pc所指向的n個字元的記憶體空間return ; }

通過指標使堆空間,程式設計中的幾個可能問題

1.動態分配失敗。返回一個空指標(NULL),表示發生了異常,堆資源不足,分配失敗。
   data = new double [m]; //申請空間
if ((data ) == 0)…… //或者==NULL
2.指標刪除與堆空間釋放。刪除一個指標p(delete p;)實際意思是刪除了p所指的目標(變數或物件等),釋放了它所佔的堆空間,而不是刪除p本身,釋放堆空間後,p成了空懸指標,不能再通過p使用該空間,在重新給p賦值前,也不能再直接使用p。
3.記憶體洩漏(memory leak)和重複釋放。new與delete 是配對使用的, delete只能釋放堆空間。如果new返回的指標值丟失,則所分配的堆空間無法回收,稱記憶體洩漏,同一空間重複釋放也是危險的,因為該空間可能已另分配,而這個時候又去釋放的話,會導致一個很難查出來的執行時錯誤。所以必須妥善儲存new返回的指標,以保證不發生記憶體洩漏,也必須保證不會重複釋放堆記憶體空間。
4.動態分配的變數或物件的生命期。無名變數的生命期並不依賴於建立它的作用域,比如在函式中建立的動態物件在函式返回後仍可使用。我們也稱堆空間為自由空間(free store)就是這個原因。但必須記住釋放該物件所佔堆空間,並只能釋放一次,在函式內建立,而在函式外釋放是一件很容易失控的事,往往會出錯,所以永遠不要在函式體內申請空間,讓呼叫者釋放,這是一個很差的做法。你再怎麼小心翼翼也可能會帶來錯誤。
類在堆中申請記憶體 :
通過new建立的物件要呼叫建構函式,通過deletee刪除物件要呼叫解構函式。
CGoods *pc;
pc=new CGoods;  //分配堆空間,並構造一個無名物件
                              //的CGoods物件;
…….
delete pc;  //先析構,然後將記憶體空間返回給堆;        堆物件的生命期並不依賴於建立它的作用域,所以除非程式結束,堆物件(無名物件)的生命期不會到期,並且需要顯式地用delete語句析構堆物件,上面的堆物件在執行delete語句時,C++自動呼叫其解構函式。
正因為建構函式可以有引數,所以new後面類(class)型別也可以有引數。這些引數即建構函式的引數。
但對建立陣列,則無引數,並只調用預設的建構函式。見下例類說明:

class CGoods{
          char Name[21];
          int  Amount;
          float Price;
          float Total_value;
public:
 CGoods(){}; //預設建構函式。因已有其他建構函式,系統不會再自動生成預設構造,必須顯式宣告。   CGoods(char* name,int amount ,float price){
           strcpy(Name,name);
           Amount=amount;
           Price=price;
           Total_value=price*amount;  }
           ……};//類宣告結束
下面是呼叫機制 :

void main(){
 int n;
 CGoods *pc,*pc1,*pc2;
 pc=new CGoods(“hello”,10,118000);
 //呼叫三引數建構函式   pc1=new CGoods();  //呼叫預設建構函式  cout<<”輸入商品類陣列元素數”<<endl;
 cin>>n;
 pc2=new CGoods[n];
//動態建立陣列,不能初始化,呼叫n次預設建構函式  
 ……
 delete pc;
 delete pc1;
 delete []pc2;  }

申請堆空間之後建構函式執行;
釋放堆空間之前解構函式執行;
再次強調:由堆區建立物件陣列,只能呼叫預設的建構函式,不能呼叫其他任何建構函式。如果沒有預設的建構函式,則不能建立物件陣列。

---------------------下面我們再來看一下指標陣列和陣列指標―――――――――――――
如果你想了解指標最好理解以下的公式 :
(1)int*ptr;//指標所指向的型別是int

  (2)char*ptr;//指標所指向的的型別是char

  (3)int**ptr;//指標所指向的的型別是int* (也就是一個int * 型指標)

  (4)int(*ptr)[3];//指標所指向的的型別是int()[3] //二維指標的宣告

(1)指標陣列:一個數組裡存放的都是同一個型別的指標,通常我們把他叫做指標陣列。
比如 int * a[10];它裡邊放了10個int * 型變數,由於它是一個數組,已經在棧區分配了10個(int * )的空間,也就是32位機上是40個byte,每個空間都可以存放一個int型變數的地址,這個時候你可以為這個陣列的每一個元素初始化,在,或者單獨做個迴圈去初始化它。
例子:
int * a[2]={ new int(3),new int(4) };     //在棧區裡宣告一個int * 陣列,它的每一個元素都在堆區裡申請了一個無名變數,並初始化他們為3和4,注意此種宣告方式具有缺陷,VC下會報錯
例如 :
int * a[2]={new int[3],new int[3]};
delete a[0];
delet a[10];
但是我不建議達內的學生這麼寫,可能會造成歧義,不是好的風格,並且在VC中會報錯,應該寫成如下 :
int * a[2];
a[0]= new int[3];
a[1]=new int[3];
delete a[0];
delet a[10];
這樣申請記憶體的風格感覺比較符合大家的習慣;由於是陣列,所以就不可以delete a;編譯會出警告.delete  a[1];
注意這裡 是一個數組,不能delete [] ;
( 2 ) 陣列指標 : 一個指向一維或者多維陣列的指標;
int * b=new int[10]; 指向一維陣列的指標b ;
注意,這個時候釋放空間一定要delete [] ,否則會造成記憶體洩露, b 就成為了空懸指標.

int (*b2)[10]=new int[10][10]; 注意,這裡的b2指向了一個二維int型陣列的首地址.
注意:在這裡,b2等效於二維陣列名,但沒有指出其邊界,即最高維的元素數量,但是它的最低維數的元素數量必須要指定!就像指向字元的指標,即等效一個字串,不要把指向字元的指標說成指向字串的指標。這與陣列的巢狀定義相一致。
int(*b3) [30] [20];  //三級指標――>指向三維陣列的指標;
int (*b2) [20];     //二級指標;
b3=new int [1] [20] [30];
b2=new int [30] [20];
      兩個陣列都是由600個整陣列成,前者是隻有一個元素的三維陣列,每個元素為30行20列的二維陣列,而另一個是有30個元素的二維陣列,每個元素為20個元素的一維陣列。
      刪除這兩個動態陣列可用下式:
delete [] b3;  //刪除(釋放)三維陣列;
delete [] b2;  //刪除(釋放)二維陣列;
再次重申:這裡的b2的型別是int (*) ,這樣表示一個指向二維陣列的指標。
b3表示一個指向(指向二維陣列的指標)的指標,也就是三級指標.

( 3 ) 二級指標的指標
看下例 :
int (**p)[2]=new (int(*)[3])[2];
       p[0]=new int[2][2];
       p[1]=new int[2][2];
       p[2]=new int[2][2];
       delete [] p[0];
       delete [] p[1];
       delete [] p[2];
       delete [] p;
注意此地方的指標型別為int (*),碰到這種問題就把外邊的[2]先去掉,然後回頭先把int ** p=new int(*)[n]申請出來,然後再把外邊的[2]附加上去;
p代表了一個指向二級指標的指標,在它申請空間的時候要注意指標的型別,那就是int (*)代表二級指標,而int (**)顧名思義就是代表指向二級指標的指標了。既然是指標要在堆裡申請空間,那首先要定義它的範圍:(int(*)[n])[2],n 個這樣的二級指標,其中的每一個二級指標的最低維是2個元素.(因為要確定一個二級指標的話,它的最低維數是必須指定的,上邊已經提到)。然後我們又分別為p[0],p[1],p[2]…在堆裡分配了空間,尤其要注意的是:在釋放記憶體的時候一定要為p[0],p[1],p[2],單獨delete[] ,否則又會造成記憶體洩露,在delete[]p 的時候一定先delete p[0]; delete p[1],然後再把給p申請的空間釋放掉 delete [] p ……這樣會防止記憶體洩露。

(3)指標的指標;
int ** cc=new (int*)[10]; 宣告一個10個元素的陣列,陣列每個元素都是一個int *指標,每個元素還可以單獨申請空間,因為cc的型別是int*型的指標,所以你要在堆裡申請的話就要用int *來申請;
看下邊的例子  (vc & GNU編譯器都已經通過);
       int ** a= new int * [2];     //申請兩個int * 型的空間
       a[1]=new int[3];        //為a的第二個元素又申請了3個int 型空間,a[1]指向了此空間首地址處
       a[0]=new int[4];        ////為a的第一個元素又申請了4個int 型空間,a[0] 指向了此空間的首地址處
       int * b;
       a[0][0]=0;
       a[0][1]=1;
       b=a[0];
  delete [] a[0]       //一定要先釋放a[0],a[1]的空間,否則會造成記憶體洩露.;
       delete [] a[1];
  delete [] a;
       b++;
       cout<<*b<<endl;       //隨機數
注意 :因為a 是在堆裡申請的無名變數陣列,所以在delete 的時候要用delete [] 來釋放記憶體,但是a的每一個元素又單獨申請了空間,所以在delete [] a之前要先delete [] 掉 a[0],a[1],否則又會造成記憶體洩露.
(4) 指標陣列 :
   
我們再來看看第二種 :二維指標陣列
int *(*c)[3]=new int *[3][2];
如果你對上邊的介紹的個種指標型別很熟悉的話,你一眼就能看出來c是個二級指標,只不過指向了一個二維int * 型的陣列而已,也就是二維指標陣列。
例子 :
 int *(*b)[10]=new int*[2][10];//
b[0][0]=new int[100];
b[0][1]=new int[100];
*b[0][0]=1;
cout <<*b[0][0]<<endl;    //列印結果為1
delete [] b[0][0];
delete [] b[0][1];
delete [] b;
cout<<*b[0][0]<<endl;    //列印隨機數
 這裡只為大家還是要注意記憶體洩露的問題,在這裡就不再多說了。
如果看了上邊的文章,大家估計就會很熟悉,這個b是一個二維指標,它指向了一個指標陣列

第二種 :
int **d[2];表示一個擁有兩個元素陣列,每一個元素都是int ** 型,這個指向指標的指標:)
   d不管怎樣變終究也是個陣列,呵呵,
   如果你讀懂了上邊的,那下邊的宣告就很簡單了:
   d[0]=new int *[10];
   d[1]=new int * [10];
delete [] d[0];
delete [] d[1];
具體的就不再多說了 :)
二 : 函式指標 

關於函式指標,我想在我們可能需要寫個函式,這個函式體內要呼叫另一個函式,可是由於專案的進度有限,我們不知道要呼叫什麼樣的函式,這個時候可能就需要一個函式指標;

int a();這個一個函式的宣告;
ing (*b)();這是一個函式指標的宣告;
讓我們來分析一下,左邊圓括弧中的星號是函式指標宣告的關鍵。另外兩個元素是函式的返回型別(void)和由邊圓括弧中的入口引數(本例中引數是空)。注意本例中還沒有建立指標變數-只是聲明瞭變數型別。目前可以用這個變數型別來建立型別定義名及用sizeof表示式獲得函式指標的大小:
unsigned psize = sizeof (int (*) ()); 獲得函式指標的大小
// 為函式指標宣告型別定義
typedef int (*PFUNC) ();

PFUNC是一個函式指標,它指向的函式沒有輸入引數,返回int。使用這個型別定義名可以隱藏複雜的函式指標語法,就我本人強烈建議我們大內弟子使用這種方式來定義;

下面是一個例子,一個簡單函式指標的回撥(在GNU編譯器上通過,在VC上需要改變一個頭檔案就OK了)

#include<iostream>              //GNU 編譯器 g++ 實現
using namespace std;
/*                              //vc 的實現
#include "stdafx.h"
#include <iostream.h>
*/

#define DF(F) int F(){  cout<<"this is in function "<<#F<<endl;/
      return 0;       /
}
//宣告定義DF(F)替代 int F();函式;
DF(a); DF(b); DF(c); DF(d); DF(e); DF(f); DF(g); DF(h); DF(i);     //宣告定義函式 a b c d e f g h i

// int (*pfunc)();              //一個簡單函式指標的宣告
typedef int(*FUNC)();   //一個函式指標型別的宣告

FUNC ff[] = {a,b,c,d,e,f,g,h,i};   //宣告一個函式指標陣列,並初始化為以上宣告的a,b,c,d,e,f,g,h,i函式

FUNC func3(FUNC vv){    //定義函式func3,傳入一個函式指標,並且返回一個同樣型別的函式指標
      vv();
      return vv;
}

/*FUNC func4(int (*vv)()){      //func3的另一種實現
      vv();
      return vv;
}*/

int main(){
      for(int i=0;i<sizeof(ff)/sizeof (FUNC);i++){  //迴圈呼叫函式指標
              FUNC r=func3(ff[ i ]);
              cout<<r()<<endl;                //輸出返回值,只是返回了0
      }
      return 0;
}
到目前為止,我們只討論了函式指標及回撥而沒有去注意ANSI C/C++的編譯器規範。許多編譯器有幾種呼叫規範。如在Visual C++中,可以在函式型別前加_cdecl,_stdcall或者_pascal來表示其呼叫規範(預設為_cdecl)。C++ Builder也支援_fastcall呼叫規範。呼叫規範影響編譯器產生的給定函式名,引數傳遞的順序(從右到左或從左到右),堆疊清理責任(呼叫者或者被呼叫者)以及引數傳遞機制(堆疊,CPU暫存器等)。
好了,先到此為止吧,寫這篇文章耗費了基本上快半天的時間了,很多事情還沒有做,等改天有時間再回來整理,所有的源程式都放在openlab3伺服器上我的目錄下lib/cpp下,大家可以去拿。不知道的登陸openlab3 然後cd ~chengx/lib/cpp就可以看到了。

還有很複雜的宣告可能也是一種挑戰 比如<<Think in c++>>裡的
int (*(*f4())[10]();的宣告,f4是一個返回指標的函式,該指標指向了含有10個函式指標的陣列,這些函式返回整形值;不是這個函式有特別之處,而是Bruce Eckel 說的“從右到左的辨認規則”是一種很好的方法,值得我們去學習,感謝他:)

最後我想應該跟大家說一下,寫程式應該就象JERRY所說的:簡單就是美;我們應該遵循一個原則 : KISS (Keep It Simple,Stupid ,儘量保持程式簡單 出自 :《Practical C programming》),把自己的程式儘量的簡單明瞭,這是個非常非常好的習慣。

由於寫的匆忙,可能其中有遺漏的地方,大家發現希望能指正:)
GOOD LUCK !