Несогласованный вывод строки в стиле C между различными операционными системами / компиляторами

#c #c 11 #g

#c #c 11 #g

Вопрос:

У меня есть программа на C :

 #include <iostream>

char * foo (char * bar, const char * baz) {
    int i = -1;

    do {
        i  ;    
        *(bar   i) = *(baz   i);
    } while (*(baz   i));

    return bar;
}

int main (int argc, char *argv[]) {
    char bar[] = "";
    char baz[] = "Hello";

    foo(bar, baz);

    std::cout << "bar: " << bar << std::endl;
    std::cout << "baz: " << baz << std::endl;
}
  

Не то чтобы это было важной частью, но требование к этой программе заключается в том, что она копирует одну строку в стиле C в другую, используя указатели.

Когда я компилирую и выполняю свой двоичный файл на своем рабочем столе Ubuntu 16.04, это то, что я вижу:

 $ g   -std=c  11 test.cpp -o test amp;amp; ./test
bar: Hello
baz: ello
  

Боже! Начальная 'H' часть baz была удалена, но я вообще не вижу, как foo меняется моя функция baz . Хм…

Таким образом, версия g на моем рабочем столе Ubuntu:

 $ g   --version
g   (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  

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

Вот вывод на macOS:

 $ g   -std=c  11 test.cpp -o test amp;amp; ./test
bar: Hello
baz: Hello
  

Вот версия g на этом ноутбуке macOS:

 $ g   --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c  /4.2.1
Apple clang version 12.0.0 (clang-1200.0.32.2)
Target: x86_64-apple-darwin19.5.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
  

При тестировании на других блоках Linux, в Windows и т. Д. он имеет правильный, ожидаемый bar результат и baz оба Hello значения.

Что происходит !?

tl; dr Программа C выводит строку в стиле C иначе на моем рабочем столе, чем на любом другом компьютере. Почему?

Комментарии:

1. char bar[] = ""; не очень большая строка. Копирование чего-либо в него может привести к переполнению буфера и вашему другу и моему неопределенному поведению . Поскольку поведение программы с неопределенным поведением не определено, рассуждения о непоследовательном поведении являются сомнительной тратой времени..

2. Примечание: возможно, вам будет while (*baz) { *bar = *baz ; } немного легче работать с мозгом. Пока есть ненулевое значение, baz получите значение baz , а затем перейдите baz к следующему символу. Сохраните значение в значение at bar , а затем продвигайтесь bar вперед .

3. Возможно, вы захотите указать длину bar между [ и ] .

4. @user4581301: единственная проблема с этим (хотя и каноническим) циклом заключается в том, что вам нужно затем перенести после цикла.

5. Sleep, do / while работает здесь немного лучше в любом случае по причинам, указанным в комментарии paxdiablo’s несколькими комментариями выше. Если вы проверяете нулевой терминатор и завершаете работу после копирования нулевого терминатора, вам не нужно беспокоиться об этом.

Ответ №1:

 char bar[] = "";
  

Это гарантирует создание области памяти длиной в один байт (в основном достаточно долго, чтобы удерживать '' ). Реализация может дать вам больше, но вы не можете полагаться на это.

Следовательно, он недостаточно велик для хранения строки "Hello" , для чего потребуется шесть байтов. Это рассматривается, например C 20 [expr.add] , с моим дополнительным акцентом:

Если выражение P указывает на элемент x[i] объекта массива x с n элементами, выражения P J и J P (где J имеет значение j ) указывают на (возможно-гипотетический) элемент x[i j] if 0 <= i j <= n ; в противном случае поведение не определено.

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

 char baz[] = "Hello";
char bar[sizeof(baz)];  // bar will be same size as baz
  

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


Хотя неопределенное поведение означает, что может произойти что угодно, то, что, скорее всего, происходит в вашем ошибочном случае, связано со следующим расположением памяти в стеке. Вы копируете символы один за другим из baz в bar $ представлением символа), что приводит к следующему до и после моментальных снимков:

      bar
      V
     --- --- --- --- --- --- --- 
    | $ | H | e | l | l | o | $ |  (before)
     --- --- --- --- --- --- --- 
    | H | e | l | l | o | $ | $ |  (after)
     --- --- --- --- --- --- --- 
          ^
         baz
  

Итак, вы можете видеть, как запись после конца bar может повлиять на другие вещи в стеке, например baz . Если макет стека был другим, эффекты, скорее всего, также будут другими.

Например, если bar бы и baz были в стеке в другом порядке, это bar не повлияло baz бы. Это почти наверняка повлияло бы на что-то еще в стеке, что привело бы к странному поведению, особенно если это что-то еще оказалось чем-то вроде адреса возврата к вызывающей функции 🙂

Суть в том, что неопределенное поведение означает именно это — вы не можете полагаться на то, что все работает должным образом.