C-указатель арифметики

Это код, который я не понимаю, он просто меняет строку.

#include <stdio.h>

void strrev(char *p)
{
  char *q = p;
  while(q && *q) ++q;
  for(--q; p < q; ++p, --q)
    *p = *p ^ *q,
    *q = *p ^ *q,
    *p = *p ^ *q;
}

int main(int argc, char **argv)
{
  do {
    printf("%s ",  argv[argc-1]); strrev(argv[argc-1]);
    printf("%s\n", argv[argc-1]);
  } while(--argc);

  return 0;
}

Единственный фрагмент кода, который я не понимаю, следующий: while(q && *q) ++q;, он используется для поиска eos. Разве это не то же самое, что while(*q) ++q;, так как q никогда не будет 0? Как автор кода может убедиться, что q или *q будут 0?

Этот код исходит из этого вопроса: Как изменить строку на C или С++?

Ответ 1

Комментарий Дэвида Хеффернана правильный. Этот код ужасен.

Точка кода, о которой вы спрашиваете, это пропустить разыменование q, если оно равно null. Поэтому автор кода считает, что q может быть нулевым. При каких обстоятельствах q может быть нулевым? Наиболее очевидным является: if p null.

Итак, посмотрим, что делает код, когда p имеет значение null.

void strrev(char *p) // Assumption: p is null
{
  char *q = p; // now q is null
  while(q && *q) ++q; // The loop is skipped, so q and p are both still null.
  for(--q; 

Итак, первое, что мы делаем, это декремент q, который является нулевым. Вероятно, это обернется, и мы получим в результате q, содержащий максимально возможный указатель.

    p < q; 

Поскольку значение null меньше, чем все, кроме null, а q больше не равно null, это верно. Мы вводим цикл...

    ++p, --q)
    *p = *p ^ *q,

И быстро разыщите нулевое значение.

    *q = *p ^ *q,
    *p = *p ^ *q;
}

Кстати, в Coverity мы называем это "прямым нулевым дефектом", то есть шаблоном, в котором путь кода указывает, что значение, как ожидается, будет потенциально нулевым, а затем на том же пути кода предполагает, что он не равен нулю. Это чрезвычайно распространено.

ОК, поэтому этот код полностью сломан, если в качестве аргумента задано значение null. Есть ли другие способы, чтобы это было нарушено? Что произойдет, если мы дадим ему пустую строку?

void strrev(char *p) // Assumption: *p is 0
{
  char *q = p; // *q is 0
  while(q && *q) ++q; // The second condition is not met so the body is skipped.
  for(--q; // q now points *before valid memory*.
       p < q  // And we compare an invalid pointer to a valid one.

Есть ли у нас гарантия на C, которая говорит, что когда вы вычитаете один из указателя на действительную память и затем сравниваете этот указатель на другой указатель, чтобы сравнение было разумным? Потому что это кажется мне невероятно опасным. Я не очень хорошо знаю стандарт C, чтобы сказать, является ли это undefined поведением или нет.

Кроме того, этот код использует ужасный "своп двух символов с xor". Зачем это делать?. Он генерирует более крупный, медленный машинный код, и его труднее читать, понимать и поддерживать. Если вы хотите обменять две вещи, заменить их.

Он также использует оператор запятой, чтобы поместить несколько операторов в один оператор, чтобы избежать ужаса скобок вокруг тела for. Какова цель этой странности? Цель кода - не показывать, сколько операторов вы знаете, это прежде всего общение с читателем кода.

Функция также изменяет свой формальный параметр, что затрудняет отладку.

Ответ 2

Код

while(q && *q)

является сокращением для

while(q != NULL && *q != '\0')

чтобы вы тестировали, если q (в начале, равном p), не является NULL. Это означает, что функция, вызванная с аргументом NULL, не будет сбой в этом цикле while. (Но он все равно будет разбиваться во втором цикле).

Ответ 3

Разве это не так, как while(*q) ++q;, так как q никогда не будет 0?

while(q && *q) используется для обеспечения q не NULL и *q не NUL. Использование while(*q) также является законным, если q не указывает на NULL.

void string_rev(char *p)
{
   char *q = p;

   if(q == NULL)
       return;

   while(*q) ++q;
   for(--q; p < q; ++p, --q)
       *p = *p ^ *q,
       *q = *p ^ *q,
       *p = *p ^ *q;

}  

Как автор кода может убедиться, что q или *q будут 0?

while(q && *q), q может быть указателем NULL, если p указывает на NULL, и цикл заканчивается перед входом в тело цикла. В противном случае из-за операции он не может быть указателем NULL. Теперь завершение цикла зависит от *q. Когда q достигает конца строки, *q становится 0, и цикл завершается.