张银峰的编程课堂

字符数组与字符串

字符数组

char数组可用来存储 'a', 'b', 'c' 这类具有可读性质的字符。我们从输出"hello"字符串开始,不同的是,这次是使用数组表达。

#include <stdio.h>

int main()
{
    char hello1[] = {'h', 'e', 'l', 'l', 'o'};
    char hello2[] = {'h', 'e', 'l', 'l', 'o', '\0'};

    //======= #1 逐字符输出
    for (int i = 0; i < sizeof(hello1) / sizeof(char); i++)
        printf("%c", hello1[i]);
    printf("##\n");

    for (int i = 0; i < sizeof(hello2) / sizeof(hello2[0]); i++)
        printf("%c", hello2[i]);
    printf("##\n");

    printf("\n");

    //======= #2 使用%s输出
    printf("%s##\n", hello2);
    printf("%s##\n", hello1);

    return 0;
}

glimix.com

两个数组的不同之处在于,hello2以转义字符'\0'结束。为了清晰的看到两者之间的不同,我以##来标识输出完成。在通过循环逐个字符输出的情况下,由于'\0'并不是一个可显字符,使得两者在显示上一致。接下来使用字符串格式控制符(%s)输出数组,对于hello1,后面却跟有奇奇怪怪的字符,这说明'\0'在字符串中有着特殊意义。

我们知道将数组传递给函数时,数组的大小信息会丢失,继而退化为对应的指针类型,为此,我们额外会传递一个元素个数的参数。而我们把hello数组传递给printf函数时,却没有遵循这一点,那printf函数是如何知道输出结束呢?从以%s格式符正确输出hello2的表现看,这个'\0'为printf函数为输出字符串指示了结束标记:碰到'\0'时我就认为字符串结束了,就像为函数传递了元素个数一般。

ZYF

'\0':我结束字符串

#include <stdio.h>

int main()
{
    char hw[] =
    {
        'h', 'e', 'l', 'l', 'o', ',',
        '\0', // 字符串结束标记
        'w', 'o', 'r', 'l', 'd',
        '\0' // 字符串结束标记
    };

    printf( "%s\n", hw );

    return 0;
}

glimix.com

可以看到,"world" 没有被输出,字符串以'\0'表示“我的话讲完了”。

'\0'是一个转义字符,它的ASCII码值是0,在C语言中用于表示字符串结束标记。

hw[6] = 0; // 使用数值0替换
printf( "%s\n", hw );

hw[6] = '0'; // 使用字符'0'替换
printf( "%s\n", hw );

hw[6] = 48; // 字符'0'的ASCII码值是48
printf( "%s\n", hw );

glimix.com

简单点

假定我们在编写一款RPG游戏,如果将一个NPC的千言万语以上面逐字符的方式存储到数组中,简直是一场噩梦!幸运的是,字符数组支持直接以字符串字面量赋值,这样生活就惬意了许多。

#include <stdio.h>
#include <string.h> // for strlen

int main()
{
    char home[] = "glimix.com";
    printf("%s\n", home);
    printf("size: %d\n", sizeof(home));
    printf("length: %d\n", strlen(home));
    return 0;
}

glimix.com

C语言的字符串以'\0'标志结束,字符串字面量同样如此,因此sizeof的结果是11个字节。但是'\0'却不属于字符串长度范畴,所以通过字符串长度测量函数strlen得到的长度是10,为了使用此函数,我们需要包含 string.h 头文件。

初始化数组home时没有指定容量,最终大小就是初始化值的个数,这等价于:

char home[11] = "glimix.com";

此时home的类型是char[11];如果数组容量小于给定的初始值个数,程序的行为是不确定的。

char home[10] = "glimix.com";

glimix.com

此时home的类型是char[10],由于没有足够的空间存储结束字符,故而在输出时访问越界,直到碰巧遇到内存中的一个0值才结束。

这里模拟了两个数组在程序中的内存表示,字符数组 glimix.com 在内存某个位置,后面是程序的其它数据,以%s输出home[10]的情况下,后续的 9F 12 83 4F 被解释为某种编码的字符并被输出,直到碰到后面的 00后,整个字符串输出结束。

g  l  i  m  i  x  .  c  o  m     程序的其它数据...
67 6C 69 6D 69 78 2E 63 6F 6D 00 6C F9 AB A0 77 68 F8 4F 00 A1 23 BF ...  // home[11]
67 6C 69 6D 69 78 2E 63 6F 6D 9F 12 83 4F 00 F0 A1 F0 F9 4F 00 3E 7C ...  // home[10]

除此之外,我们还可以直接使用字符指针指向一个字符串字面量。

#include <stdio.h>
#include <string.h>

int main()
{
    char *home = "glimix.com";
    printf("%s\n", home);
    printf("size: %d\n", sizeof(home));
    printf("length: %d\n", strlen(home));
    return 0;
}

glimix.com

二者的区别在于,字符数组是真实的存储数据,而字符指针则是仅仅指向字符串,所以前者可读可写,而后者仅是只读!程序中的这些常量字符串会在程序生成时,由编译器安插在程序的一个特殊位置,以便下次程序运行时,相应的指针变量能有效的初始化。当使用指针引用字符串时,sizeof实际测试的是sizeof(char*),即一个指针的大小,在x86平台下,通常是4个字节。

#include <stdio.h>
#include <string.h>

int main()
{
    char home1[] = "glimix.com";
    char *home2 = "glimix.com";

    home1[0] = 'G';     // 可写
    home1[1] = 'L';
    printf("%s\n", home1);

    printf("before write...");
    home2[0] = 'G';     // 不可写
    home2[1] = 'L';
    printf("%s\n", home2);
    printf("after write...");

    return 0;
}

glimix.com

我们修改home1的前两个字符为大写并正确输出,由于home2是只读的,对它的写操作导致程序引发异常。在更严格的约束下,对于home2的定义,程序并不能被编译,需要加上const关键字以指明变量的只读性质。

const char *home2 = "glimix.com";

glimix.com

ZYF

练习

1 思考: char a[10]; char b[11],a与b的类型相同吗?

2 程序中多次出现的相同常量字符串会被存储几次,编写程序进行验证。