目錄
1. 內(nèi)存和地址
2. 指針變量和地址
3. 指針變量類型的意義
4. const修飾指針
5. 指針運(yùn)算
6. 野指針
7. assert斷言
8. 指針的使用和傳址調(diào)用
9. 數(shù)組名的理解
10. 使用指針訪問數(shù)組
11. 一維數(shù)組傳參的本質(zhì)
12. 冒泡排序
13. 二級(jí)指針
14. 指針數(shù)組
15. 指針數(shù)組模擬二維數(shù)組
16. 字符指針變量
17. 數(shù)組指針變量
18. 二維數(shù)組傳參的本質(zhì)
19. 函數(shù)指針變量
20. 函數(shù)指針數(shù)組
21. 轉(zhuǎn)移表
22. 回調(diào)函數(shù)是什么?
23. qsort使用舉例
24. qsort函數(shù)的模擬實(shí)現(xiàn)
25. sizeof和strlen的對(duì)比
1. 內(nèi)存和地址
1.1 內(nèi)存
在講內(nèi)存和地址之前,我們想有個(gè)生活中的案例:
假設(shè)有?棟宿舍樓,把你放在樓里,樓上有100個(gè)房間,但是房間沒有編號(hào),你的?個(gè)朋友來找你玩,如果想找到你,就得挨個(gè)房子去找,這樣效率很低,但是我們?nèi)绻鶕?jù)樓層和樓層的房間的情況,給每個(gè)房間編上號(hào),如:
有了房間號(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
其中,每個(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. 指針變量和地址
2.1 取地址操作符(&)
理解了內(nèi)存和地址的關(guān)系,我們?cè)倩氐紺語言,在C語言中創(chuàng)建變量其實(shí)就是向內(nèi)存申請(qǐng)空間,比如:
比如,上述的代碼就是創(chuàng)建了整型變量a,內(nèi)存中申請(qǐng)4個(gè)字節(jié),用于存放整數(shù)10,其中每個(gè)字節(jié)
都有地址,上圖中4個(gè)字節(jié)的地址分別是:
0x006FFD70
0x006FFD71
0x006FFD72
0x006FFD73
那我們?nèi)绾文艿玫絘的地址呢?
這?就得學(xué)習(xí)?個(gè)操作符(&)-取地址操作符
按照我畫圖的例子,會(huì)打印處理:006FFD70
&a取出的是a所占4個(gè)字節(jié)中地址較小的字節(jié)的地址。
雖然整型變量占用4個(gè)字節(jié),我們只要知道了第一個(gè)字節(jié)地址,順藤摸瓜訪問到4個(gè)字節(jié)的數(shù)據(jù)也是可行的。
2.2 指針變量和解引用操作符(*)
2.2.1 指針變量
那我們通過取地址操作符(&)拿到的地址是?個(gè)數(shù)值,比如:0x006FFD70,這個(gè)數(shù)值有時(shí)候也是需要存儲(chǔ)起來,方便后期再使用的,那我們把這樣的地址值存放在哪?呢?答案是:指針變量中。
比如:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;//取出a的地址并存儲(chǔ)到指針變量pa中
return 0;
}
指針變量也是?種變量,這種變量就是用來存放地址的,存放在指針變量中的值都會(huì)理解為地址。
2.2.2 如何拆解指針類型
我們看到pa的類型是 int* ,我們?cè)撊绾卫斫庵羔樀念愋湍兀?
int a = 10;
int * pa = &a;
這里pa左邊寫的是 int* , * 是在說明pa是指針變量,而前面的 int 是在說明pa指向的是整型(int)
類型的對(duì)象。
那如果有?個(gè)char類型的變量ch,ch的地址,要放在什么類型的指針變量中呢??
char ch = 'w';
pc = &ch;//pc 的類型怎么寫呢?
2.2.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ì)更加靈活,后期慢慢就能理解了。
2.3 指針變量的大小
前?的內(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é)。
#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;
}
結(jié)論:
? 32位平臺(tái)下地址是32個(gè)bit位,指針變量大小是4個(gè)字節(jié)
? 64位平臺(tái)下地址是64個(gè)bit位,指針變量大小是8個(gè)字節(jié)
? 注意指針變量的大小和類型是無關(guān)的,只要指針類型的變量,在相同的平臺(tái)下,大小都是相同的。
3. 指針變量類型的意義
指針變量的大小和類型?關(guān),只要是指針變量,在同?個(gè)平臺(tái)下,大小都是?樣的,為什么還要有各種各樣的指針類型呢?
其實(shí)指針類型是有特殊意義的,我們接下來繼續(xù)學(xué)習(xí)。
3.1 指針的解引用
對(duì)比,下面2段代碼,主要在調(diào)試時(shí)觀察內(nèi)存的變化
代碼1:
//代碼1
#include <stdio.h>
int main()
{
int n = 0x11223344;
int *pi = &n;
*pi = 0;
return 0;
}
?
?代碼2:
//代碼2
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
*pc = 0;
return 0;
}
調(diào)試我們可以看到,代碼1會(huì)將n的4個(gè)字節(jié)全部改為0,但是代碼2只是將n的第?個(gè)字節(jié)改為0。
結(jié)論:指針的類型決定了,對(duì)指針解引用的時(shí)候有多大的權(quán)限(?次能操作幾個(gè)字節(jié))。
比如: char* 的指針解引用就只能訪問?個(gè)字節(jié),而?int* 的指針的解引用就能訪問四個(gè)字節(jié)。
3.2 指針+-整數(shù)
先看?段代碼,調(diào)試觀察地址的變化。
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;
printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
代碼運(yùn)行的結(jié)果如下:
我們可以看出, char* 類型的指針變量+1跳過1個(gè)字節(jié), int* 類型的指針變量+1跳過了4個(gè)字節(jié)。
這就是指針變量的類型差異帶來的變化。
結(jié)論:指針的類型決定了指針向前或者向后走一步有多大(距離)。
3.3 void* 指針
在指針類型中有?種特殊的類型是 void* 類型的,可以理解為?具體類型的指針(或者叫泛型指
針),這種類型的指針可以?來接受任意類型地址。但是也有局限性, void* 類型的指針不能直接進(jìn)?指針的+-整數(shù)和解引用的運(yùn)算。
舉例:
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a;
return 0;
}
在上面的代碼中,將?個(gè)int類型的變量的地址賦值給?個(gè)char*類型的指針變量。編譯器給出了?個(gè)警告(如下圖),是因?yàn)轭愋筒患嫒荨6褂胿oid*類型就不會(huì)有這樣的問題。
使用void*類型的指針接收地址:
#include <stdio.h>
int main()
{
int a = 10;
void* pa = &a;
void* pc = &a;
*pa = 10;
*pc = 0;
return 0;
}
VS2022編譯代碼的結(jié)果:?
這?我們可以看到, void* 類型的指針可以接收不同類型的地址,但是無法直接進(jìn)行指針運(yùn)算。
那么 void* 類型的指針到底有什么用呢?
?般 void* 類型的指針是使?在函數(shù)參數(shù)的部分,用來接收不同類型數(shù)據(jù)的地址,這樣的設(shè)計(jì)可以
實(shí)現(xiàn)泛型編程的效果。使得?個(gè)函數(shù)來處理多種類型的數(shù)據(jù)。
4. const修飾指針
4.1 const修飾變量
變量是可以修改的,如果把變量的地址交給?個(gè)指針變量,通過指針變量的也可以修改這個(gè)變量。
但是如果我們希望?個(gè)變量加上?些限制,不能被修改,怎么做呢?這就是const的作用。
#include <stdio.h>
int main()
{
int m = 0;
m = 20;//m是可以修改的
const int n = 0;
n = 20;//n是不能被修改的
return 0;
}
上述代碼中n是不能被修改的,其實(shí)n本質(zhì)是變量,只不過被const修飾后,在語法上加了限制,只
要我們?cè)诖a中對(duì)n就?修改,就不符合語法規(guī)則,就報(bào)錯(cuò),致使沒法直接修改n。
但是如果我們繞過n,使用n的地址,去修改n就能做到了,雖然這樣做是在打破語法規(guī)則。
#include <stdio.h>
int main()
{
const int n = 0;
printf("n = %d\n", n);
int*p = &n;
*p = 20;
printf("n = %d\n", n);
return 0;
}
輸出結(jié)果:
我們可以看到這??個(gè)確實(shí)修改了,但是我們還是要思考?下,為什么n要被const修飾呢?就是
為了不能被修改,如果p拿到n的地址就能修改n,這樣就打破了const的限制,這是不合理的,所
以應(yīng)該讓p拿到n的地址也不能修改n,那接下來怎么做呢?
4.2 const修飾指針變量
我們看下面代碼,來分析
#include <stdio.h>
//代碼1
void test1()
{
int n = 10;
int m = 20;
int *p = &n;
*p = 20;//ok?
p = &m; //ok?
}
void test2()
{
//代碼2
int n = 10;
int m = 20;
const int* p = &n;
*p = 20;//ok?
p = &m; //ok?
}
void test3()
{
int n = 10;
int m = 20;
int *const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
void test4()
{
int n = 10;
int m = 20;
int const * const p = &n;
*p = 20; //ok?
p = &m; //ok?
}
int main()
{
//測試?const修飾的情況
test1();
//測試const放在*的左邊情況
test2();
//測試const放在*的右邊情況
test3();
//測試*的左右兩邊都有const
test4();
return 0;
}
結(jié)論:const修飾指針變量的時(shí)候
? const如果放在*的左邊,修飾的是指針指向的內(nèi)容,保證指針指向的內(nèi)容不能通過指針來改變。
但是指針變量本?的內(nèi)容可變。
? const如果放在*的右邊,修飾的是指針變量本?,保證了指針變量的內(nèi)容不能修改,但是指針指
向的內(nèi)容,可以通過指針改變。
5. 指針運(yùn)算
指針的基本運(yùn)算有三種,分別是:
? 指針+- 整數(shù)
? 指針-指針
? 指針的關(guān)系運(yùn)算
5.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;
}
?運(yùn)行結(jié)果:
5.2 指針- 指針?
//指針-指針
#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;
}
解析:?
5.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;
}
解析:?
6. 野指針
概念: 野指針就是指針指向的位置是不可知的(隨機(jī)的、不正確的、沒有明確限制的)
6.1 野指針成因
1. 指針未初始化
#include <stdio.h>
int main()
{
int *p;//局部變量指針未初始化,默認(rèn)為隨機(jī)值
*p = 20;
return 0;
}
2. 指針越界訪問
#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;
}
3. 指針指向的空間釋放
#include <stdio.h>
int* test()
{
int n = 100;
return &n;
}
int main()
{
int*p = test();//函數(shù)調(diào)用完后會(huì)釋放空間,所以指針指向的是空地址
printf("%d\n", *p);
return 0;
}
6.2 如何規(guī)避野指針
6.2.1 指針初始化
如果明確知道指針指向哪?就直接賦值地址,如果不知道指針應(yīng)該指向哪?,可以給指針賦值NULL.
NULL 是C語言中定義的?個(gè)標(biāo)識(shí)符常量,值是0,0也是地址,這個(gè)地址是無法使用的,讀寫該地址會(huì)報(bào)錯(cuò)。
初始化如下:
#include <stdio.h>
int main()
{
int num = 10;
int*p1 = #
int*p2 = NULL;
return 0;
}
6.2.2 小心指針越界
?個(gè)程序向內(nèi)存申請(qǐng)了哪些空間,通過指針也就只能訪問哪些空間,不能超出范圍訪問,超出了就
是越界訪問。
6.2.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,看看是不是被拴起來起來的野狗,如果是不能直接使用,如果不是我們?cè)偃ナ褂谩?/span>
int main()
{
int arr[10] = {1,2,3,4,5,67,7,8,9,10};
int *p = &arr[0];
for(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;
}
6.2.4 避免返回局部變量的地址
如造成野指針的第3個(gè)例子,不要返回局部變量的地址。
7. 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ò)信息提示。
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í)程序的效率。
8. 指針的使用和傳址調(diào)用
8.1 strlen的模擬實(shí)現(xiàn)
庫函數(shù)strlen的功能是求字符串長度,統(tǒng)計(jì)的是字符串中 \0 之前的字符的個(gè)數(shù)。
函數(shù)原型如下:
size_t strlen ( const char * str );
參數(shù)str接收?個(gè)字符串的起始地址,然后開始統(tǒng)計(jì)字符串中 \0 之前的字符個(gè)數(shù),最終返回長度。
如果要模擬實(shí)現(xiàn)只要從起始地址開始向后逐個(gè)字符的遍歷,只要不是 \0 字符,計(jì)數(shù)器就+1,這樣直到 \0 就停止。
參考代碼如下:
int my_strlen(const char * str)
{
int count = 0;
assert(str);
while(*str)
{
count++;
str++;
}
return count;
}
int main()
{
int len = my_strlen("abcdef");
printf("%d\n", len);
return 0;
}
8.2 傳值調(diào)用和傳址調(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é)果如下:
我們發(fā)現(xiàn)其實(shí)沒產(chǎn)生交換的效果,這是為什么呢?
調(diào)試?下,試試呢?
?
我們發(fā)現(xiàn)在main函數(shù)內(nèi)部,創(chuàng)建了a和b,a的地址是0x00cffdd0,b的地址是0x00cffdc4,在調(diào)用
Swap1函數(shù)時(shí),將a和b傳遞給了Swap1函數(shù),在Swap1函數(shù)內(nèi)部創(chuàng)建了形參x和y接收a和b的值,
但是x的地址是0x00cffcec,y的地址是0x00cffcf0,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是失敗的了。
那怎么辦呢?
我們現(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,并達(dá)到交換的效果就好了。
#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)用。
傳址調(diào)用,可以讓函數(shù)和主調(diào)函數(shù)之間建立真正的聯(lián)系,在函數(shù)內(nèi)部可以修改主調(diào)函數(shù)中的變量;所以未來函數(shù)中只是需要主調(diào)函數(shù)中的變量值來實(shí)現(xiàn)計(jì)算,就可以采用傳值調(diào)用。如果函數(shù)內(nèi)部要修改主調(diào)函數(shù)中的變量的值,就需要傳址調(diào)用。
9. 數(shù)組名的理解
在上?個(gè)章節(jié)我們?cè)谑褂弥羔樤L問數(shù)組的內(nèi)容時(shí),有這樣的代碼:
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];
這?我們使用?&arr[0] 的方式拿到了數(shù)組第?個(gè)元素的地址,但是其實(shí)數(shù)組名本來就是地址,而且
是數(shù)組首元素的地址,我們來做個(gè)測試。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
return 0;
}
輸出結(jié)果:
我們發(fā)現(xiàn)數(shù)組名和數(shù)組首元素的地址打印出的結(jié)果一模一樣,數(shù)組名就是數(shù)組首元素(第?個(gè)元素)
的地址。
這時(shí)候有同學(xué)會(huì)有疑問?數(shù)組名如果是數(shù)組首元素的地址,那下面的代碼怎么理解呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", sizeof(arr));
return 0;
}
輸出的結(jié)果是:
如果arr是數(shù)組首元素的地址,那輸出應(yīng)該的應(yīng)該是4/8才對(duì)。
其實(shí)數(shù)組名就是數(shù)組首元素(第?個(gè)元素)的地址是對(duì)的,但是有兩個(gè)例外:
? sizeof(數(shù)組名),sizeof中單獨(dú)放數(shù)組名,這?的數(shù)組名表示整個(gè)數(shù)組,計(jì)算的是整個(gè)數(shù)組的大小,單位是字節(jié)
? &數(shù)組名,這?的數(shù)組名表示整個(gè)數(shù)組,取出的是整個(gè)數(shù)組的地址(整個(gè)數(shù)組的地址和數(shù)組首元素的地址是有區(qū)別的)
除此之外,任何地方使用數(shù)組名,數(shù)組名都表示首元素的地址。
這時(shí)有好奇的同學(xué),再試?下這個(gè)代碼:
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("arr = %p\n", arr);
printf("&arr = %p\n", &arr);
return 0;
}
運(yùn)行結(jié)果:
三個(gè)打印結(jié)果?模?樣,這時(shí)候又納悶了,那arr和&arr有啥區(qū)別呢?
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1);
return 0;
}
運(yùn)行結(jié)果:
這?我們發(fā)現(xiàn)&arr[0]和&arr[0]+1相差4個(gè)字節(jié),arr和arr+1 相差4個(gè)字節(jié),是因?yàn)?amp;arr[0] 和 arr 都是首元素的地址,+1就是跳過?個(gè)元素。
但是&arr 和 &arr+1相差40個(gè)字節(jié),這就是因?yàn)?amp;arr是數(shù)組的地址,+1 操作是跳過整個(gè)數(shù)組的。
到這?大家應(yīng)該搞清楚數(shù)組名的意義了吧。
數(shù)組名是數(shù)組首元素的地址,但是有2個(gè)例外。
10. 使用指針訪問數(shù)組
有了前面知識(shí)的?持,再結(jié)合數(shù)組的特點(diǎn),我們就可以很方便的使用指針訪問數(shù)組了。
#include <stdio.h>
int main()
{
int arr[10] = {0};
//輸?
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//輸?
int* p = arr;
for(i=0; i<sz; i++)
{
scanf("%d", p+i);
//scanf("%d", arr+i);//也可以這樣寫
}
//輸出
for(i=0; i<sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
這個(gè)代碼搞明白后,我們?cè)僭?下,如果我們?cè)俜治?下,數(shù)組名arr是數(shù)組首元素的地址,可以賦值給p,其實(shí)數(shù)組名arr和p在這?是等價(jià)的。那我們可以使用arr[i]可以訪問數(shù)組的元素,那p[i]是否也可以訪問數(shù)組呢?
#include <stdio.h>
int main()
{
int arr[10] = {0};
//輸?
int i = 0;
int sz = sizeof(arr)/sizeof(arr[0]);
//輸?
int* p = arr;
for(i=0; i<sz; i++)
{
scanf("%d", p+i);
//scanf("%d", arr+i);//也可以這樣寫
}
//輸出
for(i=0; i<sz; i++)
{
printf("%d ", p[i]);
}
return 0;
}
在第18行的地方,將*(p+i)換成p[i]
也是能夠正常打印的,所以本質(zhì)上p[i] 是等價(jià)于 *(p+i)。
同理arr[i] 應(yīng)該等價(jià)于 *(arr+i),數(shù)組元素的訪問在編譯器處理的時(shí)候,也是轉(zhuǎn)換成首元素的地址
+偏移量求出元素的地址,然后解引用來訪問的。
11. ?維數(shù)組傳參的本質(zhì)
數(shù)組我們學(xué)過了,之前也講了,數(shù)組是可以傳遞給函數(shù)的,這個(gè)小節(jié)我們討論?下數(shù)組傳參的本質(zhì)。
首先從?個(gè)問題開始,我們之前都是在函數(shù)外部計(jì)算數(shù)組的元素個(gè)數(shù),那我們可以把函數(shù)傳給?個(gè)函數(shù)后,函數(shù)內(nèi)部求數(shù)組的元素個(gè)數(shù)嗎?
#include <stdio.h>
void test(int arr[])
{
int sz2 = sizeof(arr)/sizeof(arr[0]);
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int sz1 = sizeof(arr)/sizeof(arr[0]);
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
輸出的結(jié)果:
我們發(fā)現(xiàn)在函數(shù)內(nèi)部是沒有正確獲得數(shù)組的元素個(gè)數(shù)。
這就要學(xué)習(xí)數(shù)組傳參的本質(zhì)了,上個(gè)小節(jié)我們學(xué)習(xí)了:數(shù)組名是數(shù)組?元素的地址;那么在數(shù)組傳參的時(shí)候,傳遞的是數(shù)組名,也就是說本質(zhì)上數(shù)組傳參本質(zhì)上傳遞的是數(shù)組?元素的地址。
所以函數(shù)形參的部分理論上應(yīng)該使用指針變量來接收首元素的地址。那么在函數(shù)內(nèi)部我們寫 sizeof(arr) 計(jì)算的是?個(gè)地址的大?。▎挝蛔止?jié))而不是數(shù)組的大?。▎挝蛔止?jié))。正是因?yàn)楹?數(shù)的參數(shù)部分是本質(zhì)是指針,所以在函數(shù)內(nèi)部是沒辦法求的數(shù)組元素個(gè)數(shù)的。
void test(int arr[])//參數(shù)寫成數(shù)組形式,本質(zhì)上還是指針
{
printf("%d\n", sizeof(arr));
}
void test(int* arr)//參數(shù)寫成指針形式
{
printf("%d\n", sizeof(arr));//計(jì)算?個(gè)指針變量的??
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
test(arr);
return 0;
}
總結(jié):?維數(shù)組傳參,形參的部分可以寫成數(shù)組的形式,也可以寫成指針的形式。
12. 冒泡排序
冒泡排序的核心思想就是:兩兩相鄰的元素進(jìn)行比較。
方法一:
//?法1
void bubble_sort(int arr[], int sz)//參數(shù)接收數(shù)組元素個(gè)數(shù)
{
int i = 0;
for(i=0; i<sz-1; i++)
{
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
方法二:
//?法2 - 優(yōu)化
void bubble_sort(int arr[], int sz)//參數(shù)接收數(shù)組元素個(gè)數(shù)
{
int i = 0;
for(i=0; i<sz-1; i++)
{
int flag = 1;//假設(shè)這?趟已經(jīng)有序了
int j = 0;
for(j=0; j<sz-i-1; j++)
{
if(arr[j] > arr[j+1])
{
flag = 0;//發(fā)?交換就說明,?序
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
if(flag == 1)//這?趟沒交換就說明已經(jīng)有序,后續(xù)?序排序了
break;
}
}
int main()
{
int arr[] = {3,1,7,5,8,9,0,2,4,6};
int sz = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, sz);
for(i=0; i<sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
13. 二級(jí)指針
指針變量也是變量,是變量就有地址,那指針變量的地址存放在哪??
這就是二級(jí)指針 。??
對(duì)于二級(jí)指針的運(yùn)算有:
? *ppa 通過對(duì)ppa中的地址進(jìn)行解引用,這樣找到的是 pa , *ppa 其實(shí)訪問的就是 pa .
int b = 20;
*ppa = &b;//等價(jià)于 pa = &b;
?? **ppa 先通過 *ppa 找到 pa ,然后對(duì) pa 進(jìn)行解引用操作: *pa ,那找到的是 a .
**ppa = 30;
//等價(jià)于*pa = 30;
//等價(jià)于a = 30;
14. 指針數(shù)組
指針數(shù)組是指針還是數(shù)組?
我們類比?下,整型數(shù)組,是存放整型的數(shù)組,字符數(shù)組是存放字符的數(shù)組。
那指針數(shù)組呢?是存放指針的數(shù)組。
15. 指針數(shù)組模擬二維數(shù)組?
#include <stdio.h>
int main()
{
int arr1[] = {1,2,3,4,5};
int arr2[] = {2,3,4,5,6};
int arr3[] = {3,4,5,6,7};
//數(shù)組名是數(shù)組?元素的地址,類型是int*的,就可以存放在parr數(shù)組中
int* parr[3] = {arr1, arr2, arr3};
int i = 0;
int j = 0;
for(i=0; i<3; i++)
{
for(j=0; j<5; j++)
{
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
parr[i]是訪問parr數(shù)組的元素,parr[i]找到的數(shù)組元素指向了整型?維數(shù)組,parr[i][j]就是整型?維數(shù)組中的元素。
上述的代碼模擬出二維數(shù)組的效果,實(shí)際上并非完全是二維數(shù)組,因?yàn)槊恳恍胁⒎鞘沁B續(xù)的。
16. 字符指針變量
在指針的類型中我們知道有?種指針類型為字符指針 char* ;
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
還有一種使用方式如下:
int main()
{
const char* pstr = "hello bit.";//這?是把?個(gè)字符串放到pstr指針變量?了嗎?
printf("%s\n", pstr);
return 0;
}
代碼 const char* pstr = "hello bit."; 特別容易讓同學(xué)以為是把字符串 hello bit 放到字符指針 pstr ?了,但是本質(zhì)是把字符串 hello bit. ?字符的地址放到了pstr中。
上面代碼的意思是把?個(gè)常量字符串的首字符 h 的地址存放到指針變量 pstr 中。
《劍指offer》中收錄了?道和字符串相關(guān)的筆試題,我們?起來學(xué)習(xí)?下:
#include <stdio.h>
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char *str3 = "hello bit.";
const char *str4 = "hello bit.";
if(str1 ==str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if(str3 ==str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
運(yùn)行結(jié)果:
解析:?
這?str3和str4指向的是?個(gè)同?個(gè)常量字符串。C/C++會(huì)把常量字符串存儲(chǔ)到單獨(dú)的?個(gè)內(nèi)存區(qū)域,當(dāng)幾個(gè)指針指向同?個(gè)字符串的時(shí)候,他們實(shí)際會(huì)指向同?塊內(nèi)存。但是用相同的常量字符串去初始化不同的數(shù)組的時(shí)候就會(huì)開辟出不同的內(nèi)存塊。所以str1和str2不同,str3和str4相同。
17. 數(shù)組指針變量
17.1 數(shù)組指針變量是什么?
之前我們學(xué)習(xí)了指針數(shù)組,指針數(shù)組是?種數(shù)組,數(shù)組中存放的是地址(指針)。
數(shù)組指針變量是指針變量?還是數(shù)組?
答案是:指針變量。
我們已經(jīng)熟悉:
? 整形指針變量: int * pint; 存放的是整形變量的地址,能夠指向整形數(shù)據(jù)的指針。
? 浮點(diǎn)型指針變量: float * pf; 存放浮點(diǎn)型變量的地址,能夠指向浮點(diǎn)型數(shù)據(jù)的指針。
那數(shù)組指針變量應(yīng)該是:存放的應(yīng)該是數(shù)組的地址,能夠指向數(shù)組的指針變量。
下面代碼哪個(gè)是數(shù)組指針變量?
int *p1[10];
int (*p2)[10];
思考?下:p1, p2分別是什么?
數(shù)組指針變量
int (*p)[10];
解釋:p先和*結(jié)合,說明p是?個(gè)指針變量變量,然后指著指向的是?個(gè)大小為10個(gè)整型的數(shù)組。所以p是?個(gè)指針,指向?個(gè)數(shù)組,叫 數(shù)組指針。
這?要注意:[]的優(yōu)先級(jí)要高于*號(hào)的,所以必須加上()來保證p先和*結(jié)合。
17.2 數(shù)組指針變量怎么初始化
數(shù)組指針變量是用來存放數(shù)組地址的,那怎么獲得數(shù)組的地址呢?就是我們之前學(xué)習(xí)的&數(shù)組名 。
int arr[10] = {0};
&arr;//得到的就是數(shù)組的地址
如果要存放個(gè)數(shù)組的地址,就得存放在數(shù)組指針變量中,如下:
int(*p)[10] = &arr;
我們調(diào)試也能看到 &arr 和 p 的類型是完全?致的。
數(shù)組指針類型解析:
18. 二維數(shù)組傳參的本質(zhì)
有了數(shù)組指針的理解,我們就能夠講?下二維數(shù)組傳參的本質(zhì)了。
過去我們有?個(gè)二維數(shù)組的需要傳參給?個(gè)函數(shù)的時(shí)候,我們是這樣寫的:
#include <stdio.h>
void test(int a[3][5], int r, int c)
{
int i = 0;
int j = 0;
for(i=0; i<r; i++)
{
for(j=0; j<c; j++)
{
printf("%d ", a[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);
return 0;
}
這?實(shí)參是二維數(shù)組,形參也寫成二維數(shù)組的形式,那還有什么其他的寫法嗎?
首先我們?cè)俅卫斫?下二維數(shù)組,二維數(shù)組起始可以看做是每個(gè)元素是?維數(shù)組的數(shù)組,也就是二維數(shù)組的每個(gè)元素是?個(gè)?維數(shù)組。那么二維數(shù)組的?元素就是第一行,是個(gè)?維數(shù)組。
如下圖:
所以,根據(jù)數(shù)組名是數(shù)組?元素的地址這個(gè)規(guī)則,二維數(shù)組的數(shù)組名表示的就是第一行的地址,是?維數(shù)組的地址。根據(jù)上?的例子,第一行的?維數(shù)組的類型就是 int [5] ,所以第一行的地址的類型就是數(shù)組指針類型 int(*)[5] 。那就意味著二維數(shù)組傳參本質(zhì)上也是傳遞了地址,傳遞的是第? 行這個(gè)?維數(shù)組的地址,那么形參也是可以寫成指針形式的。如下:
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for(i=0; i<r; i++)
{
for(j=0; j<c; j++)
{
printf("%d ", *(*(p+i)+j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};
test(arr, 3, 5);
return 0;
}
?總結(jié):二維數(shù)組傳參,形參的部分可以寫成數(shù)組,也可以寫成指針形式。
19. 函數(shù)指針變量
19.1 函數(shù)指針變量的創(chuàng)建
什么是函數(shù)指針變量呢?
根據(jù)前面學(xué)習(xí)整型指針,數(shù)組指針的時(shí)候,我們的類比關(guān)系,我們不難得出結(jié)論:
函數(shù)指針變量應(yīng)該是用來存放函數(shù)地址的,未來通過地址能夠調(diào)用函數(shù)的。
那么函數(shù)是否有地址呢?
我們做個(gè)測試:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
輸出結(jié)果如下:
確實(shí)打印出來了地址,所以函數(shù)是有地址的,函數(shù)名就是函數(shù)的地址,當(dāng)然也可以通過 &函數(shù)名 的方式獲得函數(shù)的地址。
如果我們要將函數(shù)的地址存放起來,就得創(chuàng)建函數(shù)指針變量咯,函數(shù)指針變量的寫法其實(shí)和數(shù)組指針非常類似。如下:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y寫上或者省略都是可以的
?
19.2 函數(shù)指針變量的使用
通過函數(shù)指針調(diào)用指針指向的函數(shù)。
#include <stdio.h>
int Add(int x, int y)
{
return x+y;
}
int main()
{
int(*pf3)(int, int) = Add;
printf("%d\n", (*pf3)(2, 3));
printf("%d\n", pf3(3, 5));
return 0;
}
輸出結(jié)果:
19.2.1 typedef關(guān)鍵字
typedef 是用來類型重命名的,可以將復(fù)雜的類型,簡單化。
比如,你覺得 unsigned int 寫起來不方便,如果能寫成 uint 就方便多了,那么我們可以使用:
typedef unsigned int uint;
//將unsigned int 重命名為uint
如果是指針類型,能否重命名呢?其實(shí)也是可以的,比如,將 int* 重命名為 ptr_t ,這樣寫:
typedef int* ptr_t;
但是對(duì)于數(shù)組指針和函數(shù)指針稍微有點(diǎn)區(qū)別:
比如我們有數(shù)組指針類型 int(*)[5] ,需要重命名為 parr_t ,那可以這樣寫:
typedef int(*parr_t)[5]; //新的類型名必須在*的右邊
函數(shù)指針類型的重命名也是?樣的,?如,將 void(*)(int) 類型重命名為 pf_t ,就可以這樣寫:
typedef void(*pfun_t)(int);//新的類型名必須在*的右邊
那么要簡化代碼2,可以這樣寫:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
20. 函數(shù)指針數(shù)組
數(shù)組是?個(gè)存放相同類型數(shù)據(jù)的存儲(chǔ)空間,我們已經(jīng)學(xué)習(xí)了指針數(shù)組,
比如:
int *arr[10];
//數(shù)組的每個(gè)元素是int*
那要把函數(shù)的地址存到?個(gè)數(shù)組中,那這個(gè)數(shù)組就叫函數(shù)指針數(shù)組,那函數(shù)指針的數(shù)組如何定義呢?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是:parr1
parr1 先和 [] 結(jié)合,說明 parr1是數(shù)組,數(shù)組的內(nèi)容是什么呢?
是 int (*)() 類型的函數(shù)指針。
21. 轉(zhuǎn)移表
函數(shù)指針數(shù)組的用途:轉(zhuǎn)移表
舉例:計(jì)算器的?般實(shí)現(xiàn):
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf("請(qǐng)選擇:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y);
ret = add(x, y);
printf("ret = %d\n", ret);
break;
case 2:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y);
ret = sub(x, y);
printf("ret = %d\n", ret);
break;
case 3:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y);
ret = mul(x, y);
printf("ret = %d\n", ret);
break;
case 4:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y);
ret = div(x, y);
printf("ret = %d\n", ret);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("選擇錯(cuò)誤\n");
break;
}
} while (input);
return 0;
}
使用函數(shù)指針數(shù)組的實(shí)現(xiàn):
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //轉(zhuǎn)移表
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 3:mul 4:div \n");
printf(" 0:exit \n");
printf("*************************\n");
printf( "請(qǐng)選擇:" );
scanf("%d", &input);
if ((input <= 4 && input >= 1))
{
printf( "輸?操作數(shù):" );
scanf( "%d %d", &x, &y);
ret = (*p[input])(x, y);
printf( "ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出計(jì)算器\n");
}
else
{
printf( "輸?有誤\n" );
}
}while (input);
return 0;
}
22. 回調(diào)函數(shù)是什么?
回調(diào)函數(shù)就是?個(gè)通過函數(shù)指針調(diào)用的函數(shù)。
如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另?個(gè)函數(shù),當(dāng)這個(gè)指針被用來調(diào)用其所指向的函數(shù) 時(shí),被調(diào)用的函數(shù)就是回調(diào)函數(shù)?;卣{(diào)函數(shù)不是由該函數(shù)的實(shí)現(xiàn)?直接調(diào)用,?是在特定的事件或條件發(fā)生時(shí)由另外的一方調(diào)用的,用于對(duì)該事件或條件進(jìn)?響應(yīng)。
第13講中我們寫的計(jì)算機(jī)的實(shí)現(xiàn)的代碼中,紅?框中的代碼是重復(fù)出現(xiàn)的,其中雖然執(zhí)?計(jì)算的邏輯是區(qū)別的,但是輸?輸出操作是冗余的,有沒有辦法,簡化?些呢?
因?yàn)榧t?框中的代碼,只有調(diào)用函數(shù)的邏輯是有差異的,我們可以把調(diào)?的函數(shù)的地址以參數(shù)的形式傳遞過去,使用函數(shù)指針接收,函數(shù)指針指向什么函數(shù)就調(diào)?什么函數(shù),這?其實(shí)使?的就是回調(diào)函數(shù)的功能。
使用回調(diào)函數(shù)改造前:
//使?回調(diào)函數(shù)改造前
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
int main()
{
int x, y;
int input = 1;
int ret = 0;
do
{
printf("******************
printf(" 1:add
printf(" 3:mul
printf("******************
printf("請(qǐng)選擇:");
scanf("%d", &input);
switch (input)
{
case 1:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y)
ret = add(x, y);
printf("ret = %d\n", r
break;
case 2:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y)
ret = sub(x, y);
printf("ret = %d\n", r
break;
case 3:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y)
ret = mul(x, y);
printf("ret = %d\n", r
break;
case 4:
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y)
ret = div(x, y);
printf("ret = %d\n", r
break;
case 0:
printf("退出程序\n");
break;
default:
printf("選擇錯(cuò)誤\n");
break;
}
} while (input);
return 0;
}
使用回調(diào)函數(shù)改造后:
//使?回到函數(shù)改造后
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int mul(int a, int b)
{
return a * b;
}
int div(int a, int b)
{
return a / b;
}
void calc(int(*pf)(int, int))
{
int ret = 0;
int x, y;
printf("輸?操作數(shù):");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
do
{
printf("******************
printf(" 1:add
printf(" 3:mul
printf("******************
printf("請(qǐng)選擇:");
scanf("%d", &input);
switch (input)
{
case 1:
calc(add);
break;
case 2:
calc(sub);
break;
case 3:
calc(mul);
break;
case 4:
calc(div);
break;
case 0:
printf("退出程序\n");
break;
default:
printf("選擇錯(cuò)誤\n");
break;
}
} while (input);
return 0;
}
23. qsort使用舉例
23.1 使用qsort函數(shù)排序整型數(shù)據(jù)
#include <stdio.h>
//qosrt函數(shù)的使?者得實(shí)現(xiàn)?個(gè)?較函數(shù)
int int_cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
23.2 使用qsort排序結(jié)構(gòu)數(shù)據(jù)
struct Stu //學(xué)?
{
char name[20];//名字
int age;//年齡
};
//假設(shè)按照年齡來?較
int cmp_stu_by_age(const void* e1, const void* e2)
{
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
//strcmp - 是庫函數(shù),是專??來?較兩個(gè)字符串的??的
//假設(shè)按照名字來?較
int cmp_stu_by_name(const void* e1, const void* e2)
{
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
//按照年齡來排序
void test2()
{
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_age);
}
//按照名字來排序
void test3()
{
struct Stu s[] = { {"zhangsan", 20}, {"lisi", 30}, {"wangwu", 15} };
int sz = sizeof(s) / sizeof(s[0]);
qsort(s, sz, sizeof(s[0]), cmp_stu_by_name);
}
int main()
{
test2();
test3();
return 0;
}
24. qsort函數(shù)的模擬實(shí)現(xiàn)
使用回調(diào)函數(shù),模擬實(shí)現(xiàn)qsort(采用冒泡的方式)。
注意:這?第?次使用?void* 的指針,講解 void* 的作用。
-
1.void指針是一種特別的指針
-
2.任何指針都可以賦值給void指針
-
3.void指針賦值給其他類型的指針時(shí)都要進(jìn)行轉(zhuǎn)換
-
4.void指針不能解引用
#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{
int i = 0;
for (i = 0; i< size; i++)
{
char tmp = *((char *)p1 + i);
*(( char *)p1 + i) = *((char *) p2 + i);
*(( char *)p2 + i) = tmp;
}
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
int i = 0;
int j = 0;
for (i = 0; i< count - 1; i++)
{
for (j = 0; j<count-i-1; j++)
{
if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
{
_swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
}
}
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
25. sizeof和strlen的對(duì)比
25.1 sizeof
在學(xué)習(xí)操作符的時(shí)候,我們學(xué)習(xí)了 sizeof , sizeof 計(jì)算變量所占內(nèi)存內(nèi)存空間大小的,單位是字節(jié),如果操作數(shù)是類型的話,計(jì)算的是使用類型創(chuàng)建的變量所占內(nèi)存空間的大小。
sizeof 只關(guān)注占用內(nèi)存空間的大小,不在乎內(nèi)存中存放什么數(shù)據(jù)。
例如:
#inculde <stdio.h>
int main()
{
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof a);
printf("%d\n", sizeof(int));
return 0;
}
運(yùn)行結(jié)果:
文章來源:http://www.zghlxwxcb.cn/news/detail-839843.html
25.2 strlen
strlen 是C語言庫函數(shù),功能是求字符串長度。函數(shù)原型如下:
size_t strlen ( const char * str );
統(tǒng)計(jì)的是從 strlen 函數(shù)的參數(shù) str 中這個(gè)地址開始向后, \0 之前字符串中字符的個(gè)數(shù)。
strlen 函數(shù)會(huì)?直向后找 \0 字符,直到找到為?,所以可能存在越界查找。
#include <stdio.h>
int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1));
printf("%d\n", strlen(arr2));
printf("%d\n", sizeof(arr1));
printf("%d\n", sizeof(arr1));
return 0;
}
25.3 sizeof 和 strlen的對(duì)比
sizeof
1. sizeof是操作符
2. sizeof計(jì)算操作數(shù)所占內(nèi)存的大小,單位是字節(jié)
3. 不關(guān)注內(nèi)存中存放什么數(shù)據(jù)
strlen
1. strlen是庫函數(shù),使用需要包含頭文件 string.h
2. srtlen是求字符串長度的,統(tǒng)計(jì)的是 \0 之前字符的隔個(gè)數(shù)
3. 關(guān)注內(nèi)存中是否有 \0 ,如果沒有 \0 ,就會(huì)持續(xù)往后找,可能會(huì)越界
文章來源地址http://www.zghlxwxcb.cn/news/detail-839843.html
PS:看到這里了,碼字不易,給個(gè)一鍵三連鼓勵(lì)一下吧!有不足或者錯(cuò)誤之處歡迎在評(píng)論區(qū)指出!?
到了這里,關(guān)于深入理解指針——C語言的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!