Dodam coś od siebie.
C++ wywodzi się z C (ma wszystkie jego cechy), a ten jest językiem niskiego poziomu, którego zalicza się również do języków wysokopoziomowych. Chodzi o to, że można w C++ pisać na poziomie nie odbiegającym właściwie od assemblera. Można wykonać w tym języku kod, który będzie komunikował się za pomocą wywołań API, BIOS lub wprost za pomocą instrukcji maszynowych. W tym celu zawiera choćby wstawki asemblerowe pozwalające na komunikowanie się za pomocą niskopoziomowych rozkazów procesora (np. IN i OUT). W C++ nie zlikwidowano tego mechanizmu, a więc C++ jest również językiem niskopoziomowym. Finito.
Co do tablic.
Tablica, to byt wirtualny istniejący wyłącznie na poziomie kodu źródłowego. Oznacza to praktycznie tylko obszar w pamięci od adresu x, do y. Nazwa tablicy oznacza zawsze to samo co wskazanie na jej pierwszy element. Nie ma po prostu innej możliwości. W zasięgu deklaracji np. int x[10], nazwa x jest równoważnikiem &x[0]. (Jeżeli ktoś nie wierzy, to niech sobie przeanalizuje kod dowolnego kompilatora. Żeby było śmieszniej działa to również w zasięgu przeciążonego operatora [], co polecam sobie sprawdzić.)
Informacja ta i tak może zostać zignorowana ponieważ instrukcja wyrażenia x[15]; wykona się mimo, że określa ona formalnie element poza zadeklarowaną tablicą x. Dla kompilatora jest to przecież *(x+15), a z przemienności dodawania *(15+x);, a stąd to samo co 15[x]; Tak więc tablica, to wytwór naszej wyobraźni. Dla kompilatora C/C++ tablica jako to co rozumiemy pod jej określeniem nigdy nie istnieje (inaczej niż struktura/klasa).
Gdyby tablica nie była bytem wirtualnym, a jej nazwa jednocześnie równoważnikiem wskazania na pierwszy element (typu takiego jak wskazanie tego elementu), to instrukcja deklaracji: typedef char(*Code)[];
nie miałaby prawa skompilować się ponieważ w specyfikacji tej nie podano rozmiaru tablicy.
Druga sprawa: sizeof nie liczy rozmiaru tablic jako takich. Jeżeli argumentem tego operatora jest wyrażenie, to wynikiem jest rozmiar agregatu, który jest potrzebny do przechowania wyrażenia, które reprezentuje. Wynik jest w bajtach - nie w ilości elementów. Wobec tego (sizeof x) może dać wynik 20, 40, 80, albo nawet zupełnie inny na maszynach z egzotyczną długością słowa maszynowego. Druga postać sizeof też nie liczy formalnie długości tablic, a jedynie wyrażenie deklaracyjne np. sizeof (int[10]). I liczy ono rozmiar pamięci jaki przydzieliłby kompilator dla definicji takiego wyrażenia. Dla kompilatora nie ma czegoś takiego jak długość tablicy bo musiałaby to być informacja rozpatrywana w ilości elementów. A tak nie jest.
Trzecia sprawa. Skoro tablica to byt wirtualny, to można ją sobie wirtualnie wytworzyć przez konwersję czy to nazw zmiennych (obiektów) lub funkcji na wskazania i przekształcenie ich w twór będący funkcjonalnie tablicą. Na przykład w zasięgu deklaracji dwóch następujących po sobie funkcji f1(), f2() i f3() można stworzyć coś takiego:
int f1(int x)
{ //instrukcje do zamazania
(void)(x /= 10, --x); //cokolwiek byle zajęło trochę miejsca
return 20;
}
int f2(int x)
{
return ++x * 10;
}
int f3(void) {}
typedef char(*Code)[];
int main(void)
{
unsigned char NOP = '\0';
Code kod_f1 = (Code)&f1;
long rozmiar_f1 = ((char*)&f2 - (char*)&f1) - 1;
Code kod_f2 = (Code)&f1;
long rozmiar_f2 = ((char*)&f3 - (char*)&f2) - 1;
printf("Wynik: %d\n", f1(5));
assert (rozmiar_f1 >= rozmiar_f2);
int i;
for(i = 0; i < rozmiar_f2; i++)
(*kod_f1)[i] = (*kod_f2)[i];
printf("Wynik: %d\n", f1(5));
}
Programik ten przenosi kod f2 do f1. Kod obu funkcji f1() jak i f2() został potraktowany jako tablica znaków (bajtów). Można tak zrobić ponieważ tablica de facto nie istnieje. To tylko wysokopoziomowy skrót dla arytmetyki wskaźników. Wynikiem tego programiku jest 20 i 60.
Mało kto po prostu rozumie, że w C i C++ są dwa, zupełnie różne rodzaje wyrażeń - te służące do obliczeń oraz wyrażenia deklaracyjne. Te pierwsze podają konkretny wynik (i dodatkowo wydziela się z nich specjalny ich rodzaj czyli L-wyrażenia, które mogą być pierwszym argumentem dla operator=()).
Natomiast wyrażenia deklaracyjne pozwalają obliczyć typ wyniku.
W obu wypadkach operator taki jak [] reprezentuje tak naprawdę dwa zupełnie różne operatory o tym samym wyglądzie i pozornie podobnym zastosowaniu. W obu wypadkach stosuje się do nich podzbiór tej samej tablicy priorytetów. Najwyraźniej nie załapał tego również szanowny (bez podtekstów) moderator.
Wszystkie operatory mają ściśle zdefiniowane priorytety i wiązania. Nie są one w specyfikacji języka ot tak - dla jaj. A to, że nie wszystkie operatory są dopuszczone do tworzenia wyrażeń deklaracyjnych, to zupełnie inna bajka. Po to jest specyfikacja języka.
I tak na koniec. Tablice i ich fatalna specyfikacja jest jednym z powodów dla których ludzie powoli odchodzą od używania C i C++. W obecnych czasach trzeba być wariatem, żeby bawić się wielowymiarowymi tablicami typu C. Nie dość, że jest to piekielnie błędotwórcze, to jeszcze wybitnie nieczytelne, a wiele interpretacji kontrowersyjnych.
Znacznie lepiej do tego nadają się kolekcje zawarte w STL - choć i one są bardzo nieczytelne w użyciu.
Oczywiście kolejne aktualizacje języka dążą do lepszego sformalizowania tablic i tym samym zabezpieczenia się przed niewłaściwym (nieprzewidzianym) ich użyciem. Stąd kolejne interpretacje, które mają jakoby wyjaśnić nowe konstrukcje. Nie zmieni to jednak faktu, że jest to robione "na siłę". Nie polepszy to projektu języka C++. Język C miał spójną koncepcję prostego niskopoziomowego języka z wieloma aspektami języka wysokopoziomowego doskonale zastępującego assembler. Jednak C++, to potworek w którym Stroustrup próbował pożenić tę dość spójną koncepcję C z zupełnie inną koncepcją. Język odniósł sukces, to fakt - ale dużym kosztem i moim zdaniem z rozpędu - głównie jako wciąż wydajne rozwinięcie C. W C++ mieszają się interpretacje niskopoziomowe wywodzące się z C z interpretacjami wysokopoziomowymi, które próbują z wsuwanej ze smakiem acz marnej mordoklejki zrobić belgijskie czekoladki. ;-)
Jakiś czas temu stwierdziłem, że obecny C++ jest tak paskudnie nieczytelny, że najlepiej przerzucić się na pisanie w innym języku. Pisanie w C++, to teraz moim zdaniem zło konieczne.
Myślę iż przykład sporów w tym temacie jest charakterystyczny dla problemów z tym językiem.
Obecnie sądzę, że do pisania wydajnych i czytelnych programów znacznie lepiej używać tandemu C + Java niż C++.
Na koniec bo prawie zapomniałem o samym temacie:
int iA[2][3][4] =
{
{
{50,1,2,3},
{4,5,6,7},
{8,9,10,11}
},
{
{12,13,14,15},
{16,17,18,19},
{20,21,22,23}
}
};
typedef int T1[4];
typedef T1 T2[3];
typedef T2 T3[2];
Typ T3, to to samo co iA. Jest to dwuelementowa tablica (tablic T2). T2, to z kolei trójelementowa tablica (tablic T1), a ta ostatnia jest dopiero czteroelementową tablicą liczb całkowitych.
Dostać się do tej tablicy można zarówno korzystając ze wskazania do samej tablicy:
int (ptr_tab)[2][3][4] = &iA;
jak i korzystając z faktu, że iA jest wskazaniem do pierwszego elementu, którym jest int()[3][4].
int (*ptr_el)[3][4] = iA; //równoważne z przypisaniem &iA[0]
Różnica polega na użyciu operatora adresu w przypisaniu.
Dostęp do elementu o wartości 4:
(ptr_tab)[0][1][0];
lub (ptr_el)[1][0];
Do załapania wyrażeń tego typu trzeba pamiętać, że priorytet operatora[] jest wyższy niż operatora (), wiązanie [] jest lewe, a () prawe.
Można to oczywiście rozwijać wg zasady a[b] == *(a + b) == b[a], ale czytelność takiego rozwinięcia jest zerowa.