1. 字符指针与字符串
在 C 语言中,字符串本质上是以\0结尾的字符数组。理解指针与字符串的关系,是掌握指针进阶的第一步。
1.1 用字符指针指向字符串
#include<stdio.h>intmain(void){char*str="hello csdn";printf("%s\n",str);return0;}这里str是一个字符指针,指向字符串常量"hello csdn"的首字符。printf从该地址开始逐字节输出,直到遇到\0为止。
1.2 字符串常量不可被修改
下面的写法会引发未定义行为:
char*str="hello";str[0]='H';/* 错误!字符串常量通常存储在只读区 */正确做法是使用字符数组来存放可修改的字符串:
charbuf[]="hello";buf[0]='H';/* 正确,buf 是栈上的可写内存 */1.3 字符数组与字符指针的区别
| 对比项 | 字符数组char s[] | 字符指针char *p |
|---|---|---|
| 内存来源 | 栈或全局可写区 | 指向字符串常量(只读区) |
| 内容可修改 | 可以 | 不可以(指向常量时) |
| 指针本身可变 | 数组名是常量,不可重新赋值 | 可以改变指向 |
sizeof含义 | 整个数组字节数 | 指针本身大小(通常 8 或 4 字节) |
#include<stdio.h>intmain(void){chars[]="csdn";char*p="csdn";printf("sizeof(s) = %zu\n",sizeof(s));/* 5,含 '\0' */printf("sizeof(p) = %zu\n",sizeof(p));/* 8 或 4,指针大小 *//* s = p; 非法,数组名不能作为左值 */p=s;/* 合法,指针可以重新指向 */return0;}2. 字符串数组
字符串数组用于存放多个字符串,常见有两种组织方式:二维字符数组和指针数组。
2.1 二维字符数组
每个字符串占据固定长度的行,适合长度相近且需要修改的场景:
#include<stdio.h>intmain(void){charnames[][16]={"zhangsan","lisi","wangwu"};for(inti=0;i<3;i++){printf("%s\n",names[i]);}return0;}names[i]表示第i行的首地址,类型为char *,可以直接传给字符串处理函数。
2.2 指针数组
每个元素是指针,指向不同长度的字符串,内存更紧凑,但指向常量时不可修改:
#include<stdio.h>intmain(void){constchar*colors[]={"red","green","blue","yellow"};for(inti=0;i<4;i++){printf("%s\n",colors[i]);}return0;}2.3 两种写法对比
- 二维字符数组:内存连续、每行定长、内容可改,适合固定长度的字符串集合。
- 指针数组:每行长度灵活、仅存地址更省内存,但若指向字符串常量则内容不可改。
3. 常用 string 库函数原型
C 标准库<string.h>提供了一系列字符串操作函数。理解它们的函数原型与实现原理,能更好地掌握指针运算。
3.1 求长度:strlen
size_tstrlen(constchar*s);返回字符串长度,不含结尾的\0。
手写实现:
#include<stddef.h>size_tmy_strlen(constchar*s){constchar*p=s;while(*p!='\0'){p++;}return(size_t)(p-s);}3.2 复制:strcpy / strncpy
char*strcpy(char*dest,constchar*src);char*strncpy(char*dest,constchar*src,size_tn);strcpy将src连同\0复制到dest,不检查目标缓冲区大小,易溢出。strncpy最多复制n个字符;若src短于n,则用\0补齐;若src达到n仍未遇\0,目标可能不以\0结尾。
手写strcpy实现:
char*my_strcpy(char*dest,constchar*src){char*ret=dest;while((*dest++=*src++)!='\0'){;}returnret;}推荐在日常开发中使用更安全的strncpy(注意手动补\0)或snprintf。
3.3 拼接:strcat / strncat
char*strcat(char*dest,constchar*src);char*strncat(char*dest,constchar*src,size_tn);strcat把src追加到dest末尾,同样不检查缓冲区大小。strncat最多追加n个字符,并始终补上\0,相对更安全。
手写strcat实现:
char*my_strcat(char*dest,constchar*src){char*ret=dest;while(*dest!='\0'){dest++;}while((*dest++=*src++)!='\0'){;}returnret;}3.4 比较:strcmp / strncmp
intstrcmp(constchar*s1,constchar*s2);intstrncmp(constchar*s1,constchar*s2,size_tn);按字典序逐字符比较:返回负数表示s1 < s2,返回 0 表示相等,返回正数表示s1 > s2。
手写strcmp实现:
intmy_strcmp(constchar*s1,constchar*s2){while(*s1!='\0'&&*s1==*s2){s1++;s2++;}return(unsignedchar)*s1-(unsignedchar)*s2;}3.5 查找字符:strchr / strrchr
char*strchr(constchar*s,intc);char*strrchr(constchar*s,intc);strchr返回字符c在字符串中首次出现的位置指针,找不到返回NULL。strrchr返回最后一次出现的位置。
手写strchr实现:
char*my_strchr(constchar*s,intc){while(*s!='\0'){if(*s==(char)c){return(char*)s;}s++;}if((char)c=='\0'){return(char*)s;/* 匹配结尾 '\0' */}returnNULL;}3.6 查找子串:strstr
char*strstr(constchar*haystack,constchar*needle);在haystack中查找子串needle,返回首次出现位置,找不到返回NULL。
手写实现:
#include<stddef.h>char*my_strstr(constchar*haystack,constchar*needle){if(*needle=='\0'){return(char*)haystack;}while(*haystack!='\0'){constchar*h=haystack;constchar*n=needle;while(*n!='\0'&&*h==*n){h++;n++;}if(*n=='\0'){return(char*)haystack;}haystack++;}returnNULL;}3.7 使用示例
#include<stdio.h>#include<string.h>intmain(void){charbuf[32]="hello";printf("len=%zu\n",strlen(buf));/* 5 */strcpy(buf,"world");strcat(buf,"!");printf("%s\n",buf);/* world! */printf("cmp=%d\n",strcmp("abc","abd"));/* -1 或负数 */printf("chr=%s\n",strchr("hello",'l'));/* llo */printf("str=%s\n",strstr("hello","ll"));/* llo */return0;}4. 指针与二维数组
二维数组在内存中依然是按行连续存储的,掌握「数组名 → 数组指针」的转换,是指针进阶的关键。
4.1 二维数组的内存布局
#include<stdio.h>intmain(void){inta[2][3]={{1,2,3},{4,5,6}};printf("a[0][0] = %d\n",a[0][0]);/* 1 */printf("%d\n",*(*a+2));/* 3 */printf("%d\n",*(*(a+1)+0));/* 4 */return0;}内存布局如下:
a的类型是「指向int[3]的指针」,即数组指针int (*)[3]。a + i跳过一整行(3 个int)。*(a + i)得到第i行的首地址,再+ j定位到该行第j个元素。- 因此
a[i][j]等价于*(*(a + i) + j)。
4.2 数组指针
数组指针专门指向一个固定长度的数组:
#include<stdio.h>intmain(void){inta[2][3]={{1,2,3},{4,5,6}};int(*p)[3]=a;/* p 指向 int[3] */for(inti=0;i<2;i++){for(intj=0;j<3;j++){printf("%d ",p[i][j]);}printf("\n");}return0;}其中int (*p)[3]表示「p 是一个指针,指向包含 3 个int的数组」。
4.3 数组指针与指针数组的区别
这是最易混淆的知识点:
int(*p)[3];/* 数组指针:指向 int[3],通常用于二维数组 */int*q[3];/* 指针数组:q 有 3 个元素,每个元素是 int* */优先级说明:[]高于*,所以:
int *q[3]:先与[3]结合成数组,元素是指针 → 指针数组。int (*p)[3]:括号让p先与*结合成指针,再指向数组 → 数组指针。
指针数组示例:
#include<stdio.h>intmain(void){intx=1,y=2,z=3;int*arr[3]={&x,&y,&z};for(inti=0;i<3;i++){printf("%d ",*arr[i]);}printf("\n");return0;}4.4 二维数组作为函数参数
二维数组传参时,第一维可以省略,第二维必须指定,本质上传入的是数组指针:
#include<stdio.h>voidprint_matrix(int(*matrix)[3],introws){for(inti=0;i<rows;i++){for(intj=0;j<3;j++){printf("%d ",matrix[i][j]);}printf("\n");}}/* 等价写法:void print_matrix(int matrix[][3], int rows) */intmain(void){inta[2][3]={{1,2,3},{4,5,6}};print_matrix(a,2);return0;}matrix[i][j]同样等价于*(*(matrix + i) + j),理解这一点后即可灵活使用指针遍历二维数组。
5. 总结
- 字符指针:
char *指向字符串首地址,指向的字符串常量不可修改。 - 字符串数组:二维字符数组内容可改、定长;指针数组灵活省内存。
- string 库函数:牢记
strlen、strcpy、strcat、strcmp、strchr、strstr等原型,体会指针移动的共性,并注意缓冲区安全。 - 指针与二维数组:
a[i][j]等价于*(*(a + i) + j);分清数组指针int (*p)[n]与指针数组int *p[n]。
抓住「指针即地址、指针加减按其所指类型为单位移动」这一本质,再结合上述原型与数组结构反复练习,就能稳步跨过指针进阶这道坎。