C приведение типов указателей

Приведе́ние (преобразование) ти́па (англ. type conversion , typecasting , coercion ) — в информатике преобразование значения одного типа в значение другого типа.

Содержание

Описание [ править | править код ]

Выделяют приведения типов:

Явное приведение задаётся программистом в тексте программы с помощью:

  • конструкции языка;
  • функции, принимающей значение одного типа и возвращающей значение другого типа.

Неявное приведение выполняется транслятором (компилятором или интерпретатором) по правилам, описанным в стандарте языка. Стандарты большинства языков запрещают неявные преобразования.

В слабо типизированных объектно-ориентированных языках, таких как C++, механизм наследования реализуется посредством приведения типа указателя на текущий объект к базовому классу (в типобезопасных, таких как OCaml, понятие о приведении типов отсутствует принципиально, и допустимость обращения к компоненту подтипа контролируется механизмом проверки согласования типов на этапе компиляции, а в машинном коде остаётся прямое обращение).

Неявное приведение типа [ править | править код ]

Неявное приведение типа в языках C/C++ [ править | править код ]

Неявное приведение типов происходит в следующих случаях [1] :

  • после вычисления операндов бинарных арифметических, логических, битовых операций, операций сравнения, а также 2-го или 3-го операнда операции «?:»; значения операндов приводятся к одинаковому типу;
  • перед выполнением присваивания;
  • перед передачей аргумента функции;
  • перед возвратом функцией возвращаемого значения;
  • после вычисления выражения конструкции switch значение приводится к целочисленному типу;
  • после вычисления выражений конструкций if , for , while , do — while значение приводится к типу bool .

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

Рассмотрим пример на языке C.

При выполнении операций сравнения и при присваивании переменные разных типов неявно приводятся к одному типу.

При неявных преобразованиях возможны побочные эффекты. Например, при приведении числа вещественного типа к целому типу дробная часть отсекается (округление не выполняется). При обратном преобразовании возможно понижение точности из-за различий в представлении вещественных и целочисленных чисел. Например, в переменной типа float (число с плавающей точкой одинарной точности по стандарту IEEE 754), нельзя сохранить число 16 777 217 без потери точности, а в 32-битной переменной целого типа int — можно. Из-за потери точности операции сравнения одного и того же числа, представленного целым и вещественным типами (например, int и float ), могут давать ложные результаты (числа могут быть не равны).

Приведённый код выведет следующее, если размер int — 32 бита и компилятор поддерживает стандарт IEEE 754:

Явное приведение типа [ править | править код ]

Приведения типов в языке C [ править | править код ]

Для явного приведения типов имя типа указывается в круглых скобках перед переменной или выражением. Рассмотрим пример.

Для вычисления последнего выражения компилятор выполняет примерно следующие действия:

  • сначала переменная C символьного типа char явно приводится к целочисленному типу int путём расширения разрядности;
  • выполняется вычисление операндов для операции умножения. Левый операнд имеет тип int . Правый операнд — константа 10 , а такие константы по умолчанию имеют тип int . Так как оба операнда оператора «*» имеют тип int , неявное приведение типов не выполняется. Результат умножения тоже имеет тип int ;
  • выполняется вычисление операндов операции сложения. Левый операнд — результат умножения имеет тип int . Правый операнд — переменная Y имеет тип int . Так как оба операнда оператора «+» имеют тип int , неявное приведение к общему типу не выполняется. Результат сложения тоже имеет тип int ;
  • выполнение присваивания. Левый операнд — переменная X имеет тип int . Правый операнд — результат вычисления выражения, записанного справа от знака «=», тоже имеет тип int . Так как оба операнда оператора «=» имеют одинаковый тип, неявное приведение типов не выполняется.

Но даже при этом возможны ошибки. Тип char может быть как знаковым ( signed char ), так и беззнаковым ( unsigned char ); результат зависит от реализации компилятора и такое поведение разрешено стандартом. Значение беззнакового типа char при преобразовании к знаковому типу int может оказаться отрицательным из-за особенностей реализации машинных инструкций на некоторых процессорах. Чтобы избежать неоднозначностей, рекомендуется явно указывать знаковость для типа char .

Приведения типов в языке C++ [ править | править код ]

В языке C++ существует пять операций для явного приведения типа. Первая операция — круглые скобки ( ( type_to ) expression_from ) поддерживается для сохранения совместимости с C. Остальные четыре операции записываются в виде

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

Операция static_cast [ править | править код ]

Назначение: допустимые приведения типов.

Операция static_cast аналогична операции «круглые скобки» с одним исключением: она не выполняет приведение указателей на неродственные типы (для этого применяется операция reinterpret_cast ).

  • преобразование между числовыми и enum, в том числе если неявное преобразование невозможно ( int → enum class ) или приводит к предупреждению «Возможная потеря точности» ( double → float );
  • приведение указателей к типу void * и наоборот;
  • приведение указателей на производные типы к указателям на базовые типы и наоборот;
  • выбор одной из нескольких перегруженных функций;
  • явный вызов конструктора с одним аргументом или перегруженной операции приведения типа;
  • приведение типа в шаблонах (компилятор уже при специализации шаблона решает, какие операции использовать);
  • приведение операндов тернарной условной операции « ?: » к одному типу (значения 2-го и 3-го операндов должны иметь одинаковый тип);

Ограничения на expression_from : нет.

Ограничения на type_to : должен существовать способ преобразования значения выражения expression_from к типу type_to , с помощью operator type_to или конструктора.

Производит ли операция static_cast код: в общем случае да (например, вызов перегруженной операции приведения типа или конструктора).

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

Операция dynamic_cast [ править | править код ]

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

Операция получает информацию о типе объекта expression_from с помощью RTTI. Если тип будет type_to или его подтипом, приведение выполняется. Иначе:

  • для указателей возвращается NULL;
  • для ссылок создаётся исключение std :: bad_cast .

Ограничения на expression_from : выражение должно быть ссылкой или указателем на объект, имеющий хотя бы одну виртуальную функцию.

Ограничения на type_to : ссылка или указатель на дочерний по отношению к expression_from тип.

Производит ли операция dynamic_cast код: да.

Логические ошибки возможны, если операции передать аргумент, не имеющий тип type_to , и не проверить указатель на равенство NULL (соответственно не обработать исключение std :: bad_cast ).

Операция const_cast [ править | править код ]

Назначение: снятие/установка модификатора(ов) const , volatile и/или mutable . Часто это применяется, чтобы обойти неудачную архитектуру программы или библиотеки, для стыковки Си с Си++, для передачи информации через обобщённые указатели void* .

Ограничения на expression_from : выражение должно возвращать ссылку или указатель.

Ограничения на type_to : тип type_to должен совпадать с типом выражения expression_from с точностью до модификатора(ов) const , volatile и mutable .

Производит ли операция const_cast код: нет.

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

При загрузке библиотеки в память процесса создаёт новый сегмент данных, в котором размещаются глобальные переменные. Код функции SomeDllFunction () находится в библиотеке и при вызове возвращает указатель на скрытый член глобального объекта класса string . Операция const_cast используется для удаления модификатора const .

Операция reinterpret_cast [ править | править код ]

Назначение: каламбур типизации — назначение ячейке памяти другого типа (не обязательно совместимого с данным) с сохранением битового представления.

Объект, возвращаемый выражением expression_from , рассматривается как объект типа type_to .

Ограничения на expression_from : выражение должно возвращать значение порядкового типа (любой из целых, логический bool или перечислимый enum ), указатель или ссылку.

Ограничения на type_to :

  • Если expression_from возвращает значение порядкового типа или указатель, тип type_to может быть порядковым типом или указателем.
  • Если expression_from возвращает ссылку, тип type_to должен быть ссылкой.

Производит ли операция reinterpret_cast код: нет.

Источники логических ошибок. Объект, возвращаемый выражением expression_from , может не иметь типа type_to . Нет никакой возможности проверить это, всю ответственность за корректность преобразования программист берёт на себя.

Запуск этого кода возвращает то, что я предполагаю как целое значение realPtr адрес.

Я все еще новичок в C ++, и мне было интересно — можно ли преобразовать указатель одного типа данных в другой, а переменная-указатель, назначенное значение которой все равно выведет правильное значение (т.е. *integerPtr распечатывает 2)?

Решение

Здесь есть 3 совершенно разных типа, с которыми вы имеете дело:

  • Указатели указывают на любое место в памяти. Все указатели в основном одного типа, но они рассматриваются как разные типы в зависимости от того, что Вы объявляете их указать на.
  • int и double — это типы, представляющие целое и действительное числовое значение; Целое число и двойное число, представляющие одно и то же числовое значение (например, 2), не будут иметь одинакового двоичного содержимого, как предусмотрено соответствующим стандартом, определяющим, как целые числа и числа с плавающей запятой хранятся в памяти.

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

Это законно, но это, скорее всего, приведет к ошибке, если представление памяти объекта Foo сродни представлению типа int.

Если вы хотите привести int к double, вы должны привести данные, приведение указателя ничего не изменит. Вот почему в C ++ 11 есть несколько приведений с разными именами, и рекомендуется использовать их, поскольку тем самым вы явно выражаете то, что хотите сделать. В вашем случае вы можете захотеть одну из двух разных вещей

reinterpret_cast

Приведение указателя на int к указателю на double, что означает, по сути, ничего не делая, кроме сообщения компилятору, что он может с уверенностью предположить, что указанные данные могут рассматриваться как double. Компилятор примет это на себя, и вы должны убедиться, что это так. Этот тип приведения считается самым опасным, поскольку, как мы все знаем, программистам нельзя доверять. Это называется reinterpret_cast:

Тот же код, что и выше, но мы выражаем опасность в страшном «reinterpret_cast». p_int и p_foo имеют одно и то же значение, мы ничего не делали, кроме выражения того факта, что теперь мы рассматриваем адрес нашего целого числа как адрес для foo.

static_cast

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

Компилятор будет искать функцию преобразования из int в double, и будет кричать на вас, если не найдет ее.

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

Другие решения

Внутреннее представление типов данных float / dobule и integer различается. В 32-битном ПК Integer будет занимать 4 байта, а двойной — 8 байтов.

Также есть серьезная ошибка в вашем коде.

В приведенном выше коде см. Жирные линии. Там вы объявили указатель на целое число с именем «integerPtr». Но фактическое значение хранимого адреса указателя на двойное число. т.е. &realPtr является двойным ** (указатель на указатель, который содержит значение типа double). И затем вы пытаетесь напечатать значение, используя * integerPtr.

Поэтому я изменил ваш код следующим образом, и он дает значение 0.

C/C++. Преобразование типов указателей

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

int *p1;
double *p2 = (double*)p1;

printf("%p
", p1); // 7FFD7000
printf("%p
", p2); // 7FFD7000

Для указателей разного типов эта область памяти по правилам интерпретации указателей рассматривается как заполненная переменными либо одного, либо другого типа. Область памяти (захват) может иметь различную структуру (байтовую, словную и др.) в зависимости от того, через какой указатель мы с ней работаем (имеется в виду тип данных указателя).
Присваивание значения указателя одного типа указателю другого типа сопровождается действием, которое называется в С преобразованием типа указателя, и которое в С++ обозначается всегда явно. На самом деле это действие является чистой фикцией (команды транслятором не генерируются). Транслятор просто запоминает, что тип указываемой переменной изменился, поэтому операции адресной арифметики и косвенного обращения нужно выполнять с учетом нового типа указателя.
Рассмотрим следующий пример. Проинициализируем переменную x типа short int и определим для нее указатель p. Напомним, что размер памяти выделяемый под переменные данного типа занимает sizeof(short int) = 2 байта. То есть 16 битов — 16 разрядов. Далее создадим указатель q, к которому приводится указатель p. Для типа данных int отводится sizeof(int) = 4 байта. То есть 32 бита — 32 разряда.

short int *p = &x;
int *q = (int*)p;

// short int pointer address = 0022FF26
printf("short int pointer address = %p
", p);
// int pointer address = 0022FF26
printf("int pointer address = %p
", q);

Как было сказано выше, в указатель q копируется адрес указателя p (что подтверждается выводом их адресов), так как у них разные типы, то адресная арифметика и косвенное обращение к памяти будет разным, поэтому каждый из них будет захватывать определенную их типом область памяти, то есть иметь разную структуру памяти. На примере видно, как ведут себя оба указателя: при разыменовании указатель p дает значение переменной x, это очевидно, указатель же q дает неожиданный результат -9961472, это связано с тем, что в область захвата памяти вошла неинициализированная область памяти, в котором лежит мусор, в примере мы обратились к этому мусору — там лежит значение -152. Стоит отметить, что в вашем случае у вас могут быть получены свои адреса и значения. Если же теперь представить полученные значения в бинарном виде и обратить внимание на типы данных указателей, то можно "увидеть" структуру рабочей области памяти, которая изображена на следующем рисунке.

Как известно функции динамического распределения (malloc, calloc, realloc) возвращают указатель типа void* на распределенную динамически память. Указатели данного типа могут быть приведены к абсолютно любому типу, но операция по извлечению данных по адресу запрещены. Из вышеприведенного ясно, что в этом случае мы столкнемся с неопределенностью в области захвата памяти!
Модифицируем вышеприведенный пример так, что бы по второму указателю при разыменовании было исключено взятие мусора

short int *p = x;
int *q = (int*)p;

// short int pointer address = 0022FF24
printf("short int pointer address = %p
", p);
// int pointer address = 0022FF24
printf("int pointer address = %p
", q);

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

Оцените статью
Много толка
Добавить комментарий