C/C++字符串的声明定义、长度、sizeof、‘\0‘的细节详解

引言

最近正着手于一个小项目,利用stm32f103c8t6单片机,将电脑音频同步转化为LED灯条上的灯光特效。在使用串口打印功能进行debug的过程中,遇见了字符串具体长度、结尾’\0’相关的问题。在查询大量资料、进行尽可能详尽的测试之后归纳整理,在这里记录一下。

声明定义字符串的四种方式

字符串一直以来都是C\C++程序中的常客,常用的有四种字符串的声明定义方式(为了省事,本文不详细区分“声明”与“定义”,下文使用"定义"统称一切相关行为。如需可参考:c语言中声明与定义的区别):

char *rick = "Wubba";
char morty[10] = "Whoa!";
char squanchy[] = "Squanch it.";
char snowball[] = {
    
    't', 'e', 's', 't', 'i', 's'};

除了第二种,需要指明为字符串预留的长度以外,形式都蛮自由。当然,自由也意味着细节的隐藏。这对于相对底层的c语言来说,正是容易出错的原因所在。

如何判断字符串的长度

c语言中的字符串定义起来很轻松简单,但使用起来却涉及众多细节,容易混淆,容易出错。这里尝试讨论清楚,字符串的长度究竟是如何判定。

情况一:rick字符串

char *rick = "Wubba";

对于第一种定义方式来说,字符串以 ‘\0’ 字符(ASCII码为0)作为结尾的标志。在定义rick时,我们并没有在末尾特别加上 ‘\0’,但是它将被自动添加到rick字符串(我们显式指出的字符串 “Wubba”)的后一位。

如果我们使用%s来输出rick字符串,得到的将完全是我们期待的"Wubba"这五个字母。

int main() {
    
    
  char *rick = "Wubba";
  printf("%s", rick);
  
  getchar();
  return 0;
}

rick字符串%s输出结果


我们如此自由地定义了rick字符串,可编译器却仍然能准确地加以解读。这种强大能力背后隐藏着的细节,却是引起无数错误的罪魁祸首。

如前文所说,我们定义的rick字符串末尾,被自动添加了 '\0’字符。通过使用for循环,可以清楚地观察到这一现象:

int main() {
    
    
  char *rick = "Wubba";

  for (int i = 0; i < 6; ++i)
    printf("%c", rick[i]);

  getchar();
  return 0;
}

for循环输出

可以发现,在输出的单词和光标之间有个空格,那便是被我们强制输出的 ‘\0’ 字符(这真的不是我打上去的 )。


我们还可以进一步测试:

printf("%d", rick[5]); //int形式输出'\0'

'\0'的int形式输出
这个圆圆的0,便是 ‘\0’ 的ASCII码表示。


如果画出rick字符串(或称“字符数组”)在内存(或称“地址”)中的示意图的话:
Wubba字符串在地址中的样子
下标为6和之后的值都是未知的,由计算机内存当下的状态决定。


其实,我们也可以手动在我们的字符串中(不限于末尾)添加 ‘\0’,在被当作字符串,使用%s输出时,会在 ‘\0’ 处被截停。

  char *rick = "Wub\0ba";
  printf("%s", rick);

"Wub\0ba"的输出结果


若用for循环强制输出的话,可以看到 ‘\0’ 的输出结果是一个空格。其后面的字符也有被正常初始化。

  char *rick = "Wub\0ba";
  for (int i = 0; i < 6; ++i)
    printf("%c", rick[i])

"Wub\0ba"for循环输出

情况二:morty字符串

情况一已经基本覆盖了c语言中字符串的基本逻辑与细节。对于其他情况的说明也就变得相对轻松很多了。

对于这一部分的主角——morty字符数组来说,与其他定义方式的最大不同在于,这种方式显式声明了字符串的长度,或者叫预留给字符串的内存空间。

  char morty[10] = "Whoa!";

作为例子,这里声明的长度显然多于实际字符串长度。那么我们的c语言会如何处理这种情况呢?

简单的测试一下,一切都逐渐变得清晰起来。

int main() {
    
    
  char morty[10] = "Whoa!";
  
  // morty数组存储内容(字符形式)
  printf("morty[]:");
  for (int i = 0; i < 15; ++i)
    printf("%4c", morty[i]);
  printf("\n");
  
  // morty数组存储内容(ASCII码形式)
  printf("ASCII:  ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", morty[i]);
  printf("\n");

  // 对应下标
  printf("index:  ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", i);

  getchar();
  return 0;
}

morty数组测试结果


这里我们使用printf的格式化功能(如%4d),将morty数组的每个字符、每个字符对应的ASCII码、元素在数组中对应的下标,以垂直对齐的方式打出来,起到了和画图一样的效果(不过比起之前的图来说丑了不止一点 )。

观察可以发现,除了我们显式列出的 “whoa!” 字符串成功初始化之外,在为morty数组预留的10个char字符空间之外的地方——下标为14处,出现了未定义的值(查ASCII表发现是 ‘.’ 字符)。然而我们为morty预留的下标0-9的位置范围中,未定义的值(下标5-9的五个元素)都为0。

这时我们可以大胆猜测:预留但未定义的元素会被默认初始化为0,即 ‘\0’。让我们来用实践揭晓真理。

实践 是检验真理的唯一标准( 检验环节

为了探究猜测是否能普遍成立,一种直观的方法就是尝试改变计算机内存状态,看是否会有反例(预留但未定义的元素表现为0以外的ASCII值)出现。
根据本人对计算机科学原理有限的了解,找出了如下建设性的测试:

int main() {
    
    
  char squirrel[10] = "follow him"; // 干扰数组
  char morty[10] = "Whoa!";

  // morty数组存储内容(字符形式)
  printf("morty[]:");
  for (int i = 0; i < 15; ++i)
    printf("%4c", morty[i]);
  printf("\n");
  
  // morty数组存储内容(ASCII码形式)
  printf("ASCII:  ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", morty[i]);
  printf("\n");

  // 对应下标
  printf("index:  ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", i);

  getchar();
  return 0;
}

得到如下结果:


morty数组实践检验环节输出


OhLaLa!看来近年浸淫于计算机世界带来的不只是知识的扩展,于此相关的直觉似乎也得到了培养。又进行了几次尝试,得到的结果类似——都符合我们的猜想。


(morty数组大小改为12,内容小改,同时在morty数组后又多定义了一个字符串后,得到的结果如下)

int main() {
    
    
  char squirrel[10] = "follow him"; // 干扰数组
  char morty[12] = "Who123a!";
  char squirrel1[10] = "follow him"; // 干扰数组

  // morty数组存储内容(字符形式)
  printf("morty[]:");
  for (int i = 0; i < 15; ++i)
    printf("%4c", morty[i]);
  printf("\n");
  
  // morty数组存储内容(ASCII码形式)
  printf("ASCII:  ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", morty[i]);
  printf("\n");

  // 对应下标
  printf("index:  ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", i);

  getchar();
  return 0;
}

实践环节更多的尝试


可是这只能让我们接近真理,还是无法抵达。得等我找到一本c语言权威书籍~~或是亲自去请教c语言创始人~~ 才能真正得知。也希望有条件的各位读者能给出解答,不胜感激。

情况三:squanchy字符串

char squanchy[] = "Squanch it.";

(专门为情况三写个小节只是为了格式美观。)

实际上,squanchy的情况与情况一完全相同,因为数组名squanchy在c语言中的含义,正是指向内存中该数组第一个元素的指针。这与rick字符串的情况如出一辙。


在设计了与情况二完全相同的测试后,得出如下结果

int main() {
    
    
  char squirrel[10] = "follow him"; // 干扰数组
  char squanchy[] = "Squanch it.";

  //squanchy数组存储内容(字符形式)
  printf("squanchy[]:");
  for (int i = 0; i < 15; ++i)
    printf("%4c", squanchy[i]);
  printf("\n");
  
  // squanchy数组存储内容(ASCII码形式)
  printf("ASCII:     ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", squanchy[i]);
  printf("\n");

  // 对应下标
  printf("index:     ");
  for (int i = 0; i < 15; ++i)
    printf("%4d", i);

  getchar();
  return 0;
}

squanchy测试结果

正如你所看到的,没有指定预留空间,编译器只为我们在字符串末尾添加了一个 ‘\0’(在下标为11的位置)。不过这通常已经足够我们用了。

情况四:snowball字符串

  char snowball[] = {
    
    't', 'e', 's', 't', 'i', 's'};
  printf("%s", snowball);

这种定义方式有很大的隐患。编译器似乎对这种方式有偏见。如果没有在字符串末尾明显地添加 ‘\0’,那也不会有任何人来帮你这么做。这可能导致的结果是:

在这里插入图片描述
说好的testis呢?这多出来的是什么?


这儿有位老兄得到的结果要更壮观些 —>传送门

sizeof()在字符串中的使用

通常情况下,使用sizeof()函数可以轻松获取数组长度。而且sizeof()尤其适用于字符串(字符数组)——c语言中char通常占内存中的1个字节,故sizeof(char)的结果为1。因此对字符串使用sizeof()得到的就是直观的字符串长度,而其他类型数组却要繁琐一点:

char rick[] = "Wubba\0";
int egg[] = {
    
    123,456,666};
printf("        Type size    Array size     Array length\n");
printf("rick:   %9d %11d %16d\n", sizeof(char), sizeof(rick), sizeof(rick)/sizeof(char));
printf("egg:    %9d %11d %16d",   sizeof(int),  sizeof(egg),  sizeof(egg)/sizeof(int));

sizeof测试结果

简单分析一下

  1. 对于int数组egg:使用sizeof()得到12。这是egg数组实际长度的4倍,因为int在这里占32位、4字节。要得到数组长度,需要使用sizeof(egg)/sizeof(int)的方式。而字符串rick两种方式得到的结果相同,原因也显而易见。
  2. 对于char数组rick:尽管我们在字符串末手动加入了 ‘\0’,编译器还是为我们多加了一个 ‘\0’(可以通过for循环%d打印测试)。这很好理解,编译器不会费工夫去识别用户有没有手动添加 ‘\0’,而是一视同仁地处理。

字符数组还是指针?

测试过程中,意外地有了新的收获。

如果对四种字符串定义方式统统使用sizeof()的话:

char *rick = "Wubba";
char rick1[] = "Wubba";
char rick2[] = {
    
    'W', 'u', 'b', 'b', 'a', '\0'};
char rick3[10] = "Wubba";

printf("%d ", sizeof(rick));
printf("%d ", sizeof(rick1));
printf("%d ", sizeof(rick2));
printf("%d ", sizeof(rick3));

在这里插入图片描述
得到的结果很amazing——虽然rick1、2和3结果都合理(注意编译器添加的’\0’也被计算在内了),可是第一个rick数组,得到的结果却是个有点荒谬的数字4!

困惑的我试着改变字符串内容长度、改变字符串名称长度、加入干扰数组…可是无论多改得多么离谱,始终无法撼动这个结果。

查阅资料发现,这种字符串定义方式,在sizeof()眼中,是另一种相似但又有所不同的东西——指针。

char *rick = "Wubba"; // 被我们理解为的字符数组
printf("rick: %d %d\n", sizeof(rick), rick);

char suffix = '?';
char *p = &suffix;   // 一个指针
printf("p:    %d %d", sizeof(p), p);

在这里插入图片描述

所以,利用rick这种明显的指针方式定义数组的话,是没法正常使用sizeof()获取数组长度的。


总结

字符串是如此的常见,我们对它熟悉,却又陌生。探究让我们更接近真理,也让我们创造出的程序更加优美。

下面对文章关键点做一总结:

  1. 除了下面这种方式以外,编译器都会自动在字符串末尾添加 ‘\0’。
	char snowball[] = {
    
    't', 'e', 's', 't', 'i', 's', '\0'}; // 千万别忘了加'\0'!
  1. 使用printf的%s方式输出时,输出会停止于第一个 ‘\0’ 之前。
  2. 字符串在定义之外的范围中的值可能很离谱,具体取决于计算机内存当下状态。这里所说的“定义”有两种情况:
    • morty[10]中的10个预留空间都属于已定义(预留但未初始化的区域会被默认初始化为’\0’)
    • 在其他三种形式中,是指我们声明时写在引号内的字符串,再加上编译器自动加上的 ‘\0’(形如snowball[]的字符串不会自动添加’\0’)
  3. 与其他类型的数组不同,字符类型数组的长度可以直接由sizeof()获取。
  4. 若对用指针形式定义的字符串(如:*rick)使用sizeof(),得到的不是字符串长度,而是一个指针所占内存空间的大小。

(完)

猜你喜欢

转载自blog.csdn.net/weixin_39591031/article/details/109726381