前面我們對於:
指針與內存都是c語言中的要點與難點
gdb是linux中的調試工具,可以讓我們直接查看內存中的數據。
我們可以看到cpu到底做了什麼事,而內存中又發生了什麼變化
main0.c:
#include <stdio.h> void change(int a, int b) { int tmp =a; a=b; b=tmp; }
int main() { int a=5; int b=3; change(a,b); printf("num a =%d num b =%d ",a,b); return 0; }
上述代碼無法實現a,b數值的交換。
改為指針類型實現代碼如下:
main1.c:
#include <stdio.h> void change(int *a, int *b) { int tmp =*a; *a=*b; *b=tmp; }
int main() { int a=5; int b=3; change(&a,&b); printf("num a =%d num b =%d ",a,b); return 0; }
為原來的變數值加上*, change函數改為傳入&a &b3和5可以成功的交換。
*
&a &b
int* a 與 int *a都是可以的,被稱為指針。& 取地址符。
int* a
int *a
&
我們要引入工具來分析
change(&a,&b)
C語言中int未初始化時,初值為隨機
int變數未初始化的默認初值,和變數的類型有關
通過gdb工具分析原理,分析結果
安裝gdb工具:
sudo apt install gdb gdb -v
gdb可以單步調試,打斷點,查看內存中變數。但是即使生成了可調試版本,還是需要源代碼.c的
.c
gcc -g main0.c -o main0_debug.out
gdb ./main0_debug.out
l
list
回車
break 行數
start
p a
print
n
$1 $2 只表明是第幾個變數。正在顯示的這行是待執行。
$1
$2
我們想看change函數裡面是啥?而不是直接執行完函數。
s
可以看到只是把數字傳進去了。
bt
f 1
q
形參與實參,函數默認傳入變數其實只是將數值傳入,而函數內部的局部變數不會改變全局中的數值。change中的形參a,b只是個代號而已。
此時傳遞的是地址。正好相差四個位元組。
下節課會介紹計算機內存的分配,什麼是堆內存,什麼是棧內存,內存地址,指針變數的實質是什麼東西。
p *a
int *a時, p a列印出的是a的內存地址, p *a列印的是這個地址里對應的值.P &a顯示a的內存地址空間
P &a
P &functionname
*a 取a這個地址的內容 &a 取a這個變數的地址
因為不知道一個指針指向的數據有多大, 所以需要在聲明一個指針變數的時候需要明確的類型。
不能交換數值的解析:只是傳值,只是change的局部變數,是實參的備份。
可以交換數值的解析加:變數加個指針,change傳入取地址符,實現交換功能。
計算機內存中最小的單位叫做位元組(Byte)
一個位元組是八個二進位位
為什麼是二進位呢?
因為我們的計算機是電子計算機,電流只有兩個狀態: 高電位(亮) 低電位(不亮)
人類習慣於十進位數字,可以將二進位與十進位進行轉換。(十個手指頭)
十進位滿十進一,二進位滿二進一
二進位寫起來太長了,為了方便我們顯示。
0x表示十六進位(滿16進1 ABCDEF)
1個16進位的數字,就可以表示4位二進位數字
計算機系統中內存是由操作系統來統一管理的,一個位元組有八個bit,也就是八個二進位位。
不管插幾個內存條,都會把內存看成一個整體來計算內存大小。可是內存也不是你想插多少就插多少的。
32位的操作系統最大只能使用4G的內存
因為32位的硬體平台上,cpu的地址匯流排是32位,也就是操作系統的定址空間是32位。
32位指的是: 給內存編號只能編到32個二進位位(這個編號就類似於我們街道的門牌號碼)
比如一個小區只有八棟樓,那麼這個編號就不能超過8.
cpu的地址匯流排有多少根,那麼編號也就只能有多少個組合。
因為地址匯流排可以存在多種狀態。
32根地址匯流排就有2的32次方個狀態
其中的一個編號就可以代表一個(內存的最小存儲單位)位元組。
所以一共可以存儲2的32次方個位元組。
1024個位元組等於1KB 1024個KB等於1MB,1024個MB等於1GB
內存分配:
1byte = 8bit(1位元組 = 8進位位)
4G內存遠遠不夠用。(64位操作系統出現)
GB T PB EB
操作系統會對所有內存進行編號。每個號碼錶示一個唯一的位元組存放地址,一個位元組可以存放8個二進位位的數據。
所以64位操作系統內存地址編號
一共64個零到64個一
左側便是我們的計算機中內存的編號示意圖,從16位的0到16位的16個f。右側則是我們每個編號對應的內存,每個位元組(byte)可以保存8個bit(狀態位)
這些內存全都要交給操作系統來管理。因為我們的一個計算機中可能同時要運行多個程序。
多個程序對同一個內存地址來進行操作的話,到底分給哪個程序呢?這會引起衝突。
內存的佔用不確定, 不需要程序員來自己管理內存
應用程序是由操作系統來調用的
main()函數就是所有函數的入口,操作系統知道入口後就能執行代碼了,程序就可以被調用了。
main()
操作系統: 除了能給內存做編號以外,還可以給內存做一定的規劃
比如在64位操作系統中: 程序員可以使用的內存只要有前面的48位就可以了。
也就是0X7fffffffffffffff(0x7fffffffffff = 01111111111111111111111111111111111111111111111)以下的。
而以上的內存空間是給操作系統內核使用的。
操作系統的內存不會被大量佔用,避免機器卡住,卡死,死機等狀態。可通過操作系統把應用程序關閉,使得操作系統更安全。
操作系統的內存不會被大量佔用,避免機器卡住,卡死,死機等狀態。
作為用戶程序的內存空間又可以進行分段,從高到低又劃分為:、
我們寫的c語言代碼,編寫的函數在編譯後存到磁碟,運行程序時,就把源代碼編譯後的二進位數據載入到內存中。將源代碼編譯之後的二進位就會被存放在代碼段。
聲明的全局變數或常量放置在數據段。
數據段的內存地址編號通常會大於代碼段。
高位內存空間分配給操作系統內核使用,低位內存空間分配給用戶程序使用。
每次調用新的函數,就將新的函數壓入棧區,正在調用的函數將位於棧頂。
64位系統中 只有前48位是給程序員使用的。 0x7fffffffffffffff ~ 0x0
劇透: 下一節中看在應用程序中棧,堆,數據段,代碼段的作用。
簡單的例子:
#include <stdio.h> int global = 0;
int rect(int a,int b) { static int count=0; count++; global++; int s=a*b; return s; }
int quadrate(int a) { static int count=0; count++; global++; int s = rect(a,a); return s; }
int main() { int a=3; int b=4; int *pa =&a; int *pb =&b; int *pglobal =&global; int (*pquadrate)(int a)= &quadrate; int s = quadrate(a); printf("%d ",s); }
rect求長方形面積,quadrate求正方形面積(內部實際調用了求長方形面積)。
int global = 0;
全局變數global
static int count=0; count++; global++;
函數內的靜態變數: count,每個函數調用內部都讓count和global加加。
main函數中聲明了一系列的指針。
int *pa =&a; int *pb =&b; int *pglobal =&global; int (*pquadrate)(int a)= &quadrate; gcc -g main.c -o main.out //加-g生成的main.out才可以用gdb進行調試 gdb ./main.out //調試
gdb調試命令:
p 變數名
f 棧標號
之所以可以調試代碼?是機器碼被載入進了我們的內存(代碼段,它位於整個內存空間的最低位)
每一行都是我們的一條指令,被存放在代碼段。但是c語言的語法是不允許我們直接操作代碼段的。
除了代碼編譯後會存在代碼段以外,還有一個地方保存我們當前程序運行的狀態,比如當前在調用哪個函數,當前調用的函數運行到多少行?並且這個函數中有哪些變數,這些變數的值是什麼
就像是一張照相機拍攝的快照,記錄當前的狀態信息。這些信息會被記錄到棧內存中。
可以列印出當前的變數值,因為這個信息被記錄在了棧內存當中。
因為還沒有運行int *pa = &a; 內存中的pa值為空。
int *pa = &a;
a裡面是3,b裡面是4,都被棧內存記錄下來了。
變數的本質是什麼?
指針的本質?
指針pa也是一個變數,它也有自己的內存地址(0x7fffffffdcc8)。而這個內存地址中保存的數據是內存地址(0x7fffffffdcc0)。
C語言中所有的變數都有類型。
我們可以看到操作系統是如何管理內存的,以及gcc這類編譯器對於我們源代碼所做的優化。
代碼段在整個內存地址中編號最小。
可以看出rect的編號小於quadrate的。代碼段中保存我們編譯之後的機器碼。計算機在執行的時候rect函數先被載入進去。quadrate函數後被載入進去。先載入進去的內存地址就更小一些。
因為這兩個函數是順序執行的,多以使用大的減小的就是rect在內存中佔用的大小。
數據段: 全局變數 & 常量都在我們的數據段當中。
可以看到數據段的地址是要比代碼段大的。
一個函數可以被多次調用,main函數可以被操作系統多次調用。
如我們多開qq
我們連續聲明了兩個變數,為何兩個變數ab的地址不連續呢?
因為這裡的地址指的是內存的首地址,如int佔四個位元組。那麼dcbc dcbd dcde dcbf都是屬於變數a的內存空間。那麼下一個內存地址的首地址就是dcc0了。
我們的b的首地址是dcc0,它也是int類型四個位元組,為啥下一個變數pa的地址不是dcc4而是dcc8呢?
這裡就涉及到我們編譯器的優化了,它為了讓cpu操作指令更快,提升程序的執行效率會對我們的源代碼做一定的優化。編譯之後的指令存儲有可能和我們編寫代碼的順序不一樣。
在代碼中我們還聲明了另一個整數類型變數s
int s = quadrate(a);
可以看到同樣為整數類型的變數s的地址與前兩個a,b是連續的。
gcc編譯器的優化,如果我們的函數中聲明了若干個整型變數,若干個指針類型變數,若干個浮點型變數,它會把我們的同一類型的變數聲明放到一起。接下來才聲明指針變數。
這樣的好處: 講到數組,指針計算的時候,解釋它的好處。
32位系統指針佔用4個位元組, 也就是32個bit, 64位系統佔用64個bit, 也就是8位元組。
可以看到指針pa的內存佔用從dcc8 到 dcd0(共8個位元組)pb從dcd0到dcd8,(共八個位元組),不管指針指向什麼,它本身內存中存放的都是內存地址,佔八個位元組。
int (*pquadrate)(int a)= &quadrate;
由dcd8 加上8個位元組。來到了dce0
代碼段中內存地址越來越大,先聲明的函數地址小,後聲明的函數地址大。
下一節中: main函數調用正方形,正方形調用長方形。搞清棧內存如何分配的,再搞清靜態變數,局部變數都是怎麼存放的,理解函數的返回值return。
進行再一次的調試。
運行到函數quadrate時,將a=3傳入。
可以看到棧中最下面的內存地址是最先分配的,如果從內存地址的大小來體現的話,main函數的內存地址大小第比較大的。
可以看到最先調用的main函數在最下面,然後是第二個調用的quadrate函數,最上面永遠是當前執行的函數。
棧的特點: 先進後出。 最後進的是rect函數,最先出去的也應該是rect函數。
可以看到越到棧頂的函數,兩個s(第一個s是存放棧頂的rect函數返回值的,第二個s是存放quadrate函數返回值的)
0x7fffffffdc74 0x7fffffffdc9c 棧頂的更小一點,也可以體現出越晚進來的,地址越小。
可以看到更早進來的main函數中s地址更大。因此棧中是越早進來越在棧底,地址越大。
可以看出我們在rect中的靜態變數count,和我們在quadrate中的靜態變數count是連續存儲的。
static int count=0;
可以看出函數內的兩個靜態局部變數count是獨立的,連續存儲的。而兩個函數中的global變數都是指向同一個地址的。
觀察大小,我們局部靜態變數count的地址值和去全局變數的地址值都很小。因此說明他們並不存放在棧中。(棧的地址很大)
我們的靜態變數,常量,包括全局變數,默認都存儲在數據段中。由於靜態變數時屬於某個函數特有的,所以靜態變數也是屬於某個函數特定的,是獨立的。全局變數是所有函數公用的,但是由於他們都在數據段中,即使一個函數被多次調用,靜態變數指向的還是數據段中的一個固定地址。不同函數里的count是不同的count,但是同一個函數不管調用多少次,這個count都指向同一塊內存。
數據段(data segment)通常是指用來存放程序中已初始化的全局變數的一塊內存區域。數據段屬於靜態內存分配。
編譯器優化代碼,把聲明時不在一起的同一類型變數,放到一起(某種程度上修改了源碼)
如聲明
int a; float b; int c;
編譯後變數a的地址和c的地址是連在一起的.CPU在編譯的時候對棧內變數的存儲地址進行優化,他會將類型相同的變數在連續地址中儲存。
地址分配: 代碼段,數據段是從下往上分配(先低地址,後高地址)。棧是從上往下分配(先高地址,後低地址)
函數中靜態變數,局部變數區別:
局部變數在棧(相對數據段而言的高地址)中,而靜態變數在數據段(低地址)中.
全局變數和靜態變數都在數據段中,但靜態變數是某個函數特有的.
下面來探究函數指針是怎麼一回事?
指針可以指向一個變數,吧變數的值取出來。函數指針?
修改我們的源代碼(上面我用的是修改過的,但是不影響上面概念的理解)
修改為:
// int s = quadrate(a); int s = (*pquadrate)(a);
函數指針在調用的時候傳入a的值進來。
gcc -g main.c -o main.out //加-g生成的main.out才可以用gdb進行調試 gdb ./main.out //調試 int (*pquadrate)(int a)= &quadrate;
這一行是一個函數指針
這裡依然可以進入函數內部,運行代碼時函數指針也可以調用函數內容。這種做法經常用於寫程序時做回調函數使用。
p &a
p *&a
&a
*&p
p &*a
*a
&*p
quadrate 本身是一個函數指針,*quadrate取出了指向的函數內容(一組指令構成),(*quadrate)(3)就表示調用函數,並傳入參數3
quadrate
*quadrate
(*quadrate)(3)
p pa
p *pa
p &pa
下面: 數組,動態堆內存創建,指針運算。
示例代碼:
#include <stdio.h> int main() { int a =3; int b =2; int array[3]; array[0] =1; array[1] =10; array[2] =100; int *p=&a; int i; for (i = 0; i < 6; i++) { printf("*p=%d ",*p); p++; } printf("------------------------------------- "); p =&a; for (i = 0; i < 6; i++) { printf("p[%d]=%d ",i,p[i] ); } return 0; }
為了說明問題,所有的數據類型統統是整型,除了指針p以外。c語言的數組類型是比較原始的,在函數內聲明,因此也在棧內存當中。指針p指向a的地址。
p是一個指針,指針的加加操作。我們類比一下,整數類型的加加,3++,下次列印就會變成4。
p[i] 指針的括弧取值,與數組取值有些類似。
gcc -g main.c -o main.out //加-g生成的main.out才可以用gdb進行調試 ./main.out //觀察結果 gdb ./main.out //調試
for循環括弧里加不加int,內存中還有區別的。
指針p指向的值等於3,1,2
int類型的內存地址和數組內存地址不連續,而是差了16位。
for (i = 0; i < 6; i++) { printf("*p=%d ",*p ); if(i == 2){ p=p+4; } else{ p++; } }
將第一個for循環中的代碼改為如上面所示。
for(i = 0; i < 6; i++) { if(i > 2){ printf("p[%d]=%d ",i+3,p[i+3] ); }else{ printf("p[%d]=%d ",i,p[i] ); } }
此時我們列印a的地址,列印p的地址是一樣的。因為我們把a的地址賦值給了p
每個整型數字佔四個位元組。
(gdb) p *p $3 = 3 (gdb) p *&a $4 = 3 (gdb) p *0x7fffffffdcc4 $5 = 3 (gdb) p * 0x7fffffffdcc4 $6 = 3
可以看到四種等價的操作。都是*加上地址,可以直接列印出內存中的數據值。
先聲明了a,再聲明了b。但是我們a的下一個內存地址中存放的卻不是b。c8地址中存放的是0
gcc編譯器有自動優化功能會把所有的同一類型的變數放到一起來聲明。因為我們還聲明過一個i變數,i也是整型的。
通常情況先寫的會在前面,這裡因為我們i在很下面聲明的,中間又隔了一個指針p
0的值就等於i的值,兩個指向同一個地址。
main函數執行的棧中,最低的地址放的a的值,接下來是i的值,i的值之後應該推測是b的值。
0x7fffffffdcc8 + 4 0x7fffffffdccc
可以看出地址順序依次增大: a i b
一直p p p的輸出很麻煩,如何方便的輸出?
x/3d 0x7fffffffdcc4
x表示要輸出內存中的值,/表示要輸出幾個值,輸出3個值。按照什麼類型來輸出。d按照十進位進行輸出。從哪個地址開始顯示呢?
d
我們還可以指定顯示變數要有多大長度,默認是4個位元組。
取九塊內存地址的內容,可以看到整數類型的數字和數組的存儲中間相差三個內存空間(地址上從頭到頭,相差16個位元組)
那些隨機值是不可控的,程序中使用到未初始化的值,會對軟體造成異常。
因為c語言不做指針的安全檢查,它會操作這個地址的值等,未初始化,有可能是其他程序使用過的值。
棧內存中, 連續的地址空間來存放整型變數和我們的數組元素。
可以看出數組是按順序放置元素的。
可以看到指針往下移動了4格,可是指針怎麼知道要加四格呢?
因為程序員在聲明指針類型的時候是整型,int佔四個位元組。所以p++的時候會一次移動4個。
這是指針的偏移運算。指針的偏移運行效率高,性能好。
p +=3;
把指針往下移三格(整數類型指針)移動12個位元組
*p =101;
將p指針所指向的值修改為101
p =&a;
讓p再次指向a的地址,不影響我們下面的列印。
可以看到原本p指向a,然後往下移動三格(忽略整型與數組中間三塊內存,四個地址差)
第一次,從a移動到i;第二次,從i移動到b;第三次,從b移動到數組第一個元素。
p[3] //等價於p +=3,也就是把p往下移動三格 *p = 101 //上面兩行合二為一的想法是錯的,因為只有下面這行才能起到理想目的。 p[3] = 101 int *p=&a; p[2]; *p = 66;
P[4]不是p往下面移動了4個位置,而是從p開始的地址往後移動4個位置取值,p指向的地址還是不變的這時候就不用跟採用p++時,再將指針歸位了。
P[4]
p++
int array[2]; int *pa =array; pa[0]=1; pa[1]=10; pa[2]=100;
如果說數組本身也是一種指針類型的話,裡面就是地址。把地址賦給地址變數就不需要加取地址符了。
任何需要用數組操作的地方,都可以用指針來代替。因為我們的指針變數本質上是內存地址,數組也是地址。
反過來就不行了,指針能做的,數組不一定能做。
int array[2]; array+=2;
上面的代碼就是錯誤的。
數組其實就是個指針常量,指針是指針變數,常量是不可更改的。array永遠都指向的是同一個地址,當然地址裡面的內容是可以改變的。
下節課: 一種特殊的數組,字元數組
小示例代碼:
#include <stdio.h> int main() { char str[]="hello"; char *str2="world"; char str3[10]; printf("input the value "); scanf("%s",str3); printf("str is %s ",str); printf("str2 is %s ",str2); printf("str3 is %s ",str3); }
聲明了一個字元數組並賦值hello,又聲明了一個字元指針,還聲明了一個長度為10的字元數組,並未初始化。
通過scanf將輸入的字元串寫入str3中。
然後進行列印。
gcc -g main.c -o main.out
生成可調試代碼。
gdb main.out //開始調試
列印str和str2的時候,都可以列印出內存中的字元串來。
str直接列印出裡面的值,因為str2指明的是指針類型,會等於一個地址0x5555555548b4。
0x5555555548b4
地址是一個很小的地址(相對於0x7),是在代碼段中的地址,是源代碼編譯就編譯進去的。 而我們的str2隻是指向這個地址而已。
可以看出str2這個指針對應的是整個數組的首地址而已。首地址對應的內存中存儲著首字母w
119是w對應的ASCII碼。第6個值,是因為上面o的那次,已經將指針後移了一位,++後移第二位。
指針忘記歸位,導致str2隻剩下三個字母。
字元類型的指針和字元數組也是可以混用的。
一般的我們通過scanf輸入,是要輸入&a,也就是要加取地址符的。
聲明str3的時候,它是一個字元數組,數組就是內存地址。str3就可以直接傳進去,不需要取地址符。
scanf("%s",str);
將輸入存放至str中。
可以看到因為數組的本質是指針常量,str指向的地址,被mtianyan2str填充。而str3的指針還指向原來的位置,所以造成str3的內容為str的後半部分。
str在創建時有五個字母+一個null結束符,但是它的數組長度是6。
所以計算str3時需要減去6個字母才可以得到。
char str4[]={h,e,l,l,o}; int len = sizeof(str4) / sizeof(char);
而採用這種單字母初始化方法,數組長度與字元個數一致。
我們嘗試向str2中寫入東西。
可以看到往str2寫數據,會出現段錯誤(核心已轉儲的錯誤)
c語言的字元串是一個字元數組,以/0結尾。有五個字元就有六個長度
gdb的x命令,可以列印地址中的值
x/個數 地址
x/6cb 地址
c
b
scanf可以將輸入存入str或str3,但是不能存入str2
堆和棧內存里才可以寫入(預留空間才可寫入),而str2是編譯之後,載入到內存的一個代碼段變數,不允許寫入。操作系統對內存做安全管理。
我們聲明一個函數把一個函數定義好了,函數所在的棧的內存就分配好了。
而我們使用malloc函數,它會為我們分配堆內存。
示例代碼2:
#include <stdio.h> int main() { char str[]="hello"; char *str2="world"; char str3[10]; printf("input the value "); str[3]=