• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

[CR] C++ (#10): Wskaźniki. Dynamiczne alokowanie pamięci

VPS Starter Arubacloud
+2 głosów
8,696 wizyt
pytanie zadane 9 kwietnia 2016 w Nasze poradniki przez Mirosław Zelent Nałogowiec (34,750 p.)

CR = Code Review. O co chodzi? Zajrzyj tutaj
Pełna lista wszystkich Code Review? Zajrzyj tutaj

https://www.youtube.com/watch?v=0DQl74alJzw

Kod #1 z odcinka:

#include <iostream>

using namespace std;

int ile;

int main()
{
    cout << "Ile liczb w tablicy: ";
    cin>>ile;

    //dynamiczna alokacja tablicy
    int *tablica;
    tablica=new int [ile];

    //pokaz kolejne adresy komorek w tablicy
    for (int i=0; i<ile; i++)
    {
        cout<<(int)tablica<<endl;
        tablica++;
    }

    delete [] tablica;
    tablica=NULL;


    return 0;
}

Kod #2 z odcinka:

#include <iostream>
#include <time.h>
#include <cstdlib>

using namespace std;

int ile;
clock_t start, stop;
double czas;

int main()
{
    cout << "Ile liczb w tablicy: ";
    cin>>ile;

    //dynamiczna alokacja tablicy
    int *tablica;
    tablica=new int [ile];

    //zacznij odliczac czas
    start = clock();
    //wczytywanie liczb do tablicy
    for (int i=0; i<ile; i++)
    {
        tablica[i]=i;
        tablica[i]+=50;
    }
    stop = clock();
    czas = (double)(stop - start) / CLOCKS_PER_SEC;
    cout<<"Czas zapisu (bez wskaznika): "<<czas<<" s"<<endl;

    delete [] tablica;

    //ponowna alokacja tablicy
     tablica=new int [ile];
	 int *wskaznik=tablica;

    //zacznij odliczac czas
    start = clock();
    //wczytywanie liczb do tablicy
    for (int i=0; i<ile; i++)
    {
        *wskaznik = i;
        *wskaznik+=50;
        wskaznik++;
    }
    stop = clock();
    czas = (double)(stop - start) / CLOCKS_PER_SEC;
    cout<<"Czas zapisu (ze wskaznikiem): "<<czas<<" s";

    delete [] tablica;

    return 0;
}

Kod #3 z odcinka:

#include <iostream>

using namespace std;

float srednia(float *tab, int ile)
{
    float suma=0;
    for (int i=0; i<ile; i++)
    {
        suma+=*tab;
		*tab=999;
        tab++;
    }
    return suma/ile;
}

int main()
{

    float tablica[3];
    tablica[0] = 1.5;
    tablica[1] = 2.3;
    tablica[2] = 0.75;

    cout<<"Srednia wynosi: "<<srednia(tablica, 3);

    cout<<endl<<tablica[0];
    cout<<endl<<tablica[1];
    cout<<endl<<tablica[2];

    return 0;
}

Kod #4 z odcinka:

#include <iostream>

using namespace std;

float srednia(float &a, float &b, float &c)
{
    return (a+b+c)/3;
}

int main()
{

    float a,b,c;
    a = 1.5;
    b = 2.3;
    c = 0.75;

    cout<<"Srednia wynosi: "<<srednia(a,b,c);


    return 0;
}

 

Paczka z odcinka: POBIERZ​

komentarz 10 czerwca 2016 przez efiku Szeryf (75,160 p.)

3 odpowiedzi

+9 głosów
odpowiedź 9 kwietnia 2016 przez Sebastian Fojcik Nałogowiec (43,020 p.)
edycja 14 maja 2016 przez Sebastian Fojcik

Program #1

Problem: crash programu w Visual Studio.

Przyczyna: nieprawidłowe napisanie programu.
Krótkie wyjaśnienie: Do operatora  delete[] można przekazać tylko wartość otrzymaną od new[], a nie inkrementowaną ileś tam razy.
Długie wyjaśnienie: Każda tablica jest jednocześnie wskaźnikiem, który pokazuje na dany obszar pamięci. Święta zasada tablic, którą każdy potrafi wyrecytować: "Nazwa tablicy jest jednocześnie wskaźnikiem na jej zerowy element". Po inkrementowaniu nazwy tablicy ta zasada przestaje obowiązywać! Nie należy uczyć, zachęcać ani pokazywać, że można inkrementować tablicę samą w sobie. Bo jak pokazuje powyższy kod, wszystko się kompiluje, program działa, do momentu wywołania delete[].
Zamiast tego proponuję uczyć od początku dobrych praktyk. Chcemy poruszać się po tablicy? Używajmy nawiasów [] albo ustawmy na tablicę wskaźnik i to jego przesuwajmy!

Krąży też opinia wśród użytkowników forum stackoverflow, że przekazywanie do delete[] przesuniętego wskaźnika (tak jak w powyższym kursie) nie zwalnia w ogóle pamięci. Jednak nigdzie nie znalazłem wiarygodnych źródeł na potwierdzenie tego.
(Jak ktoś ma Code::Blocks, to może zarezerwować tablicę na 1 GB RAM, przesunąć wskaźnik na ostatni element i zwolnić pamięć patrząc na systemowy menedżer zadań -> wydajność i napisać czy pamięć się zwolniła. Sam jestem ciekawy)

Teraz 2 ciekawe przykłady:

Tutaj wszystko działa.

const char * tablica = "Ala ma kota";
tablica++;

Ale tutaj będzie błąd kompilacji.

char tablica[] = "Ala ma kota";
tablica++;

Będzie błąd, bo kompilator trafia na zapis z nawiasami [] i wie, że to będzie tablica. Jako, iż jest to tablica, to nie pozwoli jej w ten sposób inkrementować. W pierwszym przykładzie nie może nam tego zabronić, bo jak... miałby uniemożliwić przesuwanie wskaźników w ogóle? Tak więc pozostaje tylko zdrowy rozsądek programisty albo...

Użycie stałego wskaźnika. (nikt tak nie robi, to tylko ciekawostka)

const char * const tablica = "Ala ma kota";

Wiem jednak, że stałe wskaźniki oraz wskaźniki na stałe wykraczają poza materiał tego odcinka. Wszystkie tablice utworzone jawnie w kodzie:
int tablica[ 100 ]; są domyślnie właśnie stałymi wskaźnikami.

Powtórzę więc: nie należy inkrementować samej tablicy, bo to prowadzi do poważnych problemów w przyszłości. Zamiast tego należy ustawić wskaźnik na tablicę i to jego inkrementować.

Rozwiązanie problemu: inkrementować wskaźnik pokazujący na tablicę, a nie samą tablicę, przekazać do delete[] prawidłową wartość.
Poprawiony kod: (Program #1 - poprawiony)

#include <iostream>

using namespace std;

int ile;

int main()
{
    cout << "Ile liczb w tablicy: ";
    cin >> ile;

    //dynamiczna alokacja tablicy
    int *tablica;
    tablica = new int[ ile ];
    int * wskaznik = tablica;

    //pokaz kolejne adresy komorek w tablicy
    for( int i = 0; i<ile; i++ )
    {
        cout << (int)wskaznik << endl;
        wskaznik++;
    }

    delete[] tablica;
    wskaznik = NULL;

    return 0;
}

PS. Aby program #1 nie crashował można też (po wcześniejszym inkrementowaniu tab++) cofnąć się znowu do początku (tab--) i wtedy delete[] zadziała. (haniebne rozwiązanie)

 

Program #3

W programie #3 widzimy podobną, do opisywanej wcześniej, złą praktykę inkrementacji tablicy. W tym przypadku nie jest to groźne, bo wskaźnik na pierwszy element tablicy przekazywany jest do funkcji przez wartość, więc zmiana jego położenia (tab++) nie wpłynie w żaden sposób na zmianę położenia oryginalnego wskaźnika tablicy.
Tutaj nie trzeba koniecznie zmieniać kodu (choć powinien być napisany poprawniej). Zamiast zapamiętywać, kiedy tablicę można inkrementować , a kiedy nie, ja proponuję zasadę:

Inkrementujmy tylko wskaźnik pokazujący na tablicę — nigdy samą tablicę.

Btw. Co ciekawe, w drugim programie z odcinka (#2) Autor zastosował już prawidłową iterację tablicy w oparciu o dodatkowy wskaźnik. Gdyby tego nie zrobił, wystąpiłby problem z programu pierwszego (crash).

Pozdrawiam.

komentarz 9 czerwca 2016 przez draghan VIP (106,230 p.)

A więc, MIT POTWIERDZONY. [Dubgron]

Dziękuję Dubgron za zainteresowanie! ​Właśnie o taka formę sprawdzenia mi chodziło. [Sebastian Fojcik]

Panowie, przecież to sprawdzanie nie ma żadnego sensu... Skoro zwalnianie pamięci nieprzydzielonej przez OS daje undefined-behavior, to efekt działania programu będzie się różnił na każdej platformie, a nawet (i przede wszystkim) w ramach jednej platformy a różnych wersji kompilatora. Dla kontrastu podam wynik na mojej konfiguracji, GCC 6.1.1, Linux 3.13.0-37.

Może powtórzę raz jeszcze: próbowanie udowodnienia pewnego zachowania dla programu z UB ma taki sam sens, jak powiedzieć że przecież na świecie musi być większość Polaków, skoro dookoła nie ma nie-Polaków.


Taka sytuacja ma miejsce w kursie i takie działanie jest przepuszczane w Code::Blocks. Visual Studio zakończy działanie aplikacji z błędem pokazanym na obrazku.

Chciałbym zauważyć, że nie od IDE to zależy, a od kompilatora. Mało tego, przecież VS również "przepuszcza" taki program - zostaje on skompilowany i zbudowany, daje się uruchomić, a błąd objawia się w runtime.

Program z odcinka nie zwalnia pamięci

Kwestia bezdyskusyjna - a przynajmniej to, że nie zwalnia jej prawidłowo.

Wyciek pamięci, to bardzo poważne niedociągnięcie i wszyscy zdajemy sobie z tego sprawę. Częściową winę za to ponosi kompilator używany w kursie, który przepuszcza takie błędy.

Wyciek pamięci i owszem - jest groźny. Jednak zauważ, że w danej aplikacji nie ma to żadnych konsekwencji (przynajmniej na współczesnych systemach operacyjnych). Program nic więcej do wykonania nie ma, zwalnianie pamięci dzieje się na końcu programu. Jeśli do niego nie dojdzie, to po zakończeniu działania programu, GC systemu operacyjnego i tak usunie pamięć przydzieloną dla programu. Wycieki pamięci są zjawiskiem wysoce niepożądanym (we wszystkich, ale) przede wszystkim w aplikacjach, które działają długodystansowo i zajmują coraz to nowe obszary pamięci bez zwalniania. Kiedy zakończysz życie takiego programu, to cała zajęta przez niego pamięć wraca do systemu.

Z tego co pamiętam, to w Code::Blocks (albo Dev-C++) kompilator przepuszczał również taki zapis:

int n;
cin >> n;
int tab[ n ];

I zaś kwestia rozróżnienia kompilatora od IDE. Czy kompilujesz to z pomocą C::B, czy DevCpp, czy Eclipse, czy VS, czy CLion... To nie ma znaczenia. To o czym mówisz - jeśli kompilujesz to w standardzie starszym, niż C++14 - to jest jedno z rozszerzeń GCC. Zaś runtime-sized arrays jest w standardzie C++ od C++14. - ten draft nie został ostatecznie wprowadzony

komentarz 9 czerwca 2016 przez Sebastian Fojcik Nałogowiec (43,020 p.)

No po prostu musiałeś się na siłę przyczepić do wszystkiego... -_-

1. Jeden przypadek niezwolnienia pamięci, który zaobserwował kolega oznacza, że pamięć może (ale nie musi) nie być zwolniona. I tyle. Jeśli jeden na milion przypadków zwolni Ci się pamięć, to nie powód, by podważać moje zdanie :)

2. Nie, nie mylę środowiska od kompilatora. MSVC to kompilator używany w środowisku Visual Studio i sama nazwa środowiska mówi sama za siebie o jaki kompilator chodzi w tym przypadku. Visual Studio, to Visual Studio, nie będę się rozdrabniał na kompilatory i ich wersję... nie ma tutaj takiej potrzeby. Zaraz napiszesz, że można na siłę wgrać inny kompilator do Visual Studio i wtedy nazwa środowiska nie wyznacza kompilatora... no bez przesady ;-)
Zwróciłeś uwagę na to, że użyłem słowa "przepuszcza" mimo tego, że kod się kompiluje i dodajesz, że błąd objawia się w runtime... No tak... więc moje stwierdzenie, że środowisko Visual Studio nie przepuszcza takiego błędu jest błędne :>

3. W kolejnym akapicie próbujesz mnie przekonać, że wyciek pamięci w prostych aplikacjach nie ma znaczenia ._.
IMO w kursie błędów nie powinno być żadnych. Nie musiałeś wyjaśniać, że po zakończeniu działania aplikacji, pamięć wraca do systemu. To chyba oczywiste  :-)

4. I znowu konflikt środowisko-kompilator. Oczywiście jestem świadomy, że Ty draghanie potrafisz sobie do Code::Blocksa podpiąć kompilator Javy, ale ja pisałem o kompilatorach standardowo dołączanych razem ze środowiskiem. W przypadku C::B jest to GCC, Dev-C++ może coś innego... nie wiem.
[...]kompilator przepuszczał również taki zapis:[...]
Nigdzie nie napisałem, że kompiluję za pomocą C::B :/

Zwróciłem uwagę, że taki kod udało mi się kiedyś skompilować. Tematu dalej nie badałem, przyczyn też nie znałem. Dobrze, że wyjaśniłeś dlaczego GCC tak działa. Swoją drogą, to nigdy nie przestawiałem żadnych opcji jak użytkowałem C::B i nawet nie wiedziałbym jak kompilować w najnowszym standardzie języka. Dlaczego standardowo zaraz po uruchomieniu środowiska nie kompilujemy w najnowszym wspieranym standardzie — tego nie jestem w stanie zrozumieć.
Ale naprawdę draghanie, masz za dużo czasu wolnego :-)

komentarz 9 czerwca 2016 przez draghan VIP (106,230 p.)

No po prostu musiałeś się na siłę przyczepić do wszystkiego... -_-

Skoro to rewizja kodu, to należy podać rzetelne wytłumaczenia. Nic na siłę. Spójrz jeszcze raz na mój ostatni komentarz, bo był błędny (część z rsa), za co przepraszam.

Może jeszcze raz, żeby była jasność. W programie występuje UB. Oznacza to błędnie napisany kod. Zaś skutki UB są nieprzewidywalne z definicji. O tym cały czas piszę. Raz coś się zwolni, acz wcale nie musi. Testy są tutaj zbędne i nie wnoszą nic użytecznego.

Nie przekonuję Cię ani nikogo, że wycieki pamięci są dobre i fajne. Każdy programista wie, że jest na odwrót. Mówię tylko tyle, że jeśli piszemy aplikację na współczesny desktopowy system operacyjny, to pamięć tak czy siak zostanie oddana systemowi po zabiciu programu, zaś wycieki są wyjątkowo wredne w aplikacjach long-running.

O IDE nie będę się rozpisywał, bo to rewizja kodu - ale sygnalizuję, że źle pojąłeś moją intencję. W razie chęci wyjaśnień, zapraszam na PW lub do osobnego wątku.

Dlaczego standardowo zaraz po uruchomieniu środowiska nie kompilujemy w najnowszym wspieranym standardzie — tego nie jestem w stanie zrozumieć.

W szóstej wersji GCC domyślną wersją standardu jest C++14. :) A dlaczego we wcześniejszych nie była włączona domyślnie flaga nowszego standardu... nie wiem. Zapewne przez wzgląd na kompatybilność wsteczną. Zgaduję.

komentarz 13 sierpnia 2021 przez wiaziu Nowicjusz (100 p.)

@Sebastian Fojcik, 

Z tego co pamiętam, to w Code::Blocks (albo Dev-C++) kompilator przepuszczał również taki zapis:

 

int n;
cin >> n;
int tab[ n ];

Toż to rozbój w biały dzień! Jak ktoś nie wierzy, to niech sprawdzi ;-)

Co ciekawe w lekcji o tablicach zdefiniowałem to w podobny sposób i działało dobrze (Code::Blocks). Czy mógłbyś (ewentualnie inna dobra dusza) mi odpowiedzieć dlaczego to jest rozbój? Wtedy myślałem, że jestem sprytny ;-)

komentarz 19 sierpnia 2021 przez draghan VIP (106,230 p.)

Rozmiar tablicy na stosie musi być stały i znany w czasie kompilacji, a `n` nie jest ani stałe ani jego wartość nie jest znana w czasie kompilacji.

Więcej szczegółów (prawdopodobnie więcej niż by się człowiek spodziewał) tutaj.

0 głosów
odpowiedź 14 grudnia 2017 przez dzem Nowicjusz (180 p.)

Mam pytanie odnośnie kodu #2

Nie rozumiem w jakim celu w drugiej pętli (z użyciem wskaźnika) znajduje się linijka "wskaznik++".

Aby zrozumieć co się dokładnie dzieje w trakcie wykonywania pętli dodałem odpowiednie cout'y. Dzięki temu widzę, że "wskaznik++" sprawia, iż zawartość zmiennej w drugiej pętli jest zawsze o 1 większa niż jej odpowiednik w pętli pierwszej. Po usunięciu tej linii program nadal działa i obie pętle wykonują dokładnie te same obliczenia, a chyba właśnie to miały robić, aby rzetelnie porównać czasy wykonywania pętli. Więc czemu ta linia służy?

Dodatkowo zawartość zmiennej po wykonaniu "wskaznik++" po raz ostatni daje różne wyniki. Raz jest to 0, raz 6292376, innym razem 6292632, jeszcze innym 1885417061. Tymczasem powinien on wynosić 60 (sprawdzałem to wpisując za każdym razem 10 jako "Ile liczb w tablicy"). Wie ktoś dlaczego tak się dzieje?

PS
Korzystam z Xcode

PS 2
Jeśli niedokładnie nazwałem jakieś części kodu opisując problem, to proszę od razu o poprawienie mnie ;)

 

#include <iostream>
#include <time.h>
#include <cstdlib>

using namespace std;

int ile;
clock_t start, stop;
double czas;

int main()
{
    cout << "Ile liczb w tablicy: ";
    cin>>ile;
    
    int *tablica;
    tablica = new int [ile];
   
    start=clock();
    for (int i=0; i<ile; i++)
    {
        tablica[i]=i;
        //cout<<"Zawartosc szuflady "<<i<<": "<<tablica[i]<<endl;
        tablica[i]+=50;
        //cout<<"Zawartosc szuflady "<<i<<" po +=50: "<<tablica[i]<<endl;
    }
    stop=clock();
    czas=(double)(stop-start) / CLOCKS_PER_SEC;
    cout<<"Czas zapisu (bez wskaznika): "<<czas<<endl;
    
    delete [] tablica;
    
    int *wskaznik = tablica;
    tablica = new int [ile];
    
    start = clock();
    
    for (int i=0; i<ile; i++)
    {
        *wskaznik = i;
        //cout<<"Zawartosc szuflady "<<i<<": "<<*wskaznik<<endl;
        *wskaznik+=50;
        //cout<<"Zawartosc szuflady "<<i<<" po +=50: "<<*wskaznik<<endl;
        wskaznik++; //po co to jest?
        //cout<<"Zawartosc szuflady "<<i<<" po wzkaznik++: "<<*wskaznik<<endl;
    }
    stop = clock();
    czas = (double)(stop-start) / CLOCKS_PER_SEC;
    cout<<"Czas zapisu (ze wskaznikiem): "<<czas<<endl;
    
    delete [] tablica;
    
    return 0;
}



 

komentarz 14 grudnia 2017 przez niezalogowany
edycja 14 grudnia 2017
Usuń linię wskaznik++ i przed usunięciem tablicy wypisz ją ponownie używając osobnej pętli. Nie wypisywałeś zawartości i-tej szufladki tablicy. Działałeś cały czas na tej samej pierwszej szufladce, bo nie przesuwałeś ręki (wskaźnika).
komentarz 17 grudnia 2017 przez dzem Nowicjusz (180 p.)

Usuń linię wskaznik++ i przed usunięciem tablicy wypisz ją ponownie używając osobnej pętli.

Dziękuję za wskazówkę. Niestety nie odpowiada ona na najważniejszą dla mnie kwestię, czyli czemu w ogóle służy w tym kodzie linia wskaznik++.

 Nie wypisywałeś zawartości i-tej szufladki tablicy. Działałeś cały czas na tej samej pierwszej szufladce, bo nie przesuwałeś ręki (wskaźnika)

tj. w cout'cie powinienem wpisać tablica[i] zamiast *wskaznik? Np.:

*wskaznik+=50;
        cout<<"Zawartość szuflady "<<i<<" po +=50: "<<tablica[i]<<endl;

 

1
komentarz 17 grudnia 2017 przez niezalogowany

tj. w cout'cie powinienem wpisać tablica[i] zamiast *wskaznik? Np.:

No w sumie tak też można :D

W odcinku masz coś wytłumaczone o wskaźnikach. Wskaźnik wskazuje na adres w pamięci. Inkrementacja powoduje, że wskaźnik pokazuje wartość w nowym adresie. Czyli dla typu int wskaźnik przesuwa się o 4 bajty w pamięci i może odczytać kolejną wartość. Weź np taki kod dla uproszczenia:

#include <iostream>
using namespace std;

int main()
{
    const int ile = 10;
	int *tablica = new int[ile];
	int *wskaznik = tablica;

	for (int i = 0; i < ile; i++)
	{
		*wskaznik = i;
		*wskaznik += 50;
		//wskaznik++;
		cout<<"Zawartość szuflady "<<i<<": "<<tablica[i]<<endl;
	}

    std::cout << "\nPo zakonczeniu petli:\n";

	for (int i = 0; i < ile; ++i)
	{
		cout<<"Zawartość szuflady "<<i<<": "<<tablica[i]<<endl;
	}

	delete[] tablica;

	return 0;
}

Gdy zakomentujesz inkrementację czyli przesuwanie wskaźnika to będziesz działać na tym samym obszarze pamięci czyli na pierwszym elemencie tablicy. Dlatego pierwszy element będzie wynosił 59 po zakończeniu pętli, a reszta przypadkowe wartości z pamięci.

komentarz 17 grudnia 2017 przez dzem Nowicjusz (180 p.)
Wielkie dzięki! Ten przykład świetnie wyjaśnia do czego służy linia wskaznik++ :)
0 głosów
odpowiedź 2 stycznia 2019 przez gontariusz Nowicjusz (140 p.)

Mam problem z 2 kodem z odcinka.

Mój kod:

#include <iostream>
#include <cstdlib>
#include <time.h>

using namespace std;

int ile;
clock_t start, stop;
double czas;

int main()
{
    cout << "Ile liczb w tablicy: ";
    cin >> ile;

    int *tablica;
    tablica = new int [ile];

    start=clock();

    for(int i=0; i<ile; i++)
    {
        tablica[i]=i;
        tablica[i]+=50;
    }
    stop=clock();

    czas=(double)(stop - start)/CLOCKS_PER_SEC;
    cout << "Czas zapisu (bez wskaznika): "<<czas<<endl;

    delete [] tablica;


     int *wskaznik=tablica;
    tablica = new int[ile];


    start = clock();

    for(int i=0; i<ile; i++)
    {
        *wskaznik = i;
        *wskaznik =+ 50;
        wskaznik++;
    }

    stop = clock();

    czas=(double)(stop - start)/CLOCKS_PER_SEC;
    cout << "Czas zapisu (ze wskaznikiem): "<<czas<<endl;

    delete [] tablica;

return 0;
}

Po skompilowaniu i uruchomieniu programu pojawia się:

 

Dzieje się to wyłącznie przy liczbach powyżej 300000. 

Będę wdzięczny za pomoc i wyjaśnienie.

 

 

komentarz 2 stycznia 2019 przez RafalS VIP (122,820 p.)

Pomieszałeś kolejność tych linijek:

 int *wskaznik=tablica;
    tablica = new int[ile];

do wskaznika jest przypisana stara wartosc zmiennej tablica, czyli stara, co wiecej zwolniona tablica.

Następnym razem zadaj odzielne pytanie z linkiem do pytania inspiracji.

 

komentarz 3 stycznia 2019 przez gontariusz Nowicjusz (140 p.)
Super, dzięki za podpowiedź. Teraz wszystko jasne i wszystko działa.

Podobne pytania

0 głosów
1 odpowiedź 312 wizyt
pytanie zadane 11 kwietnia 2016 w C i C++ przez aspoka Mądrala (5,290 p.)
+1 głos
2 odpowiedzi 2,087 wizyt
–1 głos
2 odpowiedzi 210 wizyt

92,454 zapytań

141,262 odpowiedzi

319,099 komentarzy

61,854 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Akademia Sekuraka 2024 zapewnia dostęp do minimum 15 szkoleń online z bezpieczeństwa IT oraz dostęp także do materiałów z edycji Sekurak Academy z roku 2023!

Przy zakupie możecie skorzystać z kodu: pasja-akademia - użyjcie go w koszyku, a uzyskacie rabat -30% na bilety w wersji "Standard"! Więcej informacji na temat akademii 2024 znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...