C++ - 指針

指針

在了解什么是指針之前,我們需要先搞清楚數據在內存中是如何存儲的,又是如何讀取的。

如果在程序中定義一個變量,在編譯的時候就給這個變量分配內存單元。系統根據程序中定義的變量類型,會分配一定長度的空間。例如:C++編譯系統一般為整型變量分配4個字節,對單精度浮點型變量分配4個字節,對字符型變量分配1個字節。內存區的每一個字節都有一個編號,這就是地址,它可以類比我們旅館中的房間號,在地址所標識的內存單元中存放數據,這相當于旅館中各個房間中居住旅客一樣。

注意:地址和內容(值)是兩個概念,如下圖:

屏幕快照 2018-08-15 下午10.50.40.png

可以看到程序已定義了變量i,j,k,編譯的時候系統分配2000-2003這4個字節給變量i,分配2004-2007這4個字節給j,分配2008-2009給變量k。在程序中我們一般是通過變量名來對內存單元進行存取操作,其實程序在經過編譯以后已經將變量名轉換為變量的地址,對變量值的存取都是通過地址進行的。

當我們執行cout<<i;語句的時候,即獲取變量i的值,根據變量名與地址的對應關系(在編譯的時候確定),找到變量i的地址2000,然后從2000開始的4個字節中取出數據,即變量的值為3,并輸出。

當我們執行cin>>i;語句的時候,即變量i賦值的時候,在執行的時候,就把值送到地址為2000開始的整型存儲單元中。如果執行語句k=i+j;那么從2000字節開始的整型變量存儲單元中取出i的值3,從2004字節開始的變量存儲單元中取出j的值6,將它們進行相加,結果9送到k所占用的2008字節開始的整型存儲單元中。

上面的這種按變量地址存儲變量值的方式稱為直接存取方式。我們還可以使用另外一種存儲方法,間接存取方式,即將變量i的地址存放到另一個變量中

看到這里,那么什么是指針呢?

其實地址就是指針,一個變量的地址稱為該變量的指針。因為通過地址能找變量單元,因此可以說,地址指向該變量單元。所以將地址形象化地稱為“指針”,意思是通過它能找到以它為地址的內存單元。

變量與指針

變量的指針其實就是變量的地址,用來存放變量地址的變量是指針變量。有沒有感覺有點繞,嘿嘿。指針變量是一種特殊的變量,用它來指向另外一個變量,“*”表示指向,比如:pointer是一個指針變量,而*pointer表示pointer所指向的變量,也就是說*pointer也代表一個變量。

4010043-a193516f8303841c.png

定義指針變量

定義指針變量的形式:基類型 *指針變量名;

C++規定所有變量在使用前必須先定義,即指定其類型。在編譯時按變量類型分配存儲空間。在Visual C++中,為每個指針變量分配4個字節的存儲空間。對指針變量必須定義為指針類型,如:

int i,j;  //定義整型的變量i,j
int *pointer1,*pointer2; //定義指針變量pointer1,pointer2

第二行開頭的int是指:所定義的指針變量是指向整型數據的指針變量,或者說pointer1和pointer2只能存儲整型數據的地址,而不能存放其他數據類型的地址。

正如上面代碼所示,pointer1和pointer2可以指向整型數據i和j,而不能指向浮點數。int是指針變量的基類型,所謂基類型就是該指針變量指向的變量的類型

注意:變量名是pointer1和pointer2而不是*pointer1和*pointer2,*表示該變量為指針變量

那么怎樣使一個指針變量指向另一個變量呢?簡單,只需要把被指向的變量的地址賦給指針變量即可。

pointer1 = &i; // 表示將變量i的地址存放到指針變量pointer1中
pointer2 = &j; // 表示將變量j的地址存放到指針變量pointer2中

通過上面賦值操作,pointer1指向了變量i,pointer2指向了變量j。

定義變量時注意幾點:

1、在定義指針變量時必須指定基類型

我們知道,不同類型的數據在計算機系統中的存儲方式和所占的字節數是不相同的。因此,如果想通過指針引用一個變量,只知道地址(如:2000)是不夠的,因為無法判斷是從地址為2000的一個字節取出字符數據,還是從2000-2003四個字節中取出int型數據,所以必須知道其類型,只有知道了數據類型,才能按存儲單元的長度以及數據的存儲形式正確的讀取數據。

其實一個變量的指針包括兩個方面的含有:

  • 一是以存儲單元編號表示的地址(如:2000)
  • 一是它指向的存儲單元的數據類型(如:int,float等),即基類型

2、怎么表示指針類型

比如:指向整型數據的指針類型表示為int *,讀作指向int的指針

3、一個指針變量只能指向同一個類型的變量,不能一會指向整型變量,一會指向單精度變量

引用指針變量

與指針變量相關的運算符:

1)&:取地址運算符
2)*:指針運算符(或稱間接訪問運算符)

如:&a表示變量a的地址,*p表示變量p所指向的存儲單元

例:通過指針變量訪問整型變量

int main(int argc, char const *argv[])
{
  int a,b; // 定義整型變量a,b
  int *pointer1, *pointer2; // 定義pointer1,pointer2為(int *)型變量,即指向整型數據的指針變量
  a = 100; b = 200; // 對a,b賦值

  pointer1 = &a; // 把變量a的地址賦給pointer1
  pointer2 = &b; // 把變量b的地址賦給pointer2

  cout<<a<<" "<<b<<endl; // 輸出a,b的值
  cout<<*pointer1<<" "<<*pointer2<<endl; // 輸出*pointer1和*pointer2的值
}

結果:

100 200
100 200

&和*運算符說明:

1、如果已經執行了“pointer1 = &a;”語句,那么&*pointer1的含義是什么?

&和*兩個運算符的優先級別相同,是按照自右而左的方向結合,因此先進行*pointer1運算,那么結果就是變量a,然后再執行&運算,即取變量a的地址。因此&*pointer1與&a相同,都是變量a的地址。

2、*&a的含義是什么?

先進行&a運算,得到a的地址,再進行*運算,即&a所指向的變量。所以*&a和*pointer1的作用一樣,都等價于變量a.

例:輸入a和b兩個整數,按先大后小的順序輸出a和b(用指針變量處理)

int main(int argc, char const *argv[])
{
  int *p1, *p2, *p, a, b;
  cout<<"請輸入兩個整數:"<<endl;
  cin>>a>>b;
  p1 = &a;
  p2 = &b;
  if (a<b) // 如果a小于b,將p1的指向和p2的指向進行交互
  {
    p = p1;
    p1 = p2;
    p2 = p;
  }
  cout<<"a= "<<a<<" b= "<<b<<endl;
  cout<<"max= "<<*p1<<" min= "<<*p2<<endl;
}

運行結果:

請輸入兩個整數:
10 15
a= 10 b= 15
max= 15 min= 10

注意一點:交互的是指針的指向,但是原來的值并未發生改變。p1的值原為&a,p2的值為&b,在交互指向之后,p1指向了&b,p2指向了&a,所以在使用*p1的時候取的是b的值,*p2取的是a的值。

用指針做函數參數

函數的參數不僅僅可以是整型、浮點型、字符型,還可以是指針類型,它的作用是將一個變量的地址傳遞給被調用函數的形參

例:同樣是上面的例子,輸入兩個整數按大小輸出

int main(int argc, char const *argv[])
{
  void swap(int *p1, int *p2); // 函數聲明
  int *p1, *p2, a, b;
  cout<<"請輸入兩個整數:"<<endl;
  cin>>a>>b;
  p1 = &a;
  p2 = &b;
  if (a<b) swap(p1, p2);
  cout<<"a= "<<a<<" b= "<<b<<endl;
  cout<<"max= "<<*p1<<" min= "<<*p2<<endl;
}

void swap(int *p1, int *p2) // 交換指針指向地址的值
{
  int temp;
  temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

運行結果:

請輸入兩個整數:
10 15
a= 15 b= 10
max= 15 min= 10

可以看到a和b的值互換了,因為當使用指針做為參數的時候,傳遞的是地址,然后在swap函數中我們將地址里面的值進行了互換,所以a,b的值進行了交換。

數組與指針

指向數組元素的指針

一個變量有地址,一個數組包含若干個元素,每個數組元素都在內存中占用存儲單元,它們都有相應的地址,指針變量既然可以指向變量,當然也可以指向數組元素。即把某個元素的地址放到一個指針變量中。所謂數組元素的指針就是數組元素的地址。

在C和C++中,數組名代表數組中第一個元素(即序號為0的元素)的地址。如:

int a[10];
int *p;
p = &a[0]; // 將元素a[0]的地址賦給指針變量p
p = a; // 同上

注意:數組名不代表整個數組,上面p=a;的作用是把a數組的首元素的地址賦給指針變量p,而不是把數組a各元素的值賦給p.

一旦將數組元素的首地址賦值給指針,那么我們可以通過指針來操作數組元素。如:

*p = 1;
*(p+1) = 2;
*(p+2) = 3;
p[3] = 4;
cout<<a[0]<<" "<<*p<<endl; //1 1
cout<<a[1]<<" "<<*(p+1)<<" "<<*(a+1)<<endl; // 2 2 2
cout<<a[2]<<endl; // 3
cout<<a[3]<<" "<<*(p+3)<<" "<<p[3]<<endl; //4 4 4

指針變量p已經指向數組中的第一個元素了,p+1表示指向同一數組中的下一個元素(并不是將p的值簡單加1),上面代碼已經告訴我們了。

簡單分析:

1、數組元素是整型,每個元素占4個字節,當執行p+1;語句的時候,意味著使p的值(即當前地址)加4個字節,以指向下一個元素。p+1的實際地址是p+1*d,d是數組元素所占的字節數。

2、p+ia+i其實是一樣的,都是a[i]的地址,或者說,它們都指向數組a第i個元素。a是數組名指向數組的首元素地址,指針p也是指向數組的首元素地址,那么對地址進行相同操作結果當然相同。

3、*(p+i)或者*(a+i)p+ia+i所指向的數組元素,即a[i]。例如上面我們為*(p+1)進行賦值,其實就是為a[1]進行賦值操作。因而*(p+i)*(a+i)a[i]這三者是等價的。

4、指向數組元素的指針變量也可以帶下標,如:p[i]與*(p+i)等價

例:有一個整型數組a,有10個元素,輸出數組的所有元素。

下標法

int main(int argc, char const *argv[])
{
  int a[10];
  cout<<"請輸入10個數:"<<endl;
  for (int i = 0; i < 10; ++i)
  {
    cin>>a[i];
  }
  cout<<"輸出:"<<endl;
  for (int i = 0; i < 10; ++i)
  {
    cout<<a[i]<<" ";
  }
}

指針法:就是將a[i]改為*(a+i)

for (int i = 0; i < 10; ++i)
{
    cout<<*(a+i)<<" ";
}

用指針變量指向數組

int main(int argc, char const *argv[])
{
  int a[10];
  int *p;
  p = a;
  cout<<"請輸入10個數:"<<endl;
  for (int i = 0; i < 10; ++i) // 輸入a[0]-a[9]
  {
    cin>>*(p+i);
  }
  cout<<"輸出:"<<endl;
  for (p = a; p < (a + 10); p++) // p先后指向a[0]-a[9]
  {
    cout<<*p<<" ";
  }
}

指針變量做函數形參

數組名代表數組首元素的地址,用數組名做函數的參數,傳遞的是數組首元素的地址,既然是地址,那么同樣可以使用指針變量做函數形參。

函數實參與形參的結合有四種形式:

實參 形參
數組名 數組名
數組名 指針變量
指針變量 數組名
指針變量 指針變量

函數與指針

指針變量也可以指向一個函數,一個函數在編譯時被分配給一個入口地址,這個函數入口地址就稱為函數的指針。可以用一個指針變量指向函數,然后通過該指針變量調用此函數。

比如:求a和b中的大者,一般情況下我們會這么寫:

int main(int argc, char const *argv[])
{
  int max(int x, int y);
  int a,b,m;
  cin>>a>>b;
  m = max(a,b);
  cout<<"max= "<<m<<endl;
}
int max(int x, int y)
{
  return x > y ? x: y;
}

但是我們也可以用一個指針變量指向max函數,然后通過該指針變量調用函數。定義指向max函數的指針變量的方法是:

int (*p)(int,int); 
int:指針變量p指向的函數的類型
p:是指向函數的指針變量
(int,int):p所指向的函數中的形參的類型

指向函數的指針變量的一般形式為:函數類型(*變量名)(函數列表)

int main(int argc, char const *argv[])
{
  int max(int x, int y); // 函數聲明
  int (*p)(int, int); // 定義指向函數的指針變量p
  int a,b,m;
  p = max; // 讓p指向函數max,表示將max的入口地址賦給p
  cin>>a>>b;
  m = p(a,b);
  cout<<"max= "<<m<<endl;
}

注意:在定義指向函數的指針變量p時,(*p)兩側的括號不能省略,表示p先與*結合,它是指針變量,然后再與后面的()結合,表示此指針指向函數,函數的返回值是整型。如果寫出了“int *p(int, int);”,由于()的優先級高于*,它就成了聲明一個函數,這個函數的返回值是指向整型變量的指針,返回指針值的函數也簡稱為指針函數。

返回指針值的函數

前面已經提到了返回指針值的函數為指針函數。定義指針函數的形式為:類型名 *函數名(參數列表);,例如:

int *a(int x, int y);

a是函數名,調用它以后能夠得到一個指向整型數據的指針(地址)。x,y是函數的形參。在a的兩側分別是*和()運算符,由于()優先級高于*,因此a先與()結合表示為函數。函數前面的*表示此函數是指針型函數(函數值是指針),int為返回的指針指向的整型變量。

指針數組

如果一個數組,其中的元素全部為指針類型數據,那么該數組就是指針數組。也就是說,指針數組中的每一個元素都相當于一個指針變量,它的值是地址。

一維數組的定義形式為:類型名 *數組名[數組長度];,例如:

int *p[4];

C中的 NULL 指針

在變量聲明的時候,如果沒有確切的地址可以賦值,為指針變量賦一個 NULL 值是一個良好的編程習慣。賦為 NULL 值的指針被稱為空指針。NULL 指針是一個定義在標準庫中的值為零的常量

#include <stdio.h>
 
int main ()
{
   int  *ptr = NULL;
   printf("ptr 的地址是 %p\n", ptr  );
   return 0;
}

當上面的代碼被編譯和執行時,它會產生下列結果:

ptr 的地址是 0x0

在大多數的操作系統上,程序不允許訪問地址為 0 的內存,因為該內存是操作系統保留的。然而,內存地址 0 有特別重要的意義,它表明該指針不指向一個可訪問的內存位置。但按照慣例,如果指針包含空值(零值),則假定它不指向任何東西

如需檢查一個空指針,可以使用 if 語句,如下所示:

if(ptr)     /* 如果 p 非空,則完成 */
if(!ptr)    /* 如果 p 為空,則完成 */

指向指針的指針

指向指針的指針,簡稱為指向指針的指針,也可以理解為二級指針。

const指針

可以指定指針變量是一個常量,或者指定指針變量指向的對象是一個常量。有以下幾種情況

指向常量的指針變量

定義這種指針變量的一般形式為:const 類型名 * 指針變量名;

不允許通過指針變量改變它指向的對象的值,例如:

int a = 12, b = 15;
const int *p = &a; //定義p為指向整型變量a的const指針變量
*p = 15; //試圖通過p改變它指向的對象a的值,非法

上面定義了p為(const int *)型的指針變量,并使其指向變量a,不能通過p來改變a的值,但是指針變量p的值(即p的指向)是可以改變的。例如:

p = &b; //p改為指向b是合法的

不要以為只要定義了(const int *)型指針變量就能保證其所指向的對象的值無法改變。例如:

a = 10;

所以用指向常量的指針變量只是限制了通過指針變量改變它指向的對象的值。如果想保證a的值始終不變,應當把a定義為常變量:

const int a = 16;

這樣p就成為了指向常變量的指針變量,無論用直接訪問方式還是間接訪問方式都無法改變a的值

常指針

指定指針變量的值是常量,即指針變量的指向不能改變。

int a = 4, b= 5;
int * const p = &a; // 指定p只能指向變量a
p = &b; // 試圖改變p的指向,不合法

定義這種指針變量的一般形式是:類型名 *const 指針變量名;

  • 這種指針變量稱為常指針變量,簡稱常指針,即指針值不能改變
  • 必須在定義時初始化,指定其指向
  • 指針變量的指向p不能改變,但指針變量的指向變量的值可以改變*p = 10;
  • 注意const和位置。const在后面

指向常量的常指針

把以上兩種疊加在一起,就是指向常量的常指針變量。即指針變量指向一個固定對象,該對象的值不能改變(指不能通過指針變量改變該對象的值)

int a = 4, b= 5;
const int * const p = &a; // 指定p只能指向變量a

p = &b; // 試圖改變p的指向,不合法
*p = 10; // 試圖改變p的值,不合法
a = 10; // 直接改變a的值,合法

定義這種指針變量的一般形式為:const 基本類型名 * const 指針變量名

void指針類型

可以定義一個基類型為void的指針變量(即(void *)型變量),它不能指向任何類型的數據。注意:不要把指向void類型理解為能指向“任何類型”的數據,而應該理解為“指向空類型”或“不確定的類型”的數據

如果指針變量不指定一個確定的數據類型,它是無法訪問任何一個具體的數據的,它只通過了一個地址。在c中用malloc函數開辟動態存儲空間,函數的返回值是該空間的起始地址,由于該空間尚未使用,其中沒有數據,談不上指向什么類型的數據,故返回一個void *型的指針,表示它不指向確定的具有類型的數據。

顯然這種指針是過渡型的,它必須轉換為指定一個確定的數據類型的數據,才能訪問實際存在的數據,否則它是沒有任何用處的,在實際使用該指針變量時,要對它進行類型轉換,使之適合于被賦值的變量的類型。

int a = 3; // 定義a為整型變量
int *p1 = &a; // p1指向int型變量
char *p2 = "new"; // p2指向char型變量
void *p3; // p3為無類型指針變量
p3 = (void *)p1; // 將p1的值轉換為void *類型,然后賦值給p3
//cout << *p3 << endl; //p3不能指向確定類型的變量,*p3非法
cout << *(int *)p3 << endl; // 把p3的值轉換為(int *)型,可以指向指針變量a
p2 = (char *)p3; // 將p3的值轉換為char *類型,然后賦值給p2,輸出3
printf("%d", *p2); //合法,輸出a的值

可以把非void型的指針付給void型指針變量,但是不能把void型指針直接付給非void型指針變量,必須先進行強轉換

引用

對于一個數據可以建立一個“引用”,它的作用是為一個變量起一個別名。這是C++對C的一個重要擴充。

假如有一個變量a,想給它起一個別名b,可以這樣寫:

int a; // 定義a為整型變量
int &b = a; // 聲明b是a的引用

以上聲明了b是a的引用,即b是a的別名,經過這樣的聲明后,使用a或b的作用相同,都代表同一變量。可以這樣理解引用,通過b可以引用a。聲明變量b為引用,并不需要另外開辟內存單元來存放b的值,b和a占內存的同一個存儲單元,它們具有同一地址。即使變量b具有變量a的地址,如果a的值是20,那么b的值也是20

注意:

  • 在上面的聲明中,&是引用聲明符,并不代表地址,不要理解為把a的值付給b的地址。在數據類型名后面出現的&聲明符是引用聲明符,在其他場合出現的都是地址符,如:
char c;
char &d = c; // 此處的&是引用聲明g符
int *p = &a; // 此處的&是地址符
  • 引用不是一種獨立的數據類型,對引用只有聲明,沒有定義。必須先定義一個變量,然后聲明對該變量建立一個引用(別名)

  • 聲明一個引用時,必須同時使之初始化,即聲明它代表哪一個變量。

  • 在聲明一個引用后,不能再使之作為另一變量的引用。比如:聲明變量b是變量a的引用后,在其有效的范圍內,b始終與其代表的變量a相聯系,不能再作為其他變量的引用(別名)。下面的用法不對:

 int a1, a2;
 int &b = a1; // 聲明b是a1的引用
 int &b = a2; // 試圖使b又變成a2的別名,不合法
  • 不能建立引用數組。如:
int a[5];
int &b[5] = a; //錯誤,不能建立引用數組
int &b = a[0]; //錯誤,不能作為數組元素的別名
  • 不能建立引用的引用。如:
int a = 3;
int &b = a; // 聲明b是a的別名,正確
int &c = b; //試圖建立引用的引用,錯誤
  • 可以取引用的地址。如已聲明b是a的引用,則&b就是變量a的地址&a
int *p;
*p = &b;
引用作為函數參數

C++之所以增加引用機制,主要是把它作為函數參數,以擴充函數傳遞數據的功能。

函數參數傳遞有兩種情況

1、將變量名作為實參和形參。這時傳給形參的是變量的值,傳遞是單向的。如果在執行函數形參的值發生變化,并不傳回給實參。因此在調用函數時,形參和實參不是同一個存儲單元。

要求將變量i和j的值互換。下面的程序無法實現

int main(int argc, const char * argv[]) {
    void swap(int , int); // 函數聲明
    int i = 3, j = 5;
    swap(i, j); // 調用函數swap
    cout << i << "" << j << endl; // i和j的值并未互換

    return 0;
}

void swap(int a, int b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

為了解決這個問題,采用傳遞變量地址的方法

2、傳遞變量的地址。形參是指針變量,實參是一個變量的地址,調用函數時,形參(指針變量)得到實參變量的地址,因此指向實參變量單元。

int main(int argc, const char * argv[]) {

    void swap(int *, int *); // 函數聲明
    int i = 3, j = 5;
    swap(&i, &j); // 調用函數swap
    cout << i << "" << j << endl; // 5 3

    return 0;
}

void swap(int *p1, int *p2)
{
    int temp;
    temp = *p1;
    *p1 = *p2;
    *p2 = temp;
}

其實上面這種虛實結合的方法仍然是“值傳遞”方式,只是實參的值是變量的地址而已。通過形參指針變量訪問主函數中的變量(i和j),并改變它們的值

3、以引用作為形參,在虛實結合時建立變量的引用,使形參名作為實參的“引用”。即形參成為實參的引用

int main(int argc, const char * argv[]) {
    void swap(int &, int &); // 函數聲明
    int i = 3, j = 5;
    swap(i, j); // 實參為整型變量
    cout << i << " " << j << endl; // 5 3
    return 0;
}

void swap(int &a, int &b) // 行參是引用
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}

在定義swap函數聲明行參時,指定a和b是整型變量的引用。注意:此處&a不是a的地址,而是指a是一個整型變量的引用(別名),&是引用聲明符。由于是形參,不必對它初始化,即未指定它們是哪個變量的別名。

當main函數調用swap函數時,進行虛實結合,把實參變量i和j的地址傳給形參a和b。這樣i和a的地址相同,二者是同一變量,即a成為i的別名,同理b成為j的別名。那么在swap函數中使a和b的值對換,顯然i和j的值同時也改變了。這就是地址傳遞方式

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容