進階語法
指針與數組
#include <stdio.h>
int main(){
int arr[5]={1,2,3,4,5};
// 依次打印數組每個元素的地址
for (int i = 0; i < 5; i++){
printf("p: %x\n",&arr[i]);
}
return 0;
}
打印結果
p: 22fe30
p: 22fe34
p: 22fe38
p: 22fe3c
p: 22fe40
由上例可驗證,數組的內存空間是連在一起的,它的第一個元素地址是0x22fe30
,第二個元素的地址是0x22fe34
,緊隨其後。因爲是int
數組,每個元素都需要佔用4個字節空間,因此地址的間隔也是4。
指針的算術運算
#include <stdio.h>
int main(){
int arr[5]={1,2,3,4,5};
// 聲明指針p,指向數組的首元素
int *p = &arr[0];
// 將指針變量加1,表示偏移一個單位
printf("arr[0]=%d address=%x\n",*p, p);
printf("arr[1]=%d address=%x\n",*(p + 1), (p+1));
printf("arr[2]=%d address=%x\n",*(p + 2), (p+2));
return 0;
}
打印結果:
arr[0]=1 address=22fe30
arr[1]=2 address=22fe34
arr[2]=3 address=22fe38
同理,如果我們取數組最後一個元素的地址,然後對指向最後一個元素的指針執行減1運算,那麼指針就會像前偏移,指向倒數第二個元素。
學會了指針的運算,再結合解引用,就可以使用指針遍歷數組。但是千萬要注意,指針偏移時不能越界,也就是說指針必須始終小於或等於數組的最後一個元素的地址,不能超過最後一個元素。
指針變量本質上就是一個32位的整型,內存地址本身也就是一個編號,因此對指針進行算術運算、比較運算都是合理的。
#include <stdio.h>
int main(){
int arr[5]={1,2,3,4,5};
int *p = &arr[0];
// 使用指針遍歷數組
for (; p <= &arr[4]; p++){
printf("%d\n",*p);
}
return 0;
}
打印結果:
1
2
3
4
5
當然,對於指向數組首元素的指針,我們仍然可以使用下標訪問。但是一定要確認,該指針當前是否還指向數組首元素,如果你對指針做過偏移運算,那麼它就不再指向首元素,這時使用下標訪問,很可能導致訪問越界。
#include <stdio.h>
int main(){
int arr[5]={1,2,3,4,5};
int *p = &arr[0];
for (int i = 0; i < 5; i++){
printf("%d\n",p[i]);
}
return 0;
}
數組名與指針
#include <stdio.h>
int main(){
int arr[5]={1,2,3,4,5};
int *p = &arr[0];
printf("p=%x\n",p);
printf("arr=%x\n",arr);
return 0;
}
打印結果:
p=22fe30
arr=22fe30
可以看到,實際上數組名這個變量保存的就是數組的首元素地址。但是數組變量和指向它首元素的指針變量又是完全不同的兩個概念。那麼數組名和指針又有什麼區別呢?
- 類型不同。如上,變量
p
是指針類型,變量arr
是數組類型 - 性質不同。
p
是變量,可以修改值,重新指向其他地址。arr
內部保存的指針是個常量,不能修改和運算。 - 數組類型可以使用
sizeof
運算,求得整個數組的內存大小,而對指針p
進行sizeof
運算,只能得到當前指針所佔用的內存大小。
現在我們明白了,就算數組名和指針保存的值相同,它們也是兩個完全不同的概念。但是我們知道了數組名保存的是首元素地址,那麼以後就可以簡化代碼
int arr[5]={1,2,3,4,5};
// 直接使用數組名對指針變量進行初始化,省略&arr[0]的寫法,效果是同等的
int *p = arr;
到這裏,大家應該能明白上一章函數部分中,數組做函數的形式參數時,自動退化爲指針是什麼意思了吧。一旦將數組作爲函數的參數,實際上都是將數組的首元素地址複製給了函數的形參,即使你聲明的是數組類型的形參也一樣。
// 形參聲明爲數組類型:char ch[] ,沒用!
// 實際上仍然會退化爲指針,編譯器不允許在函數傳參時,對數組內容進行復制操作,無法實現值傳遞
// 因此,ch實際上是一個char *類型的指針而已
void convstr(char ch[], int flags);
我們可以寫個簡單代碼驗證
#include <stdio.h>
void test(int a[]){
// 真正的數組類型,是不能進行指針運算的
// 因此a不是一個數組類型,它就是個指針類型
printf("a=%x\n",a++);
}
int main(){
int arr[5]={1,2,3,4,5};
test(arr);
return 0;
}
我們上面已經總結了,數組名內部的指針是個常量,不能進行運算,而test
函數的形參數組a
卻可以++
運算,說明數組做形參,自動退化爲指針類型。
指針與字符串
弄清楚了指針與數組的關係,再看指針與字符串其實就水到渠成了。
#include <stdio.h>
int main(){
// 使用字符串指針表示字符串
char *greet = "hello, Alex";
printf("address=%x\n",greet);
printf("%s\n",greet);
return 0;
}
打印 結果:
address=404000
hello, Alex
需要注意,使用字符串指針時,指針本身就表示了字符串,而不要對其進行解引用。
使用字符串指針時,要注意指向字面常量和指向字符數組的區別
#include <stdio.h>
int main(){
char *str1 = "hello, Alex";
char str2[] = "hello, Alice";
str1[0] = 'f'; //報錯,不可修改
str2[0] = 'f';
printf("%s\n",str1);
printf("%s\n",str2);
return 0;
}
可以看到,指針str1
指向的是一個字面常量,這個字面常量和數組str2
所在的內存區域是不同的,它是隻讀的,不能做修改。而str2
是一個字符數組,裏面的元素是可以修改的。
字符串的進階
實現一個類似strlen
的函數,計算字符串的長度。
#include <stdio.h>
int len(char *str){
int i = 0;
for (; *str !='\0'; str++,i++);
return i;
}
int main(){
char *str1 = "hello,Alex";
char str2[] = "hello,Alice";
printf("%d\n",len(str1));
printf("%d\n",len(str2));
return 0;
}
打印結果:
10
11
實現簡單正則表達式匹配器
下面的實例來自經典圖書《代碼之美》,這段程序使用簡單的30來行代碼,實現了一個簡單正則表達式匹配器,其代碼之簡潔優雅,可爲楷模,也充分展示出了C程序的簡潔高效特點。
字符 | 含義 |
---|---|
c | 匹配任意字母c |
. |
匹配任意單個字符 |
^ |
匹配字符串的開頭 |
$ |
匹配字符串的結尾 |
* |
匹配前一個字符的0個或多個出現 |
#include <stdio.h>
int match(char *regexp, char *text);
int matchhere(char *regexp,char *text);
int matchstar(int c,char *regexp,char *text);
// 創建main函數,測試match函數的功能,其返回1表示匹配成功,0表示無匹配
int main(){
char *str1 = "+8613277880066";
// 檢測字符串str1是否以"+86"開頭
printf("%d\n",match("^+86",str1));
// 檢測字符串str1尾部是否包含"66"子串
printf("%d\n",match("66$",str1));
// 字符串str1中是否包含子串"132"
printf("%d\n",match("132",str1));
// 是否包含3xxx2樣式的字符串,xxx可以是任意多個或者0個字符
printf("%d\n",match("3*2",str1));
// 是否包含3x2樣式的子串,x是單個任意字符,這裏不包含
printf("%d\n",match("3.2",str1));
return 0;
}
// 在text中查找正則表達式regexp
int match(char *regexp, char *text){
if (regexp[0] == '^'){
return matchhere(regexp+1,text);
}
do{ //即使字符串爲空也必須檢查
if (matchhere(regexp,text)) return 1;
} while (*text++ != '\0');
return 0;
}
// 在text開頭查找regexp
int matchhere(char *regexp,char *text){
if (regexp[0]=='\0') return 1;
if (regexp[0]=='*') {
return matchstar(regexp[0],regexp+2,text);
}
if (regexp[0]=='$' && regexp[1] == '\0') {
return *text == '\0';
}
if (*text !='\0' && (regexp[0] == '.' || regexp[0]==*text)) {
return matchhere(regexp+1,text+1);
}
return 0;
}
int matchstar(int c,char *regexp,char *text){
do{ // 通配符* 匹配零個或多個實例
if (matchhere(regexp,text)) return 1;
} while (*text!='\0' && (*text++ == c || c == '.'));
return 0;
}
打印結果:
1
1
1
1
0
可以看到,只有最後一個不包含,我們的測試字符串是一個手機號碼,其中沒有"3x2"
這樣格式的子串,只有一個32
子串。
本例非常經典,值得大家好好學習,如無法理清邏輯,建議使用調試功能,跟蹤程序的執行流程,幫助理解程序的邏輯。我們可以在match
函數中打上一個斷點,vscode中使用【F5】快捷鍵開啓調試
在左邊窗口查看變量的值,配合使用快捷鍵【F10】執行下一行代碼,遇到函數調用時,使用快捷鍵【F11】進入被調用的函數中繼續單步調試
最後說明一下關於,*text++
的用法,這裏自增運算符++
的優先級高於解引用運算符*
,因此實際上的運算順序是*(text++)
,只是絕大多數時候都會省略括號。關於自增運算符,我們在前面的章節長篇大論的講解了一番,並不是無的放矢,實際上++
運算結合指針是很常用的用法,如仍不清楚這裏*text++
的值,請返回 程序員C語言快速上手——基礎篇(三) 算術運算符章節重新學習++
的用法。
指針常量與常量指針
指針常量
指針常量僅指向唯一的內存地址,一旦被初始化後,就不能再指向其他地址。簡單說就是指針本身是常量。
聲明格式:【指針類型】 const
【變量名】
int n = 7;
int l = 10;
//聲明並初始化指針常量
int* const p1 = &n;
p1 = &l; // 錯誤,無法編譯!指針常量不能再指向其他地址
// 普通指針,可以指向其他地址
int *p2 = &n;
p2 = &l;
聲明指針常量時需要注意,星號是緊挨類型的,在之前的章節已經講過,int*
普通類型加星號合起來纔是表示指針類型,因此const
關鍵字是修飾指針變量本身的。當我們對指針常量使用解引用符修改內容時不受影響。
int n = 7;
int* const p1 = &n;
//可使用解引用符,修改指針常量所指向的內存空間的值
*p1 = 1; //相當於n=1
當然,也有人喜歡使用另一種風格來聲明指針常量,將星號與const
緊挨
int n = 7;
int *const p1 = &n;
常量指針
常量指針的意思是說指針所指向的內容是個常量。既然內容是個常量,那就不能使用解引用符去修改指向的內容。但指針自己本身卻是個變量,因此它仍然可以再次指向其他的內容。
聲明格式:const
【指針類型】 【變量名】
int n = 7;
int l = 10;
//聲明常量指針
const int *p1 = &n;
*p1 = 0; // 錯誤,無法編譯!不能修改所指向的內容
p1 = &l; //它可以再指向其他地址
指向常量的常量指針
指向常量的常量指針,即將上述兩種結合到一起,簡單說就是指針自己本身是一個常量,它指向的內容也是一個常量。因此它既不能修改指向的內容,也不能重新指向新地址。
聲明格式:const
【指針類型】const
【變量名】
int n = 7;
int l = 10;
//聲明指向常量的常量指針
const int* const p1 = &n;
*p1 = 0; // 錯誤! 不能修改指向的內容
p1 = &l; //錯誤! 不能重新指向新地址