7

Начал читать учебник "Программирование: Введение в профессию" Том №2 "Системы и сети" автора А.В. Столярова, в котором рассматривается язык программирования Си. В теме про указатели и строковые массивы (стр.96) есть небольшой пример реализации процедуры копирования из одной строки в другую. Собственно так выглядит код (на стр.94 есть еще один пример):

    void stringCopy(char* dest, const char* src)
    {
        while ((*dest++ = *src++));    
    }

Автор настаивает, что в коде есть ошибка. Причем сам пример часто мелькает в интернете в качестве "суперкороткого решения", и автор относится к этому отрицательно. Сам же автор конкретной ошибки не указывает, однако просит разобраться (для себя), лишь намекая на побочный эффект, который легко не заметить. Вот что автор пишет. введите сюда описание изображения Вот так выглядит правильный пример кода автора учебника:

     while (*src)
     {
         *dest = *src;
         dest++;
         src++;
     }
     *dest = '\0'

Собственно я искал хоть какое-то упоминание о ошибке в этом коде, но никто даже не интересовался, кроме как принципом работы кода. В общем если разобраться в приоритетах операций - все становится на свои места (я так сначала думал с разгону):

1.Оказывается что постфиксный инкремент/декремент имеют отличную от префиксных аналогов реализацию. Что точно уж объясняет работу ошибочного кода. Я проверил он работает 100%. https://ravesli.com/urok-40-inkrement-dekrement-pobochnye-effekty/

#include <stdlib.h>
void stringCopy(char* dest, const char* src)
{
    while ((*dest++ = *src++));// почему-то неправильная запись с ошибкой (хотя все работает)
    /*
    Правильная запись
    while (*src)
    {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';
    */
}
int main()
{
    char* src = malloc(15);
    char* dest = malloc(15);
    *(src + 0) = '\0';
    *(src + 1) = 'e';
    *(src + 2) = 'a';
    *(src + 3) = 'r';
    *(src + 4) = '!';
//дальше не продолжал инициализацию - Visual Studio туда помещает 
//мусор без нулей
stringCopy(dest, src);

//вот тут я поставил точку останова в дебаггере - мои указатели //ссылаются на нулевые строки (значит копирование успешно прошло при нулевой строке и значит что ошибка не в операторе постфиксного инкремента) free(src); free(dest); }

2.Наконец вроде бы разобрался с понятием "леводопустимых" выражений (стр 87). Получается, что уникальность реализации постфиксных инкремента и декремента имеет прямую связь с этим понятием "леводопустимости"? Или это отдельно? Чтобы уже разобраться совсем совсем.

В итоге не понятно, есть ли там ошибка или нету. Получается что проблема в наглядности этого кода и лучше писать все красиво по полочкам, ибо никогда не знаешь что стукнет в голову компилятору или сложно анализировать код? А ошибка придумана - чтобы сломать мозг тому кто будет ее искать и привести его к этому выводу?

Upd: ошибки там нет. Ошибка была в примере на стр. 94 (по видимости автора ошибкой является то, что длина высчитывается с учетом знака окончания строки, хотя это даже не правило). Просто мне показалось из-за стиля его заметок в виде крестика, что там тоже должна быть ошибка. Просто автор пытался показать, что так делать не стоит. Компактность кода в этом случае приводит к сложности понимания происходящего.

Upd: продолжив дальнейшее изучение книги я наткнулся на стр.100-103 параграф "Точки следования (sequence points). У меня конечно нет столько опыта, хотелось бы услышать имеет ли место в данной ситуации то, о чем там говорится. Ну и так как никто не ответил о "леводопустимых выражениях" - может и о них кто-то скажет в данной ситуации.

Artem
  • 497
  • Комментарии не предназначены для расширенной дискуссии; разговор перемещён в чат. – Grundy Mar 13 '22 at 01:26
  • Первый код гарантированно выполняет ++, а значит никогда не возвращает 0 (впрочем, это проще заметить представив пустую строку в качестве аргумента). Отсюда, если подумать, можно понять, что возвращаемое значение всегда на 1 больше нужного. Во втором коде проблемы вообще не вижу. Инкрементить указатель на 1 дальше конца массива стандартом разрешено. А с точками следования вроде делать нечего, поскольку каждая переменная используется ровно 1 раз за выражение. – Qwertiy Mar 13 '22 at 21:41
  • Насколько я понимаю, проблема в том, что код не проверяет размер аллоцированной памяти, и скорее всего приведёт к UB. Поэтому просто вызвать stringCopy недостаточно, нужно ещё и подсчитать размер исходной строки (а это ещё один пробег по ней, если только размер не хранится отдельно). – VladD Mar 17 '22 at 11:48

1 Answers1

4

Давайте отрефакторим:

void stringCopy(char* dest, const char* src)
{
    while ((*dest++ = *src++));    
}

a. Убираем лишние скобки ставим новые чтобы документировать порядок применения операторов:

void stringCopy(char* dest, const char* src)
{
    while (*(dest++) = *(src++));    
}

b. Разбираем цикл while:

void stringCopy(char* dest, const char* src)
{
    while (1) {    
        if (!(*(dest++) = *(src++)))
            break;
    }    
}

c. Разбираем условие в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*(dest++) = *(src++));    
        if (!c)
            break;
    }    
}

d. Выносим инкременты из присваивания:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*dest = *src);
        dest++;
        src++;    
        if (!c)
            break;
    }    
}

e. Заносим инкременты в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*dest = *src);
        if (!c) {
            dest++;
            src++;    
            break;
        } else {
            dest++;
            src++;    
        }
    }    
}

f. Инкременты перед break не меняют смысл программы. Убираем их:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        char c = (*dest = *src);
        if (!c) {
            break;
        } else {
            dest++;
            src++;    
        }
    }    
}

g. Меняем условие в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        *dest = *src;
        if (!(*src)) {
            break;
        } else {
            dest++;
            src++;    
        }
    }    
}

h. Заносим присваивание в if:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        if (!(*src)) {
            *dest = *src;
            break;
        } else {
            *dest = *src;
            dest++;
            src++;    
        }
    }    
}

i. Уточняем условие в if и присваивание перед break:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        if (*src == '\0') {
            *dest = '\0';
            break;
        } else {
            *dest = *src;
            dest++;
            src++;    
        }
    }    
}

j. Присваивание перед break выносим из цикла:

void stringCopy(char* dest, const char* src)
{
    while (1) {
        if (*src == '\0') {
            break;
        } else {
            *dest = *src;
            dest++;
            src++;    
        }
    }    
    *dest = '\0';
}

k. Убираем if:

void stringCopy(char* dest, const char* src)
{
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;    
    }    
    *dest = '\0';
}

l. Меняем условие while:

void stringCopy(char* dest, const char* src)
{
    while (*src) {
        *dest = *src;
        dest++;
        src++;    
    }    
    *dest = '\0';
}

Получили правильный с точки зрения автора учебника вариант. Каждый шаг сохраняет семантику программы. Если вы сомневаетесь в каком-то шаге, комментируйте, разберёмся.

Автор может упомянуть что в конце работы оригинальной процедуры указатели src и dest указывают на элементы за концами строк. Язык C специально разрешает указателям указывать на элемент следующий за концом массива. Этот указатель нельзя разыменовывать, но этого и не происходит.

  • 1
    Спасибо за подробный разбор! Это мне было понятно изначально. Просто почему-то мне показалось что автор уверен, что в коде есть ошибка, на что он "мягко" намекнул крестиком возле кода. И я долго искал там ошибку. А ошибка была в предыдущем его примере, с которого он плавно перешел к рассматриваемому. В общем я искал ошибку где ее нету. Спасибо за потраченное время всем участникам. Мне стыдно) – Artem Mar 12 '22 at 09:23
  • 2
    Вы задали хороший вопрос, демонстрирующий сложность синтаксиса и семантики программ на C. Стыдиться нечего. – Stanislav Volodarskiy Mar 12 '22 at 09:30
  • 2
    @Artem, для изучения С достаточно прочесть K&R (и м.б. /usr/include/*.h -)) (на всех остальных авторов можно не тратить свое время) – avp Mar 12 '22 at 12:31
  • Спасибо за совет) ну просто думаю уже осилить эту книженцию чтобы было с чем сравнивать. – Artem Mar 12 '22 at 20:36
  • @Artem, надо бы ответ принять. – Qwertiy Mar 13 '22 at 21:47
  • Я приму, но это не ответ. Просто он единственный. – Artem Mar 14 '22 at 08:35