(四)指針
指針
通過指針程序員可以直接對內存進行操作,這樣的優點是使程序緊湊、簡潔、高效;
計算機保存地址的工具就是指針。所以說:
指針:就是用來保存內存地址的變量。
注:指針地址、指針保存的地址和該地址的值 這三個概念一定不要混淆哦
什么是地址?
假如我們要去動物園,那么我們就得先知道動物園的地址,然后我們就可通過該地址找到動物園
同理,計算機要找到變量i,必須先找到i的地址,也就是i在內存中的編號,然后通過該編號,計算機訪問到了i并且對它進行操作。
那么計算機將如何獲得i的地址呢?
//通過一個例子來了解計算機獲取地址
例:
#include <iostream>
using namespace std;
int main(){
int i=1;
//&的作用是獲取變量i在內存中的地址
cout<<&i<<endl;
}
使用指針的原因
因為在操作大型數據和類時,由于指針可以通過內存地址直接訪問數據,從而避免在程序中復制大量的代碼,因此指針的效率最高。
一般來說,指針會有三大用途:
1:處理堆中存放的大型數據.
2:快速訪問類的成員數據和函數.
3:以別名的方式向函數傳遞參數.
//下面的可看可不看 沒多大作用
內存的棧和堆
一般來說,程序就是與數據打交道,在執行某一功能的時候,將該功能所需要的數據加載到內存中,然后在執行完畢的時候釋放掉內存。
數據在內存中的存放共分為以下幾個形式:
1.棧區(stack)--由編譯器自動分配并且釋放,該區域一般存放函數的參數值、局部變量的值等。
2.堆區(heap)--一般由程序員分配釋放,若程序員不釋放,程序結束時可能由操作系統回收。
3.寄存器區--用來保存棧頂指針和指令指針。
4.全局區(靜態區 static)--全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局
變量和未初始化的靜態變量在相鄰的另一塊區域.程序結束后由系統釋放。
5.文字常量區--常量字符串是放在這里的,程序結束后由系統釋放。
6.程序代碼區--存放函數體的二進制代碼。
函數參數和局部變量存放在棧中,當函數運行結束并且返回時,所有的局部變量和參數就都被系統自動清楚掉了,為的是釋放掉它們所占用
的內存空間,全局變量可以解決這個問題,但是全局變量永遠不會被釋放,而且由于全局變量被所有的類成員和函數所公用,所以它的值很
容易被修改.使用堆可以一舉解決這兩個問題。
堆和棧的區別:
1.從內存的申請方式上的不同
棧:由系統自動分配,例如我們在函數中聲明一個局部變量 int a;那么系統就會自動在棧中為變量a開辟空間。
堆:需要程序員自己申請,因此也需要指明變量的大小
2.系統相應的不同
棧:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將提示overflow,也就是棧溢出。
堆:系統收到程序申請空間的要求后,會遍歷一個操作系統用于記錄內存空閑地址的鏈表,當找到一個空間大于所申請空間的
堆結點后,就會將該結點從記錄內存空閑地址的鏈表中刪除。并將該結點的內存分配給程序,然后在這塊內存區域的首
地址處記錄分配的大小,這樣我們在使用delete來釋放內存的時候,delete才能正確地識別并刪除該內存區域的所有
變量.另外,我們申請的內存空間在堆結點上的內存空間不一定相等,這時系統就會自動將堆結點上多出來的那一部分內存
空間回收到空閑鏈表中
3.空間大小的不同
棧:在WINDOW下,棧是一塊連續的內存的區域,它的大小是2M,也有的說是1M,總之該數值是一個編譯時就確定的常數。是
由系統預先根據棧頂的地址和棧的最大容量定義好的。假如你的數據申請的內存空間超過棧的空間,那么就會提示
overflow.因此,別指望棧存儲比較大的數據
堆:堆是不連續的內存區域。各塊區域由鏈表將它們串聯起來,關于鏈表的知識將在后面了解。這里只需要知道鏈表將各個
不連續的內存區域連接起來,這些串聯起來的內存空間叫做堆,它的上線是由系統中有效的虛擬內存來定的。因此獲得
的空間比較大,而且獲取空間的方式也比較靈活。
4.執行效率的不同
棧:棧是由系統自動分配,因此速度較快.但是程序員不能對其進行操作。
堆:堆是由程序員分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來很方便。
5.執行函數時的不同
棧:在函數調用時,第一個進棧的是被調用函數下一行的內存地址。其次是函數的參數,假如參數多于一個,那么次序是從右
往左。最后才是函數的局部變量。
由于棧的先進后出原則,函數結束時正好與其想反,首先是局部變量先出棧,然后是參數,次序是從左到右,這是所有的變
量都已出棧,指針自然地指到第一個進棧的那行內存地址,也就是被調用函數的下一行內存地址。程序根據該地址跳轉到
被調用函數的下一行自動執行。
堆:堆是一大堆不連續的內存區域,在系統中由鏈表將它們串接起來,因此在使用的時候必須由程序員來安排。它的機制是
很復雜的,有時候為了分配一塊合適的內存,程序員需要按照一定的算法在堆內存中搜索可用的足夠大小的空間,如果
沒有滿足條件的空間,那么就要向系統發出申請增加一部分內存空間,這樣就才有機會分到足夠大小的內存,然后將計
算機后的數值返回。顯然,堆的運行效率比棧要低得多,而且也容易產生碎片,但是好處是堆可以存儲相當大的數據,
并且一些細節也可以由程序員來安排。
終結:
棧的內存小,但是效率高,不過存儲的數據只在函數內有效,超出函數就消失了。堆的可存儲空間可以非常大,但是容易產生
內存碎片,效率也較低,好處是靈活性比較強。比如說我們需要創建一個對象,能夠被多個函數所訪問,但是又不想使其成為
全局的,那么這個時候創建一個堆對象無疑是良好的選擇。
由于堆和棧各有優缺點,因此好多時候我們是將堆和棧結合使用的,比如在存儲一些較大數據的時候,我們將數據存放到堆中,
卻將指向該數據的指針放在棧中,這樣就可以有效的提高程序的執行速度,避免一些不該有的碎片。不過,一般來說,假如不
是特大的數據,我們都是使用棧,比如:函數調用過程中的參數,返回地址,和局部變量都存放到棧中。這樣可以大大加快程
序的運行速度。
使用指針保存內存地址
由于每一個被定義的變量都有自己的地址,因此你完全可以使用指針來存放任何已被定義的變量的地址,即使它沒有被賦值
int main(){
int i;
//定義了一個指針p,星號(*)代表變量p是一個指針.要注意它的類型也是int
int *p;
//將i 的地址 賦值給p
p=&i;
return 0;
}
空指針
我們知道指針就是用來保存內存地址的變量,因此我們定義一個指針后一定要用它來保存一個內存地址,假如我們不那么做,那么該指針就是一個
失控指針,它可以指向任何地址,并且對該地址的數值進行修改或者刪除,后果是非常可怕的.解決辦法是將指針初始化為0
int main(){
int *p;
//將指針初始化為0 這樣指針p就不會因為我們的疏忽而隨意指向任意一個地址并且修改該地址的值。
p=0;
//空指針的地址 00000000;
return 0;
}
指針和類型
由于不同類型的變量在內存中所占用的字節不同,而指針又是用來保存內存地址的變量,因此指針只能存儲與它類型相同變量的地址
int main(){
//定義了一個雙精度型變量a.編譯器接到此定義通知后會在內存中開辟一塊內存區域.該區域的大小剛好可以存放雙精度型數值
double a=3.14;
//定義了一個指向整型變量的指針變量p.編譯器知道了指針指向的類型,才能對其進行正確的處理與運算。
int *p1;
int b=6;
double *p2;
p1=&b;
p2=&a;
count<<"p1:"<<p1<<endl;
count<"p2:"<<p2<<endl;
p1++;
p2++;
count<<"p1:"<<p1<<endl;
count<"p2:"<<p2<<endl;
return 0;
}
//經過輸出發現 進行自加后 內存地址進行了改變 int類型的內存移動了4個字節 double移動了8個字節
//由于指針的類型的不同決定了指針運算方式也不同,所以我們不能將一種類型的指針賦給另一種類型的指針
用指針來訪問值
運算符*被稱為間接引用運算符,當使用星號*時,就讀取它后面變量中所保存的地址處的值
例如:
int main(){
int a=1;
int *p=0;
p=&a;
//指針變量p前面的間接運算符*的含義是:"存儲在此地址處的值".*p的意思就是讀取該地址處的值.
//由于p存儲的是a的地址,因此執行的結果是輸出a的值:1.
cout<<*p
}
指針對數值的操作
//沒什么難度 可看可不看
int main(){
//簡化書寫類似于定義一個變量有某種特殊含義
//這里的意思就是簡化short int的書寫方式為ut,即用ut就代表short int
typedef unsigned short int ut;
ut i=5;
ut *p=0;
p=&i;
cout<<"i="<<i<<endl;
cout<<"*p"<<*p<<endl;
cout<<"用指針來修改存放在i中的數據\n";
*p=90;
cout<<"i="<<i<<endl;
cout<<"*p"<<*p<<endl;
cout<<"用i來修改存放在i中的數據\n";
i=9;
cout<<"i="<<i<<endl;
cout<<"*p"<<*p<<endl;
}
更換指針保存的地址
//沒什么難度 可看可不看
int main(){
int i=0;
int j=1;
int *p=&i;
count<<*p<endl;
p=&j;
count<<*p<endl;
}
指針和堆
從文章開頭處的內存棧堆的說明中可以了解到堆的好處是可以存儲比較大的數據,而且存儲的數據只要不是程序員手動將其釋放那么就會永遠保
存到堆中,而棧存儲的數據只是在函數內有效,超出函數就消失了,而全局標量保存的數據只有程序結束才會釋放,很容易被修改
堆是一大堆不連續的內存區域,在系統中由鏈表將它們串聯起來,它無法像棧那樣對其中的內存單元命名,而系統為了數據隱秘,堆中的每個內存單
元都是匿名的,因此你必須現在堆中申請一個內存單元的地址,然后把它保存在一個指針中。這樣你只有使用該指針才可以訪問到該內存單元的數據。
采用這種匿名的內存訪問方式,而不是使用公開的全局變量,好處是只有使用特定的指針才能訪問特定的數據。這樣就避免了任何試圖修改它的
非法操作
要做到這一點,我們首先得創建一個堆,然后定義一個指向該堆的指針。這樣就只能通過該指針才能訪問堆中數據。
在C++中使用關鍵字new創建一個堆并分配內存,在new后面跟一個要分配的對象類型,編譯器根據這個類型來分配內存。
示例:
int *p;
p=new int;
第一行定義了一個指向整型的指針變量p,第二行用new在堆中創建一個int類型的內存區域,然后將該區域的內存地址賦給指針變量p。這樣p所
指向的就是這塊新建的內存區域。在這里要注意的是,new int在堆中被編譯器分配了4個字節的空間。假如是new double那么就要分配8個字節的
內存空間。
另外當如下寫時:
int *p=new int;
這樣在定義指針p的同時初始化了它的值為一個在堆中新建的int型存儲區的內存地址。你可以像使用普通指針一樣使用它,并把賦值給它所指向
的內存空間
演示代碼如下:(使用指針訪問new 存儲區)
int main(){
int *p;
//new int當在內存創建成功時返回值為內存的地址,所以可以直接賦值給p
p=new int;
//將5賦值到了p所指向的內存空間
*p=5;
cout<<*p;
return 0;
}
最后需要注意的:
由于計算機的內存是有限的,因此可能會出現沒有足夠內存而無法滿足new的請求,在這種情況下,new會返回0,該值被賦給指針后,那么該指針
就是一個空指針,空指針不會指向有效數據。new除了返回空值外,還會引發異常,具體在后面的一場錯誤處理中在介紹。
用指針刪除堆中空間
由于使用new創建的內存空間不會被系統自動釋放,因此假如你不去釋放它,那么該區域的內存將始終不能為其他數據所使用,而指向該內存
的指針是一個局部變量,當定義該指針的函數結束并返回時,指針也就消失了,那么我們就再也找不到該快內存地址,我們把這種情況叫做內存泄
漏。這種糟糕的情況將一直持續到程序結束該區域的內存才能恢復使用。因此假如你不需要一塊內存空間,那么就必須對指向它的指針使用關鍵
字delete.
int main(){
int *p=new int;
//刪除指針指向的內存空間
delete p;
p=new int;
//刪除指針指向的內存空間
delete p;
//注意不要delete兩次,加入再次刪除時雖然編譯不會報錯,但是運行時會造成程序崩潰
//delete p; 這種做法是錯誤的
return 0;
}
例2:(對內存的總結)
main(){
int *p=new int;
*p=3600;
cout<<*p<<endl;
delete p;
//這里輸出了-572662307 這個數是不確定的 因為當我們將內存釋放后,那么這塊內存就會被其他程序所使用
//所以當delete p后一定要將p清零,這樣可以防止造成錯誤操作
cout <<*p<<endl;
//將p清零,防止造成錯誤操作。
p=0;
p=new int;
*p=8;
cout<<*p<<endl;
delete p;
return 0;
}
如果以上代碼能看到并能正確的預測輸出結果,就代表你掌握了這快內容哦^_^.
內存泄漏
假如沒有刪除一個指針就對其重新賦值
例如:
int main(){
int *p=new int;
p=new int;
}
這樣就會造成內存泄漏
因為第一行定義了一個指針p并使其指向一塊內存空間,第二行又將一塊新的內存空間的地址賦給了p,這樣第一行所開辟的那塊內存空間就無法
再使用了,因為指向它的指針現在已經指向了第二塊空間
指針變量p只能保存一個地址,對它重新賦值則表示以前的地址被覆蓋,加入該地址的內存空間沒有被釋放,那么你將無法再次通過指針p訪問它.
因為此時的指針變量p記錄的是第二塊內存的地址
所以為了避免內存泄漏 在做第二個引用的時候要先 delete p;
假如暫時不想刪除第一塊內存空間,那么你可以這么做:
int *p1=new int;
int *p2=new int;
分別用兩個指針來指向兩塊內存空間,這樣每塊空間都有一個指針來指向,也就不會造成找不到某塊空間的內存泄漏現象
在堆中創建對象
我們既然可以在堆中保存變量,那么也就可以保存對象,我們可以將對象保存在堆中,然后通過指針來訪問它。
例:
int main(){
Human *p;
p=new Human;
return 0;
}
第一行定義了一個Human類的指針p,第二行使用new創建一塊內存空間,同時又調用了Human類的默認構造函數來構造一個對象,它所占用的內
存大小根據Human類對象的成員變量來決定,加入該類有兩個int型成員變量,那么該對象占用為2乘以4等于8個字節.構造函數一般都是在創建
對象時被自動調用,它的作用就是初始化該對象的成員數據.本行的右半部分創建一個對象完畢后,跟著將該對象的內存地址賦給左邊的指針變
量p
在堆中刪除對象
假如我們要刪除在堆中創建的對象,我們可以直接刪除指向該對象的指針,這樣會自動調用對象的析構函數來銷毀該對象同時釋放內存
例:
class Human{
public:
Human(){cout<<"構造函數執行中..\n"; i=999;}
~Human(){cout<<"析構函數執行中...\n";}
private:
int i;
};
int main(){
Human *p=new Human;
//跟釋放變量內存寫法一樣,在對象中這樣寫調用的是函數的析構函數
delete p;
return 0;
}
訪問堆中的數據成員
假如我們要訪問對象的數據成員和函數,我們使用成員運算符"."。
例:
Human Jack;
Jack.i;
那么如何用指針操作呢?如下:
(*p).get();
p為對象的指針,使用括號是為了保證先使用*號讀取p的內存地址中的值,即堆中對象,然后再使用成員運算符"."來訪問成員函數get()。
由于這樣做比較麻煩,因此c++專門為指針來間接訪問對象的成員設置了一個運算符: 成員指針運算符(->)。該符號可以實現讀取對象的內存
地址并且訪問該對象的成員的作用。
因此:
(*p).get(); 可以寫為 p->get();
在構造函數中開辟內存
我們可以將類的數據成員定義一個指針,然后在構造函數中開辟新空間,將該空間的地址賦給指針.而在析構函數中釋放該內存
class Human{
public:
//int是一個整型,不是類對象,所以new int(999)不會調用構造函數,而是將999這個數值存儲到新建的內存區域中
//這樣就是在構造函數中開辟一個新空間
Human(){cout<<"構造函數執行中...\n";i=new int(999);}
//在析構函數中釋放改內存,防止造成內存泄漏
~Human(){cout<<"析構函數執行中...\n";delete i;}
//*i是int類型
int get(){return *i;}
private:
int *i;
}
//該例僅僅是為了說明構造函數中也可以開辟堆中空間,在實際程序中,一個在堆中創建的對象通過成員指針再創建新空間用來保存數據并沒有
//什么意義。以為在堆中創建對象時已經為它的所有數據成員提供了保存的空間
對象在棧和堆中的不同
例:
int main(){
Human Jack;
return 0;
}
一個存儲在棧中的對象,如:
Human Jack;
會在超出作用域時,比如說遇到右大括號,自動調用析構函數來釋放該對象所占用的內存。
而一個存儲在堆中的對象,如:
Human *Jack=new Human;
delete Jack;
則需要程序員自行對其所占用的內存進行釋放.否則該對象所占用的內存直到程序結束才會被系統回收
this指針
學生在發新課本時一般都要將自己的名字寫在課本上,以說明該課本是自己的,避免與別的學生混淆.同樣對象也要在屬于自己的每個成員身上
寫下自己的名字,以證明該成員是自己的成員,而不是別的對象的成員。this變量幫助對象做到這一點,this變量記錄每個對象的內存地址,
然后通過間接訪問運算符->訪問該對象的成員。
例:
class A{
public:
//由于方法中i的值不需要改變,所以可以加修飾符const
int get()const{return i;}
void set(int x){i=x;cout<<"this變量保存的內存地址:\t"<<this<<endl;}
private:
int i;
};
int main(){
A a;
a.set(9);
cout<<"對象a的內存地址:\t"<<&a<<endl;
cout<<a.get()<<endl;
A b;
b.set(999);
cout<<"對象b的內存地址:\t"<<&b<<endl;
cout<<b.get()<<endl;
return 0;
}
//通過打印的結果可以看出:
//this變量記錄每個單獨的對象的內存地址,而this指針則指向每個單獨的對象.因此不同的對象輸出的this變量的內存地址也不同
//在默認情況下this指針是不寫的。比如第七行 this-i=x; 假如我們寫i=x; 編譯器會自動在成員變量i前面加上this指針,用來表示這
//個i成員是屬于某個對象的。
//由于this指針保存了對象的地址,因此我們可以通過該指針直接讀取某個對象的數據,它的作用將會在后面的重載運算符中得到演示,對于
//this指針的創建與刪除由系統(編譯器)來控制,所以我們不需要進行這方面的操作
指針的常見錯誤
1.刪除一個指針后一定要將該指針設置為空指針,因為刪除該指針只會釋放它所指向的內存空間,不會刪除指針,因此這個指針還存在,并且它
依然會指向原來的內存空間,因此這是如果你再次嘗試使用該指針,那么將會導致程序出錯。
指針的加減運算
指針可以進行加法運算
例:
int *p=new int;
p++;
將指針變量p中的內存地址自加。由于p指向的是int型變量,因此執行加1操作會將原來的內存地址增加4個字節
p--;
概念跟++一樣,將內存地址自減.
可以通過cout<<p<<endl;來查看結果
p=p-2;
"-2"后空間地址,因為是int型變量,因此執行-2操作會將原來的內存地址減8個字節。
指針的賦值運算
指針也可以進行賦值運算,比如說將一個變量地址賦給另一個指針變量地址
int main(){
int *p=new int;
cout<<"p:"<<p<<endl;
int *p1=new int;
cout<<"p1"<<p1<<endl;
p=p1;
cout<<"賦值后..\n";
cout<<"p:"<<p<<endl;
return 0;
}
指針的相減運算
用p-p1,將結果保存到p指向的內存空間中,這樣p指向的內存空間保存的就是p與p1的內存地址差
指針的比較運算
兩個指針之間也可以進行比較運算。
常量指針
定義常量指針:int *const p;這個指針它自身的值是不可以改變的,但是它指向的目標卻是可以改變的
例:
int main(){
int a=3;
int *const p=$a;
cout<<"a:"<<a<<endl;
a=4;
cout<<"a:"<<a<<endl;
return;
}
例2:
class A{
public:
int get() const{return i;}
void set(int x){i=x;}
private:
int i;
};
int main(){
A*p=new A;
cout<<"p:"<<p<<endl;
p=p+1;
cout<<"p:"<<p<<endl;
A *const p1=new A;
//p1=p1+1; 這樣會報錯提示不可被改變
p1->set(11);
cout<<p1->get();
return 0;
}
通過這個例子進一步說明了常量指針的特性,常量指針自身不可改變,但是它指向的目標卻可以改變.無論這個目標是變量還是對象。
指向常量的指針
假如將上面例2中的 A*const p1=new A;寫為 const A* p1=new A;那么p1就變成了指向常量的指針,該指針指向的目標是不可修改的,但是該指針可以
被修改
指向常量的指針只是限制我們修改它指向的目標,它自身是可以被修改的.
指向常量的常指針
假如 const A* const p1=new A;這種寫法,則代表指向常量的常指針,它指向的目標是不可修改的,并且該指針也不可以被修改.
二級指針
//如果一個指針變量存放的又是另一個指針變量的地址,則稱這個指針變量為指針的指針變量,也稱為"二級指針"
//通過指針訪問變量稱為間接訪問.由于指針變量直接指向變量,所以稱為"一級指針".而如果通過指向指針的指針變量來訪問變量則構成"二級指針".
地址變量 變量
地址---------------> 值
指針變量1 指針變量2 變量
地址1------------>地址2------------>值
指針到這里就結束了,下一張介紹的為 "引用"

浙公網安備 33010602011771號