C生萬物 | 從淺入深理解指針【第一部分】
一、內(nèi)存和地址
1.1 內(nèi)存
-
在講內(nèi)存和地址之前,我們想有個(gè)生活中的案例:
-
假設(shè)有一棟宿舍樓,把你放在樓里,樓上有100個(gè)房間,但是房間沒有編號(hào),你的一個(gè)朋友來找你玩,如果想找到你,就得挨個(gè)房子去找,這樣效率很低,但是我們?nèi)绻鶕?jù)樓層和樓層的房間的情況,給每個(gè)房間編上號(hào),如:
- 一樓:101,102,103…
- 二樓:201,202,203…
有了房間號(hào),如果你的朋友得到房間號(hào),就可以快速的找房間,找到你。
- 生活中,每個(gè)房間有了房間號(hào),就能提高效率,能快速的找到房間。
如果把上面的例子對(duì)照到計(jì)算中,又是怎么樣呢?
- 我們知道計(jì)算上CPU(中央處理器)在處理數(shù)據(jù)的時(shí)候,需要的數(shù)據(jù)是在內(nèi)存中讀取的,處理后的數(shù)據(jù)也會(huì)放回內(nèi)存中,那我們買電腦的時(shí)候,電腦上內(nèi)存是8GB/16GB/32GB等,那這些內(nèi)存空間如何高效的管理呢?
其實(shí)也是把內(nèi)存劃分為一個(gè)個(gè)的內(nèi)存單元,每個(gè)內(nèi)存單元的大小取1個(gè)字節(jié)。
- 計(jì)算機(jī)中常見的單位(補(bǔ)充):
- 一個(gè)比特位可以存儲(chǔ)一個(gè)2進(jìn)制的位1或者0
bit - 比特位
byte - 字節(jié)
KB
MB
GB
TB
PB
1byte = 8bit
1KB = 1024byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
1PB = 1024TB
- 其中,每個(gè)內(nèi)存單元,相當(dāng)于一個(gè)學(xué)生宿舍,一個(gè)人字節(jié)空間里面能放8個(gè)比特位,就好比同學(xué)們住的八人間,每個(gè)人是一個(gè)比特位。
- 每個(gè)內(nèi)存單元也都有一個(gè)編號(hào)(這個(gè)編號(hào)就相當(dāng)于宿舍房間的門牌號(hào)),有了這個(gè)內(nèi)存單元的編號(hào),CPU就可以快速找到一個(gè)內(nèi)存空間。
- 生活中我們把門牌號(hào)也叫地址,在計(jì)算機(jī)中我們把內(nèi)存單元的編號(hào)也稱為地址。C語言中給地址起了新的名字叫:指針。
- 所以我們可以理解為:內(nèi)存單元的編號(hào) == 地址 == 指針
1.2 究竟該如何理解編址
- CPU訪問內(nèi)存中的某個(gè)字節(jié)空間,必須知道這個(gè)字節(jié)空間在內(nèi)存的什么位置,而因?yàn)閮?nèi)存中字節(jié)很多,所以需要給內(nèi)存進(jìn)行編址(就如同宿舍很多,需要給宿舍編號(hào)一樣)。
- 計(jì)算機(jī)中的編址,并不是把每個(gè)字節(jié)的地址記錄下來,而是通過硬件設(shè)計(jì)完成的。鋼琴、吉他面沒有寫上“都瑞咪發(fā)嗦啦”這樣的信息,但演奏者照樣能夠準(zhǔn)確找到每一個(gè)琴弦的每一個(gè)位置,這是為何?因?yàn)橹圃焐桃呀?jīng)在樂器硬件層面上設(shè)計(jì)好了,并且所有的演奏者都知道。本質(zhì)是一種約定出來的共識(shí)!硬件編址也是如此~~
- 首先,必須理解,計(jì)算機(jī)內(nèi)是有很多的硬件單元,而硬件單元是要互相協(xié)同工作的。所謂的協(xié)同,至少相互之間要能夠進(jìn)行數(shù)據(jù)傳遞。但是硬件與硬件之間是互相獨(dú)立的,那么如何通信呢?答案很簡單,用"線"連起來。而CPU和內(nèi)存之間也是有大量的數(shù)據(jù)交互的,所以,兩者必須也用線連起來。不過,我們今天關(guān)心一組線,叫做地址總線。
- 我們可以簡單理解,32位機(jī)器有32根地址總線,每根線只有兩態(tài),表示0,1【電脈沖有無】,那么一根線,就能表示2種含義,2根線就能表示4種含義,依次類推。32根地址線,就能表示2^32種含義,每一種含義都代表一個(gè)地址。地址信息被下達(dá)給內(nèi)存,在內(nèi)存上,就可以找到該地址對(duì)應(yīng)的數(shù)據(jù),將數(shù)據(jù)在通過數(shù)據(jù)總線傳入CPU內(nèi)寄存器。
二、指針變量和地址
2.1 取地址操作符(&)
理解了內(nèi)存和地址的關(guān)系,我們再回到C語言,在C語言中創(chuàng)建變量其實(shí)就是向內(nèi)存申請(qǐng)空間,比如:
- 下面這段代碼變量創(chuàng)建的本質(zhì)是什么?
int main()
{
int a = 10;
return 0;
}
- 變量創(chuàng)建的本質(zhì)是:在內(nèi)存上開辟空間~~
要向內(nèi)存申請(qǐng)4個(gè)字節(jié)的空間,存放數(shù)據(jù)0
- 比如,上述的代碼就是創(chuàng)建了整型變量a,內(nèi)存中申請(qǐng)4個(gè)字節(jié),用于存放整數(shù)10,其中每個(gè)字節(jié)都有地址,上圖中4個(gè)字節(jié)的地址分別是:
0x005FFC54 0a
0x005FFC55 00
0x005FFC56 00
0x005FFC57 00
那我們?nèi)绾文艿玫絘的地址呢?
- 這里就得學(xué)習(xí)一個(gè)操作符(&)-取地址操作符
#include <stdio.h>
int main() {
int a = 10;
&a;//取出a的地址
printf("%p\n", &a);
return 0;
}
- 對(duì)于a來說,我們那到的是a所占4個(gè)字節(jié)的第一個(gè)地址(地址較小的那個(gè)字節(jié)的地址)
按照我畫圖的例子,會(huì)打印處理:005FFC54
- 雖然整型變量占用4個(gè)字節(jié),我們只要知道了第一個(gè)字節(jié)地址,順藤摸瓜訪問到4個(gè)字節(jié)的數(shù)據(jù)也是可
行的。
三、指針變量和解引用操作符(*)
3.1 指針變量
-
那我們通過取地址操作符(&)拿到的地址是一個(gè)數(shù)值,比如:0x006FFD70,這個(gè)數(shù)值有時(shí)候也是需要存儲(chǔ)起來,方便后期再使用的,那我們把這樣的地址值存放在哪里呢?答案是:
指針變量
中。 -
比如:
int main() {
int a = 10;
int* pa = &a;//取出a的地址并存儲(chǔ)到指針變量pa中
return 0;
}
- 其中pa叫做指針變量~~,因?yàn)槭谴娣胖羔樀淖兞浚越凶鲋羔樧兞?/li>
- 指針變量也是一種變量,這種變量就是用來存放地址的,存放在指針變量中的值都會(huì)理解為地址。
3.2 如何拆解指針類型
- 我們看到pa的類型是
int*
,我們該如何理解指針的類型呢?
int a = 10;
int* pa = &a;
- 這里pa左邊寫的是
int*
,*
是在說明pa是指針變量,而前面的int
是在說明pa指向的是整型(int)類型的對(duì)象。
- 那如果有一個(gè)char類型的變量ch,ch的地址,要放在什么類型的指針變量中呢?
char ch = 'w';
pc = &ch;//pc 的類型怎么寫呢?
- 既然是char類型的變量,那肯定就要放在char類型的指針變量里~~
char ch = 'w';
char* pc = &ch;
3.3 解引用操作符
-
我們將地址保存起來,未來是要使用的,那怎么使用呢?
在現(xiàn)實(shí)生活中,我們使用地址要找到一個(gè)房間,在房間里可以拿去或者存放物品。 -
C語言中其實(shí)也是一樣的,我們只要拿到了地址(指針),就可以通過地址(指針)找到地址(指針)指向的對(duì)象,這里必須學(xué)習(xí)一個(gè)操作符叫解引用操作符(*)。
#include <stdio.h>
int main()
{
int a = 100;
int* pa = &a;
*pa = 0;
return 0;
}
- 上面代碼中第7行就使用了解引用操作符, *pa 的意思就是通過pa中存放的地址,找到指向的空間,pa其實(shí)就是a變量了;所以pa = 0,這個(gè)操作符是把a(bǔ)改成了0.有同學(xué)肯定在想,這里如果目的就是把a(bǔ)改成0的話,寫成a = 0; 不就完了,為啥非要使用指針呢?
- 其實(shí)這里是把a(bǔ)的修改交給了pa來操作,這樣對(duì)a的修改,就多了一種的途徑,寫代碼就會(huì)更加靈活,后期慢慢就能理解了。
四、指針變量的大小
- 前面的內(nèi)容我們了解到,32位機(jī)器假設(shè)有32根地址總線,每根地址線出來的電信號(hào)轉(zhuǎn)換成數(shù)字信號(hào)后是1或者0,那我們把32根地址線產(chǎn)生的2進(jìn)制序列當(dāng)做一個(gè)地址,那么一個(gè)地址就是32個(gè)bit位,需要4個(gè)字節(jié)才能存儲(chǔ)。
- 如果指針變量是用來存放地址的,那么指針變的大小就得是4個(gè)字節(jié)的空間才可以。
- 同理64位機(jī)器,假設(shè)有64根地址線,一個(gè)地址就是64個(gè)二進(jìn)制位組成的二進(jìn)制序列,存儲(chǔ)起來就需要
- 8個(gè)字節(jié)的空間,指針變的大小就是8個(gè)字節(jié)。
- 接下來我們再看看下面這個(gè)~~
int main()
{
int num = 10;
int* p = #
char ch = 'w';
char* pc = &ch;
printf("%zd\n", sizeof(p));
printf("%zd\n", sizeof(pc));
return 0;
}
-
我們這里是x86環(huán)境下,猜猜這里這個(gè)
p
和pc
的大小是多少?p是4個(gè)字節(jié)?pc是1個(gè)字節(jié)? -
讓我們來看看~~
-
可以看到,都是4個(gè)字節(jié),指針變量的大小是固定的,不要以為
char*
類型的就小,看不起char*
類型的指針~~
x86環(huán)境下為什么
char*
的指針變量和int*
的指針變量都是4個(gè)字節(jié)呢?
-
指針變量是干什么呢?是為了存放地址的
-
那指針變量的大小是取決于存放一個(gè)地址需要多大的空間?。。?/p>
-
地址都是32個(gè)0/1組成的二進(jìn)制序列的話,那么存放這個(gè)地址需要的空間的大小就是4個(gè)字節(jié),所以指針變量的大小都是4個(gè)字節(jié)~~
-
同樣x64環(huán)境,64根地址線,地址就是64個(gè)0/1組成的二進(jìn)制序列,存放這樣的地址,需要8個(gè)字節(jié),所以指針變量的大小就是8個(gè)字節(jié)~~
-
如果不相信,我們在VS中試一下~~
#include <stdio.h>
//指針變量的大小取決于地址的大小
//32位平臺(tái)下地址是32個(gè)bit位(即4個(gè)字節(jié))
//64位平臺(tái)下地址是64個(gè)bit位(即8個(gè)字節(jié))
int main()
{
printf("%zd\n", sizeof(char*));
printf("%zd\n", sizeof(short*));
printf("%zd\n", sizeof(int*));
printf("%zd\n", sizeof(double*));
return 0;
}
- 32位下:
- 64位下:
- 俗話說,不要在門縫里看人,把人看扁了
- 今天,我要告訴你,不要在門縫里看指針,把
指針
看扁了~~
結(jié)論:
- 32位平臺(tái)下地址是32個(gè)bit位,指針變量大小是4個(gè)字節(jié)
- 64位平臺(tái)下地址是64個(gè)bit位,指針變量大小是8個(gè)字節(jié)
- 注意指針變量的大小和類型是無關(guān)的,只要指針類型的變量,在相同的平臺(tái)下,大小都是相同的。
4.1 指針變量類型的意義
-
指針變量的大小和類型無關(guān),只要是指針變量,在同一個(gè)平臺(tái)下,大小都是一樣的,為什么還要有各種各樣的指針類型呢?
-
其實(shí)指針類型是有特殊意義的,我們接下來繼續(xù)學(xué)習(xí)~~
4.2 指針的解引用
- 我們來看下面的兩段代碼,通過調(diào)試來進(jìn)行分析,觀察內(nèi)存的變化~~
代碼一:
#include <stdio.h>
int main()
{
int n = 0x11223344;
int* pi = &n;
*pi = 0;
return 0;
}
代碼二:
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = &n;
*pc = 0;
return 0;
}
- 我們按鍵盤上的
F10
,如果有的同學(xué)是筆記本,就在筆記本上按Fn+F10
,開始調(diào)試 - 打開調(diào)試->窗口->內(nèi)存->
- 這個(gè)時(shí)候我們發(fā)現(xiàn),怎么是倒著存的?不應(yīng)該是正的存嗎,這里就要涉及到一個(gè)概念,
大小端存儲(chǔ)
如果還有同學(xué)不知道的話可以看看這個(gè)章節(jié)->C生萬物 | 深度挖掘數(shù)據(jù)在計(jì)算機(jī)內(nèi)部的存儲(chǔ) - 我們回歸正題~~
- 我們把這個(gè)n的地址取出來放到pi變量里,然后解引用,把n的值改為0,我們可以看一下~~
- 可以看到,4個(gè)字節(jié)全部改為了0~~
現(xiàn)在再來看第二個(gè)代碼~~
- 我們可以看到我將n的地址放到
char*
類型的變量里,那能不能放的下? - 答案是能?。。倓偩驼f過,指針變量都是4個(gè)字節(jié),為什么放不下~~
- 然后我們繼續(xù)看,
*pc = 0
,我們這個(gè)是修改的幾個(gè)字節(jié)?
- 我們可以看到,它只修改了一個(gè)字節(jié),因?yàn)槭?code>char*的指針變量
結(jié)論:
- 指針類型是有意義的
- 指針類型是決定了指針在解引用操作時(shí)的權(quán)限,也就是一次解引用訪問幾個(gè)字節(jié),
char*
類型的指針解引用訪問1個(gè)字節(jié),int*
類型的指針一次訪問4個(gè)字節(jié)
4.3 指針±整數(shù)
- 我們先來看下面這一段代碼~~
#include <stdio.h>
int main()
{
int n = 0x11223344;
int* p = &n;
char* pc = &n;
printf("p = %p\n", p);
printf("p + 1 = %p\n", p + 1);
printf("pc = %p\n", pc);
printf("pc + 1 = %p\n", pc + 1);
return 0;
}
- 我們可以看出,
char*
類型的指針變量+1
跳過1個(gè)字節(jié),int*
類型的指針變量+1
跳過了4個(gè)字節(jié)。 - 這就是指針變量的類型差異帶來的變化。
結(jié)論:
- 指針類型是有意義的
- 指針類型決定了指針進(jìn)行+1/-1操作的時(shí)候一次跳過幾個(gè)字節(jié)
- 指針的類型決定了指針向前或者向后走一步有多大(距離)。
那有的同學(xué)會(huì)問,指針類型這些特點(diǎn),怎么是使用呢?
- 我們之前一個(gè)數(shù)組是用數(shù)組的下標(biāo)來訪問的,今天我們就用指針的方式訪問~~
我們先來回憶一下數(shù)組的方式~~
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//下標(biāo)的方式
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++) {
printf("%d ", arr[i]);
}
return 0;
}
- 可以看到,我們已經(jīng)將數(shù)組中的元素已經(jīng)打印出來了~~
我們再用指針的方式來訪問~~
- 我們將arr[0]的地址放入了指針p中,然后再通過
for
循環(huán)中*p
找到arr每個(gè)的元素,那找到一個(gè)元素,還想找下一個(gè)元素怎么辦?那就要加1,因?yàn)閜是整形指針,+1
跳過4個(gè)字節(jié),正好找到下一個(gè)元素
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//下標(biāo)的方式
int sz = sizeof(arr) / sizeof(arr[0]);
//int i = 0;
//for (i = 0; i < sz; i++) {
// printf("%d ", arr[i]);
//}
//指針的方式
int i = 0;
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d ", *p);
p = p + 1;
}
return 0;
}
- 我們可以看到,也全部訪問到了~~
- 那有的同學(xué)回說,我不想另外讓
p+1
,我直接*(p+i)
,這樣可以嗎?當(dāng)然可以!??!
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
//指針的方式
int i = 0;
int* p = &arr[0];
for (i = 0; i < sz; i++) {
printf("%d ", *(p + i));
}
return 0;
}
- 如果不懂的話,我們進(jìn)行畫圖理解
相信看完了上面,你對(duì)指針類型有一個(gè)對(duì)應(yīng)理解~~
五、const修飾指針
- 變量是可以修改的,如果把變量的地址交給一個(gè)指針變量,通過指針變量的也可以修改這個(gè)變量。
#include<stdio.h>
int main() {
int n = 100;
n = 200;
printf("%d\n", n);
return 0;
}
- 但是如果我們希望一個(gè)變量加上一些限制,不能被修改,怎么做呢?這就是const的作用。
int main() {
const int n = 100;
n = 200;//err
printf("%d\n", n);
return 0;
}
- 上述代碼中n是不能被修改的,其實(shí)n本質(zhì)是變量,只不過被const修飾后,在語法上加了限制,只要我們在代碼中對(duì)n就行修改,就不符合語法規(guī)則,就報(bào)錯(cuò),致使沒法直接修改n。
- 但是如果我們繞過n,使用n的地址,去修改n就能做到了,雖然這樣做是在打破語法規(guī)則。
- 那有人這樣想,這樣不能修改,那我繞個(gè)彎,把n的地址取出來,交給一個(gè)指針變量p,然后進(jìn)行修改~~
int main() {
const int n = 100;
int* p = &n;
*p = 200;
printf("%d\n", n);
return 0;
}
- 哎!它怎么修改了?就比如說有個(gè)門,門鎖上了,看見有個(gè)窗戶,我從窗戶進(jìn)去了,這個(gè)行為就是鉆窗戶行為~~
- 這里一個(gè)確實(shí)修改了,但是我們還是要思考一下,為什么
n
要被const
修飾呢?就是為了不能被修改,如果p
拿到n
的地址就能修改n
,這樣就打破了const
的限制,這是不合理的,所以應(yīng)該讓p拿到n的地址也不能修改n,那接下來怎么做呢?
5.1 const修飾指針變量
const修飾指針有兩種情況:
- const放在
*
的左邊 - const放在
*
的右邊
- 首先我們先將
const
放在*
的左邊~~
int main() {
int m = 100;
int n = 10;
const int* p = &n;
*p = 0;
p = &m;
printf("%d\n", n);
return 0;
}
- 這里可以看到
p
指向的值不能被修改 - p變量還是可以被修改的~~
- 然后我們先將
const
放在*
的左邊~~
int main() {
int m = 100;
int n = 10;
int* const p = &n;
*p = 0;
p = &m;
printf("%d\n", n);
return 0;
}
- 可以看到p指向的對(duì)象可以被修改~~
結(jié)論:
-
cons
t如果放在*
的左邊,修飾的是指針指向的內(nèi)容,保證指針指向的內(nèi)容不能通過指針來改變。但是指針變量本身的內(nèi)容可變。 -
const
如果放在*
的右邊,修飾的是指針變量本身,保證了指針變量的內(nèi)容不能修改,但是指針指向的內(nèi)容,可以通過指針改變。
- 那有的同學(xué)說我這樣放,可不可以?可以?。?!
int const * p = &n;
int *const p = &n;
- 這兩種方法都是可以的,我們只是關(guān)注的是
const
放在*
左邊還是右邊
六、指針運(yùn)算
6.1 指針± 整數(shù)
- 因?yàn)閿?shù)組在內(nèi)存中是連續(xù)存放的,只要知道第一個(gè)元素的地址,順藤摸瓜就能找到后面的所有元素。
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
#include <stdio.h>
//指針+- 整數(shù)
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));//p+i 這里就是指針+整數(shù)
}
return 0;
}
- 接下來我們就調(diào)試起來看一看
- 可以看到已經(jīng)放進(jìn)去了
- 這里
p+i
就訪問到每一個(gè)元素了~~
6.2 指針-指針
- 指針-指針是有前提的,指針和指針兩個(gè)指針的指向同一塊空間
- 我們先來看下面的這個(gè)一個(gè)代碼,輸出的結(jié)果是多少?
int main() {
int arr[10] = { 0 };
int ret = &arr[9] - &arr[0];
printf("%d", ret);
return 0;
}
- 答案是9~~,我們來分析一下:
-
也可以這樣理解,
&arr[0]+9
—>>>&arr[9]
-
結(jié)論: 指針-指針得到的絕對(duì)值,是指針和指針之間元素的個(gè)數(shù)
- 那有的同學(xué)說,那這個(gè)有什么用呢?
- 還記得有一個(gè)函數(shù)
strlen
嗎? -
strlen
的功能是求字符串長度,如果有同學(xué)不了解這個(gè)函數(shù)的話可以去cplusplus網(wǎng)站上看一下 - 我們來看一下怎么使用
int main()
{
char arr[] = "abcdef";
int len = strlen(arr);
printf("%d\n", len);
return 0;
}
- 可以看到它已經(jīng)求出字符串長度了~~
- 我們知道字符串的結(jié)束標(biāo)志是
\0
,讓我求長度,我就統(tǒng)計(jì)\0
之前出現(xiàn)字符的個(gè)數(shù)
- 那我們現(xiàn)在自己模擬實(shí)現(xiàn)一下這個(gè)函數(shù)~~
- 我們這里寫成
my_strlen
,我們把數(shù)組傳參,然后形參以指針接收,指針指向了數(shù)組首元素的地址,然后我們定義個(gè)計(jì)數(shù)器count,如果p!=\0
,count++
,p++
,最后返回count的個(gè)數(shù)~~
int my_strlen(char* p)
{
int count = 0;
while (*p != '\0')
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
- 可以看到也統(tǒng)計(jì)出來個(gè)數(shù)了~~
- 我們再寫出另一個(gè)版本,接下來繼續(xù)看~~
- 我們知道指針減去指針得到的是元素之間的個(gè)數(shù)
- 首先我記錄一下起始位置,讓p++,一直找到
\0
為止,最后返回p-s就得到了元素的個(gè)數(shù)~~ - 我們來看一下代碼和結(jié)果~~
#include <stdio.h>
int my_strlen(char* s)
{
char* p = s;
while (*p != '\0')
p++;
return p - s;
}
int main()
{
printf("%d\n", my_strlen("abc"));
return 0;
}
明白了上面的內(nèi)容,我們再來將一個(gè)指針的關(guān)系運(yùn)算~~
6.3 指針的關(guān)系運(yùn)算
- 所謂的關(guān)系運(yùn)算,就是比較大小
- 我們來看一下例子~~
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz) //指針的大小比較
{
printf("%d ", *p);
p++;
}
return 0;
}
- 這里我們比較的是兩個(gè)地址的大小關(guān)系~~
- 我們可以看到,也是可以打印出來的~~
七、野指針
- 概念: 野指針就是指針指向的位置是不可知的(隨機(jī)的、不正確的、沒有明確限制的)
7.1 野指針成因
指針未初始化:
#include <stdio.h>
int main()
{
int* p;//局部變量指針未初始化,默認(rèn)為隨機(jī)值
*p = 20;
return 0;
}
- 可以看到這里報(bào)了錯(cuò),如果你有看過我寫的函數(shù)的棧幀創(chuàng)建與銷毀就明白了,局部變量不初始化默認(rèn)里面放的是
cccccccc
- 所以一個(gè)局部變量不初始化的話,會(huì)得到一個(gè)隨機(jī)值~~
- 而隨便的一個(gè)地址,能解引用嗎?
不能?。?!
- 一塊空間你要想使用,你需要先申請(qǐng)拿到這塊空間
指針越界訪問:
- 我們先來看代碼
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = &arr[0];
int i = 0;
for (i = 0; i <= 11; i++)
{
//當(dāng)指針指向的范圍超出數(shù)組arr的范圍時(shí),p就是野指針
*(p++) = i;
}
return 0;
}
-
我們這里的
p++
是先執(zhí)行的,而p++
是先使用后++
-
我們這里可以看到,我把
arr[0]
的地址放入了*p
指針變量,然后我們進(jìn)行遍歷賦值,那我們這里判斷條件是不是就越界訪問了,超出了數(shù)組的范圍,當(dāng)指針指向的范圍超出數(shù)組arr的范圍時(shí),p就是野指針,這是很危險(xiǎn)的~~ -
我們這里還可以調(diào)試一下看看~~
- 可以看到,數(shù)組
arr
造到了破壞,而越界訪問的內(nèi)容也修改了,如此可見,這多么的危險(xiǎn)?。?!
7.2 指針指向的空間釋放
- 我們先來看一下下面的代碼~~
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int* p = test();
printf("%d\n", *p);
return 0;
}
- 這里創(chuàng)建了一個(gè)函數(shù)
test()
,返回一個(gè)地址,既然這個(gè)函數(shù)返回的是一個(gè)地址,那我用一個(gè)指針來接收,然后*p
這個(gè)地址 - 這個(gè)
n
在出這個(gè)函數(shù)的時(shí)候被銷毀了,這就會(huì)造成指針指向的空間釋放~~
八、如何規(guī)避野指針
8.1 指針初始化
- 如果明確知道指針指向哪里就直接賦值地址
- 如果不知道指針應(yīng)該指向哪里,可以給指針賦值
NULL
什么意思呢,我們用代碼來說~~
- 這里的p非常明確的指向了a,直接賦值~~
int main()
{
int a = 10;
int* p = &a;
return 0;
}
- 我們這里假設(shè)在這里創(chuàng)建了個(gè)
ptr
,我現(xiàn)在不用,但我可以在后面才會(huì)用,但是這個(gè)指針變量不能空著,這個(gè)時(shí)候我們要給他初始化NULL
int main()
{
int a = 10;
int* p = &a;
int* ptr = NULL;
return 0;
}
- NULL是什么呢?本質(zhì)上就是空指針,我們這個(gè)時(shí)候可以右鍵,轉(zhuǎn)到定義里看一下
-
那有的同學(xué)會(huì)說,那我直接給他賦值為0,可不可以呢?本質(zhì)上是可以的,但是,當(dāng)我們賦值為0了,有的時(shí)候會(huì)以為是一個(gè)整數(shù),而我們賦值為
NULL
的時(shí)候,那就很明顯了,一看就是空~~ -
NULL
是C語言中定義的一個(gè)標(biāo)識(shí)符常量,值是0,0也是地址,這個(gè)地址是無法使用的,讀寫該地址會(huì)報(bào)錯(cuò)。 -
這里我們也可以看到~~
8.2 小心指針越界
- 一個(gè)程序向內(nèi)存申請(qǐng)了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就是越界訪問
- 這個(gè)誰都幫不了你,只能自己小心,不要越界~~
- 一個(gè)程序員想寫bug,誰都攔不住~~
8.3 指針變量不再使用時(shí),及時(shí)置NULL,指針使用之前檢查有效性
- 當(dāng)指針變量指向一塊區(qū)域的時(shí)候,我們可以通過指針訪問該區(qū)域,后期不再使用這個(gè)指針訪問空間的時(shí)候,我們可以把該指針置為NULL。因?yàn)榧s定俗成的一個(gè)規(guī)則就是:只要是NULL指針就不去訪問,同時(shí)使用指針之前可以判斷指針是否為NULL。
- 我們可以把野指針想象成野狗,野狗放任不管是非常危險(xiǎn)的,所以我們可以找一棵樹把野狗拴起來,就相對(duì)安全了,給指針變量及時(shí)賦值為NULL,其實(shí)就類似把野狗栓前來,就是把野指針暫時(shí)管理起來。
- 不過野狗即使拴起來我們也要繞著走,不能去挑逗野狗,有點(diǎn)危險(xiǎn);對(duì)于指針也是,在使用之前,我們也要判斷是否為NULL,看看是不是被拴起來起來的野狗,如果是不能直接使用,如果不是我們再去使用。
- 下面我們來看一段代碼~~
- 這里我們創(chuàng)建了一個(gè)數(shù)組,然后用指針遍歷這個(gè)數(shù)組,當(dāng)遍歷完后,指針已經(jīng)指向了10的后面,指向了不屬于我們的空間
- 假設(shè)暫時(shí)不再使用p了,為了安全,我們可以把p賦值為
NULL
- 如果后面還會(huì)用到這個(gè)指針,我們再把這個(gè)p賦值,我們還可以再進(jìn)行判斷是否為
NULL
int main()
{
int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };
int* p = &arr[0];
for (int i = 0; i < 10; i++)
{
*(p++) = i;
}
//此時(shí)p已經(jīng)越界了,可以把p置為NULL
p = NULL;
//下次使用的時(shí)候,判斷p不為NULL的時(shí)候再使用
//...
p = &arr[0];//重新讓p獲得地址
if (p != NULL) //判斷
{
//...
}
return 0;
}
8.4 避免返回局部變量的地址
- 我們來看下面這一段代碼
- 首先我們先調(diào)用了一下
test
函數(shù),函數(shù)里創(chuàng)建了個(gè)數(shù)組,數(shù)組是局部變量,而我們返回了這個(gè)數(shù)組的地址,我們使用了一個(gè)指針變量p
來接收,當(dāng)test
函數(shù)返回的時(shí)候局部變量已經(jīng)被操作系統(tǒng)回收了,這就會(huì)造成野指針,如果有看過函數(shù)的棧幀的創(chuàng)建與銷毀的話就明白了 - 返回??臻g地址的問題,很容易造成野指針的問題~~
int* test()
{
//局部變量
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
//.....
return arr;
}
int main()
{
int* p = test();//p就是野指針
return 0;
}
九、assert斷言
- assert.h 頭文件定義了宏assert() ,用于在運(yùn)行時(shí)確保程序符合指定條件,如果不符合,就報(bào)錯(cuò)終止運(yùn)行。這個(gè)宏常常被稱為“斷言”。
assert(p != NULL);
- 上面代碼在程序運(yùn)行到這一行語句時(shí),驗(yàn)證變量
p
是否等于NULL
。如果確實(shí)不等于NULL ,程序繼續(xù)運(yùn)行,否則就會(huì)終止運(yùn)行,并且給出報(bào)錯(cuò)信息提示。 - 我們可以看到,什么都不會(huì)發(fā)生~~
#include <assert.h>
int main()
{
int a = 10;
int* p = &a;
assert(p != NULL);
return 0;
}
- 當(dāng)我們賦值為了一個(gè)空指針時(shí),可以看到會(huì)報(bào)錯(cuò)誤
- 通過這樣一個(gè)方式就可以攔住他~~
#include <assert.h>
int main()
{
int a = 10;
int* p = NULL;
assert(p != NULL);
return 0;
}
-
assert()
宏接受一個(gè)表達(dá)式作為參數(shù)。如果該表達(dá)式為真(返回值非零),assert()
不會(huì)產(chǎn)生任何作用,程序繼續(xù)運(yùn)行。如果該表達(dá)式為假(返回值為零),assert()
就會(huì)報(bào)錯(cuò),在標(biāo)準(zhǔn)錯(cuò)誤流stderr
中寫入一條錯(cuò)誤信息,顯示沒有通過的表達(dá)式,以及包含這個(gè)表達(dá)式的文件名和行號(hào)。 -
assert()
的使用對(duì)程序員是非常友好的,使用assert()
有幾個(gè)好處:它不僅能自動(dòng)標(biāo)識(shí)文件和出問題的行號(hào),還有一種無需更改代碼就能開啟或關(guān)閉assert() 的機(jī)制。如果已經(jīng)確認(rèn)程序沒有問題,不需要再做斷言,就在#include <assert.h>
語句的前面,定義一個(gè)宏NDEBUG
。
#define NDEBUG
#include <assert.h>
-
然后,重新編譯程序,編譯器就會(huì)禁用文件中所有的
assert()
語句。如果程序又出現(xiàn)問題,可以移除這條#define NDBUG
指令(或者把它注釋掉),再次編譯,這樣就重新啟用了assert()
語句。 -
assert()
的缺點(diǎn)是,因?yàn)橐肓祟~外的檢查,增加了程序的運(yùn)行時(shí)間。 -
一般我們可以在
debug
中使用,在release
版本中選擇禁用assert
就行,在VS這樣的集成開發(fā)環(huán)境中,在release
版本中,直接就是優(yōu)化掉了。這樣在debug
版本寫有利于程序員排查問題,在release
版本不影響用戶使用時(shí)程序的效率。
十、指針的使用和傳址調(diào)用
一直叫傳值調(diào)用,一種叫傳址調(diào)用,接下來我們繼續(xù)看~~
10.1 傳址調(diào)用
- 學(xué)習(xí)指針的目的是使用指針解決問題,那什么問題,非指針不可呢?
例如:寫一個(gè)函數(shù),交換兩個(gè)整型變量的值 - 一番思考后,我們可能寫出這樣的代碼:
#include <stdio.h>
void Swap1(int x, int y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交換前:a=%d b=%d\n", a, b);
Swap1(a, b);
printf("交換后:a=%d b=%d\n", a, b);
return 0;
}
當(dāng)我們運(yùn)行代碼,結(jié)果如下:
- 這里怎么沒有達(dá)到交換的效果?我們來調(diào)試看一下
- 我們可以看到只有x和y交換了,而a和b沒有交換
- 我們發(fā)現(xiàn)在main函數(shù)內(nèi)部,創(chuàng)建了a和b,a的地址是
0x0039f9f0
,b的地址是0x0039f9e4
,在調(diào)用Swap1
函數(shù)時(shí),將a和b傳遞給了Swap1
函數(shù),在Swap1
函數(shù)內(nèi)部創(chuàng)建了形參x和y接收a和b的值,但是x的地址是0x0039f90c
,y的地址是0x0039f910
,x和y確實(shí)接收到了a和b的值,不過x的地址和a的地址不一樣,y的地址和b的地址不一樣,相當(dāng)于x和y是獨(dú)立的空間,那么在Swap1
函數(shù)內(nèi)部交換x和y的值,自然不會(huì)影響a和b,當(dāng)Swap1
函數(shù)調(diào)用結(jié)束后回到main
函數(shù),a和b的沒法交換。Swap1
函數(shù)在使用的時(shí)候,是把變量本身直接傳遞給了函數(shù),這種調(diào)用函數(shù)的方式我們之前在函數(shù)的時(shí)候就知道了,這種叫傳值調(diào)用。
結(jié)論: 實(shí)參傳遞給形參的時(shí)候,形參會(huì)單獨(dú)創(chuàng)建一份臨時(shí)空間來接收實(shí)參,對(duì)形參的修改不影響實(shí)參。所以Swap是失敗的了。
10.2 傳址調(diào)用
- 那怎么辦呢?
- 我們現(xiàn)在要解決的就是當(dāng)調(diào)用Swap函數(shù)的時(shí)候,Swap函數(shù)內(nèi)部操作的就是main函數(shù)中的a和b,直接將a和b的值交換了。那么就可以使用指針了,在main函數(shù)中將a和b的地址傳遞給Swap函數(shù),Swap函數(shù)里邊通過地址間接的操作main函數(shù)中的a和b就好了。
#include <stdio.h>
void Swap2(int* px, int* py)
{
int tmp = 0;
tmp = *px;
*px = *py;
*py = tmp;
}
int main()
{
int a = 0;
int b = 0;
scanf("%d %d", &a, &b);
printf("交換前:a=%d b=%d\n", a, b);
Swap2(&a, &b);
printf("交換后:a=%d b=%d\n", a, b);
return 0;
}
首先看輸出結(jié)果:
- 我們可以看到實(shí)現(xiàn)成Swap2的方式,順利完成了任務(wù),這里調(diào)用Swap2函數(shù)的時(shí)候是將變量的地址傳遞給了函數(shù),這種函數(shù)調(diào)用方式叫:傳址調(diào)用。
十一、strlen的模擬實(shí)現(xiàn)
- 這個(gè)我們上面已經(jīng)實(shí)現(xiàn)過了,這次我們再寫的全一些~~
- 我們創(chuàng)建了一個(gè)字符數(shù)組,要求出這個(gè)字符串的長度,我們寫一個(gè)
my_strlen()
- 它傳參傳的是數(shù)組首元素的地址,我們用一個(gè)
*str
的指針來接收,我們這個(gè)函數(shù)期望這個(gè)字符串來修改嗎?不期望,這里我們再加上一個(gè)const
- 這里我們需要斷言
str
,確保指針的有效性 - 現(xiàn)在
str
指向a
的,當(dāng)str!='\0'
,str++
,計(jì)數(shù)器也++,最后返回計(jì)數(shù)器~~ - 求字符串的時(shí)候沒有負(fù)數(shù)吧,我們就可以設(shè)置成
size_t
- 現(xiàn)在軟件更加健壯了,也叫魯棒性~~
計(jì)數(shù)器方式文章來源:http://www.zghlxwxcb.cn/news/detail-714959.html
size_t my_strlen(const char* str)
{
size_t count = 0;
assert(str);
while (*str)
{
count++;
str++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
size_t len = my_strlen(arr);
printf("%zd\n", len);
return 0;
}
好了,指針的第一部分就到這里就結(jié)束了~~
如果有什么問題可以私信我或者評(píng)論里交流~~
感謝大家的收看,希望我的文章可以幫助到正在閱讀的你??????文章來源地址http://www.zghlxwxcb.cn/news/detail-714959.html
到了這里,關(guān)于C生萬物 | 從淺入深理解指針【第一部分】的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!