系統性學習C語言-第十二講-深入理解指針(2)

系統性學習C語言-第十二講-深入理解指針(2)

  • 1. ` const ` 修飾指針
    • 1.1 ` const ` 修飾變量
    • 1.2 ` const ` 修飾指針變量
  • 2. 野指針
    • 2.1 野指針成因
    • 2.2 如何規避野指針
      • 2.2.1 指針初始化
      • 2.2.2 小心指針越界
      • 2.2.3 指針變量不再使用時,及時置 ` NULL ` ,指針使用之前檢查有效性
      • 2.2.4 避免返回局部變量的地址
  • 3. assert 斷言
  • 4. 指針的使用和傳址調用
    • 4.2 傳值調用和傳址調用

1. const 修飾指針

1.1 const 修飾變量

變量是可以修改的,如果把變量的地址交給?個指針變量,通過指針變量的也可以修改這個變量。

但是如果我們希望?個變量加上?些限制,不能被修改,怎么做呢?這就是 const 的作用。

#include <stdio.h>
int main()
{int m = 0;m = 20;//m是可以修改的const int n = 0;n = 20;//n是不能被修改的return 0;
}

上述代碼中 n 是不能被修改的,其實 n 本質是變量,只不過被 const 修飾后,在語法上加了限制,

只要我們在代碼中對 n 進行修改,就不符合語法規則,就報錯,致使沒法直接修改 n

在這里插入圖片描述

#include <stdio.h>
int main()
{const int n = 0;printf("n = %d\n", n);int* p = &n;*p = 20;printf("n = %d\n", n);return 0;
}

輸出結果:
在這里插入圖片描述
我們可以看到變量確實被修改了,但是我們還是要思考?下,為什么 n 要被 const 修飾呢?

就是為了不能被修改,如果 p 拿到 n 的地址就能修改 n ,這樣就打破了 const 的限制,

這是不合理的,所以應該讓 p 拿到 n 的地址也不能修改 n ,那接下來怎么做呢?

1.2 const 修飾指針變量

?般來講 const 修飾指針變量,可以放在 * 的左邊,也可以放在 * 的右邊,意義是不?樣的。

int * p;//沒有const修飾?
int const * p;//const 放在*的左邊做修飾
int * const p;//const 放在*的右邊做修飾

我們看下面代碼,來分析具體分析?下:

代碼1 - 測試無 const 修飾的情況

#include <stdio.h>
//代碼1 - 測試?const修飾的情況
void test1()
{int n = 10;int m = 20;int* p = &n;	*p = 20;//ok?p = &m; //ok?
}

在這里插入圖片描述
通過觀察可以看到編譯是可以通過的,說明代碼的操作是沒有問題的。

代碼2 - 測試 const 放在 * 的左邊情況

//代碼2 - 測試const放在*的左邊情況
void test2()
{int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?
}

在這里插入圖片描述
通過編譯結果我們可以得出,當 const 被放在 * 左邊時,我們無法對地址解引用進行更改,

編譯器會產生報錯。

代碼3 - 測試 const 放在 * 的右邊情況

//代碼3 - 測試const放在*的右邊情況
void test3()
{int n = 10;int m = 20;int * const p = &n;*p = 20; //ok?p = &m; //ok?
}

在這里插入圖片描述
通過編譯結果我們可以分析出,在 const 放在 * 的右邊,我們可以通過地址的解引用來改變變量,

但是我們不能對地址進行更改。

代碼4 - 測試 * 的左右兩邊都有 const 的情況

//代碼4 - 測試*的左右兩邊都有const
void test4()
{int n = 10;int m = 20;int const * const p = &n;*p = 20; //ok?p = &m; //ok?
}

在這里插入圖片描述
通過編譯的結果我們可以分析出,在 * 的左右兩邊都有 const 的情況下,

我們即無法對地址解引用來更改變量,也無法對地址的值進行改變。

結論:const修飾指針變量的時候

  • const 如果放在 * 的左邊,修飾的是指針指向的內容,保證指針指向的內容不能通過指針來改變。
    但是指針變量本身的內容可變。

  • const 如果放在 * 的右邊,修飾的是指針變量本身,保證了指針變量的內容不能修改,但是指針指
    向的內容,可以通過指針改變。

2. 野指針

概念: 野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)

2.1 野指針成因

1. 指針未初始化

#include <stdio.h>
int main()
{int *p;//局部變量指針未初始化,默認為隨機值*p = 20;return 0;
}

在如圖所示的代碼中,指針 p 并未進行初始化,它的地址所指向的空間是未知的,為野指針。

2. 指針越界訪問

#include <stdio.h>
int main()
{int arr[10] = {0};int *p = &arr[0];int i = 0;for(i = 0; i <= 11; i++){//當指針指向的范圍超出數組arr的范圍時,p就是野指針*(p++) = i;}return 0;
}

在如圖所示的代碼中,指針 p 指向的范圍超出了數組 arr 的范圍,它的地址所指向的空間是未知的,為野指針。

3. 指針指向的空間釋放

#include <stdio.h>
int* test()
{int n = 100;return &n;
}int main()
{int*p = test();printf("%d\n", *p);return 0;
}

在如圖的代碼中,函數 test 中定義的變量 n 為局部變量,在函數 test 結束后就會銷毀,但函數 test 返回的是變量 n 的地址,

在變量被銷毀后,這片地址的區域就是未知的,不再有意義,函數 test 返回的無意義的地址存儲在了變量 p 中,

此時變量 p 變為了野指針。

2.2 如何規避野指針

2.2.1 指針初始化

如果明確知道指針指向哪里就直接賦值地址,如果不知道指針應該指向哪里,可以給指針賦值 NULL

NULL 是 C語言 中定義的?個標識符常量,值是 0 ,0 也是地址,這個地址是無法使用的,讀寫該地址會報錯。

定義 NULL 中的文件源碼

#ifdef __cplusplus#define NULL 0
#else#define NULL ((void *)0)
#endif

初始化如下:

#include <stdio.h>int main()
{int num = 10;int*p1 = &num;int*p2 = NULL;return 0;
}

2.2.2 小心指針越界

?個程序向內存申請了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就是越界訪問。

例如:

#include<stdio.h>
int main()
{int arr[10] = { 0 };int* p = &arr[12];
}

這里我們只給數組 arr 申請了十個空間用于存儲變量,即數組 arr 的最大下標為 9 ,但時我們的 p 指針卻超出范圍,

存儲著下標 12 處的地址,這是代碼發生了指針越界,產生了野指針, p 指針現在所指向的空間時未知的。

2.2.3 指針變量不再使用時,及時置 NULL ,指針使用之前檢查有效性

當指針變量指向一塊區域的時候,我們可以通過指針訪問該區域,后期不再使用這個指針訪問空間的時候,

我們可以把該指針置為 NULL 。因為約定俗成的?個規則就是:只要是 NULL 指針就不去訪問,

同時使用指針之前可以判斷指針是否為 NULL

我們可以把野指針想象成野狗,野狗放任不管是非常危險的,所以我們可以找一顆樹把野狗拴起來,就相對安全了,

給指針變量及時賦值為 NULL ,其實就類似把野狗栓起來,就是把野指針暫時管理起來。

不過野狗即使拴起來我們也要繞著走,不能去挑逗野狗,有點危險;對于指針也是,在使用之前,我們也要判斷是否為 NULL

看看是不是被拴起來起來的野狗,如果不是我們再去使用。

int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int i = 0;for(i=0; i<10; i++){*(p++) = i;}//此時p已經越界了,可以把p置為NULLp = NULL;//下次使?的時候,判斷p不為NULL的時候再使?//...p = &arr[0];//重新讓p獲得地址if(p != NULL) //判斷{//...}return 0;}

2.2.4 避免返回局部變量的地址

如造成野指針的第 3 個例子,不要返回局部變量的地址。

3. assert 斷言

assert.h 頭文件定義了宏 assert() ,用于在運行時確保程序符合指定條件,如果不符合,就報錯終止運行。這個宏常常被稱為“斷?”。

assert(p != NULL);

上面代碼在程序運行到這一行語句時,驗證變量 p 是否等于 NULL 。如果確實不等于 NULL ,程序繼續運行,否則就會終止運?,

并且給出報錯信息提示。

assert() 宏接受?個表達式作為參數。如果該表達式為真(返回值非零),assert() 不會產?任何作用,程序繼續運行。

如果該表達式為假(返回值為零), assert() 就會報錯,在標準錯誤流 stderr 中寫入?條錯誤信息,顯示沒有通過的表達式,

以及包含這個表達式的文件名和行號。

assert() 的使用程序員是非常友好的,使用 assert() 有幾個好處:它不僅能自動標識文件和出問題的行號,

還有?種無需更改代碼就能開啟或關閉 assert() 的機制。如果已經確認程序沒有問題,不需要再做斷言,

就在 #include <assert.h> 語句的前?,定義?個宏 NDEBUG

#define NDEBUG
#include <assert.h>

然后,重新編譯程序,編譯器就會禁用文件中所有的 assert() 語句。

如果程序又出現問題,可以移除這條 #define NDEBUG 指令(或者把它注釋掉),再次編譯,這樣就重新啟用了 assert() 語句。

assert() 的缺點是,因為引入了額外的檢查,增加了程序的運行時間。

?般我們可以在 Debug 中使用,在 Release 版本中選擇禁用 assert 就?,在 VS 這樣的集成開發環境中,在 Release 版本中,

直接就是優化掉了。這樣在 debug 版本寫有利于程序員排查問題,在 Release 版本不影響用戶使用時程序的效率。

4. 指針的使用和傳址調用

庫函數 strlen 的功能是求字符串長度,統計的是字符串中 \0 之前的字符的個數。

函數原型如下:

size_t strlen ( const char * str );

參數 str 接收?個字符串的起始地址,然后開始統計字符串中 \0 之前的字符個數,最終返回長度。

如果要模擬實現只要從起始地址開始向后逐個字符的遍歷,只要不是 \0 字符,計數器就 + 1 ,這樣直到 \0 就停止。

參考代碼如下:

int my_strlen(const char * str)
{int count = 0;assert(str);  //防止傳入的指針為空while(*str)   //當 *str 不為 /0 進入循環{count++;  //計數器 + 1str++;    //將字符變更為下一個字符}return count; //返回計數器的數值
}int main()
{int len = my_strlen("abcdef");printf("%d\n", len);return 0;
}

4.2 傳值調用和傳址調用

學習指針的目的是使用指針解決問題,那什么問題,非指針不可呢?

例如:寫?個函數,交換兩個整型變量的值

?番思考后,我們可能寫出這樣的代碼:

#include <stdio.h>void Swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交換前:a=%d b=%d\n", a, b);Swap1(a, b);printf("交換后:a=%d b=%d\n", a, b);return 0;
}

當我們運行代碼,結果如下:

在這里插入圖片描述
我們發現其實沒產生交換的效果,這是為什么呢?

嘗試調試,解決問題。

在這里插入圖片描述
我們發現在 main 函數內部,創建了 aba 的地址是 0x00cffdd0b 的地址是 0x00cffdc4

在調? Swap1 函數時,將 ab 傳遞給了Swap1函數,在 Swap1 函數內部創建了形參 xy 接收 ab 的值,

但是 x 的地址是 0x00cffcecy 的地址是 0x00cffcf0xy 確實接收到了 ab 的值,

不過 x 的地址和 a 的地址不?樣,y 的地址和 b 的地址不?樣,相當于 xy 是獨?的空間,

那在 Swap1 函數內部交換 xy 的值,自然不會影響 ab ,當 Swap1 函數調?結束后回到 main 函數,

ab 的沒法交換。

Swap1 函數在使用的時候,是把變量本身直接傳遞給了函數,這種調用函數的方式我們之前在函數的時候就知道了,這種叫傳值調用。

結論:實參傳遞給形參的時候,形參會單獨創建?份臨時空間來接收實參,對形參的修改不影響實參。

所以 Swap1 是失敗的了。

那怎么辦呢?

現在要解決的就是當調用 Swap 函數的時候,Swap 函數內部操作的就是 main 函數中的 ab ,直接將 ab的值交換了。

那么就可以使用指針了,在 main 函數中將 ab 的地址傳遞給 Swap 函數,

Swap 函數里邊通過地址間接的操作 main 函數中的a和b,并達到交換的效果就好了。

#include <stdio.h>void Swap2(int*px, int*py)
{int tmp = 0;tmp = *px;*px = *py;*py = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交換前:a=%d b=%d\n", a, b);Swap2(&a, &b);printf("交換后:a=%d b=%d\n", a, b);return 0;
}

將程序改進后,我們直接將變量 ab 的地址傳入進函數中,這次通過地址對變量進行更改,就不會再出現錯誤,

通過地址操作的空間與原變量是綁定的,不再是原變量的拷貝,在改變地址所指向的變量時,我們成功對原變量進行了更改。

看輸出結果:

在這里插入圖片描述
我們可以看到實現成 Swap2 的方式,順利完成了任務,這里調用 Swap2 函數的時候是將變量的地址傳遞給了函數,

這種函數調用方式叫:傳址調用。

傳址調用,可以讓函數和主調函數之間建立真正的聯系,在函數內部可以修改主調函數中的變量;

所以未來函數中只是需要主調函數中的變量值來實現計算,就可以采用傳值調用。

如果函數內部要修改主調函數中的變量的值,就需要傳址調用。

到此,第十二講 - 深入理解指針(2)部分的內容到此結束
如對文章有更好的意見與建議,一定要告知作者,讀者的反饋對于我十分重要,希望讀者們勤勉勵學,精益求精,
我們下篇文章再見👋。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/82009.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/82009.shtml
英文地址,請注明出處:http://en.pswp.cn/web/82009.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

《高等數學》(同濟大學·第7版) 第一節《映射與函數》超詳細解析

集合&#xff08;Set&#xff09;—— 最基礎的數學容器 定義&#xff1a; 集合是由確定的、互不相同的對象&#xff08;稱為元素&#xff09;組成的整體。 表示方法&#xff1a; 列舉法&#xff1a;A {1, 2, 3} 描述法&#xff1a;B {x | x > 0}&#xff08;表示所有大于…

Spring Boot整活指南:從Helo World到“真香”定律

&#x1f4cc; 一、Spring Boot的"真香"本質&#xff08;不是996的福報&#xff09; 你以為Spring Boot只是個簡化配置的工具&#xff1f;Too young&#xff01;它其實是程序員的??摸魚加速器??。 ??經典場景還原??&#xff1a; 產品經理&#xff1a;“這個…

打字練習:平臺推薦

1.打字練習 . 1&#xff09;平臺推薦 下面推薦兩個打字練習平臺 Keybr&#xff1a;https://www.keybr.com/ TypingClub&#xff1a;https://www.edclub.com/sportal/ . 2&#xff09;平臺對比 特性KeybrTypingClub核心優勢AI智能弱項訓練結構化課程體系適合人群開發者/…

ASP.NET Core 中JWT的基本使用

文章目錄 前言一、JWT與RBAC二、JWT 的作用三、RBAC 的核心思想四、使用1、配置文件 (appsettings.json)2、JWT配置模型 (Entity/JwtSettings.cs)3、服務擴展類&#xff0c;JWT配置 (Extensions/ServiceExtensions.cs)4、用戶倉庫接口服務5、認證服務 (Interface/IAuthService.…

(19)java在區塊鏈中的應用

&#x1f517; Java在區塊鏈中的應用&#xff1a;智能合約開發全攻略 TL;DR: Java在區塊鏈領域主要通過Hyperledger Fabric、Web3j和專用JVM實現智能合約開發&#xff0c;相比Solidity具有更強的企業級支持和開發效率&#xff0c;但在執行效率和Gas消耗方面存在差異&#xff0c…

深入理解設計模式之訪問者模式

深入理解設計模式之訪問者模式&#xff08;Visitor Pattern&#xff09; 一、什么是訪問者模式&#xff1f; 訪問者模式&#xff08;Visitor Pattern&#xff09;是一種行為型設計模式。它的主要作用是將數據結構與數據操作分離&#xff0c;使得在不改變數據結構的前提下&…

div或button一些好看實用的 CSS 樣式示例

1&#xff1a;現代漸變按鈕 .count {width: 800px;background: linear-gradient(135deg, #72EDF2 0%, #5151E5 100%);padding: 12px 24px;border-radius: 10px;box-shadow: 0 4px 15px rgba(81, 81, 229, 0.3);color: white;font-weight: bold;border: none;cursor: pointer;t…

【基于STM32的新能源汽車智能循跡系統開發全解析】

基于STM32的新能源汽車智能循跡系統開發全解析&#xff08;附完整工程代碼&#xff09; 作者聲明 作者&#xff1a; 某新能源車企資深嵌入式工程師&#xff08;專家認證&#xff09; 技術方向&#xff1a; 智能駕駛底層控制 | 車規級嵌入式開發 原創聲明&#xff1a; 本文已申…

HTML Day02

Day02 0. 引言1. 文本格式化1.1 HTML文本格式化標簽1.2 HTML"計算機輸出"標簽1.3 HTML 引文&#xff0c;引用及標簽定義 2. HTML鏈接2.1鏈接跳轉原理&#xff08;有點亂可跳過&#xff09;2.2 HTML超鏈接2.3 target屬性2.4 id屬性2.4.1 id屬性在頁面內和不同頁面的定…

MIT 6.S081 2020 Lab6 Copy-on-Write Fork for xv6 個人全流程

文章目錄 零、寫在前面一、Implement copy-on write1.1 說明1.2 實現1.2.1 延遲復制與釋放1.2.2 寫時復制 零、寫在前面 可以閱讀下 《xv6 book》 的第五章中斷和設備驅動。 問題 在 xv6 中&#xff0c;fork() 系統調用會將父進程的整個用戶空間內存復制到子進程中。**如果父…

xhr、fetch和axios

XMLHttpRequest (XHR) XMLHttpRequest 是最早用于在瀏覽器中進行異步網絡請求的 API。它允許網頁在不刷新整個頁面的情況下與服務器交換數據。 // 創建 XHR 對象 const xhr new XMLHttpRequest();// 初始化請求 xhr.open(GET, https://api.example.com/data, true);// 設置請…

電腦驅動程序更新工具, 3DP Chip 中文綠色版,一鍵更新驅動!

介紹 3DP Chip 是一款免費的驅動程序更新工具&#xff0c;可以幫助用戶快速、方便地識別和更新計算機硬件驅動程序。 驅動程序更新工具下載 https://pan.quark.cn/s/98895d47f57c 軟件截圖 軟件特點 簡單易用&#xff1a;用戶界面簡潔明了&#xff0c;操作方便&#xff0c;…

機器學習與深度學習06-決策樹02

目錄 前文回顧5.決策樹中的熵和信息增益6.什么是基尼不純度7.決策樹與回歸問題8.隨機森林是什么 前文回顧 上一篇文章地址&#xff1a;鏈接 5.決策樹中的熵和信息增益 熵和信息增益是在決策樹中用于特征選擇的重要概念&#xff0c;它們幫助選擇最佳特征進行劃分。 熵&#…

【Kotlin】數字字符串數組集合

【Kotlin】簡介&變量&類&接口 【Kotlin】數字&字符串&數組&集合 文章目錄 Kotlin_數字&字符串&數組&集合數字字面常量顯式轉換數值類型轉換背后發生了什么 運算字符串字符串模板字符串判等修飾符數組集合通過序列提高效率惰性求值序列的操…

oscp練習PG Monster靶機復現

端口掃描 nmap -A -p- -T4 -Pn 192.168.134.180 PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.41 ((Win64) OpenSSL/1.1.1c PHP/7.3.10) |_http-server-header: Apache/2.4.41 (Win64) OpenSSL/1.1.1c PHP/7.3.10 | http-methods:…

近期知識庫開發過程中遇到的一些問題

我們正在使用Rust開發一個知識庫系統&#xff0c;遇到了一些問題&#xff0c;在此記錄備忘。 錯誤&#xff1a;Unable to make method calls because underlying connection is closed 場景&#xff1a;在docker中調用headless_chrome時出錯 原因&#xff1a;為減小鏡像大小&am…

Ubuntu 22.04 系統下 Docker 安裝與配置全指南

Ubuntu 22.04 系統下 Docker 安裝與配置全指南 一、前言 Docker 作為現代開發中不可或缺的容器化工具&#xff0c;能極大提升應用部署和環境管理的效率。本文將詳細介紹在 Ubuntu 22.04 系統上安裝與配置 Docker 的完整流程&#xff0c;包括環境準備、安裝步驟、權限配置及鏡…

C#獲取磁盤容量:代碼實現與應用場景解析

C#獲取磁盤容量&#xff1a;代碼實現與應用場景解析 在軟件開發過程中&#xff0c;尤其是涉及文件存儲、數據備份等功能時&#xff0c;獲取磁盤容量信息是常見的需求。通過獲取磁盤的可用空間和總大小&#xff0c;程序可以更好地進行資源管理、預警提示等操作。在 C# 語言中&a…

2025年- H56-Lc164--200.島嶼數量(圖論,深搜)--Java版

1.題目描述 2.思路 &#xff08;1&#xff09;主函數&#xff0c;存儲圖結構 &#xff08;2&#xff09;主函數&#xff0c;visit數組表示已訪問過的元素 &#xff08;3&#xff09;輔助函數&#xff0c;用遞歸&#xff08;深搜&#xff09;&#xff0c;遍歷以已訪問過的元素&…

詳細到用手撕transformer下半部分

之前我們討論了如何實現 Transformer 的核心多頭注意力機制&#xff0c;那么這期我們來完整地實現整個 Transformer 的編碼器和解碼器。 Transformer 架構最初由 Vaswani 等人在 2017 年的論文《Attention Is All You Need》中提出&#xff0c;專為序列到序列&#xff08;seq2s…