登堂入室C++之C指針

C語言中的指針

C語言和C++中的指針是否一致

在C語言中,指針變量是用來存儲地址的變量。這里面地址分幾種類型,我們下面會細說,但是無論怎樣,指針都是用來存儲地址的。

但是在C++中,指針變量存儲的不一定是地址,有可能是其它更加復(fù)雜的內(nèi)容。這里的不一定和可能是因為C語言的指針類型在C++中是完全有效的。除了C語言中的指針類型,C++擴展了指針的表達范圍,比如我們有指向成員函數(shù)的指針,也有指向成員變量的指針。這樣的指針和C語言中的指針存儲地址不同,存儲了完全不同的信息。比如一個指向類里面虛函數(shù)的指針,它沒有辦法存地址,因為只有runtime的時候才知道具體的類型是哪一個,存地址的話根本就不知道存繼承鏈路里面哪一個類對應(yīng)的函數(shù)實現(xiàn)的地址。所以它只能存給定的類型以及這個函數(shù)在虛表(vtable)里面的偏移量。所以C++里面的指針除了兼容C里面的那部分指針的含義和C語言里面的指針一樣之外,其它的含義是不一樣的。

這篇文章主要分享的就是C語言里面的指針這一部分,C++里面擴展的指針部分會在后面的文章進行分享。

C語言里面的指針

指針變量首先是一個變量,是變量就會有對應(yīng)的內(nèi)存。就跟int b;定義的變量b對應(yīng)一塊內(nèi)存,內(nèi)存對應(yīng)的位置存儲的是一個整形的數(shù)一樣,int *p;中p也是一個變量,系統(tǒng)會給它分配一塊內(nèi)存,只不過這塊內(nèi)存儲存的是一個地址而已。

取指針對應(yīng)的地址對應(yīng)的值叫做解引用(dereferencing),不同類型的指針解引用的模式不一樣。

指向變量的指針

我們可以定義一個指針,它的值存儲的是一個已經(jīng)定義的變量的地址。

int a = 10;
int *pA = &a;
c-pointer-to-var.png

從上面圖可以清晰看出pA存儲的是變量a在內(nèi)存中的地址。

其實說存儲了變量的地址這句話也不是十分準確,因為如果變量對應(yīng)的類型比較大,那么是需要很多地址空間才能存儲的。所以更加準確地說指針存儲的是變量所在內(nèi)存的首地址

要想取到指針對應(yīng)的值,我們可以解引用

int b = *pA;

這時候b的值也是10,因為*pA返回地址0xA0243529出的值,也就是10。

指向函數(shù)的指針

指針既然可以用來存儲地址,那么函數(shù)也有對應(yīng)的地址,我們自然也可以用指針來存儲對應(yīng)的函數(shù)的地址。

對于一個函數(shù),如果它有以下的簽名

return_type function_name(parameter_type_1 param1, parameter_type_2 param2, ...)

那么它對應(yīng)的函數(shù)指有如下簽名

return_type (*function_pointer)(parameter_type_1 param1, parameter_type_2 param2, ...);

比如我們有一個函數(shù)

void f() {}

我們可以定一個可以存儲它地址的函數(shù)指針

void (*funcPtr)() = f;

或者

void (*funcPtr)() = &f;

也就是對于函數(shù)指針,賦值的時候?qū)?yīng)的函數(shù)名前面是否使用取地址符是沒有關(guān)系的,兩者都是合法的,并且結(jié)果也是一樣的。

指向函數(shù)的指針和指向變量的指針都是存儲的地址,如果非要說有什么地方不一樣,那么如果變量在stack里面,那么指向變量的指針指向的是stack內(nèi)存的一個位置;如果變量實在heap里面,那么對應(yīng)的指針指向的是heap內(nèi)存的一個位置。而指向函數(shù)的指針存儲的是代碼區(qū)的某個地址。

那指向函數(shù)的指針有什么用呢?可以看一個簡單的例子

int add(int a, int b)
{
  return a + b;
}

int mul(int a, int b)
{
  return a * b;
}

int eval(int a, int b, char opcode)
{
  int (*op)(int, int) = NULL;
  
  if (opCode == '+')
  {
    op = add;
  }
  else {
    op = mul;
  }
  
  return op(a, b);
}  

int main(int argc, char* argv[])
{   
    printf("1 + 2 = %d\n", eval(1, 2, '+'));
    printf("1 * 2 = %d\n", eval(1, 2, '*'));
  
    return 0
}

對函數(shù)指針進行解引用也有兩種方式,比如

op(1, 2);
(*op)(1, 2);

這兩種模式都是可以的。

指向數(shù)組的指針

在沒有分享左值和右值的概念之前,我們依然不打算詳細說數(shù)組名的具體含義。但是這這個地方我們已經(jīng)可以澄清一個事情了:

數(shù)組名不是一個指針。

為什么呢?因為指針在內(nèi)存中是有對應(yīng)的內(nèi)存的,只是內(nèi)存里面存的數(shù)據(jù)是地址而已。而在前面講數(shù)組的文章里面我們已經(jīng)從反匯編看到了,數(shù)組是有對應(yīng)的內(nèi)存的,但是數(shù)組名并沒有。數(shù)組名只是數(shù)組對應(yīng)的內(nèi)存的一個label而已。從這點我們就已經(jīng)可以說:數(shù)組名不是一個指針。

但是我們是可以有指向數(shù)組的指針的,畢竟,數(shù)組是有對應(yīng)的內(nèi)存地址的,而指針就是存儲地址的。

回憶一下我們在[數(shù)組]()一文中提到的代碼

int main()
{
    int scores[2][3][4];
    
    print_type<decltype(&scores)>();
    print_type<decltype(scores)>();
    print_type<decltype(scores[0])>();
    print_type<decltype(scores[0][0])>();
    print_type<decltype(scores[0][0][0])>();
    
    return 0;
}
// 輸出
//int (*)[2][3][4]
//int[2][3][4]
//int (&)[3][4]
//int (&)[4]
//int &

所以如果我們要指向一個數(shù)組,如果數(shù)組的簽名是

element_type array_name[dim1][dim2][...]

那么對應(yīng)的指向這個數(shù)組的指針就是

element_type (*ptr)[dim1][dim2][...]

這個跟指向函數(shù)的指針的類型非常類似,都是把對應(yīng)的數(shù)組名或者函數(shù)名換成(*pointer_name)就可以了。

比如我們可以有

int (*arrayPtr)[2][3][4] = &scores;
int (*subArrayPtr)[3][4] = scores;

這里面需要注意的是C和C++都是強類型的語言,所以對象都需要有明確的類型。上面的兩個指針都存儲了scores這個數(shù)組的首地址,從數(shù)值上看是一樣的,但是這兩個指針有完全不同的類型。而這個不同的類型就決定了使用這個指針做算術(shù)運算和解引用都有不同的結(jié)果。

比如:

int (*addedPtr)[2][3][4] = arrayPtr + 1;

假設(shè)scores數(shù)組的首地址是a,那么addedPtr的值是多少呢?

a + 2*3*4* sizeof(int)

而且addedPtr的類型是int (*)[2][3][4]。

int (*anotherAddedPtr)[3][4] = scores + 1;

anotherAddedPtr的值是多少呢?

a + 3 * 4 * sizeof(int)

所以雖然arrayPtr, subArrayPtr存儲的值是一樣的,但是由于本身類型的不同,進行算術(shù)運算的時候就會有截然不同的結(jié)果。

對于解引用,它們之間也是不一樣的

arrayPtr[0][0][0]
subArrayPtr[0][0]

這兩個都會取到scores[0][0][0]的值,但是可以看到取值的模式是不一樣的。

指向內(nèi)存的指針

我們上面講的指針都是已經(jīng)有名稱了,比如變量名,函數(shù)名,數(shù)組名,然后使用指針指向這些名稱代表的內(nèi)存。我們當(dāng)然也可以用指針直接來存儲沒有特定名稱的內(nèi)存了,畢竟只要是地址指針就可以存儲。這就是我們經(jīng)常說到的指向malloc對應(yīng)的內(nèi)存。

int *ptr = (int*)malloc(sizeof(int));

這里需要注意的是,對于之前提到的指針,指針本身并不需要去管對應(yīng)的內(nèi)存的生存期。比如

int a = 10;
int *ptr = &a;

變量a已經(jīng)管理了對應(yīng)的內(nèi)存的生存期,是不需要指針ptr再去做任何操作的。

但是對于直接指向malloc分配的內(nèi)存的指針,用戶需要通過這個指針在適當(dāng)?shù)臅r候釋放掉對應(yīng)的內(nèi)存。

free(ptr);
ptr = NULL;

否則的話會造成內(nèi)存泄漏。

指向指針的指針

很多人很恐懼指針,就是有類似指針的指針這樣的類型。其實只需要記住指針本身有類型,存儲的都是地址這一句就好了。既然指針變量是一個變量,是有對應(yīng)的內(nèi)存的,那自然我們也可以使用另外一個指針變量來存儲這個指針對應(yīng)的內(nèi)存的首地址。

int a = 10;
int *ptrA = &a;
int **pptrToptrA = &ptrA;

里面核心點就是ptrA這個變量是有對應(yīng)的內(nèi)存的,有內(nèi)存就有對應(yīng)的內(nèi)存地址,有內(nèi)存地址就可以有另外的指針(這里就是pptrToptrA)來存儲ptrA這個變量的內(nèi)存地址。

指針使用需要注意的一些問題

多個變量定義

int *a = NULL, b;

這種定義其實只有a是指針類型,b只是普通的指針類型。我們有兩個辦法來解決這個問題,第一種是

int *a = NULL, *b = NULL;

第二種是使用typedef

typedef int* IntPtr;

IntPtr a = NULL, b = NULL;

建議使用typedef這種模式。

不恰當(dāng)釋放內(nèi)存

這里我們需要搞清楚一件事情:變量本身的釋放是系統(tǒng)管理的。一個指針變量是一個變量,這個變量本身系統(tǒng)是知道管理它的生存周期的。

比如在一個函數(shù)內(nèi)部定義的一個局部變量,在函數(shù)返回之前系統(tǒng)就會把對應(yīng)變量的內(nèi)存收回去。這個是不需要我們程序員去管的。比如

void f()
{
  int a = 10; // 系統(tǒng)會在stack為這個int變量分配一塊內(nèi)存
  int *ptrA = &a; // 系統(tǒng)會在stack為這個int指針變量分配一塊內(nèi)存
  ... // 使用
    
 // 在這個位置,雖然沒有顯式的代碼,但是編譯器會處理回收a和ptrA內(nèi)存的
 // 的事情
}  

但是,系統(tǒng)(或者說編譯器)并不會處理指針指向的內(nèi)存的回收。比如上面的ptrA指向的內(nèi)存&a,編譯器并不會去處理它的回收,它的回收是通過處理變量a的回收處理的。

這里就涉及到一個問題了:指向malloc這樣直接向系統(tǒng)請求的內(nèi)存誰來釋放?

答案就是程序員需要處理。

所以,已經(jīng)有系統(tǒng)處理的內(nèi)存,程序員不能去釋放;系統(tǒng)沒有處理的,程序員一定要去釋放。

比如上面代碼里面,如果我們這么寫就會出問題

void f()
{
  int a = 10;
  int *ptrA = &a;
  
  ...
    
  free(ptrA);
  ptrA = NULL;
}

因為ptrA對應(yīng)的變量是在處理變量a的時候就會處理,我們不能去手動釋放。或者換句話說,對于指向顯式名稱的指針,我們不要去釋放對應(yīng)的內(nèi)存。

那對于直接指向malloc分配的內(nèi)存的指針,我們就一定需要釋放對應(yīng)的內(nèi)存

void f()
{
    int *ptr = (int*)malloc(10 * sizeof(int));
    ....
    free(ptr);
  ptr = NULL;
}

變量ptr的內(nèi)存是系統(tǒng)會處理回收的,但是它指向的內(nèi)存需要我們手動釋放。

那對于手動釋放,我們也需要非常注意多個指針指向指向同一塊內(nèi)存的情況,必須保證只釋放一次。

void f()
{
  int *ptr1 = (int*)malloc(10 * sizeof(int));
  int *ptr2 = ptr1;
  ...
  ...
  free(ptr1);
  ptr1 = NULL;
  free(ptr2);
  ptr2 = NULL
}

上面代碼就會出現(xiàn)問題,同樣的一塊內(nèi)存被釋放了兩次。我們需要盡量不要使用這種alias的指針,出了問題的時候不容易判斷問題出在什么地方。

懸垂指針

還有一種情況是指針對應(yīng)的內(nèi)存已經(jīng)被釋放了,但是我們依然還在使用這個指針。這種指針稱為懸垂指針(dangling pointer)。

經(jīng)常出現(xiàn)的一種情況就是先釋放后使用:

int *ptr = (int*)malloc(10 * sizeof(10));
...
free(ptr);
...
int b = ptr[10]; // 出問題,對應(yīng)的內(nèi)存已經(jīng)釋放

上面最后的訪問是否會引起崩潰不一定,但是系統(tǒng)的表現(xiàn)肯定不是我們預(yù)期的。我們一定要避免這種情況。

另外一種就是使用了從函數(shù)返回的指向函數(shù)內(nèi)部數(shù)組的指針,比如

int* f()
{
  int scores[4] = {1,2,3,4};
  int *ptr = &scores[0];
  
  return ptr;
} 

int *scorePtr = f();
....

上面scoresPtr雖然得到了一個內(nèi)存地址,但是對應(yīng)的內(nèi)存在函數(shù)返回的時候已經(jīng)被回收,所以指針指向的位置已經(jīng)不是一個有效的地址。這時候的指針也成了懸垂指針。

上面兩種懸垂指針的情況我們都要避免,不然程序會出現(xiàn)問題。

未初始化的指針

還有一種問題是使用未初始化的指針,比如

void f()
{
    int *ptr;
    int b = *ptr;
}

因為ptr沒有初始化就是用,這時候它的值是隨機的,指向的內(nèi)存也不知道是什么內(nèi)容。對它的訪問可能造成訪問越界或者其它的未定義的情況。所以這種情況也要避免。一個好的習(xí)慣就是一開始就賦值NULL,訪問的時候判斷是否為NULL再進行操作

void f()
{
  int *ptr = NULL;
  ...
  int b = 0;
  if (NULL != ptr)
  {
    b = *ptr;
  }
}

GNU有一個擴展也可以處理這種情況,它提供了一個macro叫做RAII_VARIABLE,定義如下

#define RAII_VARIABLE(vartype,varname,initval,dtor) \
void _dtor_ ## varname (vartype * v) { dtor(*v); } \
vartype varname __attribute__((cleanup(_dtor_ ## varname))) = (initval)

然后可以如下使用

void f()
{
  RAII_VARIABLE(int*, ptr, (int*)malloc(10* sizeof(int), free)
  ....
}

跟指針相關(guān)的幾個類型

  • size_t: 用來存儲平臺相關(guān)的可取地址的區(qū)域的大小
  • ptrdiff_t: 用來處理指針的算術(shù)運算;
  • intptr_tuintptr_t: 存儲指針地址

size_t

它用作sizeof操作符的返回值,也用做類似malloc這樣函數(shù)參數(shù)。用來安全地表示系統(tǒng)可以操作的內(nèi)存的大小。在stdio.hstdlib.h中定義如下

#ifndef __SIZE_T
#define __SIZE_T
typedef unsigned int size_t
#endif

size_t一般來說可能得最大值是SIZE_MAX。

一般使用%zu, %u, $lu來打印size_t的值,因為它是無符號的,避免使用%d打印。

ptrdiff_t

用來做指針運算的,比如

int *ptr = (int*)malloc(10 * sizeof(int));
ptrdiff_t offset = 3;
int *elePtr = ptr + offset;

intptr_tuintptr_t

當(dāng)你想把指針存的地址當(dāng)做有符號整形使用的時候,你可以使用intptr_t;當(dāng)你想把其當(dāng)成無符號整數(shù)使用的時候,你可以使用uintptr_t。比如

int *ptr = NULL;
...
uintptr_t uptr = (uintptr_t)ptr;
uintptr_t rptr = uptr & 0x1;
...

更多文章可以關(guān)注公眾號”探知軒“, 第一時間可以看到。如果對C++各方面感興趣也可以私聊進c++群一起討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容