3 pytania związane z dynamiczną alokacją tablic wielowymiarowych

0

Dzień dobry.
Mam 3 pytania związane z dynamiczną alokacją tablic wielowymiarowych.

1. Co dokładnie oznaczają i czym różnią się zapisy wskaźników: int ***t , int **t, int *t? Bo wiem, że ten pierwszy używamy przy tablicach trójwymiarowych , drugiego przy dwuwymiarowych, trzeciego przy jednowymiarowych lub przy pojedynczych zmiennych. Ale, czy one się różnią i jak je odczytywać? Gdybym tworzył tablicę czterowymiarową to bym musiał najpierw zadeklarować int **** t i wtedy to by oznaczało, że deklaruję wskaźnik czterowymiarowy na inne wskaźniki ?

2. Tworząc tablicę trójwymiarową najpierw deklaruję wskaźnik int ***t (oto jak to nazwać pytam w poprzednim pytaniu). Teraz ten wskaźnik tworzy pierwszy wymiar tablicy i składa się z tablicy wskaźników wskazujących i zarazem tworzących drugi wymiar. Ten wskaźnik tworzący pierwszy wymiar wskazuje na tablicę wskaźników drugiego wymiaru t = new int ** [3]; (tak dokładnie to ten wskaźnik przechowuje adres pierwszej zmiennej tablicy wskaźników z drugiego wymiaru, czyli adres pierwszego wskaźnika drugiego wymiaru). Następnie każdy z tych wskaźników tworzących drugi wymiar wskazuje na zmienne w tworzące trzeci wymiar (np. t[i] = new int * [1] i- to pewien nr indeksu). I na koniec tworzymy zmienne trzeciego wymiaru t[i][j] = new int [4] -i,j - numery indeksów odpowiednio wymiaru pierwszego i drugiego.

Czy dobrze rozumiem?

3. Poniżej w listingu znajduje się napisany przeze mnie kod tworzenia tablicy trójwymiarowej. Czy jest on poprawny? Proszę pominąć na razie część oznaczoną jako " //przypisanie wartości trójwymiarowej tablicy " Tego dotyczyć będzie moje następne pytanie.

4. Proszę mi wyjaśnić jedną kwestię związaną z poniższym listingiem,. Jak widać dla trzeciej komórki I wymiaru (indeks 2), 2 komórki II wymiaru (indeks 1) mamy dostępną tylko 1 komórkę III wymiaru. W kodzie jest zapis t[2][1] = new int [1]; , co powinno oznaczać, że utworzona zostaje 1 komórka trzeciego wymiaru o indeksie 0 (komórka 1 wymiaru 2 z komórki 2 wymiaru 1).
Zapis t[x][y][z] oznacza: t-nazwa wskaźnika, x-komórka I wymiaru, y - komórka II wymiaru, a z - komórka III wymiaru.
Skoro tak to dlaczego, gdy wpiszę coś takiego:

//przypisanie wartości trójwymiarowej tablicy         // tu jest pewien problem*******************************
t[2][1][0]=555;
t[2][1][1]=888;
t[2][1][2]=345;

cout<<t[2][1][0]<<"\t"<<t[2][1][1]<<"\t"<<t[2][1][2]; 

To program wyświetla mi liczby 555 888 345 . Przecież ja przekroczyłem (celowo w celu sprawdzenia) wymiar w przypadku liczb 888 i 345.
Powinna zostać przypisana tylko liczba 555 indeksowy 0 z trzeciego wymiaru. Wymiar ten zawiera tylko 1 komórkę o indeksie 0, więc nie może zawierać komórek o indeksach 1 i 2.
To dlaczego kompilator utworzył komórki trzeciego wymiaru o indeksach 1 i 2 (t[2][1][1]=888; oraz t[2][1][2]=345; ), przypisał im wartości i jeszcze na dodatek można je wyświetlić,
skoro te komórki nie powinny istnieć? Czy chodzi oto, że te wartości zostały wstawione do grupy 4 komórek trzeciego wymiaru znajdujących się dalej, czyli do t[2][2] = new int [4]; //tworzymy trzeci wymiar (komórka 2 wymiaru 2 z komórki 2 wymiaru 1) ? Chodzi oto, że te komórki znajdują się za komórką t[2][1][0] i kompilator wpisał 888 i 345 odpowiednio do komórek t[2][2][0] i t[2][2][1], a ja odwołują się do t[2][1][1] i t[2][1][2] odwołuję się do t[2][2][0] i t[2][2][1]?

Listing

int ***t;

//wymiar I
t = new int ** [3];   // tworzymy pierwszy wymiar składający się z tablicy trzech wskaźników na wskaźniki drugiego wymiaru


//wymiar II
t[0] = new int * [1];  // tworzymy drugi wymiar (komórka 0 wymiaru 1) składający się z tablicy wskaźników na tablice zmiennych wymiaru trzeciego
t[1] = new int * [2];  // tworzymy drugi wymiar (komórka 1 wymiaru 1) składający się z tablicy wskaźników na tablice zmiennych wymiaru trzeciego
t[2] = new int * [3];  // tworzymy drugi wymiar (komórka 2 wymiaru 1) składający się z tablicy wskaźników na tablice zmiennych wymiaru trzeciego


//wymiar III
t[0][0] = new int [3];   //tworzymy trzeci wymiar (komórka 0 wymiaru 2 z komórki 0 wymiaru 1), czyli przypisujemy zmiennym z drugiego wymiaru zmienne tworzące trzeci wymiar

t[1][0] = new int [2];    //tworzymy trzeci wymiar (komórka 0 wymiaru 2 z komórki 1 wymiaru 1)
t[1][1] = new int [1];   //tworzymy trzeci wymiar (komórka 1 wymiaru 2 z komórki 1 wymiaru 1)

t[2][0] = new int [4];		//tworzymy trzeci wymiar (komórka 0 wymiaru 2 z komórki 2 wymiaru 1)
t[2][1] = new int [1];		//tworzymy trzeci wymiar (komórka 1 wymiaru 2 z komórki 2 wymiaru 1)
t[2][2] = new int [4];		//tworzymy trzeci wymiar (komórka 2 wymiaru 2 z komórki 2 wymiaru 1)




//przypisanie wartości trójwymiarowej tablicy         // tu jest pewien problem*******************************
t[2][1][0]=555;
t[2][1][1]=888;
t[2][1][2]=345;

cout<<t[2][1][0]<<"\t"<<t[2][1][1]<<"\t"<<t[2][1][2];


// zwalnianie pamięci;
delete [] t[0][0];  // usuwam komórki 3 wymiaru
delete [] t[1][0];  // usuwam komórki 3 wymiaru
delete [] t[1][1];  // usuwam komórki 3 wymiaru
delete [] t[2][0];  // usuwam komórki 3 wymiaru
delete [] t[2][1];  // usuwam komórki 3 wymiaru
delete [] t[2][2];  // usuwam komórki 3 wymiaru

delete [] t[0];  // usuwam komórki 2 wymiaru
delete [] t[1];  // usuwam komórki 2 wymiaru
delete [] t[2];  // usuwam komórki 2 wymiaru

delete [] t;  // usuwam komórki 1 wymiaru
0

Witaj

Gdybym tworzył tablicę czterowymiarową to bym musiał najpierw zadeklarować int **** t i wtedy to by oznaczało, że deklaruję wskaźnik czterowymiarowy na inne wskaźniki ?

Tak. Ilość gwiazdek determinuje ilość wymiarów.

Tworząc tablicę trójwymiarową najpierw deklaruję wskaźnik int ***t (oto jak to nazwać pytam w poprzednim pytaniu). Teraz ten wskaźnik tworzy pierwszy wymiar tablicy i składa się z tablicy wskaźników wskazujących i zarazem tworzących drugi wymiar. Ten wskaźnik tworzący pierwszy wymiar wskazuje na tablicę wskaźników drugiego wymiaru t = new int ** [3]; (tak dokładnie to ten wskaźnik przechowuje adres pierwszej zmiennej tablicy wskaźników z drugiego wymiaru, czyli adres pierwszego wskaźnika drugiego wymiaru). Następnie każdy z tych wskaźników tworzących drugi wymiar wskazuje na zmienne w tworzące trzeci wymiar (np. t[i] = new int * [1] i- to pewien nr indeksu). I na koniec tworzymy zmienne trzeciego wymiaru t[i][j] = new int [4] -i,j - numery indeksów odpowiednio wymiaru pierwszego i drugiego.

Czy dobrze rozumiem?

Nie do końca. Tworząc pierwszy wymiar jest to tablica wskaźników ale wskazują one na śmieci, a konkretnie na

int **

Jeżeli nie zrobisz inicjalizacji to na każdy wskaźnik do

 int **

...będą przypadały śmieci z pamięci.
Tak samo jest z trzecim wymiarem. tab[i][j] nie wskazuje na nic konkretnego dopóki nie utworzysz już konkretnego obiektu typu int.

W skrócie:

// Pierwszy wymiar równy 3;
int ***tab = new int**[3];

// Drugi wymiar równy 2;
for(int i=0;i<3;i++)
    tab[i] = new int*[2];

// I trzeci równy 4;
for(int i=0;i<3;i++)
    for(int j=0;j<2;j++)
        tab[i][j] = new int[4];

Dopiero w trzecim wymiarze tworzy się obiekty docelowego typu, w tym wypadku typu int.

//wymiar II
t[0] = new int * [1];  // tworzymy drugi wymiar (komórka 0 wymiaru 1) składający się z tablicy wskaźników na tablice zmiennych wymiaru trzeciego
t[1] = new int * [2];  // tworzymy drugi wymiar (komórka 1 wymiaru 1) składający się z tablicy wskaźników na tablice zmiennych wymiaru trzeciego
t[2] = new int * [3];  // tworzymy drugi wymiar (komórka 2 wymiaru 1) składający się z tablicy wskaźników na tablice zmiennych wymiaru trzeciego

Żle rozumiesz albo może ja źle odczytuję.
t[0] = new int*[1]; // tworzysz zerową komórkę drugiego wymiaru o rozmiarze 1;
t[1] = new int*[2]; // tworzysz pierwszą komórkę drugiego wymiaru o rozmiarze 2;
itd. Trochę to bez sensu, bo każda komórka drugiego wymiaru będzie miała o jeden wskaźnik więcej niż poprzednia :) Można tak robić, pewnie... ale nie wiem czy o to Ci tutaj chodziło.

Usuwanie jest następujące:

for(int i=0;i<3;i++){
    for(int j=0;j<2;j++)
        delete[] tab[i][j];
    delete[] tab[i];
}
delete[] tab;
tab = 0;

Najlepiej zrobić to pętlą po [wymiarze tablicy] - 1; Dlaczego? Dlatego, że tablice są niszczone przez operator delete[] rzędami / wymiarami, a nie pojedynczymi wskaźnikami.

Trochę dużo napisałeś tego tekstu :)

PS: @Dregorio - kiedyś używałem takich tablic do tworzenia węża. W grze w tablicy 3D pierwsza widoczna płaszczyzna to była plansza z wężem, a druga niewidoczna była ścieżką, po której wąż już chodził. Dzięki dwóm płaszczyznom 2D połączonym w 3D miałem historię ruchów mojego węża :)

0

No dobrze rozumiem.
Ale proszę mi powiedzieć:

  1. Co dokładnie oznaczają i czym różnią się zapisy wskaźników: int ***t , int **t, int *t? Jak mam nazywać/odczytywać te zapisy? Taki zapisint ***t mam nazywać wskaźnikiem trójwymiarowym wskazującym na wskaźniki drugiego wymiaru (tablicę wskaźników drugiego wymiaru), a int **t taki dwuwymiarowym wskaźnikiem wskazującym na wskaźniki pierwszego wymiaru (na tablicę wskaźników pierwszego wymiaru),natomiast zapis int *tma oznaczać wskaźnik jednowymiarowy wskazujący na zmienne (tablicę zmiennych)?

  2. I jeszcze chciałbym prosić o wyjaśnienie mi dlaczego w wyniku uruchomienia kodu zawartego w listingu wyświetlane są komórki tablicy, które wykraczają poza rozmiar danej grupy, czyli które nie powinny istnieć ? Jak widać dla trzeciej komórki I wymiaru (indeks 2), 2 komórki II wymiaru (indeks 1) mamy dostępną tylko 1 komórkę III wymiaru. W kodzie jest zapis t[2][1] = new int [1]; , co powinno oznaczać, że utworzona zostaje 1 komórka trzeciego wymiaru o indeksie 0 (komórka 1 wymiaru 2 z komórki 2 wymiaru 1).
    Zapis t[x][y][z] oznacza: t-nazwa wskaźnika, x-komórka I wymiaru, y - komórka II wymiaru, a z - komórka III wymiaru.
    Skoro tak to dlaczego, gdy wpiszę coś takiego:

//przypisanie wartości trójwymiarowej tablicy         // tu jest pewien problem*******************************
t[2][1][0]=555;
t[2][1][1]=888;
t[2][1][2]=345;

cout<<t[2][1][0]<<"\t"<<t[2][1][1]<<"\t"<<t[2][1][2]; 

To program wyświetla mi liczby 555 888 345 . Przecież ja przekroczyłem (celowo w celu sprawdzenia) wymiar w przypadku liczb 888 i 345.
Powinna zostać przypisana tylko liczba 555 indeksowy 0 z trzeciego wymiaru. Wymiar ten zawiera tylko 1 komórkę o indeksie 0, więc nie może zawierać komórek o indeksach 1 i 2.
To dlaczego kompilator utworzył komórki trzeciego wymiaru o indeksach 1 i 2 (t[2][1][1]=888; oraz t[2][1][2]=345; ), przypisał im wartości i jeszcze na dodatek można je wyświetlić,
skoro te komórki nie powinny istnieć? Czy chodzi oto, że te wartości zostały wstawione do grupy 4 komórek trzeciego wymiaru znajdujących się dalej, czyli do t[2][2] = new int [4]; //tworzymy trzeci wymiar (komórka 2 wymiaru 2 z komórki 2 wymiaru 1) ? Chodzi oto, że te komórki znajdują się za komórką t[2][1][0] i kompilator wpisał 888 i 345 odpowiednio do komórek t[2][2][0] i t[2][2][1], a ja odwołują się do t[2][1][1] i t[2][1][2] odwołuję się do t[2][2][0] i t[2][2][1]?

0
  1. Co dokładnie oznaczają i czym różnią się zapisy wskaźników: int ***t , int **t, int *t? Jak mam nazywać/odczytywać te zapisy? Taki zapisint ***t mam nazywać wskaźnikiem trójwymiarowym wskazującym na wskaźniki drugiego wymiaru (tablicę wskaźników drugiego wymiaru), a int **t taki dwuwymiarowym wskaźnikiem wskazującym na wskaźniki pierwszego wymiaru (na tablicę wskaźników pierwszego wymiaru),natomiast zapis int *tma oznaczać wskaźnik jednowymiarowy wskazujący na zmienne (tablicę zmiennych)?

Potrójny wskaźnik, podwójny wskaźnik, wskaźnik. Na studiach mówiliśmy na to jeszcze "wskaźnik na wskaźnik na wskaźnik".

  1. I jeszcze chciałbym prosić o wyjaśnienie mi dlaczego w wyniku uruchomienia kodu zawartego w listingu wyświetlane są komórki tablicy, które wykraczają poza rozmiar danej grupy, czyli które nie powinny istnieć ? Jak widać dla trzeciej komórki I wymiaru (indeks 2), 2 komórki II wymiaru (indeks 1) mamy dostępną tylko 1 komórkę III wymiaru. W kodzie jest zapis t[2][1] = new int [1]; , co powinno oznaczać, że utworzona zostaje 1 komórka trzeciego wymiaru...

Wiesz w ogóle jak wygląda tablica, którą utworzyłeś?
Popatrz:

Wymiar pierwszy:
[0][1][2];

Wymiar drugi:
t[0] = [0];
t[1] = [0][1];
t[2] = [0][1][2];

Wymiar trzeci;
t[0][0] = [0][1][2];
t[1][0] = [0][1];
t[1][1] = [0];

t[2][0] = [0][1][2][3];
t[2][1] = [0];
t[2][2] = [0][1][2][3];

C++ to nie Java :) Kompilator nie sprawdza tego czy przekroczyłeś długość tablicy. Przekroczyłeś ją i mażesz po pamięci albo zajętej już przez tablicę albo niezaalokowanej.
Popatrz na to:

int main(){
    int **t = new int *[2];

    t[0] = new int[1];
    t[1] = new int[2];


    t[0][0] = 3;
    t[1][0] = 1;
    t[1][1] = 2;

    delete[] t[0];
    delete[] t[1];
    delete[] t;
    t=0;
    return 0;
}

Tutaj indeksy tablicy są dobrze zaadresowane. Co wyrzuca drmemory?:

NO ERRORS FOUND:
      0 unique,     0 total unaddressable access(es)
      0 unique,     0 total uninitialized access(es)
      0 unique,     0 total invalid heap argument(s)
      0 unique,     0 total GDI usage error(s)
      0 unique,     0 total handle leak(s)
      0 unique,     0 total warning(s)
      0 unique,     0 total,      0 byte(s) of leak(s)
      0 unique,     0 total,      0 byte(s) of possible leak(s)

Zamieńmy teraz t[0][0] = 3 na t[0][1] = 3, czyli zainicjalizujmy nieistniejący indeks.
Proszę:

ERRORS FOUND:
      1 unique,     1 total unaddressable access(es)
      0 unique,     0 total uninitialized access(es)
      0 unique,     0 total invalid heap argument(s)
      0 unique,     0 total GDI usage error(s)
      0 unique,     0 total handle leak(s)
      0 unique,     0 total warning(s)
      0 unique,     0 total,      0 byte(s) of leak(s)
      0 unique,     0 total,      0 byte(s) of possible leak(s)

Pierwsza linijka błędów mówi wszystko :)

0

Rozumiem dziękuję za odpowiedź.
Mam jeszcze cztery pytania związane ze wskaźnikami na funkcje:

  1. Do czego wykorzystuje się wskaźniki na funkcje?
    Potrafię je utworzyć (przykład poniżej), ale jakie mają one zastosowania?
 int funkcja (int a, float b)             // funkcja, na którą ma wskazywać wskaźnik
{
cout<<a<<"\t"<<b<<endl;
return a+b;
}

int main ()
{
int (*wskaznik) (int, float); // deklarujemy wskaźnik na funkcję lub jak ktoś woli tworzymy wskaźnik wskazujący na funkcję przyjmujący 2 argumenty, z których pierwszy to int, a drugi to float

wskaznik = funkcja;   // przypisujemy utworzonemu wskaźnikowi adres funkcji (od tego momentu wskaźnik zaczyna wskazywać na funkcję o nazwie funkcja i zmienna wskaznik przechowuje od tego momentu adres funkcji)

cout<<wskaznik (5,6);    // wywołanie funkcji, na którą wskazuje wskaźnik przez cout dla argumentów 5 i 6

return 0;
}
  1. Dlaczego gdy przypisuję wskaźnikowi na funkcję adres funkcji to mogę napisać wskaznik = funkcja; lub wskaznik = &funkcja; i oba zapisy działają tak samo, czyli przypisują wskaźnikowi adres funkcji? Najbardziej dziwi mnie czemu działa zapis wskaznik = funkcja; ?

  2. Mam taki kod:

#include <iostream>


using namespace std;

int funkcja (int a, float b)             // funkcja, na którą ma wskazywać wskaźnik
{
cout<<"funkcja: "<<a<<"\t"<<b<<endl;
return a+b;
}

int ff(int x, float y)
{
	cout<<"\rff:  "<<x<<" "<<y<<endl;
	return 0;
}


int main ()
{

	int (*wskaznik) (int, float); // deklarujemy wskaźnik na funkcję lub jak ktoś woli tworzymy wskaźnik wskazujący na funkcję przyjmujący 2 argumenty, z których pierwszy to int, a drugi to float

	wskaznik = funkcja;   // przypisujemy utworzonemu wskaźnikowi adres funkcji (od tego momentu wskaźnik zaczyna wskazywać na funkcję o nazwie funkcja i zmienna wskaznik przechowuje od tego momentu adres funkcji)

	cout<<wskaznik (5,6)<<"\t zawartośc zmiennej wskaznik: "<<wskaznik<<endl;    // wywołanie funkcji funkcja(), na którą wskazuje wskaźnik przez cout dla argumentów 5 i 6  oraz wyświetlenie zawartości zmiennej wskaźnik


	wskaznik = &ff;  // przypisanie wskaźnikowi adresu funkcji ff()

	cout<<ff(7,8)<<"\t zawartośc zmiennej wskaznik: "<<wskaznik<<endl;


	cout<<"\r\rZawartość zmiennych funkcja i ff : "<< funkcja <<"\t"<< ff ;      // wyświetla zawartość zmiennych funkcja i ff .


	return 0;
}

Wynik uruchomienia kodu:

 funkcja: 5	6
11	 zawartośc zmiennej wskaznik: 1

ff:  7 8
0	 zawartośc zmiennej wskaznik: 1


Zawartość zmiennych funkcja i ff : 1	1

I teraz moje pytanie dlaczego w obu przypadkach zawartość zmiennej wskaźnikowej, która w pierwszym przypadku powinna przechowywać adres funkcji funkcja(), a w drugim przypadku adres funkcji ff() są takie same? No, adresy dwóch różnych funkcji chyba muszą być różne, takie same chyba być nie mogą. To dlaczego tutaj są?
Wskaźnik wg, a dokładnie zmienna wskaźnikowa (u nas o nazwie wskaznik) powinna przechowywać adres tego czegoś na co wskaźnik wskazuje. Tutaj wskaźnik wskazuje na funkcję, wobec czego chyba wskaźnik powinien przechowywać adres funkcji, na którą wskazuje.

  1. Co przechowuje "zmienna" o nazwie funkcji? Tak jak widać w powyższym kodzie na końcu wpisałem cout<<"\r\rZawartość zmiennych funkcja i ff : "<< funkcja <<"\t"<< ff ; , w wyniku czego program zwrócił wynik: Zawartość zmiennych funkcja i ff : 1 1 ? To w takim razie co przechowują zmienne, których nazwy są identyczne z nazwami funkcji? Ich adresy? Tylko, że problem tutaj jest taki, że te wartości, które one przechowują są takie same i równe 1. Na adres to raczej nie wygląda? To w takim razie cóż to jest?
0
  1. Do czego wykorzystuje się wskaźniki na funkcje?

Aktualnie w C++ są klasy i nie ma potrzeby tworzenia wskaźników na funkcje. Tzn... ja nie miałem potrzeby. W klasycznym C, żeby utworzyć sobie pseudo-obiektowość można potworzyć funkcje poza strukturą, a później potworzyć wskaźniki do nich jako obiekty w strukturze. Wyjdzie coś podobnego do klasy z publicznymi funkcjami. Gdzieś jeszcze czytałem, że w klasycznym C nie można było przekazywać funkcji jako argumentu innej funkcji - stąd potrzeba użycia wskaźnika.

  1. Dlaczego gdy przypisuję wskaźnikowi na funkcję adres funkcji to mogę napisać wskaznik = funkcja; lub wskaznik = &funkcja; i oba zapisy działają tak samo, czyli przypisują wskaźnikowi adres funkcji? Najbardziej dziwi mnie czemu działa zapis wskaznik = funkcja; ?

Z tego samego powodu, dla którego tablicę zawsze przekazuje się przez wskaźnik. To samo dotyczy funkcji. Funkcję zawsze przekazuje się przez wskaźnik przez co jej nazwa jest jednocześnie jej adresem. Pisząc ampersand odwołujesz się jawnie do adresu funkcji - nie jest to konieczne.

3 i 4...

Napisz tak jak niżej, a zobaczysz, że są to dwa różne adresy czyli dwie funkcje.

cout<<wskaznik(5,6)<<"\t zawartośc zmiennej wskaznik: "<<(void*)wskaznik<<endl;
wskaznik = &ff;  // przypisanie wskaźnikowi adresu funkcji ff()
cout<<wskaznik(7,8)<<"\t zawartośc zmiennej wskaznik: "<<(void*)wskaznik<<endl;

Rzutowanie na wskaźnik do void jest konieczne przy operatorze << jeżeli chcesz dostać adres funkcji. Czytałem kiedyś, że ma to związek z przeładowaniem operatora przesunięcia bitowego << itd, ale nie jestem pewny na 100%. Może mnie ktoś poprawi, bo nie chce mi się szukać teraz.

1 użytkowników online, w tym zalogowanych: 0, gości: 1