#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
к следующему символу. Сохраните значение в значение atbar
, а затем продвигайтесь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]
if0 <= 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
бы. Это почти наверняка повлияло бы на что-то еще в стеке, что привело бы к странному поведению, особенно если это что-то еще оказалось чем-то вроде адреса возврата к вызывающей функции 🙂
Суть в том, что неопределенное поведение означает именно это — вы не можете полагаться на то, что все работает должным образом.