Операция присваивания указателей различных типов представляет собой копирование адреса памяти содержащегося в них, то есть указателю в левой части назначается та же самая область памяти, что у правого указателя.
#include <stdio.h>
int main() {
int *p1;
double *p2 = (double*)p1;
printf("%p\n", p1); // 7FFD7000
printf("%p\n", p2); // 7FFD7000
return 0;
}
Для указателей разного типов эта область памяти по правилам интерпретации указателей рассматривается как заполненная переменными либо одного, либо другого типа. Область памяти (захват) может иметь различную структуру (байтовую, словную и др.) в зависимости от того, через какой указатель мы с ней работаем (имеется в виду тип данных указателя).
Присваивание значения указателя одного типа указателю другого типа сопровождается действием, которое называется в С преобразованием типа указателя, и которое в С++ обозначается всегда явно. На самом деле это действие является чистой фикцией (команды транслятором не генерируются). Транслятор просто запоминает, что тип указываемой переменной изменился, поэтому операции адресной арифметики и косвенного обращения нужно выполнять с учетом нового типа указателя.
Рассмотрим следующий пример. Проинициализируем переменную x типа short int и определим для нее указатель p. Напомним, что размер памяти выделяемый под переменные данного типа занимает sizeof(short int) = 2 байта. То есть 16 битов - 16 разрядов. Далее создадим указатель q, к которому приводится указатель p. Для типа данных int отводится sizeof(int) = 4 байта. То есть 32 бита - 32 разряда.
#include <stdio.h>
int main() {
short int x = 0;
short int *p = &x;
int *q = (int*)p;
// short int pointer address = 0022FF26
printf("short int pointer address = %p\n", p);
// int pointer address = 0022FF26
printf("int pointer address = %p\n", q);
// 0
printf("%d\n", *p);
// -152 (garbage)
printf("%d\n", *(++p));
// -9961472
printf("%d\n", *q);
return 0;
}
Как было сказано выше, в указатель q копируется адрес указателя p (что подтверждается выводом их адресов), так как у них разные типы, то адресная арифметика и косвенное обращение к памяти будет разным, поэтому каждый из них будет захватывать определенную их типом область памяти, то есть иметь разную структуру памяти. На примере видно, как ведут себя оба указателя: при разыменовании указатель p дает значение переменной x, это очевидно, указатель же q дает неожиданный результат -9961472, это связано с тем, что в область захвата памяти вошла неинициализированная область памяти, в котором лежит мусор, в примере мы обратились к этому мусору - там лежит значение -152. Стоит отметить, что в вашем случае у вас могут быть получены свои адреса и значения. Если же теперь представить полученные значения в бинарном виде и обратить внимание на типы данных указателей, то можно "увидеть" структуру рабочей области памяти, которая изображена на следующем рисунке.
Как известно функции динамического распределения (malloc, calloc, realloc) возвращают указатель типа void* на распределенную динамически память. Указатели данного типа могут быть приведены к абсолютно любому типу, но операция по извлечению данных по адресу запрещены. Из вышеприведенного ясно, что в этом случае мы столкнемся с неопределенностью в области захвата памяти!
Модифицируем вышеприведенный пример так, что бы по второму указателю при разыменовании было исключено взятие мусора
#include <stdio.h>
int main() {
short int x[2] = {0, 0};
short int *p = x;
int *q = (int*)p;
// short int pointer address = 0022FF24
printf("short int pointer address = %p\n", p);
// int pointer address = 0022FF24
printf("int pointer address = %p\n", q);
// 0
printf("%d\n", *p);
// 0
printf("%d\n", *q);
return 0;
}
За счет использования массива, мы смогли проинициализировать рабочую область памяти. Как итог, снова и снова убеждаемся в необходимости соблюдать аккуратность и внимательность при работе с указателями. Всем удачи в нашем нелегком ремесле :)