程序員C語言快速上手——進階篇(六)

進階語法

指針與數組

#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

可以看到,實際上數組名這個變量保存的就是數組的首元素地址。但是數組變量和指向它首元素的指針變量又是完全不同的兩個概念。那麼數組名和指針又有什麼區別呢?

  1. 類型不同。如上,變量p是指針類型,變量arr是數組類型
  2. 性質不同。p是變量,可以修改值,重新指向其他地址。arr內部保存的指針是個常量,不能修改和運算。
  3. 數組類型可以使用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; //錯誤! 不能重新指向新地址

歡迎關注我的公衆號:編程之路從0到1

編程之路從0到1

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章