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

时间:2015-01-08 14:14:18

标签: c string

所以我想确切地知道声明字符串的方式有多少。我知道有几次问过类似的问题,但我认为我的重点是不同的。作为C语言的初学者,我想知道哪种声明方法是正确和可取的,这样我才能坚持下去。

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

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

在堆栈溢出中读取一些Q& A之后,我被告知字符串文字放在内存的只读部分。因此,为了创建可修改的字符串,您需要在堆栈中创建字符串长度为+ 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;
}

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

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

1 个答案:

答案 0 :(得分:4)

您的许多困惑来自对C和字符串的常见错误理解:在您的问题标题中明确说明了这一点。 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保持相同的地址(即指向内存中的相同位置)时,比较才会评估为“真”(即除零之外的东西)的唯一时间。

C有什么常规,以及一些使生活更轻松的语法糖。

约定是“字符串”表示为以值零结尾的char序列(通常称为NUL并由特殊字符转义序列'\0'表示)。这个约定来自原始标准库的API(早在70年代),它提供了一组字符串基元,如strcpy()。这些原语对于在语言中做任何真正有用的事情都非常重要,因为通过添加语法糖,这对于程序员来说更容易生活(这是在语言从实验室中逃脱之前)。

句法糖是“字符串文字”的存在:不多也不少。在源代码中,用双引号括起来的任何ASCII字符序列都被解释为字符串文字,编译器会生成字符的副本(在“只读”内存中)和终止NUL字节符合约定。现代编译器检测重复的文字,只生成一个副本 - 但上次我看起来并不是标准的要求。因此:

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

可能会也可能不会引发断言 - 这强化了C没有本机字符串类型的陈述。 (就此而言,C ++也没有 - 它有一个String类!)

除此之外,如何使用字符串文字来初始化变量?

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

char *p = "ABC";

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

const char *p = "ABC";

这是一个“指向常量字符的指针” - 另一种说“指向只读内存”的方法。

接下来的两个表单使用字符串文字初始化数组

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

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

在第一种情况下,与以前一样,编译器创建一个包含[0x41, 0x42, 0x43, 0x00]的4字节常量数组。请注意,它会添加尾随NUL以形成“C String”。然后它保留4个字节的RAM(在堆栈中用于本地变量,或在“静态”内存中用于文件范围的变量)并插入代码以在运行时初始化它通过复制“只读” “阵列进入分配的RAM。您现在可以随意修改p1的元素。

在第二种情况下,编译器创建一个包含[0x41, 0x42, 0x43]的3字节常量数组。请注意,有没有尾随NUL。然后它保留3个字节的RAM(在堆栈上用于本地变量,或在“静态”内存中用于文件范围的变量)并插入代码以通过复制“只读”来在运行时初始化它 “阵列进入分配的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。

希望这有帮助。