一文讀懂野指針

来源:https://www.cnblogs.com/Sharemaker/archive/2022/12/05/16951429.html
-Advertisement-
Play Games

一、引子 我們都知道對指針( Pointer)的操作,實際上是對電腦記憶體地址的操作,通過訪問記憶體地址實現間接訪問該地址中保存的數據。其實就是CPU的定址方式中的間接定址。簡單概括正常使用指針時的3個步驟為: 定義指針變數 綁定指針即給指針變數賦值 解引用即間接訪問目標變數通過一個簡單的例子來看這3 ...


一、引子

        我們都知道對指針( Pointer)的操作,實際上是對電腦記憶體地址的操作,通過訪問記憶體地址實現間接訪問該地址中保存的數據。其實就是CPU的定址方式中的間接定址。簡單概括正常使用指針時的3個步驟為:

  • 定義指針變數
  • 綁定指針即給指針變數賦值
  • 解引用即間接訪問目標變數
    通過一個簡單的例子來看這3個步驟的實現:
1 int a = 5;
2 //定義指針變數p
3 int *p;
4 //綁定指針,就是給指針變數賦值,指向另一個變數a(指針的用途就是指向別的變數)
5 p = &a;
6 //將6放入p所指向的那個變數的空間中,這裡就是a的空間
7 *p = 6;

        可以看出,在定義指針變數p時,未初始化p,這個時候的p為隨機值,此時解引用p是沒有意義的,記憶體隨機值的空間是否有效我們也不得而知。

        綁定指針就是將變數a的地址賦值給指針變數p,此時p就有了意義,明確了記憶體中訪問的具體空間位置,p是指向變數a的空間的,變數a是有具體內容的,因此指針必須給它賦值才能解引用它。

        給指針變數p賦值實際上是在變數a前加一個“&”符號,這個符號是取地址符,&a就是指變數a的地址,編譯器在給每個變數分配出記憶體空間,並將a與這塊的記憶體空間地址綁定。這個地址只有編譯器知道,而程式員並不知道編譯器隨機給這段空間分配什麼隨機地址值。程式員要獲取或操作這個地址時,就需要使用取地址符。

        由上述分析看來,給p賦予了變數a地址的值是一個合法的,在記憶體中明確的地址值,這個值是受控的,同時通過訪問指針間接訪問該地址中保存的數據也是受控的,p就是一個正常的指針。

        相反,如果指針指向了記憶體中不可用的區域,或者是指針的值是非法的隨機值也就是非正常記憶體地址,那麼這個指針就是不受控的,同時通過訪問指針間接訪問該地址中保存的數據也是不受控的,同時是不可知的,此時這個指針就是野指針(Wild Pointer)

二、需要明確的一點

        野指針不同於空指針,所謂空指針,是給指針變數賦NULL值,即:

1 int *p = NULL;

        所謂NULL值在C/C++中定義為:

1 #ifdef __cplusplus         // 定義這個符號表示當前是C++環境中
2 #define NULL 0             // 在C++中NULL為0
3 #else
4 #define NULL (void *) 0    // 在C中的NULL是強制類型轉換為void *的0
5 #endif

        可以看出,給p賦值NULL值也就是讓p指向空地址。在不同的系統中,NULL並不意味等於0,也有系統會使用地址0,而將NULL定義為其他值,所以不要把NULL和0等同起來。你可以將NULL通俗理解為是空值,也就是指向一個不被使用的地址,在大多數系統中,都將0作為不被使用的地址,因此就有了這樣的定義,C或者C++編譯器保證這個空值不會是任何對象的地址。

        void *表示的是“無類型指針”,可以指向任何數據類型,在這裡void指針與空指針NULL區別:NULL說明指針不指向任何數據,是“空的”;而void指針實實在在地指向一塊記憶體,只是不知道這塊記憶體中是什麼類型的數據。

        空指針的值是受控的,但並不是有意義的,我們是將指針指向了0地址,這個0地址就是作為記憶體中的一個特殊地址,因此空指針是一個對任何指針類型都合法的指針,但並不是合理的指針,指針變數具有空指針值,表示它處於閑置狀態,沒有指向任何有意義的內容。我們需要在讓空指針真正指向了一塊有意義的記憶體後,我們才能對它取內容。即:

1 int a = 5;
2 int *p = NULL;
3 p = &a;

         NULL指針並沒有危害,可以使用if語句來判斷是否為NULL。

三、一些典型的error

        我們要知道單純的從語言層面無法判斷一個指針所保存的地址是否是合法的,等到程式運行起來,配合硬體的記憶體實際地址,才能發現指針指向的地址是否是你想要讓它指向的合理空間地址。在日常編碼過程中有一些導致野指針或者記憶體溢出的錯誤編碼方式:

1、指針變數未初始化

        任何指針在被創建的時候,不會自動變成NULL指針,因此指針的值是一個隨機值。這時候去解引用就是去訪問這個地址不確定的變數,所以結果是不可知的。

1 void main()
2 {
3     char* p;
4     *p = 6;  //錯誤
5 }

2、使用了懸垂指針

        在C或者C++中使用malloc或者new申請記憶體使用後,指針已經free或者delete了,沒有置為NULL,此時的指針是一個懸垂指針。

        free和delete只是把指針所指的記憶體給釋放掉,並不會改變相關的指針的值。這個指針實際仍然指向記憶體中相同位置即其地址仍然不變,甚至該位置仍然可以被讀寫,只不過這時候該記憶體區域完全不可控即該地址對應的記憶體是垃圾,懸垂指針會讓人誤以為是個合法的指針。

1 void main()
2 {
3     char* p = (char *) malloc(10);
4     strcpy(p, “abc”);
5     free(p);  //p所指的記憶體被釋放,但是p所指的地址仍然不變
6     strcpy(p, “def”); // 錯誤
7 }

3、返回棧記憶體指針或引用

        在函數內部定義的局部指針變數或者局部引用變數不能作為函數的返回值,因為該局部變數的作用域範圍在函數內部,該函數在被調用時,由於局部指針變數或者引用已經被銷毀,因此調用時該記憶體區域的內容已經發生了變化,再操作該記憶體區域就沒有具體的意義。

 1 char* fun1()
 2 {
 3    char* p = "hello";
 4    return p;
 5 }
 6 
 7 char* fun2()
 8 {
 9     char a = 6;
10     return &a;
11 }
12 
13 void main()
14 {
15     char* p1 = fun1(); //錯誤
16     char* p2 = fun2(); //錯誤
17 }

4、指針重覆釋放

 1 void fun(char* p, char len)
 2 {     
 3     for(char i = 0; i < len; i++)
 4     {
 5         p[i] = i;
 6     }      
 7     free(p);
 8 }
 9 
10 void main()
11 {
12     char * p1 = (char *)malloc(6 * sizeof(char)); 
13     fun(p1, 6); 
14     free(p1);  //重覆釋放指針導致錯誤  
15 }

5、數組越界

        使用的數組長度超過了定義的數組長度。

1 void main()
2 {
3     int a[6]; 
4     for(int i = 0; i<=6; i++) //錯誤
5 }

6、記憶體分配後未初始化

1 void main()
2 {
3     char* p = (char*)malloc(6); 
4     printf(p); //p未初始化
5     free(p);
6 }

7、使用的記憶體大小超過了分配的記憶體大小

1 void main()
2 {
3     char* p = (char*)malloc(6); 
4     for(int i = 0; i <= 6; i++)  //錯誤
5     {
6         p[i] = i;
7     }
8     free(p);
9 }

四、避免錯誤的註意點

        1、在定義指針變數時,要將其值置為NULL,即 char *p = NULL。

        2、在指針使用之前,需要給指針賦具體值,就是將其綁定一個可用地址空間讓其有意義,即p = &a。

        3、在使用指針前,需要判斷指針為非NULL,只有非NULL的指針才有意義。即判斷if(p != NULL)。

        4、free或者delete指針後,需要將指針值置為NULL。

        5、malloc和free,new和delete註意配對使用,當 malloc或new次數大於 free或delete 時,會產生記憶體泄漏;需要‍防止多次重覆free或者delete,當malloc或new 次數小於free或delete時,程式有可能會崩潰。

        6、使用malloc或new分配記憶體後,需要初始化,同時在使用時註意不要超過分配的記憶體大小空間。

        7、在哪個函數裡面進行的 malloc或new ,就在哪個函數裡面 free或delete,不要跨函數去釋放動態的記憶體空間。

        8、不要將局部指針變數,局部引用變數或局部數組作為函數的返回值。

        9、使用數組時一定要註意定義的數組大小,防止數組越界;或者在定義數組時可以不定義數組長度,即int a[]。

        10、在定義有指針操作相關的函數時必須指定長度信息,即void fun(char* p, char len)。

BTW:

        最後根據以上的討論,再結合以下網友的總結,我們可以更好的理解下野指針在實際程式中的危害:

        a、指向不可訪問(操作系統不允許訪問的敏感地址,譬如內核空間)的地址,結果是觸發段錯誤,這種算是最好的情況了。

        b、指向一個可用的、而且沒什麼特別意義的空間(譬如我們曾經使用過但是已經不用的棧空間或堆空間),這時候程式運行不會出錯,也不會對當前程式造成損害,這種情況下會掩蓋你的程式錯誤,讓你以為程式沒問題,其實是有問題的。

   c、指向了一個可用的空間,而且這個空間其實在程式中正在被使用(譬如說是程式的一個變數x),那麼野指針的解引用就會剛好修改這個變數x的值,導致這個變數莫名其妙的被改變,程式出現離奇的錯誤。一般最終都會導致程式崩潰,或者數據被損害。這種危害是最大的。


更多技術內容和書籍資料獲取敬請關註微信公眾號“明解嵌入式”

本文來自博客園,作者:Sharemaker,轉載請註明原文鏈接:https://www.cnblogs.com/Sharemaker/p/16951429.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 普通的切片對迭代器無法實行切片操作 1 from itertools import islice 2 3 4 def func(): 5 for i in [4, 9, 6, 2]: 6 if i % 2 == 0: 7 yield i 8 9 10 f = func() 11 res = isli ...
  • noi 1.5 39 與 7 無 關 的 數 。 1.描述 一個正整數,如果它能被7整除,或者它的十進位表示法中某一位上的數字為7,則稱其為與7相關的數.現求所有小於等於n(n < 100)的與7無關的正整數的平方和. 2.輸入 輸入為一行,正整數n(n < 100) 3.輸出 輸出一行,包含一個整 ...
  • 說明: 1. 本文基於Spring-Framework 5.1.x版本講解 2. 建議讀者對創建對象部分源碼有一定瞭解 概述 這篇講講Spring迴圈依賴的問題,網上講迴圈依賴的帖子太多太多了,相信很多人也多多少少瞭解一點,那我還是把這個問題自己梳理一遍,主要是基於以下出發點: 1. Spring到 ...
  • JZ31 棧的壓入、彈出序列 描述 輸入兩個整數序列,第一個序列表示棧的壓入順序,請判斷第二個序列是否可能為該棧的彈出順序。假設壓入棧的所有數字均不相等。例如序列1,2,3,4,5是某棧的壓入順序,序列4,5,3,2,1是該壓棧序列對應的一個彈出序列,但4,3,5,1,2就不可能是該壓棧序列的彈出序 ...
  • Hello,大家好,又是好久不見,最近太忙了(藉口)。看了下日誌,有 2 個月沒寫文章了。為了證明公眾號還活著,今天必須更新一下了。 在我們的開發過程中,總有那麼些需求是那麼的變態。常規的方案已經無法滿足。比如某些規則非常複雜,而客戶又經常要修改它。那麼我們可能需要把這部分代碼直接做為配置文件提取出 ...
  • 從技術生態發展過程及理念、產品級解決方案理念、產品系統框架及主要功能介紹、產品系統二次開發和應用案例等5個方面進行了主題發言。 ...
  • 一:背景 1.講故事 這周有個朋友找到我,說他的程式出現了記憶體緩慢增長,沒有回頭的趨勢,讓我幫忙看下到底怎麼回事,據朋友說這個問題已經困擾他快一周了,還是沒能找到最終的問題,看樣子這個問題比較刁鑽,不管怎麼說,先祭出 WinDbg。 二:WinDbg 分析 1. 托管還是非托管泄露 一直關註這個系列 ...
  • 前言 本文藉鑒文章:https://www.yuque.com/dengfenglai-esbap/kb/mc4k41?#xOxNG 在此基礎上修改了一點(照著原來的做沒成功),感謝這位師傅給的資源。 1、環境準備 1、主機:伺服器CentOs7 2、Docker版本:20.10.2 3、Docke ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...