C語言指針與數組之間的恩恩怨怨

很多初學者弄不清指針和數組到底有什么樣的關系。我現在就告訴你:他們之間沒有任何關系!只是他們經常穿著相似的衣服來逗你玩罷了。

指針就是指針,指針變量在32 位系統下,永遠占4 個byte,其值為某一個內存的地址。指針可以指向任何地方,但是不是任何地方你都能通過這個指針變量訪問到。

數組就是數組,其大小與元素的類型和個數有關。定義數組時必須指定其元素的類型和個數。數組可以存任何類型的數據,但不能存函數。


既然它們之間沒有任何關系,那為何很多人把數組和指針混淆呢?甚至很多人認為指針和數組是一樣的。這就與市面上的C 語言的書有關,幾乎沒有一本書把這個問題講透徹,講明白了。

一、以指針的形式訪問和以下標的形式訪問

下面我們就詳細討論討論它們之間似是而非的一些特點。例如,函數內部有如下定義:
A)
char *p = “abcdef”;
B)
char a[] = “123456”;

1、以指針的形式訪問和以下標的形式訪問指針
例子A)定義了一個指針變量p,p 本身在棧上占4 個byte,p 里存儲的是一塊內存的首地址。這塊內存在靜態區,其空間大小為7 個byte,這塊內存也沒有名字。對這塊內存的訪問完全是匿名的訪問。比如現在需要讀取字符‘e’,我們有兩種方式:
1)
以指針的形式:*(p+4)。先取出p 里存儲的地址值,假設為0x0000FF00,然后加上4 個字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的值。

2)
以下標的形式:p[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。p[4]這個操作會被解析成:先取出p 里存儲的地址值,然后加上中括號中4 個元素的偏移量,計算出新的地址,然后從新的地址中取出值。也就是說以下標的形式訪問在本質上與以指針的形式訪問沒有區別,只是寫法上不同罷了。

2、以指針的形式訪問和以下標的形式訪問數組
例子B)定義了一個數組a,a 擁有7 個char 類型的元素,其空間大小為7。數組a 本身在棧上面。對a 的元素的訪問必須先根據數組的名字a 找到數組首元素的首地址,然后根據偏移量找到相應的值。這是一種典型的“具名+匿名”訪問。比如現在需要讀取字符‘5’,我們有兩種方式:
1)
以指針的形式:*(a+4)。a 這時候代表的是數組首元素的首地址,假設為0x0000FF00,然后加上4 個字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的值。

2)
以下標的形式:a[4]。編譯器總是把以下標的形式的操作解析為以指針的形式的操作。a[4]這個操作會被解析成:a 作為數組首元素的首地址,然后加上中括號中4 個元素的偏移量,計算出新的地址,然后從新的地址中取出值。

由上面的分析,我們可以看到,指針和數組根本就是兩個完全不一樣的東西。只是它們都可以“以指針形式”或“以下標形式”進行訪問。一個是完全的匿名訪問,一個是典型的具名+匿名訪問。一定要注意的是這個“以XXX 的形式的訪問”這種表達方式。

另外一個需要強調的是:上面所說的偏移量4 代表的是4 個元素,而不是4 個byte。只不過這里剛好是char 類型數據1 個字符的大小就為1 個byte。記住這個偏移量的單位是元素的個數而不是byte 數,在計算新地址時千萬別弄錯了。

二、a 和&a 的區別

通過上面的分析,相信你已經明白數組和指針的訪問方式了,下面再看這個例子:
main()
{
? ?int a[5]={1,2,3,4,5};
? ?int *ptr=(int *)(&a+1);
? ?printf("%d,%d",*(a+1),*(ptr-1));
}
打印出來的值為多少呢? 這里主要是考查關于指針加減操作的理解。

對指針進行加1 操作,得到的是下一個元素的地址,而不是原有地址值直接加1。所以,一個類型為T 的指針的移動,以sizeof(T) 為移動單位。因此,對上題來說,a 是一個一維數組,數組中有5 個元素; ptr 是一個int 型的指針。

&a + 1: 取數組a 的首地址,該地址的值加上sizeof(a) 的值,即&a + 5*sizeof(int),也就是下一個數組的首地址,顯然當前指針已經越過了數組的界限。

(int *)(&a+1): 則是把上一步計算出來的地址,強制轉換為int * 類型,賦值給ptr。

*(a+1): a,&a 的值是一樣的,但意思不一樣,a 是數組首元素的首地址,也就是a[0]的首地址,&a 是數組的首地址,a+1 是數組下一元素的首地址,即a[1]的首地址,&a+1 是下一個數組的首地址。所以輸出2*(ptr-1): 因為ptr 是指向a[5],并且ptr 是int * 類型,所以*(ptr-1) 是指向a[4] ,輸出5。

這些分析我相信大家都能理解,但是在授課時,學生向我提出了如下問題:在Visual C++6.0 的Watch 窗口中&a+1 的值怎么會是(x0012ff6d(0x0012ff6c+1)呢?
上圖是在Visual C++6.0 調試本函數時的截圖。
a 在這里代表是的數組首元素的地址即a[0]的首地址,其值為0x0012ff6c。
&a 代表的是數組的首地址,其值為0x0012ff6c。
a+1 的值是0x0012ff6c+1*sizeof(int),等于0x0012ff70。


問題就是&a+1 的值怎么會是(x0012ff6d(0x0012ff6c+1)呢?

按照我們上面的分析應該為0x0012ff6c+5*sizeof(int)。其實很好理解。當你把&a+1放到Watch 窗口中觀察其值時,表達式&a+1 已經脫離其上下文環境,編譯器就很簡單的把它解析為&a 的值然后加上1byte。而a+1 的解析就正確,我認為這是Visual C++6.0 的一個bug。既然如此,我們怎么證明證明&a+1 的值確實為0x0012ff6c+5*sizeof(int)呢?很好辦,用printf 函數打印出來。這就是我在本書前言里所說的,有的時候我們確實需要printf 函數才能解決問題。你可以試試用printf("%x",&a+1);打印其值,看是否為0x0012ff6c+5*sizeof(int)。注意如果你用的是printf("%d",&a+1);打印,那你必須在十進制和十六進制之間換算一下,不要冤枉了編譯器。

另外我要強調一點:不到非不得已,盡量別使用printf 函數,它會使你養成只看結果不問為什么的習慣。比如這個列子,*(a+1)和*(ptr-1)的值完全可以通過Watch 窗口來查看。

平時初學者很喜歡用“printf("%d,%d",*(a+1),*(ptr-1));”這類的表達式來直接打印出值,如果發現值是正確的就歡天喜地。這個時候往往認為自己的代碼沒有問題,根本就不去查看其變量的值,更別說是內存和寄存器的值了。更有甚者,printf 函數打印出來的值不正確,就措手無策,舉手問“老師,我這里為什么不對啊?”。長此以往就養成了很不好的習慣,只看結果,不重調試。這就是為什么同樣的幾年經驗,有的人水平很高,而有的人水平卻很低。其根本原因就在于此,往往被一些表面現象所迷惑。printf 函數打印出來的值是對的就能說明你的代碼一定沒問題嗎?我看未必。曾經一個學生,我讓其實現直接插入排序算法。很快他把函數寫完了,把值用printf 函數打印出來給我看。我看其代碼卻發現他使用的算法本質上其實是冒泡排序,只是寫得像直接插入排序罷了。等等這種情況數都數不過來,往往犯了錯誤還以為自己是對的。所以我平時上課之前往往會強調,不到非不得已,不允許使用printf 函數,而要自己去查看變量和內存的值。學生的這種不好的習慣也與目前市面上的教材、參考書有關,這些書甚至花大篇幅來介紹scanf 和printf 這類的函數,卻幾乎不講解調試技術。甚至有的書還在講TruboC 2.0 之類的調試器!如此教材教出來的學生質量
可想而知。

三、指針和數組的定義與聲明

1、定義為數組,聲明為指針
文件1 中定義如下:
? ?char a[100];
文件2 中聲明如下(關于extern 的用法,以及定義和聲明的區別,請復習第一章):
? ?extern char *a;
這里,文件1 中定義了數組a,文件2 中聲明它為指針。這有什么問題嗎?平時不是總說數組與指針相似,甚至可以通用嗎?但是,很不幸,這是錯誤的。通過上面的分析我們也能明白一些,但是“革命尚未成功,同志仍需努力”。你或許還記得我上面說過的話:數組就是數組,指針就是指針,它們是完全不同的兩碼事!他們之間沒有任何關系,只是經常穿著相似的衣服來迷惑你罷了。下面就來分析分析這個問題:

在第一章的開始,我就強調了定義和聲明之間的區別,定義分配的內存,而聲明沒有。

定義只能出現一次,而聲明可以出現多次。這里extern 告訴編譯器a 這個名字已經在別的文件中被定義了,下面的代碼使用的名字a 是別的文件定義的。再回顧到前面對于左值和右值的討論,我們知道如果編譯器需要某個地址(可能還需要加上偏移量)來執行某種操作的話,它就可以直接通過開鎖動作(使用“*”這把鑰匙)來讀或者寫這個地址上的內存,并不需要先去找到儲存這個地址的地方。相反,對于指針而言,必須先去找到儲存這個地址的地方,取出這個地址值然后對這個地址進行開鎖(使用“*”這把鑰匙)。如下圖:

這就是為什么extern char a[]與extern char a[100]等價的原因。因為這只是聲明,不分配空間,所以編譯器無需知道這個數組有多少個元素。這兩個聲明都告訴編譯器a 是在別的文件中被定義的一個數組,a 同時代表著數組a 的首元素的首地址,也就是這塊內存的起始地址。數組內地任何元素的的地址都只需要知道這個地址就可以計算出來。

但是,當你聲明為extern char *a 時,編譯器理所當然的認為a 是一個指針變量,在32 位系統下,占4 個byte。這4 個byte 里保存了一個地址,這個地址上存的是字符類型數據。雖然在文件1 中,編譯器知道a 是一個數組,但是在文件2 中,編譯器并不知道這點。大多數編譯器是按文件分別編譯的,編譯器只按照本文件中聲明的類型來處理。所以,雖然a 實際大小為100 個byte,但是在文件2 中,編譯器認為a 只占4 個byte。

我們說過,編譯器會把存在指針變量中的任何數據當作地址來處理。所以,如果需要訪問這些字符類型數據,我們必須先從指針變量a 中取出其保存的地址。如下圖:

2、定義為指針,聲明為數組
顯然,按照上面的分析,我們把文件1 中定義的數組在文件2 中聲明為指針會發生錯誤。

同樣的,如果在文件1 中定義為指針,而在文件中聲明為數組也會發生錯誤:
文件1
? ?char *p = “abcdefg”;
文件2
? ?extern char p[];
在文件1 中,編譯器分配4 個byte 空間,并命名為p。同時p 里保存了字符串常量“abcdefg”的首字符的首地址。這個字符串常量本身保存在內存的靜態區,其內容不可更改。在文件2中,編譯器認為p 是一個數組,其大小為4 個byte,數組內保存的是char 類型的數據。在文件2 中使用p 的過程如下圖:
通過上面的分析,相信你已經知道數組與指針的的確確是兩碼事了。他們之間是不可以混淆的,但是我們可以“以XXXX 的形式”訪問數組的元素或指針指向的內容。以后一定要確認你的代碼在一個地方定義為指針,在別的地方也只能聲明為指針;在一個的地方定義為數組,在別的地方也只能聲明為數組。切記不可混淆。下面再用一個表來總結一下指針和數組的特性:


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

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

相關文章

CentOS7查看開放端口命令

CentOS7查看開放端口命令CentOS7的開放關閉查看端口都是用防火墻來控制的,具體命令如下:查看已經開放的端口:Linux代碼 firewall-cmd --list-ports 開啟端口Linux代碼 firewall-cmd --zonepublic --add-port80/tcp --permanent 命令含義&…

referer參數和addslashes()函數的騷路子

TIPS: 此函數確實是過濾用戶輸入的,當gpc未開啟的時候使用addslashes()函數進行過濾,當開啟gpc的時候直接返回,那么問題就來了,當php開啟gpc的時候直接返回字符串,但是gpc是只針GET,POST,COOKIE三種超全局變量進行過濾…

MySQL 的各個json 函數說明

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 如果英文的不方便閱讀,請參考個人收錄的另一篇中文解說:MySQL中json函數說明和舉例 只是為了方便 自己用的時候…

訂單編號,遞增且不連續(php版)

1、實現方式&#xff0c;使用while循環&#xff0c;比較low&#xff0c;200w條數據 2s public function getMany ($startNum, $count) {$num $startNum;$codes[] $startNum;for($i1;$i<$count;$i){$num1 random_int(1,9);while($num $num1) {$num1 random_int(1,9);}$…

一文搞懂:詞法作用域、動態作用域、回調函數、閉包

把以前一直只限于知道&#xff0c;卻不清晰理解的這幾個概念完完整整地梳理了一番。內容參考自wiki頁面&#xff0c;然后加上自己一些理解。 詞法作用域和動態作用域 不管什么語言&#xff0c;我們總要學習作用域(或生命周期)的概念&#xff0c;比如常見的稱呼&#xff1a;全局…

C語言函數指針

一、函數指針的定義 顧名思義&#xff0c;函數指針就是函數的指針。它是一個指針&#xff0c;指向一個函數。看例子&#xff1a;A)char * (*fun1)(char * p1,char * p2);B)char * *fun2(char * p1,char * p2);C)char * fun3(char * p1,char * p2);看看上面三個表達式分別是什么…

程序員進階之算法練習:LeetCode專場

歡迎大家前往騰訊云社區&#xff0c;獲取更多騰訊海量技術實踐干貨哦~ 本文由落影發表 前言 LeetCode上的題目是大公司面試常見的算法題&#xff0c;今天的目標是拿下5道算法題&#xff1a; 題目1是基于鏈表的大數加法&#xff0c;既考察基本數據結構的了解&#xff0c;又考察在…

vim 安裝vim-prettier

1、在.vimrc中添加 配置沒有安裝成功的話 git clone https://github.com/prettier/vim-prettier Plug prettier/vim-prettier, { do: yarn install, for: [javascript, typescript, css, less, scss, json, graphql, markdown, vue, yaml, html, php] } let g:prettier#aut…

詳解Mysql中的JSON系列操作函數

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 一、方法羅列&#xff1a; 分類 函數 描述創建jsonjson_array 創建json數組json_object 創建json對象 json_quote 將json轉成json字符串…

WEB/H5性能優化總結

我們今天來說說前端圖形渲染優化&#xff0c;因為我接下來的時間可能要開始研究webgl方面的東西&#xff0c;所以就在這里把之前做過的H5做一個總結&#xff0c;現同步發布于GERRY_BLOG&#xff0c;TiMiGerry-知乎&#xff0c;轉載請保留鏈接。靜態資源-圖片 一 、圖片格式JPEG…

C語言數組參數與指針參數

我們都知道參數分為形參和實參。形參是指聲明或定義函數時的參數&#xff0c;而實參是在調用函數時主調函數傳遞過來的實際值。 一、一維數組參數 1、能否向函數傳遞一個數組&#xff1f;看例子&#xff1a;void fun(char a[10]){char c a[3];}intmain(){char b[10] “abcd…

maven文件結構

pom.xml 用于maven的配置文件 /src 源代碼目錄 /src/main 工程源代碼目錄 /src/main/java 工程java源代碼目錄 /src/main/resource 工程的資源目錄 /src/test 單元測試目錄 /src/test/java /target 輸出目錄&#xff0c;所有的輸出都存放在這個目錄下 /target/classes 編譯之…

php如何使用高階函數

1、首先學會數組轉集合的方式 &#xff08;1&#xff09;使用collect函數 $arr [1, 2, 3, 4, 5]; $collect collect($arr); &#xff08;2&#xff09;使用array_map函數 $arr [1, 2, 3, 4, 5]; $collect array_map(function($item){ return $item *…

Git 使用,命令說明

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1. D:\ChengXu\git\Git中雙擊Git Bash啟動git窗口。 2. 這條不能放到博客&#xff0c;是我的賬號密碼。 3. 添加&#xff1a; git add …

2017ACM/ICPC亞洲區沈陽站 C Hdu-6219 Empty Convex Polygons 計算幾何 最大空凸包

題面 題意:給你一堆點,求一個最大面積的空凸包,里面沒有點. 題解:紅書板子,照抄完事,因為題目給的都是整點,所以最后答案一定是.5或者.0結尾,不用對答案多做處理 1 #include<bits/stdc.h>2 #define N 553 using namespace std;4 struct rec5 {6 double x,y;7 };8 rec…

python讀xml文件

# -*- coding:utf-8 -*- import jsonimport requestsimport oscurpathos.path.dirname(os.path.realpath(__file__))xmlpathos.path.join(curpath,read1.xml)with open(xmlpath,encoding"utf-8") as fp: bodyfp.read() print(body)轉載于:https://www.cnblogs.…

C語言數組應用

一、數組的內存布局 先看下面的例子&#xff1a;int a[5];所有人都明白這里定義了一個數組&#xff0c;其包含了5 個int 型的數據。我們可以用a[0],a[1]等來訪問數組里面的每一個元素&#xff0c;那么這些元素的名字就是a[0],a[1]…嗎&#xff1f;看下面的示意圖&#xff1a; 如…

Installation failed, deleting ./composer.json.安裝phpunit報錯解決方案

是因為你沒有裝全局的phpunit&#xff0c;安裝命令 composer global require phpunit/phpunit 之后你輸入 composer require --dev phpunit/phpunit 就發現你安裝成功了

MyBatis在Oracle中插入數據并返回主鍵的問題解決

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 前言&#xff1a;我早期用過這個方法&#xff0c;但是返回的依舊是影響行數&#xff0c;不是主鍵。 只是這種寫法可以達到我要的效果&a…

在 Intellij IDEA 里使用 OpenJFX (JavaFX)

2019獨角獸企業重金招聘Python工程師標準>>> JDK 11 把 JavaFX 剝離了出來&#xff0c;形成了單獨且開源的 OpenJFX 模塊。 本文的目的是通過簡單的例子解釋這一變化對使用 JavaFX 所造成的影響&#xff0c;并找到一種在 IDEA 2018.2 上使用它的辦法。 首先&#xf…