Wpis z mikrobloga

Dlaczego indeksujemy od zera?

Sprawa jest dość prosta.


#include

int main(void) {
int numbers[] = {1,2,3,4};
printf("numbers in general: %p -- %p\n", &numbers, numbers+0);
for (int i = 0; i < sizeof(numbers)/sizeof(numbers[0]); i++) {
printf("number no. %i: %p -- %p -- value: %d\n", i, &numbers[i], numbers+i, *(numbers+i));
}
printf("int size: %d\n", sizeof(int));
return 0;
}

Powyższy kod wyświetli nam w konsoli coś takiego:

numbers in general: 0x7ffc9f728f20 -- 0x7ffc9f728f20
number no. 0: 0x7ffc9f728f20 -- 0x7ffc9f728f20 -- value: 1
number no. 1: 0x7ffc9f728f24 -- 0x7ffc9f728f24 -- value: 2
number no. 2: 0x7ffc9f728f28 -- 0x7ffc9f728f28 -- value: 3
number no. 3: 0x7ffc9f728f2c -- 0x7ffc9f728f2c -- value: 4
int size: 4

Przeanalizujmy troszkę o cóż tu chodzi.

Zanim to zrobimy, zaznaczę tylko, że Ty, jeśli uruchomiłeś ten kod u siebie, mogłeś dostać nieco inne wyniki. To normalne.

Dla większości osób nieznajomych z C/C++ ten kod może wydawać się nieco kryptyczny, ale w gruncie rzeczy jest dość prosty.

# Co znaczy ten kd?

Zacznijmy może od linijki

printf("numbers in general: %p -- %p\n", &numbers, numbers+0);

Zakładam, że pierwsza część printa jest zrozumiała dla każdego, może poza %p - to po prostu nam mówi, że argument do wyprintowania będzie specyficznym typem danych.

# & - co to jest?

&numbers - znaczek & zaś mówi, że chcę otrzymać adres danej zmiennej - czyli jej lokację w pamięci. Bo jak dobrze wiemy, zmienne alokowane są w pamięci, w pewnym miejscu wybranym przez komputer. To miejsce zazwyczaj opisuje się jako 'adres' - czyli liczba bajtów od 'poczatku' pamięci, którą procesor musi 'przeskoczyć', by dotrzeć do danej zmiennej.

Nasza tablica (czyli taka jakby lista z Pythona, ale nie do końca), znajduje się pod adresem: 0x7ffc9f728f20 (zapis szesnastkowy), i jest to tym samym adres naszego pierwszego elementu.

Kompilator musi musi jednak wiedzieć, pod jakim adresem znajduje się następny element naszej tablicy. Skąd? Otóż prosta sprawa.

Zadeklarowaliśmy, że elementy naszej tablicy będą typu int. Typ int na komputerze, z którego korzystam, jest akurat 4 bajtowy, czyli 32 bitowy. Jest to w zasadzie standard, ale czasami są odstępstwa od reguły, zależnie od architektury, stąd też ten sizeof(int) w kodzie - zwraca on rozmiar danego typu w obecnym środowisku.

Dlatego też, jeśli 0x7ffc9f728f20 jest adresem pierwszego elementu, który zajmuje w pamięci 4 bajty o adresach: 0x7ffc9f728f20, 0x7ffc9f728f21, 0x7ffc9f728f22, 0x7ffc9f728f23, to możemy wnioskować, że następny element tej tablicy będzie po nim, pod adresem 0x7ffc9f728f24, czyli 4 bajty dalej. Następny znowu kolejne 4 bajty i tak dalej, aż do ostatniego elementu.

# Prosty wzór

Zatem adres konkretnego elementu tablicy można określić wzorem adres_pierwszego_elementu+(index*rozmiar_typu). Z takiego też wzoru korzysta komputer - za każdym razem, gdy piszesz numbers[index] kompilator tłumaczy to sobie wewnętrznie na *(numbers+(index*rozmiar_typu)). Co znaczy *? Nic innego, jak 'idź pod dany adres i weź wartość znajdującą się pod tym adresem.'

Zatem gdy napiszemy numbers[0], to nasz kompilator przetłumaczy to na *(0x7ffc9f728f20+0), czyli *(0x7ffc9f728f20), co z kolei znaczy: weź wartość z tego adresu i wstaw ją tutaj.

W przypadku np. numbers[1], będzie to *(0x7ffc9f728f20+(1*sizeof(int))) = *(0x7ffc9f728f20+(1*4)) = *(0x7ffc9f728f20+4), czyli *(0x7ffc9f728f24). Jasne? Jak dla mnie tak. Jeśli masz problem ze zrozumieniem tego konceptu, nie przejmuj się, wiele osób nie do końca rozumie wskaźniki, adres i pamięć. Ja też miałem z tym problem. Przynajmniej na początku.

Możesz wspomóc się filmikami Gynvaela czy wykładami z CS50 - kursu z Harvardu oni, jako osoby o znacznie większej wiedzy, tłumaczą całe zagadnienie znacznie lepiej niż ja.

# Jak by to wyglądało, gdybyśmy indeksowali od 1?

Załóżmy, że indeksujemy od 1. Wtedy wzór musiałby ulec modyfikacji - i wyglądałby on tak:

adrespierwszegoelementu+((index-1)*rozmiar_typu)

Innym rozwiązaniem byłoby przesunięcie lokacji pierwszego elementu tablicy o 4 bajty do przodu względem adresu samej tablicy, ale wtedy nasza tablica zajmowała by dodatkowe miejsce w pamięci i to niepotrzebnie, gdyż te pierwsze x bajtów, gdzie x to rozmiar danego typu danych, byłoby po prostu puste. To raz, dwa, że trzeba by pamiętać, że adres tablicy nie jest adresem pierwszego jej elementu.

Oba te rozwiązania są bezsensowne, bo o ile nie jest to niby dużo - kilka bajtów na każdej tablicy, czy jedna operacja odejmowania, to gdy pomnożymy to sobie przez ilość takich zmiennych, które mamy w pamięci, to już wyjdzie całkiem pokaźna sumka bajtów/operacji, które w istocie rzeczy, są zbędnie zajmowane.

Dodatkowo ileż kodu bazuje już na indeksowaniu od 0. Niemożliwym by było to wszystko zmienić.

Oczywiście, są również inne argumenty, by indeksować czy liczyć elementy od zera, jak chociażby te, głoszone przez Dijsktrę. To taki raczej znany i ważny Pan, dla tych, którzy nie kojarzą ;)

# Podsumowanie

Jak widać powyżej, indeksowanie od 1 jest nieco absurdalne, kiedy wiesz, jak proces dostępu do elementów tablicy wygląda od środka.

We wpisie użyłem troszkę 'uproszczeń', także za niedokładności z góry przepraszam.

Kto chce, może też wpaść na mojego bloga, podbić mi wyświetleń, o tutaj: https://grski.pl/indexing-from-zero.html
Albo na steemit, o tutaj: https://steemit.com/pl-artykuly/@grski/dlaczego-w-programowaniu-indeksujemy-od-zera

#programowanie #gruparatowaniapoziomu #informatyka
  • 6
Typ int na komputerze, z którego korzystam, jest akurat 4 bajtowy, czyli 32 bitowy. Jest to w zasadzie standard


@11001100110O11: akurat standard mówi co innego ( )

Wtedy wzór musiałby ulec modyfikacji - i wyglądałby on tak:

adres_pierwszego_elementu+((index-1)*rozmiartypu)


@11001100110O11: wzór by wyglądał tak samo, tylko zamiast adres_pierwszego_elementu(czy raczej zerowego) byłby adres_przed_pierwszym_elementem.
@MamCieNaHita: co do standardu, to wyczytałem:
"The exact size of integer types depends on the compiler. The de facto standard is

char is 8 bits
short is 16 bits
int is 16 bits on 16-bit systems, and 32 bits on both 32- and 64-bit systems
long may be either 32 or 64 bits"
https://stackoverflow.com/questions/3618011/c-what-is-the-default-length-of-an-int
ogółem tu o tym dywagują, ale raczej uważam, że napisałem poprawnie.
No i mi tutaj bardziej chodziło