C++之指針_1

1.1指針簡介

計算機程序在存儲數據時必須跟蹤3種基本屬性:

  • 信息存儲在何處
  • 存儲的值爲多少
  • 存儲的信息是什麼類型

一種策略是:定義一個簡單變量。聲明語句指出了值的類型和符號名,還讓程序爲值分配內存,並在內部跟蹤該存儲單元。

另外一種策略,它在開發C++類時非常重要,這種策略以指針爲基礎,指針是一個變量其存儲的是值的地址,而不是值本身。在討論指針之前,我們先看一看如何找到常規變量的地址。只需對變量應用地址運算符(&),就可以獲得它的位置:例如,如果home是一個變量,則&home是它的地址。

#include <iostream>
int main()
{
	using namespace std;
	int donuts = 6;
	double cups = 4.5;
	cout << "donuts value = " << donuts;
	cout << " and donuts address = " << &donuts << endl;
	cout << "cups value = " << cups;
	cout << " and cups address = " << &cups << endl;
	return 0;
}

//程序輸出
donuts value = 6 and donuts address = 0x0065fd40
cups value = 4.5 and cups address = 0x0065fd44

處理存儲數據的新策略剛好相反,將地址視爲指定的量,而將值視爲派生量。一種特殊類型的變量—指針,用於存儲值的地址。因此,指針名錶示的是地址。*運算符被稱爲間接值(indirect value)或解除引用(dereferencing運算符,將其應用於指針,可以得到該地址處存儲的值(這和乘法使用的符號相同;C++根據上下文來確定所指的是乘法還是解除引用)。例如,假設manyl是個指針,則 manly表示的是一個地址,而*manly表示存儲在該地址處的值。*manly與常規int變量等效。

  • 指針示例pointer.cpp

    #include <iostream>
    int main()
    {
    	using namespace std;
    	int updates = 6;
    	int * p_updates;
    	p_updates = &updates;
    	//顯示值
    	cout << "Values: updates = " << updates;
    	cout << ", *p_updates = " << *p_updates << endl;
    	//顯示地址
    	cout << "Address: &updataes = " << &updates;
    	cout << ",p_updates = " << p_updates << endl;
    	//使用指針更改值
    	*p_updates = *p_updates + 1;
    	cout << "Now updates = " << updates << endl;
    	return 0;
    }
    
    
    //輸出結果
    Value: updates = 6,*p_updates = 6
    Address: &updatas = 0x0065fd48,p_updates = 0x0065fd48
    Now updates = 7
    

    從中可知,int變量updates和指針變量 p_updates只不過是同一枚硬幣的兩面,變量updates表示值,並使用&運算符來獲得地址:而變量p_updates表示地址,並使用運算符來獲得值(參見圖4.8)。由於p_ updates指向 updates,因此*p_updates和 updates完全等價。可以像使用int變量那樣使用 *p_ updates,正如上述程序表明的,甚至可以將值賦給*p_updates,這樣做將修改指向的值,即updates

1.2指針聲明和初始化

計算機需要跟蹤指針指向的值的類型。例如,char的地址與 double的地址看上去沒什麼兩樣,但char和 double使用的字節數是不同的,它們存儲值時使用的內部格式也不同。因此,指針聲明必須指定指針指向的數據的類型。

int * p_udates;

這表明,* p_updates的類型爲int。由於*運算符被用於指針,因此p_updates變量本身必須是指針。我們說p_updates指向int類型,我們還說p_updates的類型是指向int的指針,或int* 類型,可以這樣說,p_updates是指針(地址),而 * p_updates是int,而不是指針(見圖4.9).

順便說一句,* 運算符兩邊的空格是可選的。傳統上C程序員使用這種格式:

int *ptr

這強調*ptr是一個int類型的值。而很多C++程序元使用這種格式:

int* ptr

這強調int* 是一種類型–指向int的指針。

但要知道的是,下面的聲明創建一個指針(p1)和一個int變量(p2)

int* p1,p2;

對每一個指針變量名,都需要使用一個*。

可以用同樣的句法來聲明指向其他類型的指針:

double * tax_ptr;  //指向double類型的指針
char * str;        //指向char類型的指針

由於已將tax_ptr聲明爲一個指向 double的指針,因此編譯器知道 *tax_ptr是一個 double類型的值。也就是說,它知道 *tax_ptr是一個以浮點格式存儲的值,這個值(在大多數系統上)佔據8個字節。指針變量不僅僅是指針,而且是指向特定類型的指針。 tax_ptr的類型是指向 double的指針(或 double *類型),str是指向char的指針類型(或char *)。儘管它們都是指針,卻是不同類型的指針。和數組一樣,指針都是基於其他類型的。

雖然tax_ptr和str指向兩種長度不同的數據類型,但這兩個變量本身的長度通常是相同的,也就是說, char的地址與 double的地址的長度相同,這就好比1016可能是超市的街道地址,而1024可以是小村莊的道地址一樣。地址的長度或值既不能指示關於變量的長度或類型的任何信息,也不能指示該地址上有什麼建築物。一般來說,地址需要2個還是4個字節,取決於計算機系統(有些系統可能需要更大的地址,系統可以針對不同的類型使用不同長度的地址)。

可以在聲明語句中初始化指針。在這種情況下,被初始化的是指針,而不是它指向的值。也就是說下面的語句將pt(而不是*pt)的值設置爲& higgens:

int higgens = 5;
int * pt = &higgens;

1.3指針和數字

指針不是整型,雖然計算機通常把地址當作整數來處理。從概念上看,指針與整數是截然不同的類型。整數是可以執行加、減、除等運算的數字,而指針描述的是位置,將兩個地址相乘沒有任何意義。從可以對整數和指針執行的操作上看,它們也是彼此不同的。因此,不能簡單地將整數賦給指針:

int * pt;
pt = 0xB8000000;    //類型不匹配;

在這裏,左邊是指向int的指針,因此可以把它賦給地址,但右邊是一個整數。你可能知道,0xB8000000是老式計算機系統中內存的組合段偏移地址,但這條語句並沒有告訴程序,這個數字就是一個地址。在C99標準發佈之前,C語言允許這樣賦值。但C++在類型一致方面的要求更嚴格,編譯器將顯示一條錯誤消息,通告類型不匹配。要將數字值作爲地址來使用,應通過強制類型轉換將數字轉換爲適當的地址類型:

int * pt;
pt = (int*) 0xB8000000;  //類型匹配

1.4使用new來分配內存

前面我們都將指針初始化爲變量的地址;變量是在編譯時分配的有名稱的內存,而指針只是爲可以通過名稱直接訪問的內存提供了 一個別名。指針真正的用武之地在於,在運行階段分配未命名的內存以存儲值。在這種情況下,只能通過指針來訪問內存。

下面來試試這種新技術,在運行階段爲一個int值分配未命名的內存,並使用指針來訪問這個值。這裏的關鍵所在是C++的new運算符。程序員要告訴new,需要爲哪種數據類型分配內存;new將找到一個長度正確的內存塊,並返回該內存塊的地址。程序員的責任是將該地址賦給一個指針。下面是一個這樣的示例:

int * pn = new int;

new int告訴程序,需要適合存儲int的內存。new運算符根據類型來確定需要多少字節的內存。然後它找到這樣的內存,並返回其地址。接下來,將地址賦給pn,pn是被聲明爲指向int的指針,現在,pn是地址,而*pn是存儲在那裏的值。將這種方法與將變量的地址賦給指針進行比較:

int higgens;
int * pt = &higgens;

在這兩種情況(pn和pt)下,都是將一個int變量的地址賦給了指針。在第二種情況下,可以通過名稱 higgens來訪問該int,在第一種情況下,則只能通過該指針進行訪問,這引出了一個問題:pn指向的內存沒有名稱,如何稱呼它呢?我們說pn指向一個數據對象,這裏的“對象”不是“面向對象編程”中的對象,而是一樣“東西”。術語“數據對象”比“變量”更通用,它指的是爲數據項分配的內存塊。因此,變量也是數據對象,但pn指向的內存不是變量。乍一看,處理數據對象的指針方法可能不太好用,但它使程序在管理內存方面有更大的控制權。

爲一個數據對象(可以是結構,也可以是基本類型)獲得並指定分配內存的通用格式如:

typeName * pointer_name = new typeName

需要在兩個地方指定數據類型:用來指定需要什麼樣的內存和用來聲明合適的指針。當然,如果已經聲明瞭相應類型的指針,則可以使用該指針,而不用再聲明一個新的指針。下面程序展示如何將new用於兩種不同的類型:

// ues_new.cpp
#include <iostream>
int main()
{
	using namespace std;
	int nights = 1001;
	int * pt = new int;   //分配一個int內存
	*pt = 1001;           //給該內存賦值
	
	cout << "nights value = ";
	cout << nights << ": location " << &nights << endl;
	cout << "int ";
	cout << "value = " << *pt << ": location = " << pt << endl;
	double * pd = new double; //分配一個double內存
	*pd = 10000001.0;     //給該內存賦double類型值
	cout << "double ";
	cout << "value = " << *pd << ":location = " << pd << endl;
	cout << "location of pointer pd: " << &pd << endl;
	cout << "size of pt = " << sizeof(pt);
	cout << ": size of *pt = " << sizeof(*pt) << endl;
	cout << "size of pd = " << sizeof pd;
	cout << ": size of *pd = " << sizeof(*pd) << endl;
	return 0;
	
}
//程序輸出
nights value = 1001: location = 0028F7F8;
int value = 1001: location = 00033A98
double value = 1e+007: location = 000339B8
location of pointer pd: 0028F7FC
size of pt = 4: size of *pt = 4
size of pd = 4: size of *pd = 8

當然,內存位置的準確值隨系統而異。

對於指針,需要指出的另一點是,new分配的內存塊通常與常規變量聲明分配的內存塊不同,變量和pd的值都存儲在被稱爲棧( stack)的內存區域中,而new從被稱爲堆(heap)或自由存儲區(free store)的內存區域分配內存。

1.5使用delete釋放內存

當需要內存時,可以使用new來請求,這只是C++內存管理數據包中有魅力的一個方面。另一個方面是 delete運算符,它使得在使用完內存後,能夠將其歸還給內存池,這是通向最有效地使用內存的關鍵一步。歸還或釋放(free)的內存可供程序的其他部分使用。使用 delete時,後面要加上指向內存塊的指針(這些內存塊最初是用new分配的):

int * ps = new int;   //使用new分配一個內存
...                   //使用內存
delete ps;             //使用完後釋放內存

這將釋放ps指向的內存,但不會刪除指針ps本身,例如,可以將ps重新指向另一個新分配的內存塊定要配對地使用new和 delete否則將發生內存泄漏( memory leak),也就是說,被分配的內存再也無法使用了。如果內存泄漏嚴重,則程序將由於不斷尋找更多內存而終止。

不要嘗試釋放已經釋放的內存塊,C++標準指出,這樣做的結果將是不確定的,這意味着什麼情況都可能發生。另外,不能使用 delete來釋放聲明變量所獲得的內存:

int * ps = new int;  //ok
delete ps;            //ok
delete ps;            //not ok
int jugs = 5;        //ok
int * pi = &jugs     //ok
delete pi             //不允許;內存不是由new分配的

注意:只能用 delete來釋放使用new分配的內存。對空指針使用 delete是安全的。

注意,使用 delete的關鍵在於,將它用於new分配的內存。這並不意味着要使用用於new的指針,而是用於new的地址:

int * ps = new int;   //分配內存
int * pq = ps;        //設置第二個指針指向同一個內存
delete pq;             //刪除第二個指針指向的內存

般來說,不要創建兩個指向同一個內存塊的指針,因爲這將增加錯誤地刪除同一個內存塊兩次的可能性。但稍後你會看到,對於返回指針的函數,使用另一個指針是有道理的。

1.6使用new創建動態數組

如果程序只需要一個值,則可能會聲明一個簡單變量,因爲對於管理一個小型數據對象來說,這樣做比使用new和指針更簡單,儘管給人留下的印象不那麼深刻。通常,對於大型數據(如數組、字符串和結構),應使用new,這正是new的用武之地。例如,假設要編寫一個程序,它是否需要數組取決於運行時用戶提供的信息。如果通過聲明來創建數組,則在程序被編譯時將爲它分配內存空間。不管程序最終是否使用數組,數組都在那裏,它佔用了內存。在編譯時給數組分配內存被稱爲靜態聯編( static binding),意味着數組是在編譯時加入到程序中的。但使用new時,如果在運行階段需要數組,則創建它;如果不需要則不創建。還可以在程序運行時選擇數組的長度。這被稱爲動態聯編( dynamic binding),意味着數組是在程序運行時創建的。這種數組叫作動態數組( dynamic array)。使用靜態聯編時,必須在編寫程序時指定數組的長度;使用動態聯編時,程序將在運行時確定數組的長度。

使用new創建動態數組

在C++中,創建動態數組很容易:只要將數組的元素類型和元素數目告訴new即可。必須在類型名後加上方括號,其中包含元素數目。例如,要創建一個包含10個int元素的數組,可以這樣做:

int * psome = new int [10];

new運算符返回第一個元素的地址。在這個例子中,該地址被賦給指針 psome

當程序使用完new分配的內存塊時,應使用 delete釋放它們。然而,對於使用new創建的數組,應使種格式的 delete來釋放:

delete [] psome;   //釋放一個動態數組

方括號告訴程序,應釋放整個數組,而不僅僅是指針指向的元素。請注意 delete和指針之間的方括號如果使用new時,不帶方括號,則使用 delete時,也不應帶方括號。如果使用new時帶方括號,則使用 delete時也應帶方括號。C++的早期版本無法識別方括號表示法。然而,對於 ANSIISO標準來說,new與delete的格式不匹配導致的後果是不確定的,這意味着程序員不能依賴於某種特定的行爲。下面是一個例子:

int * pt = new int;
short * ps = new short [500];
delete [] pt;    //不能這樣做
delete ps;       //不能這樣做

總之,使用new和delet時,應遵循以下規則。

  • 不要使用delet釋放不是new分配的內存
  • 不要使用delet釋放同一個內存塊兩次。
  • 如果使用new []爲數組分配內存,則應使用delet[]釋放
  • 如果使用new[]爲一個實體分配內存,則應使用delete(沒有方括號)來釋放。
  • 對空指針應用delete時安全的。

現在我們回過頭來討論動態數組。psome是指向一個int(數組第一個元素)的指針。你的責任是跟蹤內存塊中的元素個數。也就是說,由於編譯器不能對 psome是指向10個整數中的第1個這種情況進行跟蹤,因此編寫程序時,必須讓程序跟蹤元素的數目。

實際上,程序確實跟蹤了分配的內存量,以便以後使用 delete[]運算符時能夠正確地釋放這些內存,這種信息不是公用的,例如,不能使用 sizeof運算符來確定動態分配的數組包含的字節數。爲數組分配內存的通用格式如下:

type_name * pointer_name = new type_name [num_elements];

使用new運算符可以確保內存塊足以存儲 num elements個類型爲 type_name的元素,而pointer name將指向第1個元素。下面將會看到,可以以使用數組名的方式來使用 pointer_name。

使用動態數組

創建動態數組後,如何使用它呢?首先,從概念上考慮這個問題。下面的語句創建指針 psome,它指向包含10個int值的內存塊中的第1個元素:

int * psome = new int [10]; //得到佔10個int大小的內存塊

可以將它看作是一根指向該元素的手指。假設int佔4個字節,則將手指沿正確的方向移動4個字節,手指將指向第2個元素。總共有10個元素,這就是手指的移動範圍。因此,new語句提供了識別內存塊中每個元素所需的全部信息。

現在從實際角度考慮這個問題。如何訪問其中的元素呢?第一個元素不成問題。由於 psome指向數組的第1個元素,因此* psome是第1個元素的值。這樣,還有9個元素。如果沒有使用過C語言,下面這種最簡單的方法可能會令你大吃一驚:只要把指針當作數組名使用即可。也就是說,對於第1個元素,可以使用 psome[0],而不是· * psome;對於第2個元素,可以使用 psome[1],依此類推。這樣,使用指針來訪問動態數組就非常簡單了,雖然還不知道爲何這種方法管用。可以這樣做的原因是,C和C++內部都使用指針來處理數組。數組和指針基本等價是C和C++的優點之一(這在有時候也是個問題,但這是另一碼事)。首先,程序arraynew.cpp演示瞭如何使用new來創建動態數組以及使用數組表示法來訪問元素;它還指出了指針和真正的數組名之間的根本差別。

#include <iostream>
int main()
{
	using namespace std;
	double * p3 = new double [3];
	p3[0] = 0.2;
	p3[1] = 0.5;
	p3[2] = 0.8;
	cout << "p3[1] is " << p3[1] << ".\n";
	p3 = p3 + 1;
	cout << "Now p3[0] is" << p3[0] << " and ";
	cout << "p3[1] is " << p3[1] << ".\n";
	p3 = p3 - 1;
	delete [] p3;
	return 0;
}
//程序輸出
p3[1] is 0.5.
Now p3[0] is 0.5 and p3[1] is 0.8.

從中可知, arraynew.cpp將指針p3當作數組名來使用,p3[0]爲第1個元素,依次類推。下面的代碼行指出了數組名和指針之間的根本差別:

p3 = p3 + 1;   //對與指針可行,對於數組則是不可行

不能修改數組名的值。但指針是變量,因此可以修改它的值。請注意將p3加1的效果。表達式p3[0]現在指的是數組的第2個值。因此,將p3加1導致它指向第2個元素而不是第1個。將它減1後,指針將指向原來的值,這樣程序便可以給 delete[]提供正確的地址。相鄰的int地址通常相差2個字節或4個字節,而將p3加1後,它將指向下一個元素的地址,這表明指針算術有一些特別的地方。情況確實如此。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章