C Урок 14. Преобразование типов



На данном уроке мы поговорим о преобразовании типов данных в процессе работы программы из одного типа в другой.

Конечно, в идеальном случае, желательно, чтобы программа была построена таким образом чтобы лишний раз избегать всякого рода преобразований и использовать везде данные нужного типа. Но не всегда так получается и преобразование типа в ряде случаев просто необходимо. Например, мы складываем значения двух переменных типа unsigned short и, по идее у нас результат тоже должен присваиваться переменной типа unsigned short. Но мы не уверены в том, что этот результат уместится в такой тип, например, мы захотим сложить числа 65535 и 65534. Поэтому здесь без преобразования не обойтись. И таких ситуаций огромное множество, поэтому мы должны знать как происходит преобразование типов автоматически, а также как мы можем этим процессом управлять. Думаю, данный урок даст хоть и не полную картину преобразований типов, но, тем не менее, внесёт некоторую ясность в данную тему.

Для начала давайте начнём с автоматического преобразования типов, или, если это правильно назвать, с неявного приведения типов. В данном случае преобразованием типов будет управлять компилятор и мы никаких специальных операций для данного преобразования не применяем.

Если операнды некоторой операции принадлежат различным типам, то они автоматически приводятся к определённому общему типу. А к какому именно, существует ряд правил.

Также надо учесть, что автоматические преобразования типов имеют место лишь в тех случаях, когда они не влекут потери данных и при этом превращают операнды с меньшим диапазоном значений в операнды с большим диапазоном. Например, если мы складываем операнд целого типа с операндом с плавающей точкой, то первый автоматически приведётся к типу с плавающей точкой. Или, если мы складываем операнд типа int с оператором типа short, то второй автоматически превратится в число типа int, так как у него больший диапазон значений. Тем самым достигается принцип целостности информации. Если же результат наших операций мы попытаемся присвоить переменной такого типа, у которого меньший диапазон значений, то мы рискуем потерять часть информации, хотя при этом код все равно соберётся и мы получим лишь предупреждение.

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

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

 

  • long double
  • double;
  • float;
  • unsigned long long;
  • long long;
  • unsigned long;
  • long;
  • unsigned int;
  • int

 

Если в операции присутствуют операнды различных типов то компилятор вычисляет сначала операнд наивысшим приоритетом и неявно преобразовывает тип другого операнда, у которого приоритет ниже, к типу первого.

В операциях присваивания также может происходить неявное преобразование типов данных.

Например, если мы хотим значение 8-байтового типа присвоить переменной 4-байтового типа, неважно, целого типа эти данные или нет, то при этом произойдёт неявное преобразование к 4-байтовому типу, при этом старшие 4 байта отбрасываются. Это считается небезопасным приведением типов, когда возможна потеря информации.

Может быть и наоборот, если мы хотим значение 4-байтового типа присвоить переменной 8-байтового типа. Также произойдёт неявное преобразование и, наоборот, 4 старших байта добавятся и заполнятся нулями. Это будет уже безопасное приведение типа и при этом целостность информации не страдает.

Так как мы с указателями ещё не работали, то в данном уроке мы их преобразование рассматривать не будем, скажу лишь, что там всё происходит аналогично, разница в поведении указателей типа void.

Думаю, что теперь ситуация с неявным преобразованием типов теперь немного стала понятна, конечно же, полное понимание придёт лишь с практикой.

Явное преобразование типов происходит тогда, когда мы применяем специальные механизмы для приведения одного типа к строго определённому другому типу. В языке C в для явного приведения типа перед переменной или выражением, значения которых мы преобразуем к другому типу, ставится в круглых скобках тип, к которому мы данное значение преобразуем, например

 

int i;

char c = 45;

i = (int)c;

 

В данном случае значение переменной c явно преобразовывается к типу данных int, а затем уже в преобразованном виде присваивается переменной i.

Вот ещё несколько примеров явного преобразования типов данных

 

sqrt((double) n)

float f_res = (float) i / (float) j;

double f_res = (double)((float) i / (float) j);

 

Давайте теперь поэкспериментируем с преобразованием типов данных на практике в реальном коде.

Проект сделаем из проекта MYPROG13 прошлого занятия и имя ему было присвоено MYPROG14.

 

 

Откроем файл main.c и в функции main(), как обычно, удалим весь код тела кроме возврата нуля, останется от него вот это

 

int main()

{

  return 0; //Return an integer from a function

}

Удалим также вот эти константы

 

#define VAR_CONST1 12345

#define HELLO_CONST «\»Hello, world!!!\»»

 

Добавим в тело функции main() следующий код

 

 

В данном коже мы выведем значение переменной i, которая в свою очередь сначала получит значение переменной c, у которой тип имеет меньший диапазон. В этом случае должно будет до присвоения произойти неявное преобразование значения переменной c в тип int и уж затем преобразованное значение присвоится нашей переменной c. Как мы знаем, преобразование значения типа, имеющего меньший диапазон в тип, имеющий больший диапазон, является безопасным и потери данных за собой не влечёт.

Проверим работу нашего кода

 

 

Всё сработало и значение не пострадало.

Теперь закомментируем наш предыдущий код и напишем вот такой

 

 

Думаю, что тут и без теории понятно, что миллион не влезет в диапазон типа данных char. Тем не менее соберём код и посмотрим результат

 

 

Мы видим, что результат сильно изменился. Почему же именно 64?

Попробуем наш миллион перевести в шестнадцатеричный вид, получится 0x000F4240. Каждые два разряда данного числа — это один байт. Такие разряды байта также называют тетрадами. Тип char — это у нас однобайтовое знаковое целое, поэтому при преобразовании у нас от числа отбросятся 3 старших байта, а останется лишь один младший байт, а это 0x40, что в десятичном выражении и является нашим числом 64.

Данное преобразование, само собой является небезопасным, так как при его применении теряется часть информации.

Закомментируем данный код и добавим вот такой

 

 

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

Проверим это

 

 

Так и есть, остаток потерян.

 

 

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

Поэтому, закомментировав сначала предыдущий код, добавим вот такой вариант кода

 

 

Теперь другое дело

 

 

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

Поэтому мы можем сделать либо так

 

 

либо так

 

 

Результат получится тот же самый

 

 

Ну думаю, в коде с явным и неявным преобразованием у нас что-то прояснилось. Интересно ещё поработать с функциями и как происходит преобразование их аргументов.

В случае использования функций все аргументы преобразовывают свой тип по-отдельности и друг от друга никак не зависят по сравнению с использованием обычных операторов.

Добавим вот такую функцию, которая будет делить значение оного аргумента с плавающей точкой на аналогичное значение другого и возвращать результат деления

 

 

Теперь закомментируем предыдущий код в функции main() и добавим вот такой

 

 

Мы можем испугаться потери информации, так как мы с этим встретились применяя обычный оператор деления, но если у нас функция, то этого не произойдёт.

Проверим это

 

 

Происходит это потому, что каждый аргумент преобразовывается из int в float отдельно. Поэтому в случае функций можно потери информации не бояться.

Для более полного закрепления материала давайте добавим ещё пару вариантов кода с преобразованиями.

Первый такой

 

 

Мы недаром добавили комментарий, гласящий о том, что преобразование у нас небезопасное.

Всё потому, что мы сначала преобразовали значение переменной большего диапазона значений, к переменной меньшего, а затем наоборот. Но потерянной (отброшенной) информации уже не вернуть, хотя мы имеем уже 4-байтный тип, а не 2-х.

Проверим результат работы кода

 

 

Как и ожидалось, старшие 3 байта отбросились, а затем, при втором преобразовании, заполнились нулями

Напишем теперь вот такой код

 

Проверим, как он работает

 

 

Мы видим, что при безопасном преобразовании типов результат не пострадал.

Ну и, напоследок попробуем какое-нибудь бессмысленное приведение типов

 

 

Мы попытаемся обратиться к какому-нибудь элементу массива, используя в качестве индекса тип данных с плавающей точкой.

А вот и результат

 

 

Мы даже собрать такой код не сможем, поэтому с этим лучше не шутить.

Думаю, теперь мы уже вполне неплохо себя чувствуем в плане знаний по преобразованию типов. Хотя в некоторых источника выдают за преобразование типов преобразование строки числовых символов в число и обратно, либо числового символа в число. Я считаю, что это несколько другое преобразование.

Итак, на данном уроке мы познакомились с преобразованием типа, каких видов оно бывает, также узнали, чем грозит нам небезопасное преобразование типов.

Всем спасибо за внимание!

 

 

Предыдущий урок Программирование на C Следующий урок

 

 

Исходный код

 

 

Смотреть ВИДЕОУРОК (нажмите на картинку)

 

C Name

3 комментария на “C Урок 14. Преобразование типов
  1. Спасибо за старания и потраченное время.

  2. Сергей:

    Осмелюсь дополнить материал. По ходу урока задался вопросом — а зачем ставится буковка «f» в конце значения при инициализации переменной типа float. Оказывается, «f» указывает на то, что это значение с плавающей точкой, а не значение типа double (что неявно было бы иначе). Т.е., если указать «float f = 3.14;» (не написав «f» в конце), то компилятор выдаст ошибку, т.к. вы пытаетесь поместить значение типа double в переменную типа float без приведения.
    Однако, например, если переменная типа double, а значение типа float, то такая операция ошибку не даст «double d = 3.14;». Можно указать буковку «d», а можно и нет.

    Хотелось бы, чтобы в уроках на такие моменты автор тоже обращал внимание, это ж все-таки обучение программированию 🙂
    А вообще, пользуясь случаем, большое спасибо Вам за то, что Вы делаете!

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*