[БЕЗ_ЗВУКА] В этом видео мы подробнее обсудим, как ведут себя целочисленные типы, а также как они преобразуются один к другому. Начнем со следующего эксперимента. Давайте возьмем максимальное значение типа int. Как мы знаем, для этого используется numeric_limits, максимальное значение, и прибавим к нему единицу. Посмотрим, что же будет, если взять на единицу больше, чем максимум. Наверное, у нас получится не что-то больше на единицу, чем максимум, потому что это число явно не умещается в наш тип. Действительно, у нас получилось минимальное значение типа int. Хорошо. А давайте я в рамках того же эксперимента попробую вычесть единицу из минимального значения: numeric_limits<int>::min() − 1. Что же нас получится? Давайте посмотрим. Скомпилируем код, запустим, и мы видим, что минимальное значение − 1 — это максимальное значение. Итак, получается, что, во-первых, конечно же, мы получили не то, что мы ожидали. Мы не получили это число на единицу меньше минимума или число на единицу больше максимума, но мы узнали что, оказывается, если мы возьмем число, которое на единицу больше, чем максимум, то мы получим минимум. И наоборот. Вот такое вот переполнение на языке C++. Хорошо, но давайте все-таки рассмотрим чуть более практичные проблемы, мы же не будем сознательно прибавлять единицу к максимуму. Давайте рассмотрим такую задачу. Пусть у вас есть два числа типа int, например, 2.000.000.000 и 1.000.000.000, единица с девятью нулями. И мы хотим вычислить их среднее арифметическое. Наверное, мы сейчас задумаемся. Если нам гарантируется, — давайте мы гарантируем, — что исходные числа, которые помещаются в тип int, они не больше двух миллиардов, int для них подходит. И если мы задумаемся, подходит ли нам этот тип, мы, наверное, решим, что среднее двух чисел, которые меньше двух миллиардов, тоже меньше двух миллиардов, поэтому тип int нам подходит, и мы напишем вот такой код: cont << (x + y) / 2. Казалось бы, результат этого выражение помещается в тип int, поэтому у нас все будет хорошо. Давайте посмотрим, чему же равно среднее арифметическое между двумя миллиардами и одним миллиардом. Должно получиться полтора миллиарда. Но не получилось. Почему не получилось? Потому что язык C++ сначала сложил x и y, и вот здесь он получил три миллиарда, которые не уместились в тип int. Итого, получается, нам нужно внимательно думать над тем, какой тип мы выбираем для наших значений. Если промежуточные вычисления не умещаются в наш тип, то и с результатом может произойти что-то странное: у вас в процессе случится переполнение, и в итоге получится не то, что вы хотели, — какое-то большое отрицательное число, верный признак переполнения на самом деле. Итак, давайте идти дальше. Вспоминаем про беззнаковые типы. Казалось бы, довольно страшно, что у вас есть типы, которые могут хранить отрицательные значения, есть типы, которые не могут хранить отрицательные значения. Можно ли безопасно преобразовывать от одних типов другим? Давайте попробуем. Вот у меня есть переменная x со значением 2.000.000.000, и давайте я сохраню ее в переменную типа unsigned int, то есть сохраню в беззнаковую переменную, y = x, и выведу как x, так и y. Согласитесь, наверное, есть некоторые опасения, что у нас в y будет что-то не то, мы уже такого насмотрелись с этими целочисленными типами. Давайте запустим код, увидим, что все в порядке. На самом деле, если у вас ваше значение, в данном случае 2,000.000.000, умещается как в int, так и в unsigned int, то никаких проблем не возникнет. А если у меня изначально было отрицательное число x, и я его записал в беззнаковой переменной y, — конечно, у меня в y будет что-то не то. Давайте я это покажу. Действительно, изначально в x было −2.000.000.000, а в y стало 2. 200.000.000 и так далее, то есть опять же какое-то большое число. Хорошо. Вот мы увидели, что у нас бывает с целочисленными типами. Давайте вернемся к проблеме из задачи про среднюю температуру. Итак, вот у нас был некоторый код, который вычислял суммарное значение температур, и потом мы брали среднее арифметическое. В чем же здесь была проблема? Что переменная sum имеет тип int, это знаковый тип, а t.size имеет тип size_t, а это беззнаковый тип. И оказывается, когда вы написали деление, когда вы разделили sum на t.size, компилятор, чтобы вычислить частное знакового числа и беззнакового, он привел знаковое число к беззнаковому. И в этот момент случилась проблема, когда у вас сумма была отрицательной. Давайте, наверное, изучим, как же компилятор работает с арифметическими операциями и логическими операциями над типами, которые не совпадают. Итак, если у вас есть два целочисленных типа, два различных целочисленных типа, и вы хотите над ними выполнить арифметическую операцию, — сложение, умножение, вычитание, деление, — или логическую операцию, то есть сравнить их, компилятор сначала должен привести их к одному типу. Ну так же, когда вы дроби складываете, вы приводите их к общему знаменателю, так и здесь перед выполнением операции над целочисленными типами нужно привести их к одному типу. Как же это делается? В первую очередь, компилятор приводит оба типа к типу int, если они имели размер меньше int. Например, тип int_8t приведет их к типу int. Дальше, после того, как оба типа привелись к типу int, если они были меньше int, или они остались самими собой, если они имели тип, равный int или больший, у нас здесь получается два типа. Если один из них имеет размер больше, чем другой, то у нас, грубо говоря, побеждает больший тип. И тот операнд, который имеет меньший по размеру тип, приводятся к другому типу, большему, что довольно логично. А если у нас осталось после всех преобразований два типа, которые имеют одинаковый размер, но при этом разные, то есть один знаковый, а другой беззнаковый, то побеждает беззнаковый тип. Вот такие правила. Давайте посмотрим на примерах. Первый пример, который мы уже видели в примере задачи «Средняя температура», когда мы делим int на size_t. Что в этом случае происходит? Если size_t имеет размер 4 байта, и int имеет размер 4 байта, то побеждает size_t как беззнаковый тип. Если же size_t имеет размер 8 байт, а int имеет размер 4 байта, то size_t побежать просто как тип, который имеет больше размер. Итого, в любом случае у меня итоговый размер, итоговый тип будет size_t. Хорошо, другой пример. Что, если я складываю int32_t и int8_t, два числа вот таких типов. В этом случае побеждает тип int32_t как тип, который имеет больший размер. Хорошо. Что, если я умножаю int8_t и uint8_t? На самом деле здесь операция вообще никого значения не имеет, просто для наглядности мы здесь ее приводим. Если мы умножаем int8_t и uint8_t, у нас получается int, просто потому что, в первую очередь, эти типы, как имеющие размер всего 1 байт, приводятся к типу int. И, наконец, что, если я сравниваю int32_t и unit32_t? Заметьте, что здесь int32_t приведется к uint32_t, знаковый тип приведется к беззнаковому, и после этого будет осуществляться сравнение этих значений. Давайте вернемся в среду разработки., и узнаем вот что. Что, если мы забыли эти правила, которые только что узнали? Что, если мы пишем код и опасаемся, что у нас случится какое-то неявное преобразование типов? Давайте вернемся к коду из задачи со средней температурой. Вот у нас вектор наблюдения за температурой. Тут у нас − 8, − 7 и 3. Мы вычисляем сумму, итерируем все по вектору t. Вот у нас вычислилась сумма, и мы сохраняем в переменную типа int среднее арифметическое. Но теперь мы знаем, что у нас производятся какие-то преобразования между знаковыми и беззнаковыми типами. Как нам узнать тип вот этого выражения sum / t.size( )? Можно вызвать от него sizeof, можно вызвать от него numeric_limits, хотя с numeric_limits будут некоторые проблемы, нам придется auto использовать. А можно прямо попросить компилятор сказать нам, какой тип у этого выражения. Как это сделать? Есть довольно интересный способ. Можно вызвать ошибку компиляции, связаную с этим типом, например, прибавить к тому, что мы здесь видим, что-нибудь то, что мы не можем прибавить к целому числу физически, например, пустой вектор. Очень странное действие, компилятор гарантированно на нас за это поругается. Очень сильно поругался. Главное, что мы видим? Давайте прокрутим до исходной ошибки. Прокрутим до исходной ошибки, и мы видим, что у нас этот самый тип — это вектор size_type. Мы вспоминаем, что это на самом деле size_t и понимаем, что здесь у нас будет size_t, соответственно, сумма будет приводиться к беззнаковому типу. Давайте покажем еще один пример, еще одна опасность, которая нас поджидает при работе со знаковыми и беззнаковыми типами. Пример следующий. Давайте код про среднюю температуру мы немного отложим в сторонку, чтобы потом к нему вернуться и ошибку исправить. Давайте я вынесу его даже в какую-нибудь функцию, про которую на время забуду, назову ее void PrintAvg( ). [БЕЗ_ЗВУКА] Ну и тут, скажем, будет вывод среднего значения. Все. Давайте на время забудем про этот пример и сделаем вот что. Давайте попробуем сравнить знаковое и беззнаковое число, скажем, знаковую 1, лучше знаковую − 1 и беззнаковую 1. На самом деле мы только что рассмотривали этот пример, и вы можете предположить, что у нас случится, но тем не менее. Давайте выведем x < y. Сравним − 1 с 1. Казалось бы, должно быть true, то есть 1, потому что − 1 меньше 1. Запускаем код и видим 0. Почему? Потому что у нас случилось приведение знаковой − 1 к беззнаковому типу. Получилось какой-то большое число, оно, конечно, больше, чем 1, поэтому мы получили false. Давайте этот пример запишем чуть компактнее. Нельзя ли было здесь просто сравнить − 1 с 1, если я хотел продемонстрировать то, что я продемонстрировал? Ну не совсем, потому что − 1 имеет тип int, а 1 тоже имеет тип int, а я здесь хотел показать вам беззнаковый единицу. Оказывается, можно очень легко сделать единицу беззнаковой, просто добавив суффикс u. Это то же число, просто оно по умолчанию имеет тип unsinged, unsinged int. Давайте покажу, что у меня будет все то же самое. Код скомпилировался, и мы видим ошибку, кстати говоря, точнее, не ошибку, а предупреждение warning, предупреждение о том, что у нас производится сравнение между знаковым и беззнаковым числом, то есть компилятор пытался нас предупредить. Итак, это на самом деле очень хорошая новость, что компилятор пытался нас предупредить, но давайте мы в следующих видео обсудим, как же нам эти предупреждения компилятора учесть. А пока что в этом видео мы узнали, что нужно внимательно следить за переполнениями типов, в том числе за переполнениями в промежуточных вычислениях, и много узнали про неявные преобразования типов. И заодно в качестве небольшого бонуса узнали, что если вы к числу напишите суффикс u, то оно станет беззнаковым целым числом.