Краткое предисловие для начинающих: прочитать Преобразование типов переменных – это часть внутренней автоматической работы компилятора, происходящая в строгом соответствии с правилами языка программирования. Сам разработчик при написании программы в явном виде этим, как правило, не занимается. Однако, неаккуратное объявление типов переменных, или присвоение переменной значения превышающего допустимый диапазон, и даже неправильный формат записи константы, могут привести к потере данных и некорректной работе программы, при полном молчании компилятора.
Когда происходит и в чём заключается приведение типов? Таких ситуаций достаточно много. Рассмотрим наиболее опасные из них.
§
|
> Преобразование типа выражения перед присвоением переменной. | В первом разделе мы обращали своё внимание на необходимость явного указания типа объявляемой переменной. Это позволяет компилятору зарезервировать за ней нужное количество адресного пространства и определить диапазон значений, которые она способна хранить. Тем не менее, мы не застрахованы он того, что в процессе выполнения программы произойдёт попытка записать в переменную значение свыше предельно допустимого. В самых грубых случаях компилятор выдаст нам сообщение о возможной ошибке. Например, при желании записать в переменную типа unsigned char (диапазон от 0 до 255) число 400:
unsigned char a=400; // выдаст сообщение типа "integer conversion resulted in truncation”
компилятор предупреждает нас о том, что произошла попытка записать числовое значение, требующее для хранения два байта (400 это 1 в старшем байте и 144 в младшем) в однобайтовую переменную. Однако тех случаях, когда присваиваемое выражение содержит переменные, и компилятор мог бы заметить возможную потерю данных, он освобождает себя от этой обязанности, например:
unsigned char x=200, y=200;
x=x+y;
при таком варианте, не смотря на то, что значение выражение (x+y) так же равно 400, никаких предупреждений со стороны компилятора уже не последует. А в переменную x запишется только младший байт числа 400, то есть 144. И здесь компилятор трудно в чём-то упрекнуть, ведь вместо явно проинициализированной переменной в выражении может быть использован, например, приёмный регистр шины UART, в котором может оказаться любое значение, принятое от внешнего устройства.
Другой пример в этом же духе – присвоение дробного значения переменной целого типа:
float a=1.5; // Объявлена переменная с плавающей точкой.
char b=3; // Объявлена целочисленная переменная.
b=a*b; // Ожидается, что в переменную b будет записано значение 4,5.
В результате в переменной b сохранится только целая часть результата a*b – число 4.
§
|
> Преобразование результата выражения к типу наиболее точной переменной в выражении. | При таком преобразовании компилятор руководствуется следующим правилом: прежде чем начнется вычисление выражения, операторы с "низшим” типом повышаются до "высших” при этом результат также приводится к ”высшему” типу. Какой тип нужно считать ”высшим”? Тот, который без потери точности может сохранить любое допустимое значение другого типа. Так, в предыдущем примере: float a =1.5; // Объявлена переменная a с плавающей точкой. char b=3; // Объявлена целочисленная переменная.
В выражении (a*b) переменная float a имеет более высокий тип, потому что может сохранять любое целое значение из диапазона 0…255 типа char. Результат выражения (a*b) будет иметь тип float. Типичный пример неожиданности для этого случая – попытка получить дробное число делением двух целочисленных: char a=3; // Объявлена целочисленная переменная. char b=4; // Объявлена целочисленная переменная. float c; // Объявлена переменная "c" с плавающей точкой для сохранения результата. c=a/b; // Ожидается, что "c" будет равно 0,75 (¾).
В отличие от предыдущего примера, результат записывается в переменную способную хранить числа с плавающей точкой, однако компилятор в соответствии с правилом приведения, получив в результате деления число 0,75 приводит его к типу целочисленных операндов, отбросив дробную часть. В результате в переменную "c” будет записан ноль. Более реалистичный пример из жизни – расчёт измеряемого напряжения из выходного кода АЦП: int ADC; // Двухбайтовая целочисленная переменная для хранения кода АЦП. float U; // Переменная с плавающей точкой для сохранения значения напряжения. U= ADC*(5/1024); // Расчёт напряжения.
Здесь упущено из виду то, что константа в Си, как и любая переменная, тоже имеет свой тип. Его желательно указывать явно или, используя соответствующую форму записи. Константы 5 и 1024 записаны без десятичной точки и будут восприняты языком Си как целочисленные. Как следствие, результат выражения (5/1024) тоже будет приведён к целому – 0 вместо ожидаемого 0,00489. Это не случилось бы при записи выражения в формате (5.0/1024). Приведённых ошибок также можно избежать, используя оператор явного приведения типов выражений языка Си, который записывается в виде названия типа, заключённого в круглые скобки и воздействует на выражение стоящее после него. Этот оператор приводит результат выражения к явно указанному типу, не взирая на типы его операндов: c= (float) a/b; // Ожидается, что "c" будет равно 0,75 (¾); U= ADC * ( (float)5/1024 ); // Расчёт напряжения.
|