Powinienem to napisać w komentarzu, ale może komuś sie jeszcze przyda / ktoś to zweryfikuje.
To jest sterta, składająca się z komórek pamięci. Każda komórka ma swój numerek, za pomocą, którego można się do niej odnieść. Dokładnie tak samo wygląda sterta, na której pracujesz, z tym, że jest większa i Twoje alokacje nie zaczynają się od komórki numer 0. Komórki są tak samo ponumerowane. Jak wypiszesz adres jakiegoś wskaźnika to zobaczysz właśnie ten numer, który domyślnie jest wyświetlany w systemie 16-stkowym, ale to wciaż zwykła liczba.
Na początku w każdej komórce są jakieś śmieci. Losowe wartości. Obszar na szaro jest wolny, niezaalokowany.
No to alokujemy. Najpierw pojedyńczego inta:
new int;
W tym momencie pamięc wygląda tak:
Zakładając, że zajmuje 4 bajty. Zaznaczyłem na czerwono zaalokowaną pamięć. Jednak to by było bez sensu bo pamięć jest zaalokowana, ale nie wiemy gdzie. Dlatego new zwraca adres pierwszej zaalokowanej komórki. W tym przypadku zwróci numer 0. Znając ten numer pamięci możemy zrobić tak:
int *ptr = new int;
*ptr = 5;
//*(0) = 00000000 00000000 00000000 00000101
Te binarne liczby wylądują w 4 pierwszych komórkach pamięci mimo, że próbujesz je wpisać do jednego adresu. Po to własnie jest typ wskaźnika. Na jego podstawie kompilator wie ile komórek pamięci ma wykorzystać.
A teraz zaalokujemy tablice:
int* tab = new int[3];
Zakładając, że zaczęliśmy program od nowa, albo zwolniliśmy wcześniej zaalkowanego pojedyńczego inta to tak wygląda pamięć. Został zaalokowany ciągły blok pamięci na 3 inty, czyli 12 bajtów. New też zwróciło numer 0. Ale my wiemy, że startując od tego numeru przydzielono nam miejsca na 3 inty. A kompilator wie, że typ wskaźnika jest int, więc zna jego rozmiar i wiel o ile bajtów się przesunąc gdy zobaczy tab+1. Będzie on równy blokowi pamięci o numerze 0 + 1*sizeof(int) i w ogólnieniu:
wskaźnik_do_poczatku + i*sizeof(typ_elementu)
We wskaźnikach nie ma niczego magicznego. Są to zwykłe liczby. Czemu nie są zatem typem int? Bo to by było mylące i niepraktyczne. Np mając wskaźnik do wskaźnika kompilator pilnuje nas ze mozemy go 2 razy dereferencjować (gwiazdkować). Podobnie pilnuje nas, żeby nie dało się dereferencjować dowolnego inta. No i druga sprawa to wielkość pojedyńczego elementu. Kompilator musi wiedzieć na jaki typ wskaźnik wskazuje, żeby arytmetyka działała. Żeby wiedział o ile bajtów ma się przesunąc gdy zobaczy kod wsk+3. Ale są to tylko mechanizmy sprawdzania.
Żeby nie być gołosłownym napisałem taki kod:
#include <iostream>
#include <bitset>
using namespace std;
int main() {
int *ptr = new int[2];
ptr[0] = 1;
ptr[1] = 2;
cout << "numer bloku pamieci:\n"<< ptr <<endl;
long long address_number = (long long) ptr;
cout <<"adres bloku pamieci zrzutowany do liczby:\n"<< std::hex << address_number << endl;
cout << "a teraz przesuniemy się recznie\n";
long long address_number_of_1_element = address_number + sizeof(int);
cout << "recznie policzony numer bloku\n" << std::hex<< address_number_of_1_element << endl;
cout << "numer bloku obliczony przez kompilator\n" << ptr + 1 << endl;
int* very_very_hacky_pointer = (int*)address_number_of_1_element;
cout << "wartosc wyciągnięta na podstawie dereferencji liczby zrzutowanej \n";
cout<<"do wskaznika:\n" << *very_very_hacky_pointer << endl;
cout << ":OOOOOOOOOOOOOOOO to dziala" << endl<<endl;
cout << "A teraz dobierzemy sie do srodkowego bloku skladajacego sie na inta\n";
long long address = (long long)ptr + 1;
int* hacky_pointer_to_the_middle_of_int = (int*)address;
*hacky_pointer_to_the_middle_of_int = 1;
cout << bitset<32>(*ptr) << endl;
cout << "Jak widac dodalismy 1 na poczatku drugiego bajtu" << endl;
}
Mam nadzieję, że teraz rozumiesz co się dzieje gdy zrobisz:
int *x = new int[3];
int *p = x;
Po prostu p będzie zawierać numer bloku pamięci, wskaźnik do początku zaalokowanego bloku. Jest tylko jedna tablica. Wskaźnik to adres jej początku. To zwykła liczba.
PS: nie popieram rzutowania w stylu C w C++, ale (int) jest krótsze niż reinterpret_cast<int>
PSS: kod ma charakter czysto dydaktyczny, nigdy nie wykorzystuj!!!