У меня три петли над массивом элементов (char *) в C. Почему третий сбой?

При экспериментировании с методами для перехода по массиву строк в C я разработал следующую небольшую программу:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


typedef char* string;

int main() {
  char *family1[4] = {"father", "mother", "son", NULL};
  string family2[4] = {"father", "mother", "son", NULL};

  /* Loop #1: Using a simple pointer to step through "family1". */
  for (char **p = family1; *p != NULL; p++) {
    printf("%s\n", *p);
  }
  putchar('\n');

  /* Loop #2: Using the typedef for clarity and stepping through
   * family2. */
  for (string *s = family2; *s != NULL; s++) {
    printf("%s\n", *s);
  }
  putchar('\n');

  /* Loop #3: Again, we use the pointer, but with a unique increment
   * step in our for loop.  This fails to work.  Why? */
  for (string s = family2[0]; s != NULL; s = *(&s + 1)) {
    printf("%s\n", s);
  }
}

Мой конкретный вопрос касается отказа Loop # 3. При прохождении через отладчик Loops # 1 и # 2 завершаются успешно, но последний цикл выходит из строя по неизвестной причине. Я бы не спросил об этом здесь, кроме факта, что показывает мне, что у меня есть некоторые критические недоразумения относительно "&". Оператор.

Мой вопрос (и текущее понимание) таков:

family2 - это массив-указатель на char. Таким образом, когда s установлено на family2[0], мы имеем a (char*), указывающий на "отец". Поэтому принятие &s должно дать нам эквивалент family2, указывая на первый элемент family2 после ожидаемого распада указателя. Почему же тогда нет, *(&s + 1) указывает на следующий элемент, как ожидалось?

Большое спасибо,
lifecrisis


EDIT - обновление и извлеченные уроки:

Следующий список представляет собой резюме всех соответствующих фактов и интерпретаций, которые объясняют, почему третий цикл не работает, как первые два.

  • s - это отдельная переменная, содержащая копию значения (указатель-to- char) из переменной family2[0]. I.e., эти два эквивалентных значения расположены в SEPARATE местах в памяти.
  • family2[0] до family2[3] являются смежными элементами памяти, а s не имеет присутствия в этом пространстве, хотя оно содержит то же значение, которое хранится в family2[0] в начале нашего цикла.
  • Эти первые два факта означают, что &s и &family2[0] НЕ равны. Таким образом, добавление одного в &s вернет указатель на неизвестные /undefined данные, тогда как добавление одного в &family2[0] даст вам &family2[1], если требуется.
  • Кроме того, шаг обновления в третьем цикле for фактически не приводит к тому, что s будет выполняться вперед в памяти на каждой итерации. Это связано с тем, что &s является постоянным во всех итерациях нашего цикла. Это является причиной наблюдаемого бесконечного цикла.

Благодаря КАЖДОМУ за их помощь!
lifecrisis

Ответы

Ответ 1

Когда вы выполняете s = *(&s + 1), переменная s является локальной переменной в неявной области, содержащей только цикл. Когда вы делаете &s, вы получаете адрес этой локальной переменной, которая не связана с каким-либо из массивов.

Отличие от предыдущего цикла заключается в том, что там s является указателем на первый элемент в массиве.


Чтобы объяснить это немного более "графически", то, что у вас есть в последнем цикле, похоже на

+----+      +---+      +------------+
| &s | ---> | s | ---> | family2[0] |
+----+      +---+      +------------+

То есть, &s указывает на s, а s указывает на family2[0].

Когда вы делаете &s + 1, у вас есть что-то вроде

+------------+
| family2[0] |
+------------+
^
|
+---+----
| s | ...
+---+----
^   ^
|   |
&s  &s + 1

Ответ 2

Картинки очень помогают:

            +----------+
            | "father" |                                    
            +----------+         +----------+      +-------+      NULL 
   /-----------→1000            | "mother" |      | "son" |        ↑
+-----+           ↑              +----------+      +-------+        |
|  s  | ?         |                  2000            2500           |
+-----+           |                   ↑                ↑            |
 6000  6008 +----------------+----------------+--------------+--------------+
            |   family2[0]   |   family2[1]   |  family2[2]  |  family2[3]  |
            +----------------+----------------+--------------+--------------+
                  5000              5008            5016           5024

                    (    &s refers to 6000    ) 
                    ( &s+1 refers to 6008 but )
                    (   *(&s+1) invokes UB    )

Адреса, выбранные в качестве случайных целых чисел для простоты


Дело в том, что хотя оба s и family2[0] указывают на один и тот же базовый адрес строкового литерала "father", указатели не связаны друг с другом и имеют свою собственную другую ячейку памяти, где они хранятся. *(&s+1) != family2[1].

Вы нажимаете UB, когда вы делаете *(&s + 1), потому что &s + 1 - это ячейка памяти, в которую вы не должны вмешиваться, т.е. она не принадлежит к любому созданному вами объекту. Вы никогда не знаете, что хранилось там = > Undefined Поведение.

Спасибо @2501 за то, что указали несколько ошибок!

Ответ 3

#include <stdio.h>
#include <stdlib.h>
#include <string.h>


typedef char* string;

int main() {
char *family1[4] = { "father", "mother", "son", NULL };
string family2[4] = { "father", "mother", "son", NULL };

/* Loop #1: Using a simple pointer to step through "family1". */
for (char **p = family1; *p != NULL; p++) {
    printf("%s\n", *p);
}
putchar('\n');

/* Loop #2: Using the typedef for clarity and stepping through
* family2. */
for (string *s = family2; *s != NULL; s++) {
    printf("%s\n", *s);
}
putchar('\n');

/* Loop #3: Again, we use the pointer, but with a unique increment
* step in our for loop.  This fails to work.  Why? */
/*for (string s = family2[0]; s != NULL; s = *(&s + 1)) {
    printf("%s\n", s);
}
*/
for (int j = 0; j < 3; j++)
{
    printf("%d ",family2[j]);
    printf("%d\n", strlen(family2[j]));
}
printf("\n");
int i = 0;
for (string s = family2[i]; i != 3; s = (s + strlen(family2[i]) + 2),i++) {
    printf("%d ",s);
    printf("%s\n", s);
}

system("pause");

}

это пример, переработанный из вашего кода, если вы запустите его, вы найдете изменение адреса точки и семейства2, тогда вы поймете взаимосвязь цикла № 3.