Что выведется на экран в результате выполнения фрагмента программы:
Правильный ответ на вопрос «Что выведется на экран в результате выполнения фрагмента программы: s:=0; FOR j:=10 TO 15 DO begin s:=s+2*j; write (‘ j=’, j, ‘ s=’, s) end; . » по предмету Информатика. Развернутая система поиска нашего сайта обязательно приведёт вас к нужной информации. Как вариант — оцените ответы на похожие вопросы. Но если вдруг и это не помогло — задавайте свой вопрос знающим оппонентам, которые быстро дадут на него ответ!
Новые вопросы по информатике
Объём видеопамяти составляет 100 Кбайт. Графический режим работает в режиме 640 х200 пикселей. Какое максимальное кол-во цветов может содержать палитра?
Переведите число 202 из восьмеричной системы счисления в десятичную
Переведите величины из одних единиц измерения информации в другие: 1 4 Гбайта=? Кбайт 2 217 Мбайт=? Гбайт 3 13 Мбит=? бит 4 27 Гбит=? бит 5 228 бит=? Мбайт 6 227 Гбит=? Мбайт 7 231 Кбайт=? Мбит
4. CS50 на русском: Лекция #4 [Гарвард, Основы программирования, осень 2015 год]
Вводятся 3 числа, вывести на экран нечетные числа pascal
1. Посчитай, сколько бит информации содержит 19 байт 2. Посчитай, сколько байт информации содержат 2 кб 3. Посчитай, сколько байт информации содержит 144 бит
Главная » Информатика » Что выведется на экран в результате выполнения фрагмента программы: s:=0; FOR j:=10 TO 15 DO begin s:=s+2*j; write (‘ j=’, j, ‘ s=’, s) end;
Источник: abiturient.pro
Подводные камни С++. Решаем загадки неопределённого поведения, ч. 1
Изучение и понимание неопределённого поведения — важный шаг для разработчика C++, поскольку undefined behavior бывает источником серьёзных ошибок и проблем в программах. UB может проявляться в разных аспектах языка, включая операции с памятью, многопоточность, арифметические вычисления, работу с указателями и так далее.
Под катом мы погрузимся в мир неопределённого поведения в C++ и рассмотрим некоторые примеры ситуаций, в которых оно может возникать.
P.S.: Часть приведённых в статье примеров вдохновлены материалами, которые можно посмотреть в разделе «Полезные ссылки».
Привет, Хабр! Меня зовут Владислав Столяров, в МойОфис я аналитик безопасности продуктов. Чаще всего я взаимодействую с командами, которые создают решения на C и C++, и сегодня хочу обратиться к теме неопределённого поведения — рассказать, что это, в чем проявляется и как с ним работать. Это первая часть моего мини-цикла статей по UB: в ней я наглядно обозначу проблематику с помощью набора практических примеров.
Необходимая теория
Для начала приведу несколько определений из стандарта С++ (в моём авторском переводе):
- Корректно составленная программа (Well-formed program) — программа, созданная в соответствии с правилами синтаксиса, диагностируемыми семантическими правилами и правилом одного определения.
- Некорректно написанная программа (Ill-formed program) — программа, которая нарушает либо синтаксические, либо семантические правила (либо и те, и другие). Она не должна компилироваться.
- Неуточнённое поведение (Unspecified behavior) — поведение программы, где стандарт языка допускает два или более варианта и не налагает никаких других требований на выбор в каждом конкретном случае. Классическим примером неуточнённого поведения является порядок вычисления аргументов функции:
#include int f() < std::cout int h() < std::cout int foo(int i, int j) < return j — i; >int main()
В данном примере у нас есть 2 функции f и h, которые возвращают 0 и 1 и выводят в консоль F и H соответственно. Также у нас есть функция foo , которая принимает 2 числа и возвращает их разницу. При вызове функции foo из функции main порядок вызова функций f и h неуточнён и может быть любым.
Как исправить ЧЕРНЫЙ ЭКРАН при захвате игры в OBS
- Поведение, зависящее от реализации (implementation defined behavior) — неуточнённое поведение, которое задокументировано в компиляторе или среде исполнения. Это очень интересная особенность языка, различные реализации по-разному описывают значение функции pow(0,0) , тип vector::iterator и даже количество бит в байте. Подробнее можно почитать тут.
Неуточнённое поведение и поведение, зависящее от реализации объединяет один печальный фактор. Программы, которые содержат их, непереносимы.
- Неопределённое поведение (undefined behavior или просто UB) — поведение программы, которое может привести к абсолютно непредсказуемым последствиям. При этом программа корректна синтаксически и семантически.
Для более детального понимания, что это такое, рассмотрим пример:
#include int main() < while(1); >void unreachable() < std::cout
В функции main есть бесконечный цикл while(1) , который означает, что программа будет выполняться бесконечно. В данном случае цикл не имеет никакого условия выхода, поэтому программа будет выполняться до тех пор, пока не будет принудительно прервана. Функция unreachable определена, но не вызывается из функции main , поэтому она никогда не будет выполнена. Код внутри функции unreachable , который выводит строку «Hello» на стандартный вывод с помощью std::cout , не будет выполнен ни разу.
На самом же деле, всё это не совсем так — вернее даже, совсем не так. Вывод данной программы может быть любым. Всё из-за того, что по стандарту С++ бесконечный цикл в программе вызывает неопределённое поведение (в случае С11 бесконечный цикл с константой в условии не является UB). Если запустить данный код на компиляторах clang и gcc, то можно увидеть, что clang запустит недостижимую функцию unreachable , она выведет на экран «Hello» , вот подтверждение. О том, почему это происходит, подробнее мы поговорим ниже.
Зачем UB в компиляторе?
Когда задумываешься о проблеме неопределённого поведения, одним из первых в голову приходит вопрос: зачем оно вообще нужно? Есть же промышленные языки вроде Java, C# и множества других, обходящихся без этой фичи. Между тем это именно фича, и вот почему.
С моей точки зрения, С и С++ довольно продуманные языки, и наличие в них UB, конечно же, логически обосновано. Среди прочего оно позволяет:
- Не реагировать компилятору на некоторые ошибки, трудные в диагностике
- Избегать определения запутанных мест в пользу одной из стратегий реализации и в ущерб другой
- Иметь своё определение неопределённого поведения в случае с каждой реализацией компилятора
- Устранить накладные расходы на проверку разных граничных случаев
В целом же неопределённое поведение даёт компилятору неограниченный простор для оптимизаций. Наличие в коде UB создаёт так называемые «серые зоны», право не видеть которые оставляет за собой компилятор. Ниже — пара примеров, как это работает на практике.
О неожиданных оптимизациях
Вот упрощённый пример, написанный на основе реальной ошибки из ядра операционной системы Linux.
void foo(int *ptr)
Здесь функция foo присваивает значение, на которое указывает переданный указатель, в локальную переменную d . Затем, если указатель не является нулевым, она изменяет значение, на которое он указывает, на 777 .
На представленном фрагменте кода можно применить 2 оптимизации: Dead Code Elimination (DCE) и Redundant Null Check Elimination (RNCE). Вопрос только в порядке применения 🙂
Например, оптимизатор применяет DCE на локальную переменную d , которая определяется, но не используется. Тогда фрагмент кода после оптимизаций станет таким:
void foo(int *ptr)
Но если первой отработает RNCE, то код станет таким (оптимизатор видит, что ptr проверяется на NULL уже после разыменования, соответственно, проверка бессмысленна):
void foo(int *ptr)
Далее на данном фрагменте кода может запуститься DCE:
void foo(int *ptr)
Порой бывает очень грустно отлаживать падение на релизной сборке, по которой уже прошелся оптимизатор, в своем отладочном окружении, в котором уже даже нет такого кода.
Примеры неопределённого поведения
Рассмотрим несколько паттернов неопределённого поведения.
Неправильная работа с памятью
Большинство ошибок при работе с С и C++ связанно с неправильной работой с памятью. Часть из них отлавливается компилятором и операционной системой. Например, знаменитый Segfault — следствие неправильной работы с памятью. В итоге программист видит надпись segmentation fault (core dumped) под Linux.
Первая из проблем, которую можно рассмотреть — выход за границу массива. Она обычно актуальна для массивов или контейнеров, которые хранят элементы в непрерывном куске памяти. Работа с такими контейнерами при помощи operator[] является весьма распространенным действием. Вот синтетический пример, который демонстрирует это:
#include int main() < const int SIZE = 5; int* dynamicArray = new int[SIZE]; for (int i = 0; i for (int i = 0; i delete[] dynamicArray; return 0; >
В данном примере мы создаем динамический массив dynamicArray с помощью оператора new . Размер массива задается константой SIZE , равной 5 . Затем мы выполняем два цикла for . В первом цикле мы пытаемся присвоить значения элементам массива в диапазоне от 0 до 5 . Однако последний элемент массива имеет индекс 4 , так как индексация массивов в C++ начинается с 0 . В результате, при выполнении цикла происходит выход за границы массива.
Затем во втором цикле мы также пытаемся обратиться к элементам массива с индексами от 0 до 5 . Опять же, это приводит к выходу за границы массива.
Для исправления данной ошибки необходимо изменить условия циклов for на i < SIZE , чтобы гарантировать, что индексы остаются в допустимых пределах массива.
Также довольно часто возникает проблема с выделением и очисткой памяти. Для работы с динамической памятью язык С предлагает несколько функций: malloc , calloc , realloc и free для очистки памяти. Для языка С всё просто: функции, выделяющие память, возвращают указатель на начало выделенной памяти в случае удачи и NULL в случае неудачи, память чистится функцией free .
C++ предлагает операторы new и delete и их различные версии:
- При использовании оператора new , вначале выделяется память для объекта. В случае успешного выделения памяти, вызывается конструктор объекта. Однако, если конструктор выбрасывает исключение, выделенная память немедленно освобождается.
- При вызове оператора delete , всё происходит в обратном порядке. Сначала вызывается деструктор объекта для его очистки, а затем освобождается память. Важно отметить, что деструктор не должен бросать исключения.
- Оператор new[] используется для создания массива объектов, сначала выделяется память для всего массива. В случае успешного выделения памяти, вызывается конструктор по умолчанию (или другой конструктор, если есть инициализатор) для каждого элемента массива, начиная с нулевого индекса. Если какой-либо конструктор выбрасывает исключение, для всех созданных элементов массива вызывается деструктор в обратном порядке, согласно порядку, обратному вызову конструктора. После этого освобождается выделенная память.
- Для удаления массива необходимо использовать оператор delete[] . При вызове данного оператора, для каждого элемента массива вызывается деструктор в порядке, обратном вызову конструктора, после чего выделенная память освобождается.
Операторы new / new[] возвращают указатель/массив указателей для доступа к новому объекту/объектам в случае успешного выделения памяти или бросают исключение std::bad_alloc в случае неудачного выделения. Также у операторов есть перегрузки, принимающие std::nothrow , они вместо броска исключения возвращают нулевой указатель. И в случае С++17 у операторов выделения/освобождения памяти есть перегрузки, принимающие std::align_val_t , для указания выравнивания.
Важно использовать соответствующую форму оператора delete в зависимости от того, удаляется ли одиночный объект или массив. Это правило не должно быть нарушено ни при каких обстоятельствах, поскольку это может привести к возникновению неопределенного поведения, в результате которого могут произойти самые разные ситуации: утечки памяти, аварийное завершение программы.
Подытоживая, можно сказать, что довольно много ошибок происходит при неправильном комбинировании операторов для выделения/очистки памяти. Например:
- new→delete[]
- new[]→free
- new→free
- new[]→delete
- etc
При использовании оператора new[] для выделения памяти под массив объектов, их количество должно где-то храниться. Обычно в компиляторах существует 2 стратегии для этого: Over-Allocation для записи количества элементов перед самим массивом и хранение количества элементов в обособленном ассоциативном контейнере. Таким образом, когда зовётся оператор delete[] , он знает, в каком месте смотреть на количество объектов, для которых нужно позвать деструкторы и почистить память.
Частая проблема возникает при неправильном комбинировании данных операторов. Например, напишем такой фрагмент кода:
#include void foo(unsigned len) < auto inv = std::unique_ptr(new char [len]); //. >
Здесь мы решили обернуть выделение динамической памяти в умный указатель, который очистит её самостоятельно, после выхода из области видимости. Однако стоит обратить внимание, что std::unique_ptr инстанцирован типом char , а выделяется память для char[] . При вызове деструктора std::unique_ptr , он вызовет деструктор именно для типа, которым он инстанцируется, а не для массива объектов. Соответственно, удаление объекта будет производиться другой deallocation-функцией, что согласно стандарту будет неопределенным поведением; вот ссылка на соответствующий пункт стандарта.
Знаковое целочисленное переполнение
Также довольно часто возникают ситуации со знаковым целочисленным переполнением. Например, мы хотим написать простую функцию, которая выводит числа на экран:
#include int main(int argc, const char *argv[]) < for (int i = 0; i < 10; ++i) < std::cout >
Если мы скомпилируем и запустим данный код с O0 (флаг gcc для компиляции без оптимизаций), то произойдёт переполнение типа int , программа выведет на экран 1’000’000 , 2’000’000 , 8 случайных чисел — и остановится (на самом деле программа опять-таки может повести себя как угодно, всё зависит от компилятора, его версии и среды). Однако, если включить оптимизации (например, скомпилировать с флагом O3), то под gcc программа завершится аварийно, из-за того, что цикл станет бесконечным.
Почему это происходит? На самом деле, когда программист пишет код на C++, он заключает определённый «контракт» с компилятором. Разработчик обязуется писать корректный с точки зрения стандарта С++ код, а компилятор — компилировать и оптимизировать код наилучшим образом. Тогда как в примере компилятор, видя, что условие цикла ведёт к переполнению типа int и зная, что случиться этого не может, делает условие всегда true .
#include int main(int argc, const char *argv[]) < for (int i = 0; true; ++i) < std::cout >
Стоит отметить, что большинство компиляторов под оптимизациями сделают из такого кода:
bool foo(int x) < return (x + 1) >x; >
bool foo(int x)
Также отмечу, что если заменить int на unsigned , то оптимизация выполняться не будет, например, у GCC это поведение контролируется флагом -fwrapv (он включен в ядре Linux).
А из такого кода:
int foo(int x) < return (2 * x)/2; >
int foo(int x)
Неиницализированные переменные
По стандарту С++, использование неинициализированной переменной приводит к неопределённому поведению. Давайте рассмотрим пример:
#include int foo(bool c) < int x,y; y = c ? x : 777; return y; >int main()
Внутри функции foo объявляются две целочисленные переменные x и y . Затем переменной y присваивается значение, зависящее от условия. Условие c ? x : 777 означает, что если значение переменной c истинно, то в y будет присвоено значение переменной x . В противном случае, если c ложно, то в y будет присвоено значение 777 . В функции main происходит вызов функции foo с аргументом true .
Кажется, что итоговым результатом выполнения данного кода будет вывод в консоль числа, которое зависит от значения переменной x , если c истинно, или 777 , если c ложно. Однако x — неиницализированная переменная, использование которой ведёт к неопределённому поведению. Компилятор знает об этом и может оптимизировать код на основе этого знания. Таким образом, на подавляющем большинстве компиляторов данный код выведет на экран значение 777 . Не самый очевидный исход, верно?
Integral promotion
Сперва я хотел написать большой абзац о том, что такое Integral promotion, как он работает в рамках usual arithmetic conversions и зачем он нужен, однако вовремя вспомнил, что уже делал это в одной из своих статей. Там я рассказал, как писал механизм для вывода общего типа в одном известном статическом анализаторе. Если вам интересна тема, ознакомиться можно тут: Статья для тех, кто как и я не понимает, зачем нужен std::common_type.
Вот ссылка на соответствующий пункт стандарта. В нём говорится, что при операциях над целыми числами может произойти целочисленное продвижение. Например, если перемножить 2 операнда, размерностью меньше, чем int , результат будет приведён к типу int . Вот к чему это может привести:
int main()
Да, переполнение unsigned числа — это не UB, однако автоматически выведенный тип переменной c будет int . Результат выражения 65535 * 65535 больше, чем INT_MAX , соответственно, данный код приведёт к неопределённому поведению — результат программы непредсказуем.
Целочисленное деление на 0
Согласно стандарту С++, если вторым операндом бинарной операции с целыми числами / или % будет 0 , то результат — неопределённое поведение. Для деления на 0 вещественных чисел работают уже совсем другие правила, подробнее про это можно почитать тут в разделе Additive operators. Важно не перепутать вещественные числа с целыми и не написать, например, такой код для генерации значения «бесконечность»:
auto create_inf (unsigned x) < return x / 0; >
Выводы
Неопределённое поведение в C++ — феномен, результат которого невозможно предсказать. Никто не знает, как будет вести себя код, содержащий UB. Из этого следует, что при разработке ПО следует придерживаться простого и понятного кода.
Сложные и запутанные конструкции могут привести к непредсказуемым последствиям. Важным аспектом профессионализма в программировании является способность написать безопасный и надежный код, который легко читать и поддерживать. Это подразумевает использование ясных и понятных конструкций, а также следование лучшим практикам программирования.
Конечно, количество ситуаций, которые могут привести к неопределённому поведению огромно. Мы рассмотрели всего несколько распространенных случаев.
Скоро выйдет вторая часть статьи, в ней мы поговорим о том, как можно защититься от неопределённого поведения. И разберём еще больше примеров UB.
Список материалов, которые стоит изучить, чтобы глубже понять тему неопределённого поведения:
- Standard C++ (in Russian) :: Часть 2, Неопределённое поведение
- What Every C Programmer Should Know About Undefined Behavior.1
- What Every C Programmer Should Know About Undefined Behavior.2
- What Every C Programmer Should Know About Undefined Behavior.3
- Статья для тех, кто как и я не понимает, зачем нужен std::common_type
Источник: habr.com