Что выведется на экран в результате выполнения фрагмента программы s 0 for j 10

Что выведется на экран в результате выполнения фрагмента программы:

Правильный ответ на вопрос «Что выведется на экран в результате выполнения фрагмента программы: 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 .

Читайте также:
Hik connect исключение программы перезапуск мобильного клиента

#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

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru