Подтвердить что ты не робот

Каковы правила для указателей каста в C?

K & R не проходит через него, но они используют его. Я попытался посмотреть, как это работает, написав примерную программу, но все прошло не так хорошо:

#include <stdio.h> 
int bleh (int *); 

int main(){
    char c = '5'; 
    char *d = &c;

    bleh((int *)d); 
    return 0;  
}

int bleh(int *n){
    printf("%d bleh\n", *n); 
    return *n; 
}

Он компилируется, но мой оператор печати выплескивает переменные мусора (они разные при каждом вызове программы). Есть идеи?

4b9b3361

Ответ 1

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

d в main является указателем на c, который имеет тип char. A char - один байт памяти, поэтому, когда d разыменовывается, вы получаете значение в этом байте памяти. На приведенной ниже диаграмме каждая ячейка представляет один байт.

-+----+----+----+----+----+----+-
 |    | c  |    |    |    |    | 
-+----+----+----+----+----+----+-
       ^~~~
       | char
       d

При нажатии d на int* вы говорите, что d действительно указывает на значение int. В большинстве систем сегодня int занимает 4 байта.

-+----+----+----+----+----+----+-
 |    | c  | ?₁ | ?₂ | ?₃ |    | 
-+----+----+----+----+----+----+-
       ^~~~~~~~~~~~~~~~~~~
       | int
       (int*)d

Когда вы разыгрываете (int*)d, вы получаете значение, которое определяется из этих четырех байтов памяти. Значение, которое вы получаете, зависит от того, что находится в этих ячейках с пометкой ?, и от того, как int отображается в памяти.

ПК little-endian, что означает, что значение int рассчитывается таким образом (при условии, что оно охватывает 4 байта): * ((int*)d) == c + ?₁ * 2⁸ + ?₂ * 2¹⁶ + ?₃ * 2²⁴. Таким образом, вы увидите, что, пока значение является мусором, если вы печатаете в шестнадцатеричном формате (printf("%x\n", *n)), последние две цифры всегда будут 35 (это значение символа '5').

Некоторые другие системы являются большими и упорядочивают байты в другом направлении: * ((int*)d) == c * 2²⁴ + ?₁ * 2¹⁶ + ?₂ * 2⁸ + ?₃. В этих системах вы обнаружите, что значение всегда начинается с 35 при печати в шестнадцатеричном формате. Некоторые системы имеют размер int, который отличается от 4 байтов. Редкие несколько систем упорядочивают int по-разному, но вы вряд ли встретите их.

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

В некоторых системах значение int должно храниться в адресе, кратное 4 (или 2 или 8). Это называется alignment. В зависимости от того, правильно ли выровнен адрес c или нет, программа может выйти из строя.

В отличие от вашей программы, вот что происходит, когда у вас есть значение int и наведите указатель на него.

int x = 42;
int *p = &x;
-+----+----+----+----+----+----+-
 |    |         x         |    | 
-+----+----+----+----+----+----+-
       ^~~~~~~~~~~~~~~~~~~
       | int
       p

Указатель p указывает на значение int. Метка на стрелке правильно описывает, что в ячейке памяти, поэтому нет сюрпризов при разыменовании.

Ответ 2

char c = '5'

A char (1 байт) выделяется в стеке по адресу 0x12345678.

char *d = &c;

Вы получите адрес c и сохраните его в d, поэтому d = 0x12345678.

int *e = (int*)d;

Вы вынуждаете компилятор предположить, что 0x12345678 указывает на int, но int - это не только один байт (sizeof(char) != sizeof(int)). Это может быть 4 или 8 байтов в соответствии с архитектурой или даже другими значениями.

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

Ответ 3

Указатели каста обычно недействительны в C. Существует несколько причин:

  • Выравнивание

    . Возможно, что из-за соображений согласования тип указателя назначения не может представлять значение типа указателя источника. Например, если int * по своей сути выровнялся по 4 байт, отбрасывание char * до int * потеряло бы нижние бит.

  • Aliasing. В общем случае запрещается доступ к объекту, кроме как через lvalue правильного типа для объекта. Есть некоторые исключения, но если вы их не понимаете очень хорошо, вы не хотите этого делать. Обратите внимание, что сглаживание - это только проблема, если вы действительно разыщите указатель (примените к нему операторы * или -> или передайте его функции, которая будет разыменовывать ее).

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

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

  • Когда тип указателя адресата является указателем на тип структуры, члены которого точно соответствуют начальным элементам типа структуры с первоначальной направленностью. Это полезно для различных методов объектно-ориентированного программирования в C.

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

Ответ 4

У вас есть указатель на char. Как известно вашей системе, на этом адресе памяти есть значение char на sizeof(char). Когда вы произведете его до int*, вы будете работать с данными sizeof(int), поэтому вы будете печатать ваш char и некоторый мусор памяти после него как целое число.

Ответ 5

Я подозреваю, что вам нужен более общий ответ:

В C! Язык позволяет вам указывать любой указатель на любой другой указатель без комментариев.

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

Итак, когда вы полностью решаете, чтобы заботиться о том, чтобы, если данные используются из литого указателя, данные совместимы!

C оптимизирован для производительности, поэтому ему не хватает рефлексивности времени выполнения указателей/ссылок. Но у этого есть цена - вы, как программист, должны лучше заботиться о том, что вы делаете. Вы должны знать сами, если то, что вы хотите сделать, является "законным"

Ответ 6

Значение мусора на самом деле связано с тем, что вы вызывали функцию bleh() перед объявлением.

В случае С++ вы получите ошибку компиляции, но в c, компилятор предполагает, что возвращаемый тип функции int, тогда как ваша функция возвращает указатель на целое число.

Смотрите это для получения дополнительной информации: http://www.geeksforgeeks.org/g-fact-95/

Ответ 7

Что произойдет, если мы приведем тип "c" к int, и расширение памяти, выделенное для преобразования, получит адреса памяти, уже выделенные целым числом "e"?

-+----+----+----+----+----+----+-
 | | c | | | e |
-+----+----+----+----+----+----+-
     ^~~~    ^~~~
     | char  | int
     d
-+----+----+----+----+----+----+-
|    | c  | ?₁ | ?₂ | ?₃ |    | 
                      e
-+----+----+----+----+----+----+-
       ^~~~           ^~~~
       | int          | int
       d