Какие переменные я должен типизировать при выполнении математических операций в C/C++?

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

int a = 2, b = 3;
float c = (float)a / (float)b;

Если я не брошу a а также b чтобы плавать, он будет делать целочисленное деление и возвращает int.

Точно так же, если я хочу умножить 8-разрядное число со знаком на 8-разрядное число без знака, я приведу их к 16-разрядным числам со знаком перед умножением из-за страха переполнения:

u8 a = 255;
s8 b = -127;
s16 = (s16)a * (s16)b;

Как именно компилятор ведет себя в этих ситуациях, когда он не приводится вообще или когда приводится только одна из переменных? Мне действительно нужно явно привести все переменные, или только одну слева или одну справа?

9 ответов

Вопрос 1: Подразделение поплавков

int a = 2, b = 3;
float c = static_cast<float>(a) / b;  // need to convert 1 operand to a float

Вопрос 2: Как работает компилятор

Пять правил:

  • Арифметические операции всегда выполняются над значениями одного типа.
  • Тип результата совпадает с операндами (после повышения)
  • Наименьший тип арифметических операций выполняется над int.
  • ANSCI C (и, следовательно, C++) используют сохранение целочисленных значений.
  • Каждая операция выполняется в изоляции.

Правила ANSI C следующие:
Большинство из этих правил также применимы к C++, хотя не все типы официально поддерживаются (пока).

  • Если один из операндов является длинным двойным, другой преобразуется в длинный двойной.
  • Если один из операндов является двойным, другой преобразуется в двойной.
  • Если один из операндов является плавающим, другой преобразуется в плавающий.
  • Если один из операндов является длинным без знака, другой преобразуется в длинный без знака.
  • Если один из операндов длинный, другой преобразуется в длинный.
  • Если один из операндов является длинным без знака, другой преобразуется в длинный без знака.
  • Если один из операндов длинный, другой преобразуется в длинный.
  • Если один из операндов является беззнаковым целым, другой преобразуется в беззнаковое целое.
  • В противном случае оба операнда преобразуются в int.

перелив

Переполнение - это всегда проблема. Заметка. Тип результата совпадает с операндами ввода, поэтому все операции могут быть переполнены, так что да, вам нужно об этом беспокоиться (хотя язык не предоставляет какого-либо явного способа уловить это событие).

Как примечание стороны:
Разделение без знака не может переполниться, но разделение без знака может.

std::numeric_limits<int>::max() / -1  // No Overflow
std::numeric_limits<int>::min() / -1  // Will Overflow

В общем, если операнды бывают разных типов, компилятор переведет все в самый большой или самый точный тип:

Если одно число... А другое... Компилятор будет продвигать к...
------------------- ------------------- ------------ -------------------
char int int
подписано без знака без знака
char или int float float
поплавок двойной двойной

Примеры:

char + int ==> int
подписанный int + unsigned char   ==> unsigned int
float      + int             ==> float

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

4,0 + 5/3  =  4,0 + 1 = 5,0

Это связано с тем, что сначала выполняется целочисленное деление, а затем повышается результат до сложения.

Вы можете просто разыграть одного из них. Неважно, какой именно.

Всякий раз, когда типы не совпадают, "меньший" тип автоматически переводится в "больший" тип с плавающей точкой "больше", чем целочисленные типы.

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

Что касается вопроса о переполнении, нет необходимости явно приводить, поскольку компилятор неявно делает это для вас:

#include <iostream>
#include <limits>

using namespace std;
int main()
{
    signed int a = numeric_limits<signed int>::max();
    unsigned int b = a + 1; // implicit cast, no overflow here
    cout << a << ' ' <<  b << endl;
    return 0;
}

Работая над системами, критически важными для безопасности, я склонен быть параноиком и всегда использую оба фактора: float(a)/float(b) - на тот случай, если какой-то тонкий хитрость собирается укусить меня позже. Неважно, насколько хорош компилятор, как бы хорошо он ни описывался, как бы ни были точны детали в спецификациях официального языка. Паранойя: лучший друг программиста!

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

Сказав это, я всегда бросаю оба на поплавок.

Тогда есть более старые типы с поврежденным мозгом, такие как я, которые, используя устаревшие языки, просто бездумно пишут такие вещи, как

int a;
int b;
float z;

z = a*1.0*b;

Конечно, это не универсально, хорошо только для этого случая.

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

Итак, все из:

плавать с = (плавать) а / б;

float c = a / (float)b;

float c = (плавать)a / (плавать)b;

будет иметь тот же результат.

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

например, этот бросок float->int очевиден.

int a = float(foo()) * float(c); 
Другие вопросы по тегам