【C语言-指针-指针与字符串】3. 指针与字符串——字符串就是“以\0结尾的字符数组”,指针引用它最顺手

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 5
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

上节课咱们把指针和数组的关系捋清楚了——数组名就是首元素的地址,指针可以当数组名用,数组名也可以当指针用,两者在大多数情况下可以互换。

今天咱们讲指针和字符串。你先别觉得这是新东西,字符串本质上就是字符数组,只不过末尾多了个\0。所以指针和数组的那套玩法,直接搬到字符串上就完事了。但字符串有几个自己的脾气,今天咱们专门说说。

字符串的两种“身份”

在C语言里,你写一个字符串,有两种方式:

char str1[] = "Hello";    // 方式一:字符数组
char *str2 = "Hello";     // 方式二:指针指向字符串常量

这俩看着差不多,其实差别大了去了。咱们一个一个说。

方式一:char str1[] = "Hello";

这是在内存里开了一个数组,把'H','e','l','l','o','\0'这6个字符挨个存进去。数组名str1就是这块内存的首地址。你可以改里面的字符:

str1[0] = 'h';   // 合法,str1变成"hello"

因为这是你自己的数组,你想怎么改就怎么改。

方式二:char *str2 = "Hello";

这个就有意思了。"Hello"是一个字符串常量,它存放在内存的只读数据区(也叫常量区)。str2是一个指针,指向这个常量的首地址。

str2[0] = 'h';   // 危险!试图修改只读区的数据,程序崩溃

你可能会问:为啥一个是数组,一个是指针,差别这么大?

因为char str1[] = "Hello"是在栈上开辟了一块新内存,把字符串拷贝了一份进去。而char *str2 = "Hello"只是让指针指向了常量区里的那个字符串,没有拷贝。你改常量区的东西,操作系统不答应。

用指针遍历字符串

字符串以\0结尾,所以遍历的时候不用知道长度,一直往后走,撞到\0就停。

#include <stdio.h>

int main() {
    char *str = "Hello";
    
    // 方式一:用下标
    int i = 0;
    while (str[i] != '\0') {
        printf("%c", str[i]);
        i++;
    }
    printf("\n");
    
    // 方式二:用指针自己往后走
    char *p = str;
    while (*p != '\0') {
        printf("%c", *p);
        p++;
    }
    printf("\n");
    
    return 0;
}

方式二特别能体现指针操作字符串的精髓:p一开始指向'H',打印出来,然后p++走到'e',再打印,再往后走……直到*p是\0,循环结束。

这就是用指针遍历字符串的标准套路。你只要记住:字符串的结束标志是\0,指针走到\0就停。

字符串指针的算术运算

跟数组一样,指针在字符串上也可以加减。p + 1就是往后跳一个字符(一个字节,因为char占1字节)。

char *str = "Hello";

printf("%c\n", *str);       // H
printf("%c\n", *(str + 1)); // e
printf("%c\n", *(str + 4)); // o

因为char是1个字节,所以指针加1就是往后挪一个字符的位置。这比int数组简单多了——int数组加1是跳4个字节,char数组加1就是跳1个字节。

经典例题:手写字符串拷贝

咱们用指针来实现strcpy,你感受一下指针操作字符串的写法:

#include <stdio.h>

void my_strcpy(char *dest, char *src) {
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';   // 别忘了结尾的\0
}

int main() {
    char src[] = "Hello World";
    char dest[50];
    
    my_strcpy(dest, src);
    printf("%s\n", dest);   // 输出 Hello World
    
    return 0;
}

拆开看执行过程:

  • src指向'H',dest指向目标数组开头。

  • *dest = *src把'H'拷过去,然后两个指针都往后走一格。

  • 重复这个过程,直到*src是\0。

  • 循环退出后,在dest当前位置补上\0。

这个写法比用下标写更直观——你能看到两个指针在内存里同步往前挪。

还有更简练的写法,把赋值和移动合并:

void my_strcpy(char *dest, char *src) {
    while ((*dest++ = *src++) != '\0');
}

这行代码的玄机:先把*src赋给*dest,两个指针都自增,然后判断赋完的值是不是\0。能看懂就行,平时写代码不推荐这么炫技。

经典例题:手写字符串比较

用指针比较两个字符串,逐个字符比过去,遇到不一样的或者遇到\0就出结果:

#include <stdio.h>

int my_strcmp(char *s1, char *s2) {
    while (*s1 != '\0' && *s2 != '\0') {
        if (*s1 != *s2) {
            return *s1 - *s2;
        }
        s1++;
        s2++;
    }
    return *s1 - *s2;
}

int main() {
    printf("%d\n", my_strcmp("Hello", "Hello"));   // 0
    printf("%d\n", my_strcmp("Apple", "Banana"));  // 负数
    printf("%d\n", my_strcmp("World", "Hello"));   // 正数
    return 0;
}

*s1 - *s2返回的是两个字符ASCII码的差值,正数表示s1大,负数表示s1小,零表示相等。跟库函数strcmp的行为一致。

指针数组 vs 数组指针——这俩容易搞混,提前说清楚

字符串处理里经常碰到一个场景:我要存一堆字符串,比如5个名字。怎么存?

方式一:二维数组

char names[5][20] = {"Alice", "Bob", "Charlie", "David", "Eve"};

这相当于5行20列的表格,每行存一个名字。缺点是每行都固定20个字符,短名字浪费空间,长名字存不下。

方式二:指针数组

char *names[5] = {"Alice", "Bob", "Charlie", "David", "Eve"};

这是个数组,里面有5个元素,每个元素都是一个char *指针,分别指向一个字符串常量。

这两种方式用起来很像:

printf("%s\n", names[0]);   // 都能输出 Alice

但内存布局完全不同。方式一是在一个连续的二维数组里存实际字符,方式二是一个指针数组,指针指向常量区的字符串。

指针数组的好处是灵活——每个字符串长度可以不一样,不用预先分配固定大小的空间。你甚至可以在运行时让这些指针指向不同的字符串:

char *names[3];
names[0] = "Alice";
names[1] = "Bob";
names[2] = "Charlie";

几个常见的坑

坑1:试图修改字符串常量

char *str = "Hello";
str[0] = 'h';   // 崩溃!字符串常量在只读区

正确做法:如果你需要修改,用数组。

char str[] = "Hello";
str[0] = 'h';   // 安全,数组里的内容可改

坑2:字符串指针没初始化

char *str;
strcpy(str, "Hello");   // 危险!str指向未知地址

指针没指向合法内存之前,不能往里写东西。

坑3:混淆char *str和char str[]

char *s1 = "Hello";
char s2[] = "Hello";

printf("%lu %lu\n", sizeof(s1), sizeof(s2));
// 输出 8 6(指针大小 vs 数组大小)

sizeof(s1)算的是指针的大小(8字节),sizeof(s2)算的是整个数组的大小(6字节,含\0)。

坑4:用==比较字符串指针

char *s1 = "Hello";
char *s2 = "Hello";

if (s1 == s2) {   // 比的可能是地址,不是内容
    // ...
}

编译器可能会把相同的字符串常量合并到同一个地址,所以s1 == s2有时候是true,有时候是false。比较内容用strcmp,永远用strcmp。

总结一下

用指针操作字符串,核心就几句话:

字符串是以\0结尾的字符数组,数组名就是地址,所以指针可以直接指向字符串。

遍历字符串,就是让指针从开头走到\0。

char *p = "Hello"指向只读的字符串常量,不能改;char str[] = "Hello"是数组,可以改。

指针数组可以存多个字符串,每个指针指向一个独立的字符串。

char *s和char s[]在函数参数里等价,都是指针。

字符串处理是C语言里最常用的操作之一,指针操作字符串又是最自然的方式。你把这节课的内容吃透了,以后看到while (*p)、p++这种代码就不会发怵了。

回去自己动手,把字符串拷贝、比较、连接的指针版本都敲一遍。跑出正确结果之后,再故意改错——比如忘了补\0、指针不初始化——看看程序会出什么问题。错误见多了,你就知道该怎么写了。下节课咱们讲指针和函数的更深层关系——函数指针。那玩意儿才开始有点绕,但你现在把基础打好了,到时候就不怕。