Собственно, попытаюсь выразить свои мысли по этому поводу в следующей небольшой зарисовке под названием "Указатели и ОЗУ - уровни абстракций"
Итак, ОЗУ. Для начала рассмотрим 8-битные AVR. Каждая ячейка ОЗУ - один байт, имеет некий
адрес - физический и абсолютный, который, допустим, имеет длину 2 байта (зависит от размера ОЗУ), но в принципе это не главное - это имеет значение только когда мы этот адрес будем куда-то помещать и хранить-передавать. Для простоты будем писать адрес десятичным числом (хотя программисты привыкли к 16-ричному представлению
) Вот и все. При написании на asm можно как угодно это использовать.
Что же нам предлагает С? Вводится понятие
указателя. Причем, на определенный тип. Что это значит? Имхо, это "всего навсего" значит - как извлекать значение и как инкрементировать указатель. Например, беру указатель некоей переменной типа char. Компилятор знает, что он положил эту переменную в ячейку ОЗУ (которая как раз 8-битная) с номером 915. Числовое значение указателя = 915 (тип int). Если я теперь пытаюсь получить значение переменной по этому указателю, компилятор запросит значение этой ячейки ОЗУ и выдаст её содержимое. Если я определяю переменную типа массив char, то компилятор располагает её в одной области ОЗУ, так что все элементы идут последовательно по адресам. В принципе, это не единственный способ определить массив в ОЗУ, но так с ним гораздо проще работать. В этом случае, если наш указатель на ячейку с номером 915 указывает на 0-й элемент массива, то прибавив к указателю 1-цу мы получим 916 - адрес 1-го элемента массива, и таким образом можем перебирать элементы (собственно, так это на самом деле и происходит
)
Теперь возьмем указатель на int. Int у нас лежит уже в 2-х ячейках ОЗУ. Но само числовое значение указателя будет то же самое по разрядности. Указатель (в своем числовом значении) будет хранить адрес
младшего байта нашей переменной. А компилятор будет знать, что раз это указатель на int, то чтобы взять значение переменной, надо младший байт прочитать из ячейки с номером указателя - 915, а старший из следующей - 916. Хотя сам указатель будет хранить всегда числовое значение адреса одной ячейки ОЗУ. Если же мы определим указатель на массив int, то мы так же можем перебирать элементы массива, инкрементируя указатель - но в данном случае наш умный компилятор, зная что это указатель на int, будет инкрементировать его не на следующий байт ОЗУ а
через байт - на ячейку 917, хотя мы указали инкрементирование на 1-цу! То же самое происходит и со структурами, и с массивами структур и вообще с любыми объектами в ОЗУ. Всего 2 вещи: указатель всегда содержит адрес одной ячейки и компилятор помнит насколько его надо увеличить, чтобы получить следующий элемент.
А теперь самое интересное - возможные следствия этого
Абсолютное значение любого указателя - одно и то же по типу и разрядности - наши пресловутые 915. Но с этого адреса мы можем извлечь из памяти и char, и int, и вообще любое значение любого типа
Весь вопрос в том - осознанно ли мы это делаем или нет
Например, определив указатель на массив типа int, мы можем присвоить его значение (те же 915) другому указателю на char. И получить по значению этого указателя младший байт переменной int. А при последовательном инкрементировании этого указателя мы возьмем старший байт (из ячейки 916), потом младший байт следующего элемента (917) и т.д.
Потому что наш умный компилятор будет знать, что раз это указатель на char, то инкрементировать его надо на 1-цу а не через байт
Или, например, мы взяли указатель на переменную char, присвоили его значение указателю на int и читаем по нему. Младший байт прочитается из ячейки char а старший - из следующей, где в это время может лежать совершенно что угодно. Это все упирается в философский вопрос о том, знаем ли мы что находится в ОЗУ когда читаем оттуда по адресам.
Когда же может произойти "переопределение типа" указателя? При прямом присваивании значения указателя одного типа другому, при передаче в качестве параметра в функцию, при чтении значения указателя из переменной и т.д., подозреваю что способов много. Неприятные последствия этого очевидны - мы читаем из ОЗУ или пишем в него совсем не то и не по тем адресам, что
мы думаем там находится. Выше приведенный пример с вроде бы красивым обращением к разным байтам длинных переменных по указателям char с одной стороны подкупает своей стильностью, но я бы не советовал пользоваться такими методами. Потому что, например, при переносе кода на 32-разрядные системы, все может поломаться совсем
Потому что там char и int могут храниться в одной и той же 32-разрядной ячейке ОЗУ. А если система позволяет, то компилятор может вообще засовывать переменные char в разные группы разрядов 32-битной ячейки ОЗУ - хоть 4 в одну, имея в виду векторный адрес переменной (адрес ячейки + адрес байта: 0,1,2,3).
Проще говоря - заранее не зная, между какими структурами ОЗУ будет переноситься ваш код, подобные хитрости с указателями лучше не применять. Да и вообще, имхо их лучше не применять - чтобы жизнь была проще!
ЗЫ ну что, похож я на писателя книг по устойчивому кодированию, который ни одной программы в жизни не написал?