字符数组与字符串
字符数组
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;
}

两个数组的不同之处在于,hello2以转义字符'\0'结束。为了清晰的看到两者之间的不同,我以##来标识输出完成。在通过循环逐个字符输出的情况下,由于'\0'并不是一个可显字符,使得两者在显示上一致。接下来使用字符串格式控制符(%s)输出数组,对于hello1,后面却跟有奇奇怪怪的字符,这说明'\0'在字符串中有着特殊意义。
我们知道将数组传递给函数时,数组的大小信息会丢失,继而退化为对应的指针类型,为此,我们额外会传递一个元素个数的参数。而我们把hello数组传递给printf函数时,却没有遵循这一点,那printf函数是如何知道输出结束呢?从以%s格式符正确输出hello2的表现看,这个'\0'为printf函数为输出字符串指示了结束标记:碰到'\0'时我就认为字符串结束了,就像为函数传递了元素个数一般。
'\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;
}

可以看到,"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 );

简单点
假定我们在编写一款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;
}

C语言的字符串以'\0'标志结束,字符串字面量同样如此,因此sizeof的结果是11个字节。但是'\0'却不属于字符串长度范畴,所以通过字符串长度测量函数strlen得到的长度是10,为了使用此函数,我们需要包含 string.h 头文件。
初始化数组home时没有指定容量,最终大小就是初始化值的个数,这等价于:
char home[11] = "glimix.com";
此时home的类型是char[11];如果数组容量小于给定的初始值个数,程序的行为是不确定的。
char home[10] = "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;
}

二者的区别在于,字符数组是真实的存储数据,而字符指针则是仅仅指向字符串,所以前者可读可写,而后者仅是只读!程序中的这些常量字符串会在程序生成时,由编译器安插在程序的一个特殊位置,以便下次程序运行时,相应的指针变量能有效的初始化。当使用指针引用字符串时,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;
}

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

练习
1 思考: char a[10]; char b[11],a与b的类型相同吗?
2 程序中多次出现的相同常量字符串会被存储几次,编写程序进行验证。