Autor Wątek: lista podczepiana i wskaznik  (Przeczytany 2168 razy)

Offline _user

  • Użytkownik

# Kwiecień 18, 2015, 23:04:30
czesc nie rozumiem paru rzeczy w kodzie i prosilbym o wyjasnienie, bede wdzieczny, kod zamieszczam ponizej:
#include <stdio.h>
 #include <stdlib.h>
 
 typedef struct element {
   struct element *next;
   unsigned long val;
 } el_listy; /* jaki jest typ el_listy ? typ el_listy to struct element ? czym jest el_listy */
 
 el_listy *first; /* I co to jest wskaznik na cala strukture el_listy, i ma wartosc 2 tak ? bo potem bedzie first->val=2, czyli el_listy *first na poczatku ma wartosc 2 czy NULL jak next ? To pierwsza rzecz teraz w drugiej kolejnosci opisywania moich niejasnosci przejde do funkcji main. */
 
 void dodaj_do_listy (el_listy *lista, unsigned long liczba)
 {
   el_listy *wsk, *nowy;
   wsk = lista;
   while (wsk->next != NULL)
     {
     wsk = wsk->next; /* Tutaj nie wiem po co to juz wgl, bo w funkcji jest_pierwsza wskaznikowi dalismy juz wartosc NULL tak ?*/
     }
   nowy =(el_listy*) malloc (sizeof(el_listy));
   nowy->val = liczba; /* jesli dodamy liczbe do nowy.val bedzie ona zmieniona automatycznie w first.val ? Pozatym jak jedna zmienna "val" w strukturze el_listy przechowuje tyle liczb ?  W ostatniej funkcji mam te same problemy co w tych.*/
   nowy->next = NULL;
   wsk->next = nowy;
 }
 
 void wypisz_liste(el_listy *lista)
 {
   el_listy *wsk=lista;
   while( wsk != NULL )
     {
     printf ("%lu\n", wsk->val);
     wsk = wsk->next;
     }
 }
 
 int jest_pierwsza(el_listy *lista, int liczba)
 {
   el_listy *wsk; /* tutaj el_listy *first = el_listy *wsk  - tak ? */
   wsk = lista;
   while (wsk != NULL) { /* i tutaj mam problem bo nie wiem jaka wartosc ma ten el_listy *wsk, jesli NULL to po co ten while */
     if ((liczba%wsk->val)==0) return 0;
        wsk = wsk->next; /* a tego kompletnie nie rozumiem, jak dziala to przesuwanie, ale opisze jak ja to widze i prosilbym o poprawe, wiec:
wsk = wsk->next   wydaje mi sie ze jest rownowazne z: el_listy *wsk = el_listy *wsk->next   - tak czy nie ? no bo teraz przypisalismy chyba wartosc NULL do "wsk" tak ? Chyba zadego przesuwania tu nie bylo tylko nadanie wsk wartosci NULL ktora ma next, tak ? teraz do funkcji dodaj_do_listy */
 
     }
     return 1;
 }
 
 int main ()
 {
   unsigned long i = 3;
   const unsigned long END = 1000;
   first =(el_listy*) malloc (sizeof(el_listy));
   first->val = 2;   / * tutaj first.val = 2 i first.next = NULL, a wiec el_listy *first jaka ma wartosc ? Oraz *first to byl wskaznik na strukture el_listy i teraz jest przekazywany nie jako wskaznik ale samo first do funkcji "jest_pierwsza" "dodaj_do_listy" oraz "wypisz_liste" tak ? I teraz chcialbym przejsc do funkcji "jest_pierwsza".*/
   first->next = NULL;
   for (;i!=END;++i) {
     if (jest_pierwsza(first, i))
       dodaj_do_listy (first, i);
       }
   wypisz_liste(first);
   return 0;
 }

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Xirdus

  • Moderator

  • +1
# Kwiecień 18, 2015, 23:09:09
Czego konkretnie nie rozumiesz? Jak chcesz to mogę wytłumaczyć co znaczy "int main()" itd. ;)

Offline Xender

  • Użytkownik

  • +3
# Kwiecień 19, 2015, 00:19:42
0. Wróć do tutoriala albo znajdź lepszy.
Dopisałem ten punkt prawie pod koniec pisania postu.
Im dalej w las, tym gorzej. :P

0a. https://www.youtube.com/playlist?list=PL3B37AA87E119944E
Bierz te ze słowem "Pointery" w tytule.


1. el_listy - ten typedef to typowy idiom C.

Gdy w C definiujesz strukturę, to nazwa zdefiniowanej struktury brzmi "struct foo".
Jak gdzieś napiszesz samo "foo", to kompilator powie, że nie zna typu o takiej nazwie.
Typedef definiuje alias - od teraz "el_listy" jest aliasem na "struct element".

Czasem stosuje się też konstrukcję:
typedef struct {
    ....
} name;
Gdzie po struct nie ma żadnej nazwy - do definiuje anonimową strukturę, i od razu tworzy do niej alias.
Wtedy AFAIK jedynym sposobem odwołania się jest "name", nie "struct name".

Nie jestem pewien, czy można stosować
typedef struct name {
    ....
} name;
Gdzie nazwa jest 2x taka sama. Sprawdź.


2. el_listy *first - wskaźnik na obiekt typu el_listy AKA instancję el_listy.
Wskaźników na struktury w C nie ma.
Wskaźników na struktury/klasy w C++ nie ma.

Dygresja: W Pythonie są referencje na klasy, bo klasa (jak i funkcja) jest tam też obiektem, jak każdy inny ("klasa/funkcja jest obiektem pierwszej klasy" - to trochę niefortunne stwierdzenie, bo pierwsze wystąpienie słowa "klasa" oznacza typ zdefiniowany przez użytkownika, a drugie - że rządzi się tymi samymi prawami, co inne obiekty).
Patrz:
https://en.wikipedia.org/wiki/First-class_function
https://en.wikipedia.org/wiki/First-class_citizen

Więc określenie "wskaźnik na strukturę" jest ściśle rzecz biorąc niepoprawne.
W praktyce z kontekstu często jest jasne, że chodzi o "wskaźnik na obiekt klasy".
Taki skrót myślowy jest powszechny i akceptowalny.
Ale jak napisałeś o "wskaźniku na całą strukturę" - lepiej się upewnić, że odróżniasz pojęcie struktury/typu od obiektu/instancji.


3. "first" jest zmienną globalną, więc zostanie zainicjalizowany zerem (w wypadku liczb) / nullem (w wypadku wskaźników).

Dygresja: to nie zawsze jest to samo. Patrz: https://gynvael.coldwind.pl/?id=399


4. Nie zrobisz wiele z nullem.
Ta pętla to iteracja po liście celem znalezienia ostatniego elementu.


5. Jak wpiszesz wartość (dodawania tam nie ma) do nowy->val (nie nowy.val), to first->val (nie first.val) nie zostanie ruszone, chyba, że nowy i first wskazują na ten sam obiekt.
Ale to by było bez sensu.

Jedno pole val w strukturze el_listy przechowuje jedną liczbę.
To, co masz, to linkedlista - każdy element zawiera wskaźnik do następnego.
Widzisz ten malloc? To alokacja pamięci na stercie (heap) na nowy obiekt.
Dalej jest wpisywanie wartości do każdego pola nowego obiektu - konieczne - pamięć ze sterty nie jest inicjalizowana. Oznacza to tyle, że dopóki nic nie wpiszesz do tych zmiennych, będzie tam siedzieć Unspecified Value.

Mógłbym powiedzieć o komórkach pamięci, o tym, że malloc nie zeruje pamięci i o tym, że zawartosć tych komórek będzie przypadkowa (nie mylić z losową!).
Źle, źle, źle.

Używanie Unspecified Value zahacza o Undefined Bahaviour.

Unspecified Value to nie "śmieci po innych wartościach, które siedziały w tych komórkach pamięci".
Undefined Behaviour to nie "program się wykrzaczy".
Może tak będzie w 90% przypadków. Na pozostałe 10% się nadziejesz.
I to, na co się nadziejesz, będzie włócznią, nie kolcem w stopie.
http://catb.org/jargon/html/N/nasal-demons.html
Praktyczne przykłady?
Kompilator może wyoptymalizować (wyeliminować) kawałek kodu z programu, bo wyjdzie z założenia, że nie popełniłeś UB linijkę wcześniej.

Np. definiując sobie (w C++) referencję na obiekt wskazywany przez wskaźnik, zapewniasz kompilator, że we wskaźniku nie ma nulla.
Jeśli teraz linijkę dalej masz sprawdzenie, czy wskaźnik nie jest nullem - zostanie ono usunięte.
Wskaźnik nie może być nullem, bo wcześniej już się odwołałeś do tego, na co pokazuje, a przecież nie popełniłbyś UB.
Jakby utworzenie tej referencji było *po* null-checku - nie ma problemu.
I nie, taki program wcale nie musi iść w krzaki. Jeśli potem tej referencji nigdzie nie użyłeś, najpierw może zostać wyoptymalizowany null-check, potem istnienie samej referencji. :>

5. "tutaj el_listy *first = el_listy *wsk  - tak" - nie.
el_listy *wsk to definicja zmiennej lokalnej.
Zmienne lokalne nie są zainicjalizowane.
Dopóki czegoś do nich nie przypiszesz, siedzi tam Unspecified Value.
Kompilator może wyoptymalizowac całe wyrażenie, w którym Unspecified Value bierze udział.
Ludzie z Debiana tak kiedyś załatwili seedowanie generatora liczb pseudolosowych w jakimś programie, którego ważną funkcją była kryptografia.
Nie wiem, czy nawet nie w Kernelu.

Jak załatwili?
Do wyrażenia inicjalizującego seed dodali xor z niezainicjalizowaną zmienną lokalną.
Myśleli kategoriami "jakiś śmieć ze stosu, będzie dodatkowa entropia".
Ale twórcy kompilatora (GCC) myśleli kategoriami standardu C - kompilator wyoptymalizował całe wyrażenie, w którego skład wchodziło Unspecified Value.

6. Spokojna głowa - linijkę niżej masz "wsk = lista;" - teraz wsk wskazuje na listę.
Wartość wpisana do zmiennej, widmo UB przy odczycie zażegnane.

7. W takim razie warunek w while jak najbardziej zasadny - wsk wskazuje na listę, a ta nie jest nullem.
Potem wsk będzie wskazywał na kolejne elementy (węzły) listy, aż dojdzie do końca - wtedy nie będzie wskazywał na nic (będzie miał wartość NULL), więc warunek będzie fałszywy i pętla zostanie zakończona.

8. wsk = wsk->next - przypisanie do wsk tego, co leży w wsk->next.
Leży tam wskaźnik na następny element (węzeł) listy.

"wydaje mi sie ze jest rownowazne z: el_listy *wsk = el_listy *wsk->next   - tak czy nie ?" - nie.
To się zwyczajnie nie skompiluje.
wsk jest już zdefiniowane.
Po prawej stronie w inicjalizacji nie może stać instrukcja, a jedynie wyrażenie.
Nie ma tu żadnego przypisania nulla (chyba, że w wsk->next siedzi null). Ale to wiadomo dopiero w runtime.
To jest jak najbardziej poprawna iteracja po linkedliście.


9. "a wiec el_listy *first jaka ma wartosc ?" - Linijkę wyżej masz malloc.
first ma wartość adresu pamięci, pod którą zmieści się obiekt typu el_listy (w tej linijce pamięć nie jeszcze jeszcze zainicjalizowana, ciężko tu mówić o obiekcie, jak pola zawierają tylko Unspecified Value).

"Oraz *first to byl wskaznik na strukture el_listy i teraz jest przekazywany nie jako wskaznik ale samo first do funkcji "jest_pierwsza" "dodaj_do_listy" oraz "wypisz_liste" tak ?" - nie.
Jest wskaźnikiem.
Jest przekazywany jak to, czym jest.
Przekazywany jest adres jakiegoś obiektu w pamięci (bo tym jest wskaźnik).
Gwiazdka nie jest częścią nazwy zmiennej typu wskaźnikowego.
Gwiazdka jest częścią specyfikacji typu - okresla, że typ jest wskaźnikowy.
Zmienna tego typu jest wskaźnikiem.
Gwiazdka w wyrażeniach (nie dekralacjach/definicjach) (i nie ta od mnożenia, ta jednoargumentowa) to dereferencja wskaźnika / odwołanie przez wskaźnik.

Offline koirat

  • Użytkownik

# Kwiecień 19, 2015, 00:41:29
To ja dodam jeszcze coś od siebie. Poprawcie jak bym się mylił bo daleko mi do guru z c++.
typedef struct element {
   struct element *next;
   unsigned long val;
 } el_listy;

Zapewne zastanawiasz się po co nam "element" oraz "el_list" zarazem.
Problem jest w tym że struktura zawiera wskaźnik na strukturę tego samego typu. Co za tym idzie nie możemy użyć el_listy wewnątrz niej gdyż typedef następuje później niż definicja struktury element.

Offline _user

  • Użytkownik

# Kwiecień 19, 2015, 01:54:41
Dzieki Xirdus niezle to opisales, duzo pomoglo ! :)

Ale jeszcze dalej nie rozumiem dwoch rzeczy.
Gdzie ten program przechowuje te wszystkie zmienne ? Przeciez jest ich wiele, w first->val, w nowy->val, czy gdzie ?
Oraz jakby ktos umial inaczej przedstawic to wsk=wsk->next, nie rozumiem na jakiej zasadzie to wykonuje ta literacje.
Jedyne co mi przychodzi do glowy to to ze:  wsk = wsk->next to pierwsze "wsk" to jest wskaznik na adres listy tak ? next w strukturze tez jest wskaznikiem na adres tej samej struktury, wsk->next to wskaznik i element(->)next to tez wskaznik na adres tej samej struktury, tak ? Ale nie rozumiem w jaki sposob by to rownanie mialo powodowac literacje.
« Ostatnia zmiana: Kwiecień 19, 2015, 02:08:21 wysłana przez _user »

Offline timus

  • Użytkownik

# Kwiecień 19, 2015, 02:16:34
Gdzie ten program przechowuje te wszystkie zmienne ? Przeciez jest ich wiele, w first->val, w nowy->val, czy gdzie ?
Program przechowuje te zmienna/struktury gdzieś w pamięci, funkcja malloc służy właśnie do pozyskania dodatkowej pamięci dla programu.

Oraz jakby ktos umial inaczej przedstawic to wsk=wsk->next, nie rozumiem na jakiej zasadzie to wykonuje ta literacje.
wsk wskazuje adres gdzie znajduje jest jakiś obiekt(struktura/zmienna czy jak tam chcesz to nazywać) w pamięci, natomiast wsk->next wskazuje następny obiekt w kolejce. I tak działa magia tej iteracji.

Proponuje sobie poćwiczyć pointery, alokowanie pamieci, itp. Filmiki Gynvael'a które podlinkował Xender idealnie sie nadają.

Edit:
btw. Co to jest  literacja? Iteracja po literach? :D
« Ostatnia zmiana: Kwiecień 19, 2015, 02:19:31 wysłana przez timus »

Offline _user

  • Użytkownik

# Kwiecień 19, 2015, 02:36:25
Moze pomylilo mi sie o iteracja ;p

Chyba nie do konca rozumiem indeksliste, jakby ktos z was znal jakis link do artykulu o tym po polsku byloby swietnie.
Ale wracajac do wsk->next wiem ze ma wskazywac na nastepny obiekt ale jak ? przeciez w strukturze el_listy next jest wskaznikiem na adres struktury w ktorej sie znajduje, tak ? A wiec to byloby slowami: wskaznik na adres struktury el_lista = wskaznik -> na element ktory jest wskaznikiem do struktury el_listy, tak czy sie myle ?
« Ostatnia zmiana: Kwiecień 19, 2015, 02:44:03 wysłana przez _user »

Offline timus

  • Użytkownik

# Kwiecień 19, 2015, 03:35:48
next wskazuje na następny obiekt z listy, tak ja mówi jego nazwa. Wydaje mi się, że mylisz definicje struktury z obiektem w pamięci.
 
Na moje oko to jest to jednokierunkowy linked list, nie znam słowa indekslista(i google tez mi nie pomaga :-( , a z indeksami tez to ma mało wspólnego), Gynvael zrobił kilka nagrań o linked listach: link, może one pomogą ci zrozumieć jak one działają. Dorzucam ci jeszcze link do wiki, może pomoże: link.

Offline Xender

  • Użytkownik

# Kwiecień 19, 2015, 10:37:22
przeciez w strukturze el_listy next jest wskaznikiem na adres struktury w ktorej sie znajduje, tak ? A wiec to byloby slowami: wskaznik na adres struktury el_lista = wskaznik -> na element ktory jest wskaznikiem do struktury el_listy, tak czy sie myle ?

Wskaźnik nie wskazuje na strukturę, wskazuje na pamięć.
To, jak traktować to, co jest w tej pamięci, mówi deklaracja typu typu:
struct foo* bar; // bar jest wskaźnikiem (adresem w pamięci) obiektu typu struct foo.

Za każdym razem, kiedy wołasz malloc(), dostajesz kawałek pamięci o wielkości o którą poprosiłeś*, z którym możesz zrobić, co chcesz.
W szczególności, możesz ten adres przypisać do wskaźnika na dany typ.
Jak jeszcze wpiszesz coś do tej pamięci (przypominam, że jest niezainicjalizowana), to można mówić, że ten pointer wskazuje teraz na obiekt danego typu.

Wskaźnik nie pokazuje na strukturę.
Struktura to opis typu.
Jest jeden wbudowany typ int, a zmiennych typu int możesz mieć w programie wiele.
Strukturę definiujesz jeden raz, a jej instancji (obiektów jej typu) możesz mieć wiele.


* No, czasem można nie dostać adresu zaalokowanej pamięci, o którą się prosiło, tylko NULL.
Przykład - jak poprosi się o więcej pamięci, niż wynosi wirtualna przestrzeń adresowa (4GiB na 32-bitowych architekturach), albo jakieś systemowe limity.

Dlatego teoretycznie powinno się sprawdzać wynik malloc.
W praktyce, jeśli nie masz sensownego pomysłu, co zrobić, gdy pamięci nie dostaniesz (fallback do algorytmu o mniejszej złożoności pamięciowej (więc pewnie większej obliczeniowej)? Zwolnić jakieś cache gdzieś indziej?), to można pozwolić programowi iść w krzaki...

Offline _user

  • Użytkownik

# Kwiecień 19, 2015, 14:10:50
Kurde ten coldwind nie dosc ze pisze tak ze prawie nic nie widac to jeszcze daje takie nazwy zmiennym i strukturom zeby jeszcze bardziej sie wszystko zlewalo.... Ale pozatym bardzo przydatne linki, obejrzalem z 2-3 razy i okej, dzieki przydalo sie.
« Ostatnia zmiana: Kwiecień 19, 2015, 16:09:07 wysłana przez _user »

Offline _user

  • Użytkownik

# Kwiecień 19, 2015, 16:30:47
I opisze nizej to jak widze dzialanie tego przesuwania, odpiszcie czy dobrze czy zle a wiec:

   while (wsk->next != NULL)
     {
     wsk = wsk->next; /* przesuwamy wsk aż znajdziemy ostatni element */
     }

Przy pierwszym odwiedzeniu tego while'a wsk mialo wartosci val=2 i next=null wiec nie trzeba bylo przesuwac i przeszlo dalej, poszlismy dalej do wsk->next zostaly wprowadzone wartosci val=nowa pierwsza liczba i next->null, gdy drugi raz odwiedzamy ten while, wsk ma wartosci: val=2 i next=gdzie val=nowa liczba i next nexta=null, wiec przypisujemy wsk wartosc val znajdujacej sie w next i petla wykonuje sie znowu a teraz wsk ma juz w val wartosc val=nowa liczba i next=null, i petla sie konczy i wchodzimy znowu tam gdzie przypisujemy wsk->next nowa liczbe, potem znowu szukamy nastepnej liczby i znowu odwiedzamy while teraz nasz wsk ma tak jak wtedy wartosc val=2 i next=ktory ma wartosc val=druga z kolei liczba pierwsza a next nie ma wartosci null(tylko wartosc val=trzecia liczba pierwsza i next=null) wiec przypisujemy wsk wartosc val=druga liczba pierwsza i next=val=trzecia liczba pierwsza i next=null i wykonujemy znowu, teraz wsk ma wartosc val=druga(liczba pierwsza) i next= val=trzecia liczba i next =null czyli przypisujemy wsk ta wartosc i wchodzimy znowu, teraz wsk ma wartosc val=trzecia liczba pierwsza i next=null i petla sie konczy,  Troche dlugo to opisalem ale dobrze czy zle ? ;p

Offline Xirdus

  • Moderator

# Kwiecień 19, 2015, 19:31:53
Dzieki Xirdus niezle to opisales, duzo pomoglo ! :)
Znowu? :/

I opisze nizej to jak widze dzialanie tego przesuwania, odpiszcie czy dobrze czy zle a wiec:
W połowie się zgubułem, ale wydaje mi się że rozumiesz już te kolejki. Za to prawdopodobnie nie rozumiesz wskaźników - musisz zacząć odróżniać wartość wskaźnika od wartości obiektu wskazywanego przez wskaźnik.

Offline voytech

  • Użytkownik

# Kwiecień 20, 2015, 02:26:37
Jeżeli chodzi o wskaźniki to spróbuj prześledzić poniższy program. Drukowanie zawartości może co nieco rozjaśnić jak to w ogóle działa. Poza tym el_listy to zła nazwa dla typu. Często  typy zaczynają się od wielkiej litery "NazwaTypu", albo z małej ale kończą na _t, np. "nazwatypu_t". Warto przyjąć jakąś dobrą konwencję i się tego trzymać. Ogólnie to może wyglądać np. tak:
Kod: (c) [Zaznacz]
// zapowiedz typu prostego i wskaźnikowego (forward declaration)
typedef struct Struktura Typ;
typedef Typ* WskaznikTypu;

/*
 * struktura o nazwie "Struktura" zawierajaca pole "dane" typu int
 * oraz pole "adres" bedace wskaznikiem na miejsce w pamieci
 * gdzie bedzie znajdowac sie obiekt typu "typedef struct Struktura"
 * albo krocej obiekt typu "Typ"
 */
struct Struktura {
int dane;
WskaznikTypu adres;
        //Typ *adres;  // <- można tak
        //struct Struktura *adres;  // <- tak też można
};

Można typy danych napisać w krótszej formie, bez zapowiadania typów:
Kod: (c) [Zaznacz]
typedef struct Struktura {
int dane;
struct Struktura *adres;
} Typ, *WskaznikTypu;
nazwa typu i struktury mogą być takie same:
Kod: (c) [Zaznacz]
typedef struct Typ {
int dane;
struct Struktura *adres;
} Typ, *WskaznikTypu;
można też stosować struktury anonimowe, ale "struct NazwaStruktury" nie znajdzie się w tzw. "tag space" i nie będzie można skorzystać z zapowiedzi typów (forward declaration):
Kod: (c) [Zaznacz]
typedef struct {
int dane;
struct Struktura *adres; // <- !! compilation error
} Typ, *WskaznikTypu;

Warto powstawiać drukowanie adresów i zawartości wskaźników do swojego programu albo skorzystać z debugera. Ułatwi Ci to analizę programu i pozwoli lepiej zrozumieć wskaźniki, ogranicz tylko liczbę END do 10 a potem możesz sobie na kartce narysować elementy, ich adresy, zawartość i prześledzić jak to wszystko jest powiązane.

Kod: (c) [Zaznacz]
void test() {
// Trzy sposoby na deklaracje wskaźników na strukturę
// 1. bez definicji typu (dużo pisania słówka "struct":
// struct Struktura *e1, *e2, *e3, *e4;
// 2. z definicją typu (dużo pisania gwiazdek):
// Typ *e1, *e2, *e3, *e4;
// 3. z definicją wskaźnika typu <- najbardziej eleganckie
WskaznikTypu e1, e2, e3, e4;

e1=e2=e3=e4=NULL;

        printf("sizeof(int)=%I64d, sizeof(Typ)=%I64d, sizeof(WskaznikTypu)=%I64d\n", sizeof(int), sizeof(Typ), sizeof(WskaznikTypu));
printf("adres zmiennej e1,e2,e3,e4 (&e1,&e2,&e3,&e4) =\n\t0x%p\n\t0x%p\n\t0x%p\n\t0x%p\n\n", &e1,&e2,&e3,&e4);
printf("zawartosc zmiennej e1,e2,e3,e4 =\n\t0x%p\n\t0x%p\n\t0x%p\n\t0x%p\n\n", e1, e2, e3, e4);

//zmienne e* będą zawierac adres miejsca gdzie bedzie obiekt typu "Typ"
e1 = (WskaznikTypu)malloc(sizeof(Typ));
e2 = (WskaznikTypu)malloc(sizeof(Typ));
e3 = (WskaznikTypu)malloc(sizeof(Typ));
e4 = (WskaznikTypu)malloc(sizeof(Typ));
printf("zawartosc zmiennej e1,e2,e3,e4 (po malloc) =\n\t0x%p\n\t0x%p\n\t0x%p\n\t0x%p\n\n", e1, e2, e3, e4);
e1->adres = e2; // pole "adres" obiektu e1 wskazuje na miejsce w pamieci gdzie znajduje sie obiekt e2
e2->adres = e3;
e3->adres = e4;
e4->adres = NULL;

printf("adres obiektu e2 (e1->adres)                      = 0x%p\n", e1->adres);
printf("adres obiektu e3 (e1->adres->adres)               = 0x%p\n", e1->adres->adres);
printf("adres obiektu e4 (e1->adres->adres->adres)        = 0x%p\n", e1->adres->adres->adres);
printf("adres obiektu e4 (e1->adres->adres->adres->adres) = 0x%p\n", e1->adres->adres->adres->adres);

WskaznikTypu e23 = (WskaznikTypu)malloc(sizeof(Typ));
printf("\nzawartosc zmiennej e23 =\t0x%p\n", e23);
//wstawiamy e23 w liste miedzy e2 i e3
e23->adres = e2->adres;
e2->adres = e23;

printf("adres obiektu e2  (e1->adres)                      = 0x%p\n", e1->adres);
printf("adres obiektu e23 (e1->adres->adres)               = 0x%p\n", e1->adres->adres);
printf("adres obiektu e3  (e1->adres->adres->adres)        = 0x%p\n", e1->adres->adres->adres);
printf("adres obiektu e4  (e1->adres->adres->adres->adres) = 0x%p\n", e1->adres->adres->adres->adres);
}

Ps. dodaj_do_listy działa dość nieefektywnie, za każdym razem zaczynasz przechodzić listę od początku do końca i wtedy dodajesz nowy element. Możesz zadeklarować jeszcze jedną zmienną globalną "last" i tam trzymać adres ostatniego elementu listy, wtedy "while" będzie zbyteczny.

Ps2. jeszcze raz przypominam, w trakcie nauki wal printf-y gdzie tylko się da i analizuj wydruk, np.:
Kod: (c) [Zaznacz]
void wypisz_liste(el_listy *lista) {
el_listy *wsk = lista;
printf("\nwypisz_liste(lista=0x%p) &wsk=0x%p, &lista=0x%p\n", lista, &wsk, &lista);
while (wsk != NULL) {
printf("\twsk=0x%p, wsk->val=%lu, wsk->next=0x%p\n", wsk, wsk->val, wsk->next);
wsk = wsk->next;
}
}