程序员C语言快速上手——进阶篇(六) ﹏ヽ暗。殇╰゛Y 2022-01-15 17:11 256阅读 0赞 ### 文章目录 ### * 进阶语法 * * 指针与数组 * * 指针的算术运算 * 数组名与指针 * 指针与字符串 * * 字符串的进阶 * * 实现简单正则表达式匹配器 * 指针常量与常量指针 * * 指针常量 * 常量指针 * 指向常量的常量指针 * 欢迎关注我的公众号:编程之路从0到1 # 进阶语法 # ## 指针与数组 ## #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 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70] 同理,如果我们取数组最后一个元素的地址,然后对指向最后一个元素的指针执行减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程序的简洁高效特点。 <table> <thead> <tr> <th>字符</th> <th>含义</th> </tr> </thead> <tbody> <tr> <td>c</td> <td>匹配任意字母c</td> </tr> <tr> <td><code>.</code></td> <td>匹配任意单个字符</td> </tr> <tr> <td><code>^</code></td> <td>匹配字符串的开头</td> </tr> <tr> <td><code>$</code></td> <td>匹配字符串的结尾</td> </tr> <tr> <td><code>*</code></td> <td>匹配前一个字符的0个或多个出现</td> </tr> </tbody> </table> #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)); // 是否包含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[1]=='*') { 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】快捷键开启调试 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70 1] 在左边窗口查看变量的值,配合使用快捷键【F10】执行下一行代码,遇到函数调用时,使用快捷键【F11】进入被调用的函数中继续单步调试 最后说明一下关于,`*text++`的用法,这里自增运算符`++`的优先级高于解引用运算符`*`,因此实际上的运算顺序是`*(text++)`,只是绝大多数时候都会省略括号。关于自增运算符,我们在前面的章节长篇大论的讲解了一番,并不是无的放矢,实际上`++`运算结合指针是很常用的用法,如仍不清楚这里`*text++`的值,请返回 [程序员C语言快速上手——基础篇(三)][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][0_1] [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70]: /images/20220115/752c5f2c144344a0aa252d726bab8111.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9hcmN0aWNmb3guYmxvZy5jc2RuLm5ldA_size_16_color_FFFFFF_t_70 1]: /images/20220115/bfdf4c352c2747e094a102382ce03408.png [C]: https://blog.csdn.net/yingshukun/article/details/90728550#_42 [0_1]: /images/20220115/fe62c654be8d406cba6dc25b2e221a73.png
相关 程序员C语言快速上手——基础篇(三) 文章目录 小拓展:C语言中int的正确使用姿势 语法基础 表达式 算术运算符 关系运算符 待我称王封你为后i/ 2022年01月23日 00:19/ 0 赞/ 282 阅读
相关 程序员C语言快速上手——高级篇(九) 文章目录 高级篇 结构体 背景 结构体的声明与使用 结构体变量的初始化 太过爱你忘了你带给我的痛/ 2021年12月09日 20:55/ 0 赞/ 352 阅读
还没有评论,来说两句吧...