C中到底有多少种方法来声明String? (需要有关修饰符和范围的帮助)

Exactly how many ways to declare String in C? (need help on modifiers and scope)

所以我想知道究竟有多少种方法来声明字符串。我知道类似的问题已经被问过好几次了,但我认为我的关注点不同。作为一个C初学者,想知道哪种声明方式是正确的,更可取的,这样才能坚持下去。

我们都知道可以通过以下两种方式声明字符串。

char str[] = "blablabla";
char *str = "blablabla";

在阅读了stack-overflow中的一些问答后,我被告知字符串文字被放置在内存的只读部分。因此,为了创建可修改的字符串,您需要在堆栈中创建一个字符串长度 + 1 的字符数组,并将每个字符复制到数组中。 这就是第一行代码所做的。

然而,第二行代码所做的是创建一个字符指针,并将指针分配给位于内存只读部分的第一个字符的地址。所以这种声明不涉及逐字复制。

如有错误请告知

到目前为止,它似乎很好理解,但真正让我困惑的是修饰符。例如,有人建议

const char *str = "blablabla";

而不是

char *str = "blablabla";

因为如果我们做类似的事情

*(str + 1) = 'q';

它会导致可怕的未定义行为。

但有些人更进一步,提出类似

的建议
static const char *str = "blablabla";

并说这会将字符串放入静态内存中 永远不会修改以避免未定义的行为。

那么实际上声明字符串的#right# 方法是什么?

此外,我也有兴趣了解声明字符串时的范围。

例如,

(你可以忽略这些例子,正如其他人指出的那样,它们都是错误的)

#include <stdio.h>

char **strPtr();

int main()
{
  printf("%s", *strPtr());
}

char **strPtr()
{
  char str[] = "blablabla";
  char *charPtr = str;
  char **strPtr = &charPtr;
  return strPtr;
}

将打印一些垃圾值。

但是

#include <stdio.h>

char **strPtr();

int main()
{
  printf("%s", *strPtr());
}

char **strPtr()
{
  char *str = "blablabla";

  /*As point out by other I am returning the address of a local variable*/
  /*This causes undefined behavior*/
  char **strPtr = &str;
  return strPtr;
}

将完美运行。 (不,它没有,这是未定义的行为。)

我想我应该把它留作另一个问题。 这个问题太长了。

您的很多困惑来自关于 C 和字符串的常见 mis-understanding:您的问题标题中明确说明了这一点。 C 语言没有原生的字符串类型,所以实际上在 C 语言中有 种方法来声明字符串。

花一些时间阅读 Does C Have a String type?,这很好地解释了这一点。

从您不能(明智地)执行以下操作这一事实可以看出这一点:

char *a, *b;
// code to point a and b at some "strings"
if (a == b)
{
   // do something if strings compare equal
}

if 语句将比较 指针 的值,而不是它们寻址的内存的内容。因此,如果 ab 指向两个不同的内存区域,每个区域包含相同的数据,则比较 a == b 将失败。如果 ab 持有相同的地址(即指向内存中的相同位置),比较将评估为 "true"(即非零的东西)的唯一时间。

C 所拥有的是一种约定,以及一些让生活更轻松的语法糖。

惯例是 "string" 表示为以零值结尾的 char 序列(通常称为 NUL 并由特殊字符转义序列 '[=23=]' 表示) ).该约定来自原始标准库(早在 70 年代)的 API,它提供了一组字符串原语,例如 strcpy()。这些原语对于在语言中做任何真正有用的事情来说是如此基础,以至于通过添加语法糖使程序员的生活变得更轻松(这都是在语言脱离实验室之前)。

句法糖就是"string literals"的存在:不多也不少。在 源代码 中,任何包含在双引号中的 ASCII 字符序列都被解释为字符串文字,并且编译器会生成字符的副本(在 "read-only" 内存中)加上一个终止 NUL 字节以符合约定。现代编译器检测重复的文字并且只生成一个副本——但这不是我上次查看时标准的要求。因此:

assert("abc" == "abc");

可能会也可能不会提出断言 - 这强化了 C 没有本机字符串类型的声明。 (就此而言,C++ 也没有——它有一个字符串 class!)

有了这个,你如何使用字符串文字来初始化一个变量?

您将看到的第一个(也是最常见的)表单是这个

char *p = "ABC";

在这里,编译器在程序的 "read only" 部分留出 4 个字节(假设 sizeof(char) ==1)的内存,并用 [0x41, 0x42, 0x43, 0x00] 初始化它。然后它用该数组的地址初始化 p 。您应该注意到这里进行了一些 const 转换,因为字符串文字的基础类型是 const char * const(指向常量字符的常量指针)。这就是为什么通常建议您将其写成:

const char *p = "ABC";

这是 "pointer to a constant char" - "pointer to read only memory" 的另一种说法。

接下来的两种形式使用字符串文字来初始化数组

char p1[] = "ABC";
char p2[3] = "ABC";

请注意,两者之间存在重大差异。第一行创建一个 4 字节数组。第二个创建一个 3 字节数组。

在第一种情况下,和以前一样,编译器创建一个包含 [0x41, 0x42, 0x43, 0x00] 的 4 字节常量数组。请注意,它添加尾随 NUL 以形成 "C String"。然后它保留四个字节的 RAM(在堆栈上用于局部变量,或在 "static" 内存中用于文件范围内的变量)并插入代码以在 在 运行 时间 通过将 "read only" 数组复制到分配的 RAM 中。您现在可以随意修改 p1 的元素。

在第二种情况下,编译器创建一个包含 [0x41, 0x42, 0x43] 的 3 字节常量数组。请注意,有 no 尾随 NUL。然后它保留 3 个字节的 RAM(在堆栈上用于局部变量,或在 "static" 内存中用于文件范围内的变量)并插入代码以在 运行 时间 通过将 "read only" 数组复制到分配的 RAM 中。您现在又可以随意修改 p2 的元素了。

两个数组 p1p2 的大小差异很关键。下面的代码(如果你 运行 它)将演示它。

char p1[] = "ABC";
char p2[3] = "ABC";

printf ("p1 = %s\n", p1); // Will print out "p1 = ABC"
printf ("p2 = %s\n", p2); // Will print out "p2 = ABC!@#$%^&*"

第二个 printf 的输出是不可预测的,理论上可能会导致您的代码崩溃。它似乎可以工作,只是因为太多的 RAM 充满了零,最终 printf 找到一个终止 NUL。

希望这对您有所帮助。