天天看點

最詳細的【指針】詳解---C語言從入門到精通

最詳細的【指針】詳解---C語言從入門到精通

個人名片:

🐼作者簡介:一名大一在校生

🐻‍❄️個人首頁:​​小新愛學習.​​

🕊️系列專欄:零基礎學java ----- 重識c語言

🐓每日一句:身懶,必會毀了你家的身材;心懶,一定會毀了你的夢想!

文章目錄

  • ​​指針🎊​​
  • ​​1.指針是什麼?👻​​
  • ​​2.指針和指針類型🐸​​
  • ​​帶*指針類型的定義🐯​​
  • ​​指針類型的意義🐧​​
  • ​​3.野指針​​
  • ​​4.指針運算🙈​​
  • ​​指針算數運算:🐇​​
  • ​​指針關系運算:🦖​​
  • ​​5.指針和數組🐻‍❄️​​
  • ​​用指針引用數組元素🦕🐟​​
  • ​​指針與數組的差別🐷​​
  • ​​6.二級指針(指向指針的指針)🐒🦍​​
  • ​​7.指針數組(數組每個元素都是指針)🐺​​

指針🎊

1.指針是什麼?👻

指針,是C語言中的一個重要概念及其特點,也是掌握C語言比較困難的部分。指針也就是**記憶體位址**,指針變量是用來存放記憶體位址的變量,在同一CPU構架下,不同類型的指針變量所占用的存儲單元長度是相同的,而存放資料的變量因資料的類型不同,所占用的存儲空間長度也不同。有了指針以後,不僅可以對資料本身,也可以對存儲資料的變量位址進行操作。
指針描述了資料在記憶體中的位置,标示了一個占據存儲空間的實體,在這一段空間起始位置的相對距離值。在 C/C++語言中,指針一般被認為是指針變量,指針變量的内容存儲的是其指向的對象的首位址,指向的對象可以是變量(指針變量也是變量),數組,函數等占據存儲空間的實體。
最詳細的【指針】詳解---C語言從入門到精通

總的來說所:指針也就是記憶體位址,指針變量是用來存放記憶體位址的變量。就像其他變量或常量一樣,您必須在使用指針存儲其他變量位址之前,對其進行聲明。

指針變量聲明的一般形式為:

type *var_name;

type 是指針的基類型,

var_name 是指針變量的名稱。

*星号是用來指定一個變量是指針。

舉例:

`#include <stdio.h>
 
int main ()
{
    int var_runoob = 10;
    int *p;              // 定義指針變量
    p = &var_runoob;
 
   printf("var_runoob 變量的位址: %p\n", p);
   return 0;
}`      
最詳細的【指針】詳解---C語言從入門到精通

也可以這麼說:指針就是一個變量,變量裡面存放的是位址,指針就是位址,位址就是指針

2.指針和指針類型🐸

在C語言中,指針類型就是資料類型,是給編譯器看的,也就是說,指針類型與數組、int、char這種類型是平級的,是同一類的

最詳細的【指針】詳解---C語言從入門到精通

帶*指針類型的定義🐯

double* pa;
int* pb;//定義了一個整型指針變量 pa,該指針變量隻能指向基類型為 int 的整型變量,即隻能儲存整型變量的位址。
short* pc;
char* pd;
float* pe;
struct* p people;      

總結:

  1. 任何變量都可以帶 * ,加上 * 以後變成新的類型,統稱“指針類型”。
  2. *可以是任意多個。

指針類型的意義🐧

指針類型的意義:

1.指針的類型決定了指針走一步走多遠(指針的步長)

2.指針類型決定了對指針解引用的時候能操作幾個位元組

1. 指針類型決定了對指針解引用的時候能操作幾個位元組

2. char*p; p的指針解引用就隻能通路一個位元組

3. intp; p的指針解引用就能通路四個位元組

4. doublep; *p能夠通路8個位元組

2.指針類型決定了:指針走一步走多遠(指針多長)

1. intp;p+1–>4個位元組

2. char p; p+1–>1個位元組

3. double*p; p+1–>8個位元組

最詳細的【指針】詳解---C語言從入門到精通

3.野指針

概念: 野指針就是指針指向的位置是不可知的(随機的、不正确的、沒有明确限制的)指針變量在定義時如果未初始化,其值是随機的,指針變量的值是别的變量的位址,意味着指針指向了一個位址是不确定的變量,此時去解引用就是去通路了一個不确定的位址,是以結果是不可知的。

一、野指針

1、指針變量中的值是非法記憶體位址,進而形成野指針

   2、野指針不是NULL指針,是指向不可用記憶體位址的指針

   3、NULL指針并無危害,很好判斷,也很好調試

   4、C語言中無法判斷一個指針所儲存的位址是否合法      

二、産生野指針的原因:

  1. 指針變量未初始化

    任何指針變量剛被建立時不會自動成為NULL指針,它的預設值是随機的,它會亂指一氣。是以,指針變量在建立的同時應當被初始化,要麼将指針設定為NULL,要麼讓它指向合法的記憶體。如果沒有初始化,編譯器會報錯“ ‘point’ may be uninitializedin the function ”。

  2. 指針釋放後之後未置空

    有時指針在free或delete後未指派 NULL,便會使人以為是合法的。别看free和delete的名字(尤其是delete),它們隻是把指針所指的記憶體給釋放掉,但并沒有把指針本身幹掉。此時指針指向的就是“垃圾”記憶體。釋放後的指針應立即将指針置為NULL,防止産生“野指針”。

  3. 指針操作超越變量作用域

    不要傳回指向棧記憶體的指針或引用,因為棧記憶體在函數結束時會被釋放。

總結:

1、局部指針變量沒有初始化

  2、指針所指向的變量在指針之前被銷毀

  3、使用已經釋放過的指針

  4、進行了錯誤指針運算

  5、進行了錯誤的強制類型轉換      

三、設計指針基本原則(避免野指針)

1、絕不傳回局部變量和局部數組的位址

  2、任何變量在定義後必須0初始化

  3、字元數組必須确認0結束符後才能成為字元串

  4、任何使用與記憶體操作相關的函數必須指定長度資訊      

四、常見記憶體錯誤

1、結構體成員指針未初始化

  2、結構體成員指針未配置設定足夠的記憶體

  3、記憶體配置設定成功,但并未初始化

  4、記憶體操作越界      

五、總結

記憶體錯誤是實際産品開發中最常見的問題,然而絕對大多數的bug都可以通過遵循基本的程式設計原則和規範來避免。

是以,在學習的時候要牢記和了解記憶體操作的基本原則,目的和意義

 1、記憶體錯誤的本質源于指針儲存的位址為非法值

      --指針變量未初始化,儲存随機值

      --指針運算導緻記憶體越界

 2、記憶體洩露源于malloc和free不比對

       --當malloc次數多于free時,産生記憶體洩露

       --當malloc次數少于free時,程式可能崩潰      

4.指針運算🙈

指針算數運算:🐇

指針是一個用數值表示的位址。是以,您可以對指針執行算術運算。可以對指針進行四種算術運算:++、–、+、-。

自增運算

例:

int *ptr;   //假設ptr是一個指向位址為1000的整型指針
ptr++;    //運算完後ptr指向位置1004      

指針每一次遞增(遞減),表示指針指向下一個(上一個)元素的存儲單元

注意:

  • 這個運算不會影響記憶體中元素的實際值
  • 指針在遞增或遞減時跳躍時的位元組數取決于指針所指向變量資料類型的長度(int 4個位元組 char 1個位元組)

增減運算

指針變量加上或減去一個整形數。加幾就是向後移動幾個單元,減幾就是向前移動幾個單元。

//定義三個變量,假設它們位址為連續的,分别為 4000、4004、4008
int x, y, z;

//定義一個指針,指向 x
int *px = &x;

//利用指針變量 px 加減整數,分别輸出 x、y、z
printf("x = %d", *px);* 

//px + 1,表示,向前移動一個單元(從 4000 到 4004)
//這裡要**先(px + 1),再*(px + 1)擷取内容**,因為單目運算符“*”優先級高于雙目運算符“+”
printf("y = %d", *(px + 1));    
printf("z = %d", *(px + 2));      

第二種類型的指針運算具有如下的形式: 指針—指針

隻有當兩個指針都指向同一個數組中的元素時,才允許從一個指針減去另一個指針,如下所示:

最詳細的【指針】詳解---C語言從入門到精通

減法運算的值是兩個指針在記憶體中的距離(以數組元素的長度為機關,而不是以位元組為機關),因為減法運算的結果将除以數組元素類型的長度。

例: 利用遞歸實作求字元串長度

#include<stdio.h>
int my_strlen(char* str)
{
  char* start = str;
  char* end = str;
  while (*end != '\0')
  {
    end++;
  }
  return (end - start);
}
int main()
{
  //strlen - 求字元串長度
  //遞歸--模拟實作strlen
  char arr[] = "hmm";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;

}      

指針關系運算:🦖

假定有指針變量 px、py;

隻有當兩個指針指向同一個數組中的元素時,才能進行關系運算。

當指針px和指針py指向同一數組中的元素時,

則有:

1. px > py 表示 px 指向的存儲位址是否大于 py 指向的位址

2. px < py 表示 px 指向的存儲位址是否小于 py 指向的位址

3. px == py 表示 px 和 py 是否指向同一個存儲單元

4. px == 0 和 px != 0 表示 px 是否為空指針

例1:

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values; vp < &values[N_VALUES];) {
    *vp++ = 0;
}      

vp不斷的向右移動,每次移動前都會先使用間接通路符把目前值初始化為0,最後vp指向了values數組最後一個元素的後面一個位置,這是合法的,指針可以指向它,它有合法的位址,但是不能使用間接通路符通路,因為那裡存儲的什麼東西,我們并不知道。

最詳細的【指針】詳解---C語言從入門到精通

例2:

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES]; vp > &values[0] ;) {
    *--vp = 0;
}      

vp首先指向了values的最後一個元素的後面一個位置,然後不斷左移,每次左移過後再使用間接通路符初始化為0,。

例3:

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES-1]; vp >= &values[0] ; vp--) {
    *vp = 0;
}      

這個版本的思路和版本2一樣,隻不過把*–vp操作拆分開了,但是這個版本中vp指向第一個元素values[0]後,下一個循環就指向了values數組前面的位置去了,這是前面講的規則不允許的,是絕不允許和數組第一個元素前面的位置的指針進行比較的有可能在有些機器上運作正确,但也絕對不允許。

标準規定:

允許指向數組元素的指針與指向數組最後一個元素後面的那個記憶體位置的指針比較,但不允許與指向第一個元素之前那個内現存位置的指針進行比較。

5.指針和數組🐻‍❄️

int main()
{
  int arr[10] = { 0 };
  printf("%p\n", arr);//位址-首元素位址
  printf("%p\n", arr+1);
  
  printf("%p\n", &arr[0]);
  printf("%p\n",&arr[0]+1);

  printf("%p\n", &arr);
  printf("%p\n", &arr+1);
  //1. &arr- &數組名--數組不是首元素位址-數組名表示整個數組--&數組名  取出的是整個數組的位址
  // 2. sizeof(arr) --sizeof(數組名)--數組名表示的整個數組--sizeof(數組名)計算的是整個數組的大小
  return 0;
}      
最詳細的【指針】詳解---C語言從入門到精通

用指針引用數組元素🦕🐟

引用數組元素可以用“下标法”,這個在前面已經講過,除了這種方法之外還可以用指針,即通過指向某個數組元素的指針變量來引用數組元素。

數組包含若幹個元素,元素就是變量,變量都有位址。是以每一個數組元素在記憶體中都占有存儲單元,都有相應的位址。指針變量既然可以指向變量,當然也就可以指向數組元素。同樣,數組的類型和指針變量的基類型一定要相同。

# include <stdio.h>
int main(void)
{
    int a[] = {1, 2, 3, 4, 5};
    int *p = &a[0];
    int *q = a;
    printf("*p = %d, *q = %d\n", *p, *q);
    return 0;
}      

輸出結果是:

*p = 1, *q = 1

程式中定義了一個一維數組 a,它有 5 個元素,即 5 個變量,分别為 a[0]、a[1]、a[2]、a[3]、a[4]。是以 p=&a[0] 就表示将 a[0] 的位址放到指針變量 p 中,即指針變量 p 指向數組 a 的第一個元素 a[0]。而 C 語言中規定,“數組名”是一個指針“常量”,表示數組第一個元素的起始位址。是以 p=&a[0] 和 p=a 是等價的,是以程式輸出的結果 *p 和 *q 是相等的,因為它們都指向 a[0],或者說它們都存放 a[0] 的位址。

指針與數組的差別🐷

指針 數組
儲存資料的位址,任何存入指針變量 p 的資料都會被當作位址來處理 儲存資料,數組名 a 代表的是數組首元素的首位址,&a 是整個數組的首位址
間接通路資料,首先取得指針變量 p 的内容,把它當做位址,然後從這個位址提取資料或向這個位址寫入資料。 指針可以以指針的形式通路 "(p+i)" 也可以以下标的形式通路 “p[i]”。但其本質都是先取 p 的内容後加上“isizeof(類型)”位元組作為資料的真正位址. 直接通路資料,數組名 a 是整個數組的名字,數組内每個元素并沒有名字。隻能通過"具名+匿名"的方式來通路其某個元素,不能把數組當一個整體進行讀寫操作。數組可以以指針的形式通路"(a+i)“,也可以以下标的形式通路"a[i]”。但其本質都是 a 所代表的數組首元素的首位址加上"isizeof(類型)"位元組來作為資料的真正位址
通常用于動态資料結構 通常用于存儲固定數目且資料類型相同的元素
需要 malloc 和 free 等相關的函數進行記憶體配置設定 隐式配置設定和删除
通常指向匿名資料 自身即為數組名

6.二級指針(指向指針的指針)🐒🦍

指針可以指向一份普通類型的資料,例如 int、double、char 等,也可以指向一份指針類型的資料,例如 int *、double *、char * 等。

如果一個指針指向的是另外一個指針,我們就稱它為二級指針,或者指向指針的指針。

假設有一個 int 類型的變量 a,p1是指向 a 的指針變量,p2 又是指向 p1 的指針變量,

最詳細的【指針】詳解---C語言從入門到精通
int a =100;
int *p1 = &a;
int **p2 = &p1;      

指針變量也是一種變量,也會占用存儲空間,也可以使用&擷取它的位址。C語言不限制指針的級數,每增加一級指針,在定義指針變量時就得增加一個星号*。p1 是一級指針,指向普通類型的資料,定義時有一個*;p2 是二級指針,指向一級指針 p1,定義時有兩個*。

想要擷取指針指向的資料時,一級指針加一個*,二級指針加兩個*,三級指針加三個*,以此類推,

#include <stdio.h>
int main(){
    int a =100;
    int *p1 = &a;
    int **p2 = &p1;
    int ***p3 = &p2;
    printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
    printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
    printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
    printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
    return 0;
}      
最詳細的【指針】詳解---C語言從入門到精通

以三級指針 p3 為例來分析上面的代碼。*p3等價于((p3))。p3 得到的是 p2 的值,也即 p1 的位址;(p3) 得到的是 p1 的值,也即 a 的位址;經過三次“取值”操作後,((*p3)) 得到的才是 a 的值。假設 a、p1、p2、p3 的位址分别是 0X00A0、0X1000、0X2000、0X3000,它們之間的關系可以用下圖來描述:

最詳細的【指針】詳解---C語言從入門到精通

結論:

編譯器總是要為函數的每個參數制作臨時副本,指針參數p的副本是 p,編譯器使 p = q(但是&p != &q,也就是他們并不在同一塊記憶體位址,隻是他們的内容一樣,都是a的位址)。如果函數體内的程式修改了p的内容(比如在這裡它指向b)。在本例中,p申請了新的記憶體,隻是把 p所指的記憶體位址改變了(變成了b的位址,但是q指向的記憶體位址沒有影響),是以在這裡并不影響函數外的指針q。因為傳了指針q的位址(二級指針**p)到函數,是以二級指針拷貝(拷貝的是p,一級指針中拷貝的是q是以才有問題),(拷貝了指針但是指針内容也就是指針所指向的位址是不變的)是以它還是指向一級指針q(p = q)。在這裡無論拷貝多少次,它依然指向q,那麼p = &b;自然的就是 q = &b;了。

最詳細的【指針】詳解---C語言從入門到精通

7.指針數組(數組每個元素都是指針)🐺

如果一個數組中的所有元素儲存的都是指針,那麼我們就稱它為指針數組。指針數組的定義形式一般為:

dataType *arrayName[length];

括号裡面說明arrayName是一個數組,包含了length個元素,括号外面說明每個元素的類型為dataType *。

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定義一個指針數組
    int *arr[3] = {&a, &b, &c};//也可以不指定長度,直接寫作 int *arr[]
    //定義一個指向指針數組的指針
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}      

運作結果:

16, 932, 100

16, 932, 100

arr 是一個指針數組,它包含了 3 個元素,每個元素都是一個指針,在定義 arr 的同時,我們使用變量 a、b、c 的位址對它進行了初始化,這和普通數組是多麼地類似。

parr 是指向數組 arr 的指針,确切地說是指向 arr 第 0 個元素的指針,它的定義形式應該了解為int *(parr),括号中的表示 parr 是一個指針,括号外面的int *表示 parr 指向的資料的類型。arr 第 0 個元素的類型為 int *,是以在定義 parr 時要加兩個 *。

第一個 printf() 語句中,arr[i] 表示擷取第 i 個元素的值,該元素是一個指針,還需要在前面增加一個 * 才能取得它指向的資料,也即 *arr[i] 的形式。

第二個 printf() 語句中,parr+i 表示第 i 個元素的位址,*(parr+i) 表示擷取第 i 個元素的值(該元素是一個指針),**(parr+i) 表示擷取第 i 個元素指向的資料。

繼續閱讀