C/C++:指針初學

整理自計蒜客-CS 112: C++ 程序設計

找不到的 是心頭的悸動

指針是什么

指針是一個變量,其儲存的是值的地址,而不是值本身。指針提供了另一種訪問內存空間的方法:雖然我們不知道變量的名稱,但我們可以通過變量存放的地址訪問它。


指針的初始化

  • 可以直接初始化:
//<數據類型> *指針變量名 = 賦值;
//指針的數據類型,表示指針所指向的數據的數據類型
int a = 1;
int array[] = { 1,2,3,4,5 }; 
int *p1 = &a;
int *p2 = array; 或者 int *p2 = &array[0]

如果int *p2 = &array;將不能通過編譯,原因是數據類型不匹配。

  • 也可以先定義,再賦值:
//對象指針:
Node node1;
Node *p3 = &node1;
//函數指針:
int add(int x,int y);
int (*p3)(int,int);
p3 = add;

指針的運算

  • 算數運算:
    指針可以跟 整數 進行加法和減法的運算,但是運算規則比較特殊——對指針進行加減運算的結果,與指針本身的類型密切相關??梢钥闯觯?code>指針+1移動了一種數據類型在內存中存放的字節數。不過,空指針和函數指針是不能進行算數運算的(無法通過編譯),因為無法確定移動多少個字節。
//整數指針:
int *p1 = &a;
cout << p1 << " " << p1+1 <<endl;
//字符指針:
char c[] = "people";
char *p2 = &c;
cout << p2 << " " << p2+1 <<endl; 
printf("%p %p\n",p2,p2+1); 
//對象指針:
Node *p3 = &node;
cout << p3 << " " << p3+1 <<endl;
//函數指針:
int (*p4)(int,int) = add;
printf("%p\n",p4);
cout << p4 <<endl;

輸出結果:
0x7ffe597f872c 0x7ffe597f8730 people eople 0x7ffe597f8760 0x7ffe597f8761 0x7ffe597f8750 0x7ffe597f8760 0x4009ed 1
1)以上使用cout輸出字符指針的時候結果跟想象中不太一樣?
答:這是因為cout對象認為char的地址是字符串的地址,因此它打印該地址處的字符,然后繼續打印后面的字符,直到遇到空字符(\0)為止。
2)為什么用cout輸出函數指針會得到1呢?
答:這里先挖個坑...

  • 關系運算&邏輯運算
    指針變量的關系運算,指的是 指向相同類型數據的指針之間,進行的關系運算。 如果兩個相同類型的指針相等,就表示兩個指針指向的是同一個地址——不同類型的指針之間,或者指針與非 0 整數之間的比較是沒有意義的。
    但是有一種情況是特殊的——指針可以跟 整數 0 之間進行比較,0專門用于表示空指針,即指針變量中保存的地址是空的,不指向任何有效的地址。
    關系運算的結果經常用于邏輯運算。
    int a = 9,b = 7;
    int *p[4] = {&a,&a,&b,NULL};
    //判斷前后指針是否相等
    cout << "equal?" <<endl;
    for(int i=0;i<4;i++){
        if(p[i]==p[i+1]) {
            cout << "YES ";
        }
        else cout << "NO ";
    }
    cout <<endl;
    //判斷是否為空指針
    cout << "NULL?" <<endl;
    for(int i=0;i<4;i++){
        if(p[i]==NULL) {
            cout << "YES " <<endl;
        }
        else cout << "NO ";
    }
    cout << endl;

輸出結果:
equal? YES NO NO YES NULL? NO NO NO YES
值得注意的是,越界的指針數組元素p[4]是一個空指針。


空指針

為什么我們需要空指針呢?因為有的時候,我們在聲明一個指針的時候,并沒有一個確定的地址值可以賦給它,當程序運行到某個時刻的時候,才會將某個地址賦值給這個指針。這樣,在指針定義但沒有使用的這段時間里,它的值是不確定的——要是誤用了這個不確定的指針的話,就很有可能會造成不可預見的錯誤(比如意外地把某個不該變更的值給改掉了),因此在這種情況下,我們首先應該將地址設置為空。

除了給指針賦值0NULL使其為空,在C++11標準中,我們還可以使用nullptr關鍵字來表示空指針,用法跟NULL基本相同(需要引用命名空間std中的對應標識符)。


指針與數組

數組的本質,實際上是一串連續的相同大小的內存空間——比如說,對于整形數組int a[10],它在內存中就是連續排列的十個可以容納一個整形變量的內存空間。而數組的名稱,其實就是一個常量指針,即不能被賦值的指針。作為一個指針,數組名指向的是數組的第一個元素。

指針加減運算的特點,使得它可以特別被用于處理存儲在一段連續內存空間中的同類數據。而數組正好是具有一定順序關系的,若干同類型變量的集合體——數組元素的存儲,在物理上與邏輯上都是連續的,數組名就是變量的首地址。如果有數組array[5],那么array&array[0]是相同的。

  • 要訪問數組元素,下面兩種方法是等效的:
int *p = array;
cout << array[10] <<endl;
cout << *(p+10) <<endl;
  • 此外,如果我們要把數組作為函數的形參的話,那么它實際上是等價于把指向數組元素類型的指針作為形參的——例如,下面三個寫法,出現在形參列表中就是等價的:
void f(int p[]);
void f(int p[3]);
void f(int *p);

指針數組

如果一個數組的所有元素都是指針變量,那么這就是一個指針數組。指針數組的每一個元素都必須是同一類型的指針。指針數組有一個神奇的應用:

//創建一個指針數組,其元素分別指向三個數組
int line1[]={1,0,0};
int line2[]={0,1,0};
int line3[]={0,0,1};
int *pLine[3]={line1,line2,line3};

//用類似二維數組的形式訪問三個數組
for(int i=0;i<3;i++){
    for(int j=0;j<3;j++){
      cout << pLine[i][j] << “ ”;
    }
}

輸出結果:
1 0 0 0 1 0 0 0 1

上個例子中的pLine在使用上跟一個二維數組沒有區別,但是在存儲方式上,它跟真正的二維數組并不相同:

二維數組在內存中,是以行優先的方式按照一維順序關系存放的。因此,對于二維數組,可以將其理解成一個一維數組的一維數組,其首地址為數組名,元素個數就是行數——而它的每一個元素,就是一個一維數組。

然而,對于指針數組pLine,它的三個“元素數組”在內存中,并不是連續存放的——訪問line2或者line3的時候,首先要在pLine中找出對應的元素指針,即為指向line2或者line3頭元素的地址,然后再通過指針跳轉到要訪問的數組。

使用指針數組的情形

對象指針

跟基本類型的變量一樣,每一個對象在初始化之后,都會在內存中占據一定的空間——所以我們同樣也可以通過地址來訪問一個對象。盡管對象同時包含了數據和函數兩種成員,但是對象所占據的內存空間只用于存放數據成員——函數成員并不在每一個對象的存儲副本之中。對象指針就是用于存放對象地址的變量——對象指針遵循一般變量指針的各種規則。

  • 通過對象名,我們可以訪問對象成員——同樣,通過對象指針,我們可以訪問對象的成員,以下三種方法完全等價:
//假設已有Line類
cout << line1.getLength() <<endl;
cout << line_ptr->getLength() <<endl;
cout << (*line_ptr).getLength() <<endl;
  • this指針
    對于類的成員函數來說,我們可以直接在函數體內訪問成員變量——例如,如果對象Line有一個成員變量length的話,那么我們就可以直接在成員函數內訪問這個成員:
int getLength(){return length;}

而實際上,C++ 為每一個類的非靜態成員函數(就是沒有static關鍵字的成員函數),都提供了一個隱含的指針this,當我們寫下return length;的時候,編譯器執行的實際上是return this->length;。
this指針明確地指出了函數當前所操作的數據所屬的對象——它是成員函數隱藏的一個形參,當我們在成員函數中操作對象的數據成員的時候,我們其實就是在使用this指針。
然在一般情況下,我們不需要特別把this指針寫出來——但是如果函數的形參列表中的參數跟成員變量重名的話,那么由于標識符作用域覆蓋,我們將沒法直接通過成員變量名來訪問它。當然我們也可以選擇更改形參名——但是更好的辦法是通過this指針來訪問成員變量,這樣我們可以讓代碼擁有更好的可讀性:

void setLength(int length){
    this->length=length;
}

另一種解決方法是使用初始化列表:

void setLength(int length):length(length){
}

函數指針

以上我們使用的指針都是指向數據的——而在程序運行的時候,不僅數據要占據內存空間,執行程序的代碼也會被存入到內存,并占據一定的空間。每一個函數都有函數名,而實際上這個函數名就表示函數的代碼在內存中的起始地址。在程序中可以像使用函數名一樣,使用指向函數的指針來調用函數——也就是說,一旦函數指針指向了某個函數,那么它與函數名就具有同樣的作用。

函數名在表示函數代碼起始地址的同事,也包括函數的返回值類型,以及參數的個數、類型、排列次序等信息。因此,在通過函數名調用函數的時候,編譯器就可以自動檢查實參與形參是否相符,用函數的返回值參與其他運算時,能夠自動進行類型一致性檢查。而函數指針也具有同樣的效果。

  • 聲明:
    聲明一個函數指針時,需要提供構造一個函數需要的所有信息——包括函數的返回值和形式參數列表,如下所示:
返回值類型 (* 函數指針名)(形參表)

由于對函數指針的定義在形式上比較復雜,如果在程序中出現多個這樣的定義,那么多次重復這樣的定義會相當繁瑣。這里我們有一種很方便的解決方案——使用typedef。例如:

typedef int (* DoubleIntFunction)(double);

這里我們聲明了DoubleIntFunction為“有一個double形參,返回類型為int的函數的指針”的類型的別名——接下來,如果我們需要聲明這個類型的變量的時候,我們就可以直接進行使用:

DoubleIntFunction funcPtr;

這樣我們就可以直接使用這個類型的指針funcPtr了。

  • 賦值:
函數指針名=函數名;

注意這里的“函數名”必須是一個已經聲明過的函數,并且必須具有跟函數指針相同返回類型跟相同參數表的函數。賦值之后你就可以像使用函數一樣,使用函數指針了。

  • 使用:
int add(int x,int y) {
    return x+y;
}
int (*func_ptr)(int,int);
func_ptr = add;
//以下二者完全等價
cout << func_ptr(2,3) <<endl;
cout << add(2,3) <<endl;
  • C++ 11 提供的lambda 表達式,它可以替代函數指針的作用。

動態內存分配

動態內存解決了諸如“用戶輸入XX個數據,那么我應該開多大的數組?”之類的只能在程序運行時才能確定的問題。那跟指針有什么關系呢?這是因為我們申請的動態內存時,返回的就是指向這個這個動態內存首地址的指針。

在 C++ 中,動態內存分配可以保證程序在運行的過程中,可以按照實際需要申請適量的內存,等到使用結束之后我們還可以將其釋放——這種在程序運行的過程中申請和釋放的存儲單元也稱為堆對象,而動態內存分配所調用的內存空間則稱為堆內存。建立和刪除堆對象使用以下兩個運算符:newdelete。

  • new的功能是動態分配內存,其語法形式如下所示:
new 數據類型(初始化參數列表);

以上語句的作用是在程序運行的過程中,申請分配用于存放指定類型數據的內存空間,然后根據參數列表中給出的值來進行初始化。如果內存申請成功,那么new運算符就會返回一個指向新分配內存區域首地址的指針——我們可以通過這個指針來訪問堆對象。

  • 例如我們申請一個int類型的內存空間:
int *point;
point=new int(2);

以上,系統動態分配了用于存放int類型數據的內存空間,然后用初始值2賦值,得到的地址返回給point指針變量。我們也可以這么寫,但注意區別:

int *point = new int;//沒有初值
int *point = new int();//初始值為0
  • 還可以解決“開多大數組?”的問題:
cin >> n;
int *a=new int[n];

其中,方括號內的表達式表示數組長度,它可以是任何能夠得到正整數值的式子。

  • 除了數組類型跟基本類型之外,new運算符還可以建立一個類的實例對象:
//假設已有類Node
Node *node_ptr;
node_ptr = new Node();

如果要建立一個對象的話,那么這里的“參數列表”就要跟對象所屬類的構造函數一一對應:如果不寫括號或者括號里為空的話,那么就會調用類的默認構造函數;而如果寫了對應的參數的話就會調用類所具有的對應的構造函數。

  • delete的作用是刪除一個用new建立的對象,回收其申請的內存。
    所有用new分配的內存,都必須使用delete進行回收,否則會導致動態分配的內存無法回收,造成內存泄露!另外,deletenew是一一對應的,不能delete一個不是用new建立的對象,否則會出現“段錯誤”之類的問題。
    使用方法比較簡單——如果你覺得一個堆對象已經不再被需要,那么你直接將其刪除即可,如下所示:
delete node_ptr;//對于基本類型或者對象的指針
delete[] array_ptr;//對于指向數組的指針

注意如果要刪除的是一個數組的話,那么后面的那對方括號不可省略。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容

  • 題目類型 a.C++與C差異(1-18) 1.C和C++中struct有什么區別? C沒有Protection行為...
    阿面a閱讀 7,695評論 0 10
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,529評論 1 51
  • 基本概念 1a general-purpose programming language用于創建計算機程序。藝術類...
    伍帆閱讀 1,333評論 0 1
  • C++運算符重載-下篇 本章內容:1. 運算符重載的概述2. 重載算術運算符3. 重載按位運算符和二元邏輯運算符4...
    Haley_2013閱讀 1,450評論 0 49
  • 這是一個騰訊網上的幾條話,覺得很有道理,就想著記下來,或許會有用呢。 1、開始和你欣賞的人在一起;(這個道理我是贊...
    淚言丫頭閱讀 277評論 0 0