首先給讀者提醒到,博客裏面指針的內容,所有的代碼都是在Qt平臺下完成。Qt平臺有一個好處就是定義的變量在再次編譯的時候地址不會改變,對於我們很多理解有很大的幫助。
指針在C語言中的靈魂
這篇博客我們說明整個C語言裏面號稱令人頭疼的指針。
指針,無疑是 C 語言的精華,沒有掌握指針,也就沒有掌握 C 語言。
指針讓 C 語言更像是結構化的低級語言,其實指針的概念本身很簡單,難就難在指針和我們學到的任何類型都會產生關係,指針和數組,指針和函數,指針和字符串,指針和結構體,指針和內存管理,指針和結構體…指針難就難在因爲所有的數據類型定義的變量都要保存在內存中,而指針可以直接操作內存,所有在內存中的數據結構均可用指針來訪問,這就是指針難的地方。
指針給 C 語言帶來了更簡便的操作,更優的效率,更快的速度。但是在寫程序的過程中幾乎所有遇到段錯誤,崩潰等都和指針有關,所以它是天使,也是魔鬼。
所以指針呢,你說它好,確實很好,能夠訪問到底層的內存。但是如果使用不好,就像玩一些危險的兵器,很有優勢,但是搞不好會割到自己。
因爲指針類型可以指向任何一種數據類型,並且通過地址操作數據,所以在編碼的時候可以使得代碼更加簡潔,操作更加方便,並且在一些情況下,我們只能運用指針才能解決,C指針是C語言的靈魂,我們在這篇博客對於指針有一個比較簡單的認識並且說明一些指針的基本操作。
認識內存
線性的內存
在說明指針之前,我們需要先鋪墊一下內存的知識,前面我們說明的一維數組行和二維數組的行列都僅是一種邏輯上的體現,最終數據是要保存到內存中,一維數組和二維數組在內存中都是線性的,內存的線性是物理基礎。
內存中的每個字節都對應一個地址,通過地址才能找到每個字節
也就是說8個位組成一個字節是編址的最小單位。
二維數組的邏輯存儲
二維數組的物理存儲
#include <stdio.h>
#include <stdlib.h>
int main()
{
int arr[3][4] = { 0 };
for (int i = 0; i < 3; i++)
{
for(int j = 0; j < 4; j++)
{
printf("%p\n", &arr[i][j]);
}
}
return 0;
}
運行結果爲:
無論我們在程序中表現爲一維數組,二維數組,三維數組,n維數組也好,數據最終存儲在內存中都是線性的,並且數組在內存中是一段連續的存儲空間。
通過上面代碼演示,我們看到二維數組在內存中的元素地址,得出二維數組是一段連續的內存空間。
一維數組的邏輯和存儲是一致的,均是線性的(我們已經在一維數組博客進行詳細的說明)。
二維數組的邏輯是二維的,但是二維數組的存儲是線性的。
存儲是線性的原因:由內存的物理特性決定的。
變量的地址與大小
變量的地址
在這裏 我們介紹兩個符號:
取變量地址的符號 & (引用符)
取變量值的符號 * (解引用符)
#include <stdio.h>
int main()
{
char a;
short b;
int c;
double d;
return 0;
}
上面使用不同基本類型定義的變量對應內存中的一段存儲空間,該段存儲空間佔用一定的字節數,可能是 1個字節,2個字節,也可能是 4 或是 8 個字節。
我們把類型所佔的內存空間單獨拿出來看圖解如下:
把上面類型放在線性內存圖解如下:
那麼現在有一個問題:
上面所有的類型所佔的內存空間,char類型佔一個字節,就只包含一個地址,但是short,int,double等類型所佔字節數超過一個字節的時候,那麼取地址的時候取到的是哪一個地址呢?
我們先給出說明:
對於變量取地址,除了char類型佔一個字節,也就佔一個地址,其他的類型包含多個字節也就擁有多個地址,當我們對於一個變量取地址的時候,拿到的是低位字節的地址。
即低位字節的地址表示變量的地址
理解:當變量不是一個字節的時候,指針指向變量的低地址
圖解說明:
接下來我們打印變量的地址:
代碼演示:
#include <stdio.h>
int main()
{
char a; short b; int c; double d;
printf("&a = %#x\t%d\n", &a,&a);
printf("&b = %#x\t%d\n", &b,&b);
printf("&c = %#x\t%d\n", &c,&c);
printf("&d = %#x\t%d\n", &d,&d);
return 0;
}
打印結果爲:
通過上面代碼演示,我們就可以看到不同類型變量的地址,並且可以推斷出不同類型變量所佔字節數。
我們先用指針變量進行說明,後面會詳細說明指針變量
#include <stdio.h>
int main()
{
char a = 1;
short b = 2;
int c = 3;
double d = 4;
char* pa = &a;
short* pb = &b;
int* pc = &c;
double* pd = &d;
return 0;
}
圖示理解爲:
地址的大小
指針變量所佔內存的大小:在我的內存模型和編碼規則博客中已經介紹了內存的編址問題,這裏我們直接提出結論,不清楚的讀者可以返回查看。在32位機,能夠識別的最大內存是4G的內存(實際不到4G,其他設備驅動和內核佔用內存不能被用戶所訪問到)內存分配的邏輯(虛擬)地址是從0X000000到0XFFFFFF 這是內存中的所有地址範圍。那麼8位的16進制數組就可以訪問內存種所有的地址單元,8位16進制數據也就是32位2進制數據,所以我們只需要32位2進制數據就可以訪問到所有的內存地址,32位就是4個字節,所有類型的指針都是32位,4字節。
我們通過打印不同類型變量地址的大小來驗證32位機所有的地址都佔4個字節。
#include <stdio.h>
int main()
{
char a; short b; int c; double d;
printf("sizeof(&a) = %d\n", sizeof(&a));
printf("sizeof(&b) = %d\n", sizeof(&b));
printf("sizeof(&c) = %d\n", sizeof(&c));
printf("sizeof(&d) = %d\n", sizeof(&d));
return 0;
}
測試結果爲:
我們可以看到,32位機,不同類型的變量所佔字節數不同,但是不同類型變量取地址的時候,取出來地址的大小都是4個字節。
通過運算的方式,我們可以求得變量的地址。
32 位機的情況下,無論是什麼類型的指針大小均爲4字節。
64 位機的情況下,無論是什麼類型的指針大小均爲8字節。這是由當前機型的地址總線決定的。
間接訪問內存
我們拿到的變量的地址,其實,地址就是指針了。除了變量,我們還可以通過指針的方式間接的訪問內存。
我們前面已經說明了引用符&和解引用符 * ,具體用法如下:
#include <stdio.h>
int main()
{
char a = 1;
short b = 2;
int c = 3;
long d = 4;
float e = 1.2;
double f = 2.3;
printf("打印出變量地址爲:\n");
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
printf("&c = %p\n", &c);
printf("&d = %p\n", &d);
printf("&e = %p\n", &e);
printf("&f = %p\n", &f);
printf("打印出變量地址的大小爲:\n");
printf("sizeof(&a) = %d\n", sizeof(&a));
printf("sizeof(&b) = %d\n", sizeof(&b));
printf("sizeof(&c) = %d\n", sizeof(&c));
printf("sizeof(&d) = %d\n", sizeof(&d));
printf("sizeof(&e) = %d\n", sizeof(&e));
printf("sizeof(&f) = %d\n", sizeof(&f));
printf("通過取地址和解引用訪問到變量的值爲:\n");
printf("a = %d\n", *(&a));
printf("b = %d\n", *(&b));
printf("c = %d\n", *(&c));
printf("d = %ld\n", *(&d));
printf("e = %f\n", *(&e));
printf("f = %f\n", *(&f));
return 0;
}
打印結果爲:
小結
指針和普通類型,構造類型,函數,結構體,內存管理,鏈表等所有的學習都可以聯繫到一起從而使編程更加靈活,並且指針能夠對於底層內存進行操作,這使得指針操作效率更高,但是如何應用好指針比較困難,所以指針這也是指針被稱作C語言靈魂的原因。