#include <iostream>
#include <cstddef>
int main() {
char tab[] = "Ala ma kota.";
std::size_t tab_len = sizeof(tab) / sizeof(*tab);
for(std::size_t i = 0; i < tab_len; ++i) {
std::cout << tab[i] << ": " << static_cast<void *>(tab + i) << '\n';
}
}
1. Standard języka C i C++ gwarantuje że arytmetyka wskaźników w tablicy pracuje poprawnie dla zakresu od pierwszego elementu w tablicy (indeks zerowy) do ostatniego elementu w tablicy plus 1 (indeks jednego elementu za tablicą czyli typowo jej długość). Nie gwarantuje natomiast że uzyskasz jakikolwiek sensowny wynik dla indeksu -1 czy -10. Tam może nie być czytelnych danych lub nawet pamięci.
2. static_cast<X> nie oznacza "dodania static" a prostą czyli statyczną zamianę jednego typu w inny. Uprzywilejowany jest tu void * (void gwiazdka) bo zawsze da się na niego zamienić statycznie inny typ i zawsze będzie to informacją dla jakichkolwiek wyprowadzeń na konsolę że chodzi Ci o "surowy adres w pamięci".
3. Standard nie gwarantuje że typ int jest tak samo przechowywany jak char na danej platformie systemowej. Może mieć nawet dedykowaną przestrzeń pamięci oraz sposoby wykonywania dostępu do niego.
4. Bezpieczeństwo rzutowania w C++ wynika ze ścisłej kontroli jakie rodzaje konwersji są dopuszczalne w każdym z przypadków rzutowania. Tu masz opisy tych rodzajów rzutowań: http://en.cppreference.com/w/cpp/language/explicit_castLepiej jest robić takie rzutowania bo jasno pokazujesz intencję działania. Rzutowanie w stylu C jest brutalne i pozbawione kontroli oraz trudne do odszukania w dużym projekcie. Tak więc stosowanie printf(...) tylko do wyświetlenia adresu (i tylko to ma usprawiedliwiać jego użycie) jest co najmniej ... "nieeleganckie". Odpowiednikiem rzutowania w stylu C jest reinterpret_cast<X> dla C++.
5. Wskaźnik przechowuje informację o typie danych na jakie wskazuje. Tak dzieje się dla int * czy char * ale już nie dla void * . Sam void * mówi wyraźnie "pozbawiam się typu i interesuje mnie wyłącznie adres". Sam typ void jest oznaczeniem... braku typu. Z racji przechowywania typu, dodanie 1 do np. int *, powoduje przeskoczenie na adres w którym leżałaby następna wartość o zakres adresów wystarczających do pomieszczenia typu int. Dla char *, masz gwarancję że będzie to następny adres bo typ char mapowany jest zawsze na 1 bajt.
6. Typ tablicowy (czyli np: int tab[12]; ) zobowiązuje do trzymania 12 wartości typu int. Jednocześnie jest wskaźnikiem na pierwszy element. Jeśli zrobisz na nim + 1, z definicji samego typu, przeskoczy o "12 elementów typu int". Jeśli chodzi o adresy będzie to: 12 razy sizeof(int). Otrzymasz więc adres tuż za tablicą. W świetle tego co pisałem w pkt. 1, jest to poprawny adres, obliczenia na adresach będą wykonane poprawnie ale nie oznacza to że możesz pod tym adresem coś zapisać bezkarnie bo skąd wiesz co w nim umieścił kompilator?
Mam nadzieję że teraz jasne. Odpowiedzi na takie pytania znajdziesz w dowolnej książce dla początkujących do C lub C++.