前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >程序员C语言快速上手——进阶篇(六)

程序员C语言快速上手——进阶篇(六)

作者头像
arcticfox
修改2019-06-26 19:13:48
5600
修改2019-06-26 19:13:48
举报
  • 进阶语法
    • 指针与数组
      • 指针的算术运算
      • 数组名与指针
    • 指针与字符串
      • 字符串的进阶
        • 实现简单正则表达式匹配器
    • 指针常量与常量指针
      • 指针常量
      • 常量指针
      • 指向常量的常量指针

进阶语法

指针与数组

 1 #include <stdio.h>
 2
 3 int main(){
 4    int arr[5]={1,2,3,4,5};
 5
 6    // 依次打印数组每个元素的地址
 7    for (int i = 0; i < 5; i++){
 8        printf("p: %x\n",&arr[i]);
 9    }
10    return 0;
11 }

打印结果

1 p: 22fe30
2 p: 22fe34
3 p: 22fe38
4 p: 22fe3c
5 p: 22fe40

由上例可验证,数组的内存空间是连在一起的,它的第一个元素地址是0x22fe30,第二个元素的地址是0x22fe34,紧随其后。因为是int数组,每个元素都需要占用4个字节空间,因此地址的间隔也是4。

指针的算术运算

 1 #include <stdio.h>
 2
 3 int main(){
 4    int arr[5]={1,2,3,4,5};
 5
 6    // 声明指针p,指向数组的首元素
 7    int *p = &arr[0];
 8
 9    // 将指针变量加1,表示偏移一个单位
10    printf("arr[0]=%d  address=%x\n",*p, p);
11    printf("arr[1]=%d  address=%x\n",*(p + 1), (p+1));
12    printf("arr[2]=%d  address=%x\n",*(p + 2), (p+2));
13
14    return 0;
15 }

打印结果:

1 arr[0]=1  address=22fe30
2 arr[1]=2  address=22fe34
3 arr[2]=3  address=22fe38

在这里插入图片描述 同理,如果我们取数组最后一个元素的地址,然后对指向最后一个元素的指针执行减1运算,那么指针就会像前偏移,指向倒数第二个元素。

学会了指针的运算,再结合解引用,就可以使用指针遍历数组。但是千万要注意,指针偏移时不能越界,也就是说指针必须始终小于或等于数组的最后一个元素的地址,不能超过最后一个元素。

指针变量本质上就是一个32位的整型,内存地址本身也就是一个编号,因此对指针进行算术运算、比较运算都是合理的。

 1 #include <stdio.h>
 2
 3 int main(){
 4    int arr[5]={1,2,3,4,5};
 5
 6    int *p = &arr[0];
 7
 8    // 使用指针遍历数组
 9    for (; p <= &arr[4]; p++){
10        printf("%d\n",*p);
11    }
12    return 0;
13 }

打印结果:

1 1
2 2
3 3
4 4
5 5

当然,对于指向数组首元素的指针,我们仍然可以使用下标访问。但是一定要确认,该指针当前是否还指向数组首元素,如果你对指针做过偏移运算,那么它就不再指向首元素,这时使用下标访问,很可能导致访问越界。

 1 #include <stdio.h>
 2
 3 int main(){
 4    int arr[5]={1,2,3,4,5};
 5
 6    int *p = &arr[0];
 7
 8    for (int i = 0; i < 5; i++){
 9        printf("%d\n",p[i]);
10    }
11    return 0;
12 }

数组名与指针

 1 #include <stdio.h>
 2
 3 int main(){
 4    int arr[5]={1,2,3,4,5};
 5
 6    int *p = &arr[0];
 7
 8    printf("p=%x\n",p);
 9    printf("arr=%x\n",arr);
10
11    return 0;
12 }

打印结果:

1 p=22fe30
2 arr=22fe30

可以看到,实际上数组名这个变量保存的就是数组的首元素地址。但是数组变量和指向它首元素的指针变量又是完全不同的两个概念。那么数组名和指针又有什么区别呢?

  1. 类型不同。如上,变量p是指针类型,变量arr是数组类型
  2. 性质不同。p是变量,可以修改值,重新指向其他地址。arr内部保存的指针是个常量,不能修改和运算。
  3. 数组类型可以使用sizeof运算,求得整个数组的内存大小,而对指针p进行sizeof运算,只能得到当前指针所占用的内存大小。

现在我们明白了,就算数组名和指针保存的值相同,它们也是两个完全不同的概念。但是我们知道了数组名保存的是首元素地址,那么以后就可以简化代码

1    int arr[5]={1,2,3,4,5};
2
3    // 直接使用数组名对指针变量进行初始化,省略&arr[0]的写法,效果是同等的
4    int *p = arr;

到这里,大家应该能明白上一章函数部分中,数组做函数的形式参数时,自动退化为指针是什么意思了吧。一旦将数组作为函数的参数,实际上都是将数组的首元素地址复制给了函数的形参,即使你声明的是数组类型的形参也一样。

1 // 形参声明为数组类型:char ch[] ,没用!
2 // 实际上仍然会退化为指针,编译器不允许在函数传参时,对数组内容进行复制操作,无法实现值传递
3 // 因此,ch实际上是一个char *类型的指针而已
4 void convstr(char ch[], int flags);

我们可以写个简单代码验证

 1 #include <stdio.h>
 2
 3 void test(int a[]){
 4    // 真正的数组类型,是不能进行指针运算的
 5    // 因此a不是一个数组类型,它就是个指针类型
 6    printf("a=%x\n",a++);
 7}
 8
 9 int main(){
10    int arr[5]={1,2,3,4,5};
11    test(arr);
12    return 0;
13}

我们上面已经总结了,数组名内部的指针是个常量,不能进行运算,而test函数的形参数组a却可以++运算,说明数组做形参,自动退化为指针类型。

指针与字符串

弄清楚了指针与数组的关系,再看指针与字符串其实就水到渠成了。

 1 #include <stdio.h>
 2
 3 int main(){
 4    // 使用字符串指针表示字符串
 5    char *greet = "hello, Alex";
 6
 7    printf("address=%x\n",greet);
 8    printf("%s\n",greet);
 9    return 0;
10  }

打印 结果:

1 address=404000
2 hello, Alex

需要注意,使用字符串指针时,指针本身就表示了字符串,而不要对其进行解引用。

使用字符串指针时,要注意指向字面常量和指向字符数组的区别

 1 #include <stdio.h>
 2
 3 int main(){
 4    char *str1 = "hello, Alex";
 5    char str2[] = "hello, Alice";
 6
 7    str1[0] = 'f';  //报错,不可修改
 8    str2[0] = 'f';
 9
10    printf("%s\n",str1);
11    printf("%s\n",str2);
12    return 0;
13 }

可以看到,指针str1指向的是一个字面常量,这个字面常量和数组str2所在的内存区域是不同的,它是只读的,不能做修改。而str2是一个字符数组,里面的元素是可以修改的。

字符串的进阶

实现一个类似strlen的函数,计算字符串的长度。

 1 #include <stdio.h>
 2
 3 int len(char *str){
 4    int i = 0;
 5    for (; *str !='\0'; str++,i++);
 6    return i;
 7 }
 8
 9 int main(){
10    char *str1 = "hello,Alex";
11    char str2[] = "hello,Alice";
12
13    printf("%d\n",len(str1));
14    printf("%d\n",len(str2));
15    return 0;
16 }

打印结果:

1 10
2 11
实现简单正则表达式匹配器

下面的实例来自经典图书《代码之美》,这段程序使用简单的30来行代码,实现了一个简单正则表达式匹配器,其代码之简洁优雅,可为楷模,也充分展示出了C程序的简洁高效特点。

 1  #include <stdio.h>
 2
 3  int match(char *regexp, char *text);
 4  int matchhere(char *regexp,char *text);
 5  int matchstar(int c,char *regexp,char *text);
 6
 7  // 创建main函数,测试match函数的功能,其返回1表示匹配成功,0表示无匹配
 8  int main(){
 9    char *str1 = "+8613277880066";
10
11    // 检测字符串str1是否以"+86"开头
12    printf("%d\n",match("^+86",str1));
13    // 检测字符串str1尾部是否包含"66"子串
14    printf("%d\n",match("66$",str1));
15    // 字符串str1中是否包含子串"132"
16    printf("%d\n",match("132",str1));
17    // 是否包含3xxx2样式的字符串,xxx可以是任意多个或者0个字符
18    printf("%d\n",match("3*2",str1));
19    // 是否包含3x2样式的子串,x是单个任意字符,这里不包含
20    printf("%d\n",match("3.2",str1));
21    return 0;
22  }
23
24  // 在text中查找正则表达式regexp
25  int match(char *regexp, char *text){
26    if (regexp[0] == '^'){
27        return matchhere(regexp+1,text);
28    }
29    do{  //即使字符串为空也必须检查
30        if (matchhere(regexp,text)) return 1;
31    } while (*text++ != '\0');
32    return 0;
33 }
34  // 在text开头查找regexp
35  int matchhere(char *regexp,char *text){
36    if (regexp[0]=='\0') return 1;
37    if (regexp[0]=='*') {
38        return matchstar(regexp[0],regexp+2,text);
39    }
40
41    if (regexp[0]=='$' && regexp[1] == '\0') {
42        return *text == '\0';
43    }
44
45    if (*text !='\0' && (regexp[0] == '.' || regexp[0]==*text)) {
46        return matchhere(regexp+1,text+1);
47    }
48    return 0;
49  }
50
51  int matchstar(int c,char *regexp,char *text){
52    do{   // 通配符* 匹配零个或多个实例
53        if (matchhere(regexp,text)) return 1;
54    } while (*text!='\0' && (*text++ == c || c == '.'));
55    return 0;
56  }

打印结果:

1  1
2  1
3  1
4  1
5  0

可以看到,只有最后一个不包含,我们的测试字符串是一个手机号码,其中没有"3x2"这样格式的子串,只有一个32子串。

本例非常经典,值得大家好好学习,如无法理清逻辑,建议使用调试功能,跟踪程序的执行流程,帮助理解程序的逻辑。我们可以在match函数中打上一个断点,vscode中使用【F5】快捷键开启调试

在这里插入图片描述 在左边窗口查看变量的值,配合使用快捷键【F10】执行下一行代码,遇到函数调用时,使用快捷键【F11】进入被调用的函数中继续单步调试

最后说明一下关于,*text++的用法,这里自增运算符++的优先级高于解引用运算符*,因此实际上的运算顺序是*(text++),只是绝大多数时候都会省略括号。关于自增运算符,我们在前面的章节长篇大论的讲解了一番,并不是无的放矢,实际上++运算结合指针是很常用的用法,如仍不清楚这里*text++的值,请返回 程序员C语言快速上手——基础篇(三) 算术运算符章节重新学习++的用法。

指针常量与常量指针

指针常量

指针常量仅指向唯一的内存地址,一旦被初始化后,就不能再指向其他地址。简单说就是指针本身是常量。

声明格式:【指针类型】 const 【变量名】

 1    int n = 7;
 2    int l = 10;
 3
 4    //声明并初始化指针常量
 5    int* const p1 = &n;
 6    p1 = &l; // 错误,无法编译!指针常量不能再指向其他地址
 7
 8    // 普通指针,可以指向其他地址
 9    int *p2 = &n;
10    p2 = &l;

声明指针常量时需要注意,星号是紧挨类型的,在之前的章节已经讲过,int* 普通类型加星号合起来才是表示指针类型,因此const关键字是修饰指针变量本身的。当我们对指针常量使用解引用符修改内容时不受影响。

1    int n = 7;
2    int* const p1 = &n;
3    //可使用解引用符,修改指针常量所指向的内存空间的值
4    *p1 = 1;    //相当于n=1

当然,也有人喜欢使用另一种风格来声明指针常量,将星号与const紧挨

1    int n = 7;
2    int *const p1 = &n;

常量指针

常量指针的意思是说指针所指向的内容是个常量。既然内容是个常量,那就不能使用解引用符去修改指向的内容。但指针自己本身却是个变量,因此它仍然可以再次指向其他的内容。

声明格式:const【指针类型】 【变量名】

1    int n = 7;
2    int l = 10;
3
4    //声明常量指针
5    const int *p1 = &n;
6    *p1 = 0; // 错误,无法编译!不能修改所指向的内容
7
8    p1 = &l; //它可以再指向其他地址

指向常量的常量指针

指向常量的常量指针,即将上述两种结合到一起,简单说就是指针自己本身是一个常量,它指向的内容也是一个常量。因此它既不能修改指向的内容,也不能重新指向新地址。

声明格式:const【指针类型】const 【变量名】

1    int n = 7;
2    int l = 10;
3
4    //声明指向常量的常量指针
5    const int* const p1 = &n;
6    *p1 = 0; // 错误! 不能修改指向的内容
7    p1 = &l; //错误! 不能重新指向新地址

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程之路从0到1 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 进阶语法
    • 指针与数组
      • 指针的算术运算
      • 数组名与指针
    • 指针与字符串
      • 字符串的进阶
    • 指针常量与常量指针
      • 指针常量
      • 常量指针
      • 指向常量的常量指针
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档