与 char 混淆 **

Confused with char **

如果我写

//case 1
char *animals[2] = {"cat", "dog"};
char **animal_ptr = animals; 
printf("%s\n", *(animal_ptr + 1)); //fine

并且,以不同的方式:

//case 2
char *animal = "cat";
char **animal_ptr = &animal;
 *(animal_ptr + 1) = "dog";
printf("%s\n", *(animal_ptr + 1)); //fine

所以,我被上面的两个例子弄糊涂了。

  1. 在案例 1 中,我了解到 animal_ptr 是指向指针集合的指针,并且由于指针包含地址,因此我不需要添加 &。但是在情况 2 中,我必须添加一个 & 才能工作,即使 animal 已经是一个指针。为什么?

  2. 在这两种情况下,为什么可以接受通过另一个指针修改字符串文字?据我所知,当你 声明一个字符串,如 char *x = "..etc";,它被放置在内存中的一个不能修改的部分。那么为什么在情况1中animalanimal_ptr都可以修改字符串?

  3. 为什么 strcpy(*(animal_ptr + 1), "bird") 失败,并且程序停止,即使赋值在 2 中有效?

  4. 在案例 2 中:

    • 当我这样做 printf("%s", *animal_ptr) 时,它工作正常并且对我有意义。
    • 当我执行 printf("%s", *animal) 时,它会停止。为什么?

谢谢,抱歉有很多问题。

在这两种情况下,animal_ptr 都是双指针:指向另一个指针的指针。在情况 1 中,它专门指向数组中的第一个元素,在情况 2 中,它指向唯一的一个。

您可以将指针变量视为具有 "levels",其中 lv2 指针是指向另一个指针的指针,而 lv3 指针指向 lv2,依此类推。声明变量时,每个 *[] "increase" 您的级别加 1。分配值时,* 访问指针内的信息,因此 "lowers" 一个级别,并且 & 询问该指针在内存中的位置,因此 "increasing" 它。

P.S: 情况2,你实际上搞砸了!您正在覆盖 animal_ptr+1 指向的位置,该位置可能被该函数堆栈中的另一个变量(大量时间弄乱您的代码)使用,或者根本不属于堆栈,因此出现段错误!

编辑:为了更好地回答您的问题:

Q(1): animal 是一个 lv1 指针,因此 &animal 是一个 lv2 指针,与你分配给它的 char** animal_ptr 相同。

Q(2): 改变指针变量的值和直接修改它指向的位置的值有很大区别。即使您可以使用它们中的任何一个获得类似的结果,这取决于您的代码。

Q(3): "dog" 是一个包含 4 个字符的数组 ('d','o','g','[=17=]'),在第 1 种情况下,您试图将其替换为包含 5 个字符的数组,"bird".在情况 2 中,animal_ptr+1 指向天知道哪里会出现段错误。

Q(4): 如果你看懂了我的烂水平解释,你现在应该明白*animal是一个char(动物字符串的第一个字母),而不是一个char指针(字符串),因此不能'不会以 %s 格式打印。

在这样的赋值中,类型 char*[] 可以衰减为 char**,因此您不需要 address-of 运算符。在第二种情况下,您只有一个 char*,您需要获取它的地址才能获得 char**。

如果文字不是常量,则不会将其放入某些只读部分。在这种情况下,它们可能是在每次调用函数时在堆栈上创建的。

如果有什么东西在工作,并不意味着代码是正确的。在你的第二种情况下,你没有在第一个之后分配第二个 char* ,因此增加 animal_ptr 并取消引用它是不合法的并且具有未定义的行为。在那之后你做的任何事情都可能会工作或崩溃或做任何事情。

在第一种情况下,您声明了一个长度为 2 的指针数组。在第一种情况下,您的赋值 **animal_ptr = animals 不需要 &animals,因为在 C 中,一个变量指向一个数组是一个指针。

你的第二个案例一点问题都没有。当您进行赋值时 **animal_ptr = &animal 您可以有效地将 **animal_ptr 视为指向长度为 1 的指针数组的指针。因此您的赋值 *(animal_ptr+ 1) = "dog" 尝试将指向数组 "dog" 的指针分配给该数组中不存在的第二个位置。这可能看起来成功了(即您的程序可能没有崩溃),但实际上您通过对内存中尚未分配的位置进行分配来破坏内存。

在您的第 3 季度中,您现在试图将数组 "bird" 复制到 animal_ptr+1 指向的内存中,从而使情况 2 中的错误更加复杂。我们已经知道 animal_ptr+1 是一个未分配的位置,使用起来不安全。但另外它指向的位置(由于您的分配错误)是另一个字符串文字。

至于第四季度。内存损坏后没有任何东西可以安全使用。

指针和字符串的问题是你必须跟踪它们的 space 被分配到哪里,以及它是否可写。您还必须了解使用 strcpy 复制字符串与重新排列指针之间的区别。

当您使用 "cat""dog" 等字符串常量时,编译器会自动为字符串分配 space,但这些字符串不可修改。 (也就是说,您不能使用 strcpy 在它们之上复制新字符串。)

在您的情况 1 中,您有一个 "array" 两个字符串。有几种思考方式。由于字符串是一个字符数组,而您有两个字符串,您可以将其视为 "two-dimensional array"。这就是为什么 animals 有一个 * 和一对括号 [],以及为什么 animal_ptr 有两个 *。或者,由于 char * 是我们通常在 C 中引用字符串的方式,您可以看到 animals 是两个字符串的数组。

检查所有内容的分配也很重要。编译器负责 "cat""dog"。您将 animals 分配为大小为 2 的数组,因此已处理完毕。最后,animal_ptr 被设置为指向 animals 所在的位置,因此它也得到了适当的分配。 (但请注意 animalsanimal_ptr 指的是相同的存储。)

但是,您的情况 2 的情况有所不同。您从只有一个字符串 "cat" 开始,由一个指针 animal 指向。您再次使用指向指针的指针 animal_ptr 指向那个字符串。到目前为止一切正常,但我们所拥有的相当于一个 one 字符串数组。相当于

char *animals[1]={"cat"};
char **animal_ptr=animals;

所以当您稍后说 *(animal_ptr+1)="dog" 时,您正在写入 "array" 中不存在的单元格。你最终覆盖了内存的其他部分。有时您可以侥幸逃脱,有时它会导致您程序的其他部分出现错误,有时它会使您的程序崩溃。

如果我们不写 *(animal_ptr+1)="dog" 而是写等价的

,可能会更容易看清这一点
animal_ptr[1] = "dog";

由于animal_ptr相当于一个1元素数组,唯一合法的下标是[0],而不是[1]

现在回答您的具体问题:

Q1。在这两个示例中,animal_ptr 都是指向指针的指针。在情况 1 中,animal 是一个数组,在 C 中,您会自动获得指向数组第一个元素的指针,而无需使用显式 &。在情况 2 中,animal 是一个简单的指针,因此您需要一个显式的 & 来获取其地址,以生成 animal_ptr 所需的指向指针的指针。

Q2。在这两种情况下,您都不会修改任何字符串。你是对的,字符串本身是不可写的,但是数组 animals in case 1 可写的,所以可以插入新的指针。你可以说 animals[0] = "chicken"animals[1] = "pig" 直到奶牛回家。情况2,你可以说animal = "chicken"animal_ptr[0] = "pig",因为第一个指针(animal)是可写的,但你不能修改animal_ptr[1],不是因为它不可写, 但因为它不存在。

Q3。您不能将 strcpy 与这些示例中的任何一个一起使用,因为它试图将新字符复制到您现有的字符串之一中,但失败了,因为您现有的所有字符串都是编译器分配的并且不可写。

如果您想查看 strcpy 是如何工作的,您必须分配一些可写存储空间供 strcpy 复制到。你可以做类似

的事情
char newarray[10];
animal_ptr[0] = newarray;
strcpy(animal_ptr[0], "bird");

animal_ptr[0] = malloc(10);
strcpy(animal_ptr[0], "bird");

Q4。当你做 printf("%s",*animal_ptr) 相当于

printf("%s", animal_ptr[0]);

printf %s 想要一个指针,而你给了它一个。但是,当您写 printf("%s", *animal) 时,表达式 *animal 正在获取 animal 指向的第一个字符,可能是 "cat" 中的字母 'c'。然后,您将获取该字符并要求 printf 将其打印为字符串,并使用 %s。但是正如我们刚刚看到的,%s 需要一个指针。所以它试图打印内存中地址99处的字符串(因为'c'的ASCII值是99),然后崩溃了。

关于指针赋值与 strcpy

的更多信息

关于 C 中的字符串和指针的另一件事是,一开始很容易混淆分配它们的含义。 C 没有真正的 "first-class" 内置字符串类型,字符串表示为字符数组/指针的方式使我们必须始终牢记 和指针指针指向什么

为了更清楚地说明这一点,让我们换个话题,考虑一下指向整数的指针。假设我们有

int four = 4;
int five = 5;
int *intptr = &four;

所以我们有一个指针intptr,它指向的是值4(恰好也是变量four的值)。如果我们再说

intptr = &five;

我们正在更改指针的值。以前指向four/4,现在指向five/5。现在假设我们说

*intptr = 6;

在这种情况下,我们更改了指向的值intptr 指向与之前相同的位置 (&five),但我们将该位置的值从 5 更改为 6。 (当然,现在如果我们说 printf("%d", five) 我们会有点奇怪地得到 6。)

现在,回到字符串。如果我有

char *animal = "cat";

然后我说

animal = "dog";

我更改了指针指向的位置。它过去指向包含字符串 "cat" 的编译器分配的只读内存片,现在它指向包含字符串 [=23= 的不同的编译器分配的只读内存片].

现在假设我说

strcpy(animal, "elephant");

在这种情况下,我没有改变指针animal,我要求strcpy将新字符写入animal指向的位置。但是,请记住,animal 当前指向包含字符串 "dog" 的编译器分配的只读内存。由于它是只读的,因此尝试在此处写入新字符的尝试失败了。即使它没有因为那个原因失败,我们也会遇到不同的问题,因为 "elephant" 当然比 "dog".

归根结底,在 C 中有两种完全不同的 "assign" 字符串的方法:分配指针和调用 strcpy。但他们真的完全不同。

您可能听说过 "you can't compare strings using ==, <, or >; you have to call strcmp"。比较运算符比较指针(这通常不是您想要的),而 strcmp 比较指向的字符。但是当涉及到赋值时,您可以 以任何一种方式进行:通过重新分配指针,通过使用strcpy复制字符。

但是如果你正在调用 strcpy,你总是必须确保目标指针 (a) 确实指向某处,并且它指向的内存区域 (b) 足够大并且 ( c) 可写。目标指针通常会指向一个你分配的字符数组,或者用malloc获得的一个动态内存区域。 (这就是我在回答您的问题 3 时向您展示的内容。)但是目标不能是指向编译器分配的字符串的指针。这很容易犯我的错误,但它不起作用,如您所见。

  1. 在C语言中,数组可以看作是不可赋值的指针。从数组到指针的转换是自动的。所以你总是可以这样写

    int array[5];
    int *ptr = array; 
    

你看到的是一个指向指针的指针。它将地址的地址存储到变量中。没有什么能阻止我们这样做

    int **array[5];
    int ***ptr = array; 

并在指向指针的指针方面更深入。

  1. 变量声明中没有const,所以数据是可写的。

  2. 和 4. 第二个代码片段非常混乱。正如其他人所说,它会破坏记忆。你所做的是获取指向请求字符串的指针地址,移动一个指针并保存在这个指向 "dog" 字符串的未准备的内存指针中。正如其他人所说,如果内存损坏,则无法确定任何事情,即使是正确的程序执行也是如此。

//case 1
char *animals[2]={"cat","dog"};
char **animal_ptr=animals; 
printf("%s\n",*(animal_ptr+1));      //fine
printf("%s\n",  animal_ptr[1]));     //equivalent
printf("%d\n", &animals == animals); // prints 1 i.e. true


//case 2
char *animal="cat";
char **animal_ptr=&animal;
 *(animal_ptr+1)="dog";
printf("%s\n",*(animal_ptr+1)); // fine
printf("%s\n", animal);         // prints cat

Q(1)

案例 1:

  • animals 是指向 char 类型
  • 的指针的 2 元素数组
  • 特别是 animals 本身是一个指针(指向指向 char 的第一个元素)
  • animal_ptr是指向char类型
  • 的指针 类型 char*
  • animals 分配给类型 char*
  • *animal_ptr
  • 注意 animals&animals 持有相同的地址 - 它们持有相同的值,但类型不同(参见 here 的解释)

案例 2:

  • animal 是指向 char 类型的指针(字符数组用 "cat" 初始化)
  • 特别是animal是一个指针(指向第一个元素,即字符'c'
  • animal_ptr是指向char类型
  • 的指针 类型 char*
  • *animal_ptr 被分配了指针 animal 地址,其类型为 char* ( 再读animal指向一个char&animal指向一个char*)
  • 我真的不清楚赋值,你基本上是在定义一个新字符串 "dog"
  • 注意你仍然可以打印 "cat"

Q(2)

我看不到你在哪里修改了字符串:-(

  • *animal_ptr指向cat字符串
  • *animal_ptr+1 持有另一个地址并指向 "dog" 字符串

打印出来,你会看到。

Q(3)

我认为您没有重新分配,而是从该地址定义了一个新字符串。该字符串的长度为 3,您想要复制一个长度为 4 的字符串。因此它失败了。

Q(4)

printf("%s", *animal) 更改为 printf("%c", *animal),因为此时打印的是字符,而不是字符串。

仅仅因为您可以将数组视为指针并不意味着指针指向数组。对指针指向的位置内存中的内容错误是现实世界中错误的主要来源之一。这就是为什么了解编译器正在为您分配什么(全局或在堆栈上)以及您需要为自己分配什么(通过动态分配,也称为 malloc)非常重要。

因此,您的案例 1 代码:

/* case 1 line 1 */ char *animals[2] = { "cat","dog" }

编译器预分配了两个四字节的内存块,存储了连续的字符'c'、'a'、't',第一个是0,'d', 'o'、'g',第二个为零。因为这些字符串是编译器从字符串文字生成的,所以它们(至少在理论上)是只读的。尝试将代码中的 'c' 更改为 'b' 会调用可怕的 "undefined behavior"。如果您要求编译器报告可疑代码(通常称为警告),您在此处执行的初始化会发出编译器的投诉。

编译器还预先分配了一个包含两个 char * 指针的数组,其中存储了预先分配的 "cat" 字符串的地址,以及预先分配的 "dog" 字符串的地址。这个数组叫做动物。编译器知道它为数组分配了 space,一旦数组达到 "out of scope",编译器就会释放 space。动物数组有一个地址,可以存储在其他指针变量中。

/* case 1 line 2 */ char **animal_ptr=animals;

此处编译器为 char ** 变量预分配存储空间 animal_ptr 然后使用动物数组的地址对其进行初始化。

从这里我们开始看到数组变量和指针变量之间的区别。设置 animal_ptr = animals 是完全合法的,但是设置 animals = animal_ptr 永远是不合法的。这里的要点是数组可以用作它等效类型的指针,但它不是指针。

/* case 1 line 3 */ printf("%s\n",*(animal_ptr+1));

指针加法定义为将指针的内容(在本例中为动物数组的地址)增加其指向的内容的大小。在这种情况下 animal_ptr 指向一个 char *。 animal_ptr+1 将是指向动物数组(又名 &animals[1])中第二个元素的地址的指针。取消引用 *(animal_ptr+1) 产生一个 char * 指针,其值为 "dog" 字符串的地址。 Printf 的 %s 字符串解析器然后使用该地址打印字符串 dog.

/* case 2 line 1 */ char *animal="cat";

编译器为字符串 ('c'、'a'、't'、0) 预分配存储空间。编译器为指针 animal 预分配 space (sizeof char *) 并使用 "cat" 字符串的地址对其进行初始化(此处会发生与之前相同的警告)。

/* case 2 line 2 */ char **animal_ptr=&animal;

编译器为指针animal_ptr预分配存储空间space (sizeof char **),并用动物指针的地址对其进行初始化;

/* case 2 line 3 */  *(animal_ptr+1)="dog";

首先,编译器为 "dog" 字符串预分配 space,然后它尝试存储分配块的地址……某处。

这是主要错误。 animal_ptr 保存着 animal 的地址,但在该地址分配的存储空间只够一个指针(编译器在第 1 行中预先分配)。当您在此处执行 animal_ptr+1 时,您已将指针移到编译器为动物分配的 space 之外。因此,您(可能)有一个有效的内存地址,但它没有指向已知已分配的内存位置。这是未定义的行为,并且无法预测取消引用此内存(在此处写入或在下一行从中读取)的结果。可以肯定的是,您刚刚将 "dog" 字符串的地址存储在内存中刚好超过动物指针的任何内容之上。

/* case 2 line 4 */ printf("%s\n",*(animal_ptr+1)); //fine

好吧,你可以说它很好,但事实并非如此。您再次取消引用存储在未分配内存中的指针。如果你幸运的话,这行得通。如果你不走运,你只是破坏了你的堆栈,当你继续前进时,会发生一些完全意想不到的事情。这种事情正是特权利用所基于的那种错误。

回答您的具体问题:

  1. 指针只是内存地址,但编译器关心它认为它们指向的是什么。如果 2 动物的类型为 'char *',而 animal_ptr 的类型为 'char **'。 &animal 有类型 'char **' 所以编译器接受了。

  2. 您的代码中没有任何内容试图修改 "dog" 和 "cat" 字符串的内容。即使他们有,它可能仍然有效,因为通过强制非 const 指针修改 const 变量是未定义的行为。在加载程序将字符串放在可写内存中的系统上,它可能会起作用。在将字符串放在只读内存中的系统上,您会遇到内存错误(例如分段错误)。

  3. 嗯,这要看strcpy放在哪里了。我猜你的意思是代替第 2 行第 3 行?在那种情况下 *(animal_ptr + 1) 是一个未初始化的指针,因此当 strcpy 尝试进行复制时,谁知道它正在尝试写入字符串的位置。

  4. *animal 是 char 类型。 %s 需要 char * 类型的内容。当 printf 试图取消引用传入的值时,它不是一个有效的指针,因此它会进入谁知道在哪里尝试读取字符串。