C語言的靈魂——指針(2)

前言:上期我們介紹了如何理解地址,內存,以及指針的一些基礎知識和運算;這期我們來介紹一下const修飾指針,野指針,assert斷言,指針的傳址調用。
上一篇指針(1)

文章目錄

  • 一,const修飾指針
    • 1,const修飾變量
    • 2,const修飾指針變量
      • 第一種const在*左邊
      • 第二種const在*右邊
      • 第三種const在*左和右邊
  • 二,野指針
    • 1,野指針的成因
    • 2,如何規避野指針
  • 三,assert斷言
  • 四,指針的使用和傳址調用
    • 1,傳值調用
    • 2,傳址調用
  • 五,strlen的模擬實現

一,const修飾指針

1,const修飾變量

我們先從const修飾變量說起,在學函數的時候我們知道被const修飾后的變量在C語言中變成了常變量,它是一個變量但具有常量屬性(不變的屬性)是不能被修改的。

#include<stdio.h>
int main()
{int n=0;n=100;//沒有加上const之前n可以修改const int m=0;m=100;//加上了const之后m就不能被修改了return 0;
}

在這里插入圖片描述
我們可以看到被const修飾后的m修改時會報錯,那 我們怎么驗證它是一個變量呢?還記得在數組篇說過:數組定義變量的大小時必須是一個常量而不能是一個變量嗎?這樣我們就可以用數組來檢驗,如果數組報錯說明是變量,不報錯說明是常量。

#include<stdio.h>
int main()
{const int n=10;int arr[n]={0};return 0;
}

在這里插入圖片描述
我們們看到編譯器報錯就說明被const修飾的n就是一個變量。那么既然const修飾變量會導致變量不能被修改,那么const修飾指針會有什么樣的結果呢?接下來就來看看const修飾指針會有什么樣的效果。

延用上面的例子分析一下為什么m不能被修改?假如想修改該怎么做呢?

#include<stdio.h>
int main()
{int n=0;n=100;//沒有加上const之前n可以修改const int m=0;m=100;//加上了const之后m就不能被修改了return 0;
}

上述代碼中m是不能被修改的,其實m本質是變量,只不過被const修飾后,在語法上加了限制,只要我們在代碼中對m就?修改,就不符合語法規則,就報錯致使沒法直接修改n。

我們說指針的好處就是可以間接訪問內存,但如果我們繞過m取得m的地址然后再去修改m就可以做到了,雖然這是在打破語法規則但確實能夠達到我們的目的。

#include<stdio.h>
int main()
{const int m=0;m=100;//加上了const之后m就不能被修改了int *p=&m;*p=100;printf("%d\n",*p);return 0;
}

在這里插入圖片描述
顯然m被修改了,但是思考一下我們加const的用意是什么?我們其實是要固定m的值使他不能被修改,但是我們卻有方法讓他修改,這就打破了const的限制所以這與我們的初衷是違背的。這是一個漏洞要避免這個漏洞要怎么做呢?
答案是用const來修飾指針,使指針變量p拿不到m的地址從而無法間接修改m的值。

2,const修飾指針變量

首先const修飾指針變量有三種情況:

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

第一種const在*左邊

我們先來看一段代碼:

#include<stdio.h>
int main()
{int n=0;int *p1=&n;*p1=100;printf("%d\n",*p1);//沒加const 打印的結果是100int m=0;const int *p2=&m;*p2=100printf("%d\n",*p2);//加了const *p2這行代碼報錯return 0;
}

在這里插入圖片描述

由此我們可以知道在 *號 左邊加上const是限制解引用這個操作,即限制修改指針變量p所指向的變量的內容(即 *p ),但能否修改指針變量本身( p )來改變p所儲存的地址從而再解引用改變所想要改變的值呢?我們不妨寫個代碼來驗證一下:

#include<stdio.h>
int main()
{int n = 0;const int* p = &n;int m = 100;//創建第三變量int* x = &n;//要想改變n的值只能再重新創建一個指針變量p = &m;//*p = 100;//指針變量p在*左邊加了const 所以解引用已經被禁用*x = 10;printf("*p=m=%d\n", *p);printf("*x=n=%d\n", *x);printf("n=%d\n", n);return 0;
}

在這里插入圖片描述
從運行結果我們可以得出3個結論: 1,要想改變*p 必須借助第三變量m才能改變,但是是不會改變n的值的!2,指針變量p被const修飾后要想改變n的值只能通過再創建一個指針變量來改變!3,指針變量被const修飾后,*p 是無論如何都不能使用了,即使是改變了p也依然不能解引用!

第二種const在*右邊

來看一段代碼
還是上面的代碼我們改一下

#include<stdio.h>
int main()
{int n = 0;int* const p = &n;//在*右邊加const//p = &m;//指針變量p在*右邊加了const p所存的地址就已經固定不能更改了*p = 100;printf("*p=%d\n", *p);printf("n=%d\n", n);return 0;
}

在這里插入圖片描述

從運行結果上來看我們可以知道在*右邊加上const 是限制了指針變量p,*p 是可以使用的。上面的代碼中由于 *p 可以使用所以改變*p 就相當于改變了n。

第三種const在*左和右邊

顯然我們知道const放在*的左右兩邊會導致*p 和p都無法使用,下面來看代碼:

#include<stdio.h>
int main() 
{ int n = 10; int m = 20; int const * const p = &n; *p = 20; //ok? nop = &m; //ok? no
}

將代碼復制到vs里就會發現報錯,結果當然與我們猜想的一樣。下面給出一個圖來進行總結:
在這里插入圖片描述

結論:const修飾指針變量的時候
? const如果放在的左邊,修飾的是指針指向的內容,保證指針指向的內容不能通過指針來改變。 但是指針變量本?的內容可變。
? const如果放在
的右邊,修飾的是指針變量本?,保證了指針變量的內容不能修改,但是指針指 向的內容,可以通過指針改變。

二,野指針

我們先來了解一下它的概念:野指針就是指針指向的位置是不可知的(隨機的、不正確的、沒有明確限制的)。

1,野指針的成因

野指針有幾種成因分別是:

  1. 指針未初始化
    int *p; *p=20 *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; 
} 
  1. 指針指向的空間被回收
#include <stdio.h> 
int* test() 
{ int n = 100; return &n; 
}
int main() 
{ int*p = test(); printf("%d\n", *p); return 0; 
}

由于n是一個局部變量,在函數調用完后就已經被回收了 (還給操作系統了),所以返回n的地址是一個隨機值。
指針指向的內存空間不屬于當前程序,這個時候就是野指針。

2,如何規避野指針

了解了野指針的成因后我們自然有辦法去規避它。

1. 及時初始化
如果我們不明確指針指向的對象就及時給指針初始化:int *p=NULL

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

如果我們明確指針所指向的對象就要及時給指針初始化:int n=0; int *p=&n;
2.小心指針越界
以上面的代碼來舉例:

#include <stdio.h> 
int main() 
{ int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i = 0; i <= 10; i++) //i必須小于等于10防止指針越界{ *(p++) = i; }//程序走到這指針就越界了要即使置為空指針*p=NULL;int m=0;*p=&m;//下次使用該指針的時候再進行判斷if(*p!=NULL){}return 0; 
} 

當指針變量指向?塊區域的時候,我們可以通過指針訪問該區域,后期不再使?這個指針訪問空間的 時候,我們可以把該指針置為NULL。因為約定俗成的?個規則就是:只要是NULL指針就不去訪問, 同時使?指針之前可以判斷指針是否為NULL。

為了更加深入的理解我們舉一個例子:

我們可以把野指針想象成野狗,野狗放任不管是?常危險的,所以我們可以找?棵樹把野狗拴起來, 就相對安全了,給指針變量及時賦值為NULL,其實就類似把野狗栓起來,就是把野指針暫時管理起來。
不過野狗即使拴起來我們也要繞著?,不能去挑逗野狗,有點危險;對于指針也是,在使?之前,我 們也要判斷是否為NULL,看看是不是被拴起來起來的野狗,如果是不能直接使?,如果不是我們再去 使?。

3. 不要返回局部變量的地址
當局部變量的作用域與該指針的作用域不同時,給指針返回局部變量的地址就相當于沒有初始化指針變成了野指針。

三,assert斷言

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

舉個例子我們運行下面的代碼看看會有什么結果:

#include<stdio.h>
#include<assert.h>//assert.h 頭文件定義了宏 assert() 所以要包含assert.h
int main()
{int n=0;int *p=NULL;assert(p!=NULL);
}

在這里插入圖片描述

上?代碼在程序運行到這一行語句 assert(p!=NULL) 時,驗證變量 p 是否等于 NULL 。如果確實不等于 NULL ,程序 繼續運行,否則就會終止運行,并且給出報錯信息提示。
assert() 宏接受?個表達式作為參數。如果該表達式為真(返回值?零), assert() 不會產? 任何作?,程序繼續運?。如果該表達式為假(返回值為零), assert() 就會報錯,在標準錯誤 流 stderr 中寫??條錯誤信息,顯?沒有通過的表達式,以及包含這個表達式的文件名和行號。

當然assert不僅僅能用來判斷指針,還可以判斷非指針的問題,上代碼:

#include<stdio.h>
#include<assert.h>
int main()
{int a=10;scanf("%d",&a);assert(a==10);
}

我們輸入15看看有什么結果:
在這里插入圖片描述
我們看到編譯器直接報錯,其原因是assert括號內表達式的值為假返回0所以編譯器直接報錯,那如果我們輸入10呢?
在這里插入圖片描述
我們可以看到輸入10編譯器就不會報錯,因為assert括號內表達式值為真返回非0所以不報錯。

assert() 的使?對程序員是?常友好的,使? assert() 有?個好處:

1. 它不僅能?動標識文件和 出問題的行號

*2.還有?種無需更改代碼就能開啟或關閉 assert() 的機制

該機制是如果已經確認程序沒有問 題,不需要再做斷,就在 #include <assert.h> 語句的前?,定義?個宏 NDEBUG 。 例如(以上面的代碼來舉例):

#define NDEBUG
#include<stdio.h>
#include<assert.h>
int main()
{int a=10;scanf("%d",&a);assert(a==10);
}

我們剛剛輸入15編譯器會報錯,現在我們在 #include<assert.h> 語句前面加了 #define NDEBUG 這句話后輸入15看看會不會報錯:
在這里插入圖片描述
發現沒有報錯,但前提是要在 #include<assert.h> 這句話前 加上 #define NDEBUG 這句話才行!

然后,重新編譯程序,編譯器就會禁用文件中所有的 assert() 語句。如果程序?出現問題,可以移 除這條 #define NDEBUG 指令(或者把它注釋掉),再次編譯,這樣就重新啟?了 assert() 語句。

這么好用的assert當然也有缺點:

> assert() 的缺點是,因為引?了額外的檢查,增加了程序的運?時間。 ?般我們可以在 Debug 中使?,在 Release 版本中選擇禁? assert 就?,在 VS 這樣的集成開 發環境中,在 Release 版本中,直接就是優化掉了。這樣在debug版本寫有利于程序員排查問題, 在 Release 版本不影響??使?時程序的效率。

介紹完了assert我們就來看看指針的使用和傳址調用

四,指針的使用和傳址調用

我們學習指針就是為了使用指針來解決問題,但有什么問題是非指針不可得呢?
舉個例子,寫一個函數完成兩個數得交換(我們先用函數的傳值調用)看看能不能實現:

1,傳值調用

#include<stdio.h>
void swap1(int a,int b)
{int z=0;z=a;a=b;b=z;
}
int main()
{int a=10;int b=20;printf("交換前a=%d b=%d\n",a,b);swap1(a,b);printf("交換后a=%d b=%d\n",a,b);return 0;
}

在這里插入圖片描述

我們看到并沒有交換,因為這是傳值調用形參只是實參的一份零時拷貝,形參的改變不影響實參。所以Swap1是失敗的了。


如果對傳值調用還不理解也可以看看我在函數篇講的形參和實參就知道了
傳送門:函數(上)


那怎么辦呢?

我們現在要解決的就是當調?Swap函數的時候,Swap函數內部操作的就是main函數中的a和b,直接 將a和b的值交換了。那么就可以使?指針了,在main函數中將a和b的地址傳遞給Swap函數,Swap 函數?邊通過地址間接的操作main函數中的a和b,并達到交換的效果就好了。

2,傳址調用

還是上面的代碼我們修改一下:

#include<stdio.h>
void swap2(int *pa,int *pb)
{int z=0;z=*pa;//z=a*pa=*pb;//a=b*pb=z;//b=z
}
int main()
{int a=10;int b=20;int *pa=&a;int *pb=&b;printf("交換前a=%d b=%d\n",a,b);swap2(&a,&b);printf("交換后a=%d b=%d\n",a,b);return 0;
}

在這里插入圖片描述

在這里插入圖片描述

我們將a和b的地址傳給形參pa和pb這樣形參和實參就共用一塊內存空間,所以形參的改變會影響實參。這就是傳址調用!

傳址調用,可以讓函數和主調函數之間建立真正的聯系,在函數內部可以修改主調函數中的變量;所 以未來函數中只是需要主調函數中的變量值來實現計算,就可以采用傳值調用。如果函數內部要修改 主調函數中的變量的值,就需要傳址調用。

五,strlen的模擬實現

學完了const和assert斷言后我們對上次模擬strlen的代碼進行修改:

1. 加上const修飾
2. 加上assert斷言

#include <assert.h>
size_t my_strlen(const char* p)//在*左邊加const防止arr內容被修改
{size_t count = 0;assert(p != NULL);//加上assert斷言避免傳入的是空指針!while (*p){count++;//計數器p++;}return count;
}int main()
{char arr[] = "abcdef";//a b c d e f \0size_t len = my_strlen(NULL);printf("%zd\n", len);return 0;
}

好了以上就是本章的全部內容啦!
感謝能夠看到這里的讀者,如果我的文章能夠幫到你那我甚是榮幸,文章有任何問題都歡迎指出!制作不易還望給一個免費的三連,你們的支持就是我最大的動力!

在這里插入圖片描述

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

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

相關文章

Android studio 創建aar包給Unity使用

1、aar 是什么&#xff1f; 和 Jar有什么區別 aar 和 jar包 都是壓縮包&#xff0c;可以使用壓縮軟件打開 jar包 用于封裝 Java 類及其相關資源 aar 文件是專門為 Android 平臺設計的 &#xff0c;可以包含Android的專有內容&#xff0c;比如AndroidManifest.xml 文件 &#…

ASP.NET Core中Filter與Middleware的區別

中間件是ASP.NET Core這個基礎提供的功能&#xff0c;而Filter是ASP.NET Core MVC中提供的功能。ASP.NET Core MVC是由MVC中間件提供的框架&#xff0c;而Filter屬于MVC中間件提供的功能。 區別 中間件可以處理所有的請求&#xff0c;而Filter只能處理對控制器的請求&#x…

基礎篇05-圖像直方圖操作

本節將簡要介紹Halcon中有關圖像直方圖操作的算子&#xff0c;重點介紹直方圖獲取和顯示兩類算子&#xff0c;以及直方圖均衡化處理算子。 目錄 1. 引言 2. 獲取并顯示直方圖 2.1 獲取&#xff08;灰度&#xff09;直方圖 (1) gray_histo算子 (2) gray_histo_abs算子 (3…

MySQL | Navicat安裝教程

MySQL | Navicat安裝教程 &#x1fa84;個人博客&#xff1a;https://vite.xingji.fun 簡介 Navicat 是一款流行的 圖形化數據庫管理工具&#xff0c;由 PremiumSoft 公司開發&#xff0c;支持多種主流數據庫系統&#xff08;如 MySQL、MariaDB、SQL Server、Oracle、Postgre…

硬件實現I2C案例(寄存器實現)

一、需求分析 二、硬件電路設計 本次案例需求與前面軟件模擬案例一致&#xff0c;這里不再贅述&#xff0c;不清楚可參見下面文章&#xff1a;軟件模擬I2C案例&#xff08;寄存器實現&#xff09;-CSDN博客 值得注意的是&#xff0c;前面是軟件模擬I2C&#xff0c;所以并沒有…

基于SpringBoot養老院平臺系統功能實現六

一、前言介紹&#xff1a; 1.1 項目摘要 隨著全球人口老齡化的不斷加劇&#xff0c;養老服務需求日益增長。特別是在中國&#xff0c;隨著經濟的快速發展和人民生活水平的提高&#xff0c;老年人口數量不斷增加&#xff0c;對養老服務的質量和效率提出了更高的要求。傳統的養…

matlab simulink 汽車四分之一模型輪胎帶阻尼

1、內容簡介 略 matlab simulink121-汽車四分之一模型輪胎帶阻尼 可以交流、咨詢、答疑 2、內容說明 略 3、仿真分析 略 4、參考論文 略

w196Spring Boot高校教師科研管理系統設計與實現

&#x1f64a;作者簡介&#xff1a;多年一線開發工作經驗&#xff0c;原創團隊&#xff0c;分享技術代碼幫助學生學習&#xff0c;獨立完成自己的網站項目。 代碼可以查看文章末尾??聯系方式獲取&#xff0c;記得注明來意哦~&#x1f339;贈送計算機畢業設計600個選題excel文…

【鴻蒙開發】第二十四章 AI - Core Speech Kit(基礎語音服務)

目錄 1 簡介 1.1 場景介紹 1.2 約束與限制 2 文本轉語音 2.1 場景介紹 2.2 約束與限制 2.3 開發步驟 2.4 設置播報策略 2.4.1 設置單詞播報方式 2.4.2 設置數字播報策略 2.4.3 插入靜音停頓 2.4.4 指定漢字發音 2.5 開發實例 3 語音識別 3.1 場景介紹 3.2 約束…

數據分析:企業數字化轉型的金鑰匙

引言&#xff1a;數字化浪潮下的數據金礦 在數字化浪潮席卷全球的背景下&#xff0c;有研究表明&#xff0c;只有不到30%的企業能夠充分利用手中掌握的數據&#xff0c;這是否讓人深思&#xff1f;數據已然成為企業最為寶貴的資產之一。然而&#xff0c;企業是否真正準備好從數…

Starrocks 對比 Clickhouse

極速查詢的單表查詢 StarRocks 在極速查詢方面上做了很多&#xff0c;下面著重介紹四點&#xff1a; 1&#xff09;向量化執行&#xff1a;StarRocks 實現了從存儲層到查詢層的全面向量化執行&#xff0c;這是 StarRocks 速度優勢的基礎。向量化執行充分發揮了 CPU 的處理能力…

Vue 入門到實戰 八

第8章 組合API與響應性 目錄 8.1 響應性 8.1.1 什么是響應性 8.1.2 響應性原理 8.2 為什么使用組合API 8.3 setup組件選項 8.3.1 setup函數的參數 8.3.2 setup函數的返回值 8.3.3 使用ref創建響應式引用 8.3.4 setup內部調用生命周期鉤子函數 8.4 提供/注入 8.4.1 …

Java使用aspose實現pdf轉word

Java使用aspose實現pdf轉word 一、下載aspose-pdf-21.6.jar包【下載地址】&#xff0c;存放目錄結構如圖&#xff1b;配置pom.xml。 <!--pdf to word--> <dependency><groupId>com.aspose</groupId><artifactId>aspose-pdf</artifactId>…

使用Node.js搭配express框架快速構建后端業務接口模塊Demo

使用Node.js搭配express框架快速構建后端業務接口模塊Demo&#xff01;實際開發中&#xff0c;有很多項目&#xff0c;其實都是可以使用node.js來完成對接mysql數據庫的&#xff0c;express確實使用起來非常簡單&#xff0c;入手快&#xff0c;效率非常高。下面是一個簡單的案例…

Python----Python高級(并發編程:協程Coroutines,事件循環,Task對象,協程間通信,協程同步,將協程分布到線程池/進程池中)

一、協程 1.1、協程 協程&#xff0c;Coroutines&#xff0c;也叫作纖程(Fiber) 協程&#xff0c;全稱是“協同程序”&#xff0c;用來實現任務協作。是一種在線程中&#xff0c;比線程更加輕量級的存在&#xff0c;由程序員自己寫程序來管理。 當出現IO阻塞時&#xff0c;…

Unity 加載OSGB(webgl直接加載,無需轉換格式!)

Unity webgl加載傾斜攝影數據 前言效果圖后續不足 前言 Unity加載傾斜攝影數據&#xff0c;有很多的插件方便好用&#xff0c;但是發布到網頁端均失敗&#xff0c;因為webgl 的限制&#xff0c;IO讀取失效。 前不久發現一個開源項目: UnityOSGB-main 通過兩種方式在 Unity 中…

【Block總結】PSA,金字塔擠壓注意力,解決傳統注意力機制在捕獲多尺度特征時的局限性

論文信息 標題: EPSANet: An Efficient Pyramid Squeeze Attention Block on Convolutional Neural Network論文鏈接: arXivGitHub鏈接: https://github.com/murufeng/EPSANet 創新點 EPSANet提出了一種新穎的金字塔擠壓注意力&#xff08;PSA&#xff09;模塊&#xff0c;旨…

【重新認識C語言----結構體篇】

目錄 -----------------------------------------begin------------------------------------- 引言 1. 結構體的基本概念 1.1 為什么需要結構體&#xff1f; 1.2 結構體的定義 2. 結構體變量的聲明與初始化 2.1 聲明結構體變量 2.2 初始化結構體變量 3. 結構體成員的訪…

如何在Vscode中接入Deepseek

一、獲取Deepseek APIKEY 首先&#xff0c;登錄Deepseek官網的開放平臺&#xff1a;DeepSeek 選擇API開放平臺&#xff0c;然后登錄Deepseek后臺。 點擊左側菜單欄“API keys”&#xff0c;并創建API key。 需要注意的是&#xff0c;生成API key復制保存到本地&#xff0c;丟失…

電腦開機提示按f1原因分析及終極解決方法來了

經常有網友問到一個問題&#xff0c;我電腦開機后提示按f1怎么解決&#xff1f;不管理是臺式電腦&#xff0c;還是筆記本&#xff0c;都有可能會遇到開機需要按F1&#xff0c;才能進入系統的問題&#xff0c;引起這個問題的原因比較多&#xff0c;今天小編在這里給大家列舉了比…