指針(I)

基礎知識

物理內存

? 維修電腦師傅眼中的內存:內存在物理上是由一組DRAM芯片組成的.


軟件內存

? 操作系統將RAM(分為兩大類:SRAMDRAM)等硬件和軟件結合起來,給程序員提供的一種對內存使用的抽象。這種抽象機制使得程序使用的是虛擬存儲器,而不是直接操作和使用真實存在的物理存儲器,所有的虛擬地址形成的集合就是虛擬地址空間。 如下圖:

程序員眼中的內存

系統中的實際內存排列:


系統中的內存排列

? 內存是一個很大的,線性的字節數組(平坦尋址)。每一個字節都是固定的大小,由8個二進制位組成。最關鍵的是,每一個字節都有一個唯一的編號,編號從0開始,一直到最后一個字節。如上圖中,這是一個256M的內存,他一共有256x1024x1024 = 268435456個字節,那么它的地址范圍就是 0 ~268435455 。

? 系統中的內存每一個字節都有一個唯一的編號。因此,在程序中使用的變量,常量,甚至函數等數據,當它們被載入到內存中后,都有自己唯一的編號,這個編號就是這個數據的地址,指針就是這樣形成的。


  • bit(位): 一個二進制數據0或1,是1bit

  • byte(字節): 存儲空間的基本計量單位,1byte = 8 bit;

  • 1 字母 = 1 byte = 8 bit;

  • 1 漢字 = 2 byte = 16 bit;

64 位的編譯器各類型字節數顯示

//不同的編譯器位數,其字節數顯示是不同的。通過在程序中打印:
printf("%lu", sizeof(int));   //在Xcode中輸出:4,說明是64位的編譯器


char:      1個字節

short:     2個字節

int:       4個字節

long:      8個字節

float:      4個字節

double:    8個字節

? 查看操作系統位數,在終端輸入:uname -a 在末尾有x86_64說明是64位的操作系統。




指針介紹

int a = -12;
char b = M;
float c = 3.14
a、b、c內存存放示意圖

變量尋址

3.png

編譯器將變量名和地址進行掛鉤,內存中沒有存放變量的名字;

? 指針的值實質是內存單元(即字節)的編號,所以指針單獨從數值上看,也是整數,他們一般用16進制表示。指針的值(虛擬地址值)使用一個機器字的大小來存儲,也就是說,對于一個機器字為w位的電腦而言,它的虛擬地址空間是0~2w - 1 ,程序最多能訪問2w個字節。
? 所以可以把指針看做是一個變量,其值為另一個變量的地址,即內存位置的直接地址。

//地址是計算機內存中的某個位置,而指針是專門用來存放地址的特殊類型變量
type *pointer;

type:      指針的基類型;
*   :      申明指針;
pointer:   指針變量;//作用:是用來存放地址和對地址進行索引

DEMO圖

指針各類型介紹

? 要掌握指針的四方面的內容:指針的類型、指針所指向的類型、指針的值(或者叫指針所指向的內存區)、指針本身所占據的內存區。

  • 指針的類型: 從語法的角度看,把指針聲明語句里的指針名字去掉,剩下的部分就是指針的類型;

  • 指針所指向的類型:決定了編譯器將把那片內存區里的內容當做什么來看待。從語法上看,你只須把指針聲明語句中的指針名字和名字左邊的指針聲明符*去掉,剩下的就是指針所指向的類型;

  • 指針的值:是指針本身存儲的數值,這個值被編譯器當作一個地址,而不是一個一般的數值。在64 位程序里,所有類型指針的值都是一個64 位整數,因為64 位程序里內存地址全都是64 位長。指針的值代表那個內存地址的開始,長度為sizeof(數據類型)的一片內存區。以后,我們說一個指針的值是XX,就相當于說該指針指向了以XX 為首地址的一片內存區域;我們說一個指針指向了某塊內存區域,相當于說該指針的值是這塊內存區域的首地址。指針的值指針所指向的類型是兩個完全不同的概念。在上圖中,指針所指向的類型已經有了,但由于指針還未初始化,所以它所指向的內存區是不存在的,或者說是無意義的。

  • 指針本身所占據的內存區:指針本身占用的內存可以用函數sizeof(數據類型)測一下就知道了。在64 位平臺里,指針本身占據了8 個字節的長度。指針本身占據的內存這個概念在判斷一個指針表達式是否是左值時很有用。



//普通的整型變量
int p; 

//首先從P 處開始,先與*結合,所以說明P 是一個指針,然后再與int 結合,說明指針所指向的類型為int 型.所以P是一個返回整型數據的指針
int *p;  

//首先從P 處開始,先與[]結合,說明P 是一個數組,然后與int 結合,說明數組里的元素是整型的,所以P 是一個由整型數據組成的數組  
int p[3]; 

//首先從P 處開始,先與[]結合,因為其優先級比*高,所以P 是一個數組,然后再與*結合,說明數組里的元素是指針類型,
//然后再與int 結合,說明指針所指向的類型是整型的,所以P 是一個返回整型數據的指針所組成的數組  
int *p[3]; 

//首先從P 處開始,先與*結合,說明P 是一個指針然后再與[]結合,說明指針所指向的內容是一個數組,
//然后再與int 結合,說明數組里的元素是整型的.所以P 是一個指向整型數據組成的數組的指針  
int (*p)[3]; 

//首先從P 開始,先與*結合,說是P 是一個指針,然后再與*結合,說明指針所指向的元素是指針,然后再與int 結合,說明該指針所指向的元素是整型數據.
//由于二級指針以及更高級的指針極少用在復雜的類型中,所以后面更復雜的類型我們就不考慮多級指針了,最多只考慮一級指針.  
int **p; 

//從P 處起,先與()結合,說明P 是一個函數,然后進入()里分析,說明該函數有一個整型變量的參數,然后再與外面的int 結合,說明函數的返回值是一個整型數據 
int p(int); 

//從P 處開始,先與指針結合,說明P 是一個指針,然后與()結合,說明指針指向的是一個函數,然后再與()里的int 結合,說明函數有一個int 型的參數,
//再與最外層的int 結合,說明函數的返回類型是整型,所以P 是一個指向有一個整型參數且返回類型為整型的函數的指針  
Int (*p)(int); 

//從P 開始,先與()結合,說明P 是一個函數,然后進入()里面,與int 結合,說明函數有一個整型變量參數,然后再與外面的*結合,說明函數返回的是一個指針,
//然后到最外面一層,先與[]結合,說明返回的指針指向的是一個數組,然后再與*結合,說明數組里的元素是指針,然后再與int 結合,說明指針指向的內容是整型數據.
//所以P 是一個參數為一個整數據且返回一個指向由整型指針變量組成的數組的指針變量的函數. 
int *(*p(int))[3]; 




指針的操作

指針的基本使用

打印:
a = 10, &a = 0x7ffeefbff4fc
b = 0x7ffeefbff4fc, *b = 10, &b = 0x7ffeefbff4f0
上圖可以看到,當打印 c 時就會Crash,報錯看圖。


void contactAddressAndPointer()
{
    int num = 97;
    //定義一個指針pointer
    int *pointer;
    //對  num  取地址(&num, 使用連字號&運算符訪問的地址), 賦值給指針變量pointer
    pointer = #

    /*
      int num=97;
      //在定義指針pointer的同時將num的地址賦給指針pointer
      int *pointer=#
    */

    //pointer:指向的變量num的地址;
    // &pointer:指針pointer的地址,因為指針也是一個變量自己也有地址的;
    //&num:變量 num 的地址;
    printf("地址:\np:%p,  &p:%p,  &num:%p\n\n",pointer, &pointer, &num);

    //*pointer:變量 num 的值;
    //i:變量 i 的值;
    printf("值:\n指針變量*pointer:%d,  num的值:%d\n",*pointer,num);
}

打印值:
地址: p:0x7ffeefbff57c, &p:0x7ffeefbff570, &num:0x7ffeefbff57c

值: 指針變量*pointer:97, num的值:97

? 用來保存 指針 的變量,就是指針變量。如果指針變量pointer保存了變量 num的地址,則就說:pointer指向了變量num,也可以說pointer指向了num所在的內存塊 ,這種指向關系,在圖中一般用 箭頭表示。


1.png

? 指針變量pointer指向了num所在的內存塊 ,即從地址0028FF40開始的4個byte 的內存塊。



變量和指針

變量和指針的存放區別


取址

    int pointerFunc(int x, int b){
        return x + b;
    }


    int num = 97;
    float scrore = 10.00F;
    int arr[3] = {1, 2, 3};
    int (*ptr)(int, int);
    
    int *p_num = #
    float *p_scrore = &scrore;
    int (*p_arr)[3] = &arr; //指向含有3個int元素的數組的指針 
    ptr = *pointerFunc;
    const char *msg = "Hello Pointer";
    
    printf(" p_num: %p, *p_num: %d, &num:%p\n", p_num, *p_num, &num);
    printf(" p_scrore: %p,  *p_scrore: %f, &num:%p\n", p_scrore, *p_scrore, &scrore);
    printf(" p_arr: %p, *p_arr:%p, &arr:%p\n", p_arr, *p_arr, &arr);
    printf(" pointerFunc 地址:%p, &pointerFunc: %p, *pointerFunc:%p\n", pointerFunc, &pointerFunc, *pointerFunc);
    printf(" msg: %p,  *msg:%c,&msg:%p\n", msg, *msg, &msg);
    int n = (*ptr)(10, 5);
    printf(" n = %d", n);

打印:
p_num: 0x7ffeefbff534, *p_num: 97, &num:0x7ffeefbff534
p_scrore: 0x7ffeefbff530, *p_scrore: 10.000000, &scrore:0x7ffeefbff530
p_arr: 0x7ffeefbff54c, *p_arr:0x7ffeefbff54c, &arr:0x7ffeefbff54c
pointerFunc 地址:0x100001360, &pointerFunc: 0x100001360, *pointerFunc:0x100001360
msg: 0x100001c60, *msg:H,&msg:0x7ffeefbff508
n = 15

特殊的情況,他們并不一定需要使用&取地址:

  • 數組名的值就是這個數組的第一個元素的地址。
  • 函數名的值就是這個函數的地址。
  • 字符串字面值常量作為右值時,就是這個字符串對應的字符數組的名稱,也就是這個字符串在內存中的地址。


解址

? 實質是:從指針指向的內存塊中取出這個內存數據。
? 對一個指針解地址,就可以取到這個內存數據,解地址 的寫法,就是在指針的前面加一個*號。

    int age = 19;
    int*p_age = &age;
    *p_age  = 20;  //通過指針修改指向的內存數據
    
    printf("age = %d\n",*p_age);   //通過指針讀取指向的內存數據
    printf("age = %d\n",age);

打印:
age = 20
age = 20

解引用獲取值

注意:

  • 指針所保留的是內存中一個地址,它并不保存指向的數據的值本身。因此,務必確保指針對應一個已經存在的變量或者一塊已經分配了的內存;

  • 星號有兩種用途:

    • 用于創建指針:int *myPointer = &myInt;
    • 對指針進行解引用:*myPointer = 3998;


指針之間的賦值

? 指針賦值和int變量賦值一樣,就是將地址的值拷貝給另外一個。指針之間的賦值是一種淺拷貝,是在多個編程單元之間共享內存數據的高效的方法。

//通過指針 p1 、 p3 都可以對內存數據 num 進行讀寫,如果2個函數分別使用了p1 和p3,那么這2個函數就共享了數據num。
int num = 10;
int* p1  = & num;
int* p3 = p1;


printf("num值沒改變:num= %d, *p1= %d, *p3= %d", num, *p1, *p3);
    
num = 100;
printf("\n\nnum值改變后:num= %d, *p1= %d, *p3= %d", num, *p1, *p3);

輸出值:
num值沒改變:num= 10, *p1= 10, *p3= 10

num值改變后:num= 100, *p1= 100, *p3= 100

淺拷貝示意圖


指針的屬性

int main(void)
{
    int num = 97;
    int *p1  = #
    char* p2 = (char*)(&num);

    printf("%d\n",*p1);    //輸出  97
    putchar(*p2);          //輸出  a
    return 0;
}

指針的值:很好理解,如上面的num 變量 ,其地址的值就是0028FF40 ,因此 p1的值就是0028FF40。數據的地址用于在內存中定位和標識這個數據,因為任何2個內存不重疊的不同數據的地址都是不同的。
指針的類型:指針的類型決定了這個指針指向的內存的字節數并如何解釋這些字節信息。一般指針變量的類型要和它指向的數據的類型匹配。

由于num的地址是0028FF40,因此p1 和 p2的值都是0028FF40
*p1 : 將從地址0028FF40 開始解析,因為p1是int類型指針,int占4字節,因此向后連續取4個字節,并將這4個字節的二進制數據解析為一個整數 97。
*p2 : 將從地址0028FF40 開始解析,因為p2是char類型指針,char占1字節,因此向后連續取1個字節,并將這1個字節的二進制數據解析為一個字符,即'a'。

同樣的地址,因為指針的類型不同,對它指向的內存的解釋就不同,得到的就是不同的數據。





下一篇:指針(II)


參考資料:
C 語言指針詳解

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

推薦閱讀更多精彩內容