【软件开发底层知识修炼】二十七 C/C++中的指针与数组是不同的
- 上幾篇文章學習了ABI-應用程序二進制接口:【軟件開發底層知識修煉】二十六 ABI-應用程序二進制接口 學習總結文章目錄
- 本篇文章就指針與數組的聯系與區別來學習學習
文章目錄
- 1 疑問
- 2 指針與數組是不相等的
- 3 解決疑問
- 4 總結
1 疑問
在具體用文字理論來說明指針與數組的區別之前,先看一下下面的代碼例子,這兩個程序輸出的結果是一樣的么?不一樣的話,分別輸出什么?
- main.c
- define.c
將上述兩個程序放到同一文件夾下進行編譯運行:
- gcc -g main.c define.c -o test.out
- .test.out
運行結果如下:
- 但是如果我把main.c中的extern char* g_name; 換成extern char g_name[]; 的話,程序運行就可以通過,并且可以得到預期的結果。
對于這個結果,我想并不是很多人可以理解的。這個問題放到后面解釋。下面我們先來看看指針與數組的一些基本概念。
2 指針與數組是不相等的
- 指針
- 指針的本質就是一個變量,它保存的目標值是一個內存地址。這個內存地址是另一個變量或者不管什么東西的地址
- 指針運算與 * 操作符配合使用能夠模擬數組的行為
- 數組
- 數組是一段連續的內存空間的別名
- 數組名可看做指向數組第一個元素的常量指針。
在C語言中指針與數組在某些層面是具有等價關系的,注意這里說的是某層面。比如下面的代碼層面,指針與數組的操作就是相等的:
那么,既然我們已經學習了那么多匯編的知識,上面的指針與數組的操作在匯編層面(或者叫做二進制層面)是否相等?我們以實際的例子來說明,編譯下面代碼,并生成匯編代碼,查看test函數的匯編代碼:
#include <stdio.h>int test() {int a[3] = {0};int* p = a;p[0] = 1; // a[0] = 1p[1] = 2; // a[1] = 2a[2] = 3; // p[2] = 3 }int main() {test();return 0; }- gcc -g test.c -o test.out
- objdump -S test.out > test.s 生成test.s反匯編代碼
查看test.s中的test函數中的匯編代碼,如下:
int test() {8048394: 55 push %ebp8048395: 89 e5 mov %esp,%ebp8048397: 83 ec 10 sub $0x10,%espint a[3] = {0}; 804839a: c7 45 f0 00 00 00 00 movl $0x0,-0x10(%ebp) //a[0]的值80483a1: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp)80483a8: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp)int* p = a; //指針p指向數組a的第一個元素,4字節80483af: 8d 45 f0 lea -0x10(%ebp),%eax80483b2: 89 45 fc mov %eax,-0x4(%ebp)p[0] = 1; // a[0] = 1 由于是在第一個位置,沒必要使用add $0x0,%eax80483b5: 8b 45 fc mov -0x4(%ebp),%eax80483b8: c7 00 01 00 00 00 movl $0x1,(%eax)p[1] = 2; // a[1] = 2 可以看出有兩次尋址的過程80483be: 8b 45 fc mov -0x4(%ebp),%eax //首先把指針p存的地址取出來傳給eax寄存器80483c1: 83 c0 04 add $0x4,%eax //然后將eax+480483c4: c7 00 02 00 00 00 movl $0x2,(%eax) //最后將數值2傳給eax寄存器中存的地址所在的內存處,注意這句話的理解。a[2] = 3; // p[2] = 380483ca: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%ebp) //可以看出如果是數組的話,直接將值賦值給對應內存處,而不用像指針那樣進行兩次地址的操作 }80483d1: c9 leave 80483d2: c3 ret對于上面的匯編代碼,應該并不是很多人都可以理解。不理解也無所謂,能夠看出我們的問題所在即可。
- 首先看上面,對于p[0]=1; p[1]=2; 這兩段代碼,它們所對應的匯編代碼,由于p[0]比較特殊,所以看p[2]的。上線的注釋也是比較詳細了,由此我們知道如果將指針當做數組來使用,首先需要取出指針所存儲的地址,然后將地址值+4,然后在加了4的地址處賦值,這很明顯是兩次尋址操作。一次是從指針中取出地址,二是根據這個地址再找到相應的內存然后進行賦值。
- 但是對于 a[2] = 3; 這段話,看上面的匯編代碼,很明顯,就是直接進行一次內存操作。這顯而易見。
由此我們可以粗略的得出以下結論:
然后就是,在大多數情況下,編譯器做了很多的工作,它讓程序員可以更高效的寫代碼,所以在很多情況中,指針和數組在語言編寫層面,是一樣的,就像上線的示例代碼一樣。
3 解決疑問
上一節內容我們學會了指針與數組的一些區別,現在就來看看最開始的疑問,最開始main.c和define.c編譯運行后,為什么會產生錯誤,并且為什么是段錯誤呢?下面就一點點揭開迷霧。
- 首先我們要知道的前提知識點,C/C++編譯器的天生缺陷
- C/C++編譯器由4個子部件組成,分別是預處理器,編譯器,匯編器,鏈接器
- 每個子部件之間獨立工作,相互之間沒有通信
- 對于語法的檢查與規范只在編譯器(是指第二個子部件的編譯器)編譯階段有效(如:類型約束和保護成員)
- 編譯器認為,每一個源文件都是相互獨立的,對各個源文件單獨進行編譯(當然最后是需要將各個單獨編譯后的文件進行鏈接的)。這個是導致上面錯誤代碼的直接原因。具體還看下面的分析。
那么對于上面的幾條知識點,我們使用下面的圖解進行說明:
- 上面圖示中說了在兩個文件中類型不一致導致運行時錯誤,當然這是表面原因,并且如果是其他的類型(不是指針的類型),有可能就不會出錯。所以我們還需要深挖這其中的錯誤。
- 針對我們的代碼的話,就是在main.c中將g_name聲明為指針,那么編譯器進行編譯的時候,就是單獨編譯main.c文件,并且將g_name按照指針的方式進行編譯。那么由第二節的內容知道,指針的操作是需要兩次尋址的。 這里我我們先記住,下面的分析會用上。
為了能夠更加清楚的說清楚問題,下面我們針對上述的main.c與define.c的編譯的過程簡單的用圖表示一下:
- 上面最后將define.c中的數組g_name的首地址與main.c中代表的指針g_name鏈接起來,具體如何鏈接呢?請看以下圖示:
上面的圖示分析如果能看懂的話,就知道g_name 是一個占有4字節的指針,而g_name 是一個指向數組首地址的值。如果我們注意到前面所說的指針作為數組是需要兩次尋址操作的話,我們就應該知道,如果使用g_name 的話,首先將它存的地址:“D.T.” 取出來,可以看到,它本身應該存的是地址,但是現在是一串字符。然后用這個“地址”來尋址另一個內存地址處。到這里,就明了了,上面的一串字符所代表的地址處是一個未定義的,是一個野地址!!!也就是說在運行的時候,此時g_name是一個野指針!!!這必然會產生段錯誤了!!!
- 這就是為什么,產生的錯誤是段錯誤。真正的原因歸根結底是野指針的原因。
對于上面存在的問題,我們盡量使用以下的方法來解決:
- 盡可能不使用跨文件的全局變量,也就是非static的全局變量
- 當必須使用時,在統一固定的頭文件中聲明global.h
- 其他源文件包含上述global.h即可
4 總結
- 在進行總結前,這里務必再次將聲明與定義的區別說明一下:
下面是針對本文的指針與數組的區別的總結
- C/C++語言中的指針與數組在某些語言層面上的使用時等價的
- 指針與數組在二進制層面是完全不等的
- C/C++編譯器忽略了源碼之間的依賴關系
- 如果一定要使用跨文件之間的全局變量的話,最好將全局變量放到一個統一的頭文件global.h中
- 然后其他源文件包含global.h即可
對于上面的分析,如果沒有懂,可以加左側群,進群進行交流。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的【软件开发底层知识修炼】二十七 C/C++中的指针与数组是不同的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 傲腾™,企业应用加速利器!
- 下一篇: 【C++深度剖析教程14】经典问题解析三