Laboratorium 6: Dynamiczny przydział pamięci. dr inż. Arkadiusz Chrobot dr inż. Grzegorz Łukawski



Podobne dokumenty
Temat: Dynamiczne przydzielanie i zwalnianie pamięci. Struktura listy operacje wstawiania, wyszukiwania oraz usuwania danych.

Co to jest sterta? Sterta (ang. heap) to obszar pamięci udostępniany przez system operacyjny wszystkim działającym programom (procesom).

Wskaźniki. Informatyka

Wskaźniki. Przemysław Gawroński D-10, p marca Wykład 2. (Wykład 2) Wskaźniki 8 marca / 17

Laboratorium 3: Tablice, tablice znaków i funkcje operujące na ciągach znaków. dr inż. Arkadiusz Chrobot dr inż. Grzegorz Łukawski

ZASADY PROGRAMOWANIA KOMPUTERÓW

Uzupełnienie dot. przekazywania argumentów

Wskaźnik może wskazywać na jakąś zmienną, strukturę, tablicę a nawet funkcję. Oto podstawowe operatory niezbędne do operowania wskaźnikami:

Wstęp do programowania INP003203L rok akademicki 2018/19 semestr zimowy. Laboratorium 2. Karol Tarnowski A-1 p.

Podstawy programowania. Wykład 6 Wskaźniki. Krzysztof Banaś Podstawy programowania 1

Wskaźniki i dynamiczna alokacja pamięci. Spotkanie 4. Wskaźniki. Dynamiczna alokacja pamięci. Przykłady

Tablice, funkcje - wprowadzenie

Lab 9 Podstawy Programowania

1 Podstawy c++ w pigułce.

dynamiczny przydział pamięci calloc() memset() memcpy( ) (wskaźniki!! )

Wskaźniki w C. Anna Gogolińska

Wstęp do Programowania, laboratorium 02

Struktury. Przykład W8_1

Podstawy programowania. Wykład 6 Złożone typy danych: struktury, unie. Krzysztof Banaś Podstawy programowania 1

DYNAMICZNE PRZYDZIELANIE PAMIECI

Laboratorium 6: Ciągi znaków. mgr inż. Leszek Ciopiński dr inż. Arkadiusz Chrobot dr inż. Grzegorz Łukawski

. Podstawy Programowania 2. Dwukierunkowa lista liniowa. Arkadiusz Chrobot. 7 kwietnia 2019

Algorytmy i złożoności. Wykład 3. Listy jednokierunkowe

Tablice (jedno i wielowymiarowe), łańcuchy znaków

Języki i metodyka programowania. Wskaźniki i tablice.

Struktury czyli rekordy w C/C++

Programowanie w języku C++

1 Podstawy c++ w pigułce.

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

KURS C/C++ WYKŁAD 6. Wskaźniki

wykład III uzupełnienie notatek: dr Jerzy Białkowski Programowanie C/C++ Język C - zarządzanie pamięcią, struktury,

. Podstawy Programowania 2. Jednokierunkowa lista liniowa. Arkadiusz Chrobot. 28 marca 2017

Tablice deklaracja, reprezentacja wewnętrzna

Pliki. Informacje ogólne. Obsługa plików w języku C

Podstawy programowania komputerów

Dynamiczny przydział pamięci w języku C. Dynamiczne struktury danych. dr inż. Jarosław Forenc. Metoda 1 (wektor N M-elementowy)

Wskaźniki. Programowanie Proceduralne 1

METODY I JĘZYKI PROGRAMOWANIA PROGRAMOWANIE STRUKTURALNE. Wykład 02

Zmienne, stałe i operatory

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Podstawy programowania skrót z wykładów:

Programowanie w C++ Wykład 5. Katarzyna Grzelak. 16 kwietnia K.Grzelak (Wykład 1) Programowanie w C++ 1 / 27

Argumenty wywołania programu, operacje na plikach

Wykład 3 Składnia języka C# (cz. 2)

Podstawy Programowania 2 Jednokierunkowa lista liniowa. Plan. Jednokierunkowa lista liniowa. Jednokierunkowa lista liniowa. Notatki. Notatki.

Zmienne i struktury dynamiczne

Struktura programu. Projekty złożone składają się zwykłe z różnych plików. Zawartość każdego pliku programista wyznacza zgodnie z jego przeznaczeniem.

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Język C++ Różnice między C a C++

Języki programowania obiektowego Nieobiektowe elementy języka C++

Część XVII C++ Funkcje. Funkcja bezargumentowa Najprostszym przypadkiem funkcji jest jej wersja bezargumentowa. Spójrzmy na przykład.

main( ) main( void ) main( int argc, char argv[ ] ) int MAX ( int liczba_1, liczba_2, liczba_3 ) źle!

/* dołączenie pliku nagłówkowego zawierającego deklaracje symboli dla wykorzystywanego mikrokontrolera */ #include <aduc834.h>

Powyższe wyrażenie alokuje 200 lub 400 w zależności od rozmiaru int w danym systemie. Wskaźnik wskazuje na adres pierwszego bajtu pamięci.

Ćwiczenie nr 6. Poprawne deklaracje takich zmiennych tekstowych mogą wyglądać tak:

Podstawy Programowania 2 Dwukierunkowa lista liniowa. Plan. Wstęp. Implementacja. Notatki. Notatki. Notatki. Notatki.

Wstęp do programowania

Ćwiczenie 4. Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1.

Podstawy Programowania 2 Wskaźniki i zmienne dynamiczne. Plan. Wskaźniki - krótka powtórka. Wskaźniki - krótka powtórka Wartości wskaźników.

Podstawy programowania w języku C++

Konwersje napis <-> liczba Struktury, unie Scanf / printf Wskaźniki

Wskaźniki. nie są konieczne, ale dają językowi siłę i elastyczność są języki w których nie używa się wskaźników typ wskaźnikowy typ pochodny:

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Ćwiczenie 3 stos Laboratorium Metod i Języków Programowania

Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1. Kraków 2013

Część 4 życie programu

Typy wyliczeniowe Konwersje napis <-> liczba Struktury, unie Scanf / printf Wskaźniki

ISO/ANSI C - funkcje. Funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje

4. Tablica dwuwymiarowa to jednowymiarowa tablica wskaźników do jednowymiarowych tablic danego typu.

Języki programowania. Przetwarzanie tablic znaków. Część druga. Autorzy Tomasz Xięski Roman Simiński

Podstawy programowania 2. Przygotował: mgr inż. Tomasz Michno

. Podstawy Programowania 2. Wskaźniki i zmienne dynamiczne. Arkadiusz Chrobot. 25 lutego 2019

Podstawy programowania w języku C++

Podstawy programowania

Materiał. Typy zmiennych Instrukcje warunkowe Pętle Tablice statyczne Funkcje Wskaźniki Referencje Tablice dynamiczne Typ string Przeładowania funkcji

Programowanie i struktury danych

// Liczy srednie w wierszach i kolumnach tablicy "dwuwymiarowej" // Elementy tablicy są generowane losowo #include <stdio.h> #include <stdlib.

Laboratorium 5: Tablice. Wyszukiwanie binarne

Materiał uzupełniający do ćwiczen z przedmiotu: Programowanie w C ++ - ćwiczenia na wskaźnikach

Programowanie w C++ Wykład 5. Katarzyna Grzelak. 26 marca kwietnia K.Grzelak (Wykład 1) Programowanie w C++ 1 / 40

DANE TEKSTOWE W JĘZYKU C/C++ - TABLICE ZNAKOWE

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

W2 Wprowadzenie do klas C++ Klasa najważniejsze pojęcie C++. To jest mechanizm do tworzenia obiektów. Deklaracje klasy :

Biblioteka standardowa - operacje wejścia/wyjścia

// Potrzebne do memset oraz memcpy, czyli kopiowania bloków

Podstawy programowania w języku C++

Programowanie proceduralne INP001210WL rok akademicki 2018/19 semestr letni. Wykład 6. Karol Tarnowski A-1 p.

Języki i metodyka programowania. Wprowadzenie do języka C

Wstęp do programowania

Globalne / Lokalne. Wykład 15. Podstawy programowania (język C) Zmienne globalne / lokalne (1) Zmienne globalne / lokalne (2)

Wstęp do programowania INP001213Wcl rok akademicki 2017/18 semestr zimowy. Wykład 12. Karol Tarnowski A-1 p.

Jeśli chcesz łatwo i szybko opanować podstawy C++, sięgnij po tę książkę.

WYKŁAD 10. Zmienne o złożonej budowie Statyczne i dynamiczne struktury danych: lista, kolejka, stos, drzewo. Programy: c5_1.c, c5_2, c5_3, c5_4, c5_5

Informacje ogólne. Karol Trybulec p-programowanie.pl 1. 2 // cialo klasy. class osoba { string imie; string nazwisko; int wiek; int wzrost;

Struktury, unie, formatowanie, wskaźniki

Szablony klas, zastosowanie szablonów w programach

Wykład 1: Wskaźniki i zmienne dynamiczne

Wykład 5 Wybrane zagadnienia programowania w C++ (c.d.)

Ghost in the machine

. Podstawy Programowania 2. Stos i jego zastosowania. Arkadiusz Chrobot. 6 marca 2018

Transkrypt:

Laboratorium 6: Dynamiczny przydział pamięci dr inż. Arkadiusz Chrobot dr inż. Grzegorz Łukawski 15 maja 2015

1. Wprowadzenie Instrukcja poświęcona jest dynamicznemu przydziałowi i zwalnianiu pamięci w języku c. Opisano w niej również podstawy tworzenia dynamicznych struktur danych. W rozdziale pierwszym zawarte są informacje na temat funkcji umożliwiających alokację i zwalnianie pamięci w trakcie wykonania programu. Rozdział drugi zawiera opis dwóch funkcji, które przydatne są gdy trzeba wykonać pewne proste operacje na zawartości określonych obszarów pamięci operacyjnej. W rozdziale trzecim przedstawiony jest sposób definiowania typów bazowych najczęściej używanych dynamicznych struktur danych. Rozdział czwarty zawiera opis mechanizmu wykrywania wycieków pamięci (ang. memory leaks debugger), który oferowany jest przez kompilator gcc. Ostatni rozdział zawiera przykładowy program używający dynamicznego przydziału pamięci. 2. Funkcje przydziału i zwalniania pamięci Język c oferuje trzy standardowe funkcję do dynamicznego przydziału pamięci i jedną funkcję do jej zwalniania. Aby z nich skorzystać należy w kodzie źródłowym programu załączyć plik nagłówkowy stdlib.h. 2.1. Przydział pamięci Najprostszym sposobem dynamicznego przydziału obszaru pamięci operacyjnej jest użycie funkcji malloc(). Funkcja ta przyjmuje jeden argument, którym jest wyrażony w bajtach rozmiar przydzielanego obszaru pamięci. Argument ten może być podany jako zwykła liczba, ale zazwyczaj do określenia potrzebnej liczby bajtów używamy operatora sizeof(). Funkcja malloc() zwraca wartość typu void *, którą jest adres początku przydzielonego obszaru. Jeśli nie uda się alokacja pamięci o podanym rozmiarze, to funkcja ta zwróci wartość null. Po każdym wywołaniu malloc() powinniśmy sprawdzać, czy nie wystąpił taki przypadek. Zwrócony adres możemy zapisać do zmiennej wskaźnikowej dowolnego typu, ale zaleca się wykonać przy tym rzutowanie na na typ tej zmiennej. Zostanie to pokazane w przykładowym programie. Obszar pamięci przydzielony przez tę funkcję nie jest zerowany i może zawierać przypadkowe wartości. Funkcja calloc() działa podobnie jak malloc(), została jednak stworzona w celu ułatwienia przydziału pamięci na tablice. Pobiera zatem dwa argumenty. Pierwszym jest liczba elementów w tworzonej tablicy, a drugim jest rozmiar pojedynczego elementu wyrażony w bajtach. Przydzielony przez nią obszar pamięci jest wyzerowany. Pod innymi względami nie różni się od malloc() i wszystkie zamieszczone wyżej uwagi do tej funkcji stosują się również do niej. Funkcja realloc() służy do zmiany (zmniejszenia lub zwiększenia) rozmiaru przydzielonego obszaru pamięci. Zawartość pamięci otrzymanej po wywołaniu tej funkcji nie jest inicjowana, co jest szczególnie ważne, jeśli chcemy powiększyć przydzielony obszar bez utraty jego zawartości. Funkcja realloc() jako argumenty przyjmuje wskaźnik na obszar, którego wielkość trzeba zmienić oraz nowy rozmiar obszaru wyrażony w bajtach. Jeśli pierwszy z argumentów będzie miał wartość null, to realloc() zadziała tak samo jak malloc(). Jeśli drugi z argumentów będzie miał wartość zero, to funkcja zwolni przydzielony blok pamięci. Typ wartości zwracanej przez realloc() jest taki sam jak dla wcześniej opisanych funkcji przydziału pamięci. Jeśli zmiana wielkości obszaru się nie powiedzie, a może mieć to miejsce tylko w przypadku jego powiększania, to zwracana jest wartość null, ale sam obszar zostaje nienaruszony i wskaźnik przekazany funkcji jako pierwszy argument będzie nadal wskaźnikiem poprawnym. Wartość null jest zwracana przez funkcję również wtedy, gdy jej drugi argument miał wartość zero. 2.2. Zwalnianie pamięci Do zwalnienia pamięci przydzielonej dowolną z wyżej opisanych funkcji służy funkcja free(). Funkcja ta nie zwraca żadnej wartości, ale przyjmuje jeden argument będący wskaźnikiem na obszar pamięci do zwolnienia. Jeśli ten wskaźnik będzie miał przed wywołaniem free() wartość null, to nie zostanie wykonana żadna operacja. Jeśli jednak będzie on wskazywał na już zwolniony obszar pamięci, to sposób zachowania się funkcji fee() w takim wypadku nie jest określony. Zaleca się zatem, aby zaraz po wywołaniu tej funkcji wskaźnikowi, który był jej argumentem nadać wartość null. Zapobiega to również przypadkowemu użyciu wskaźnika na zwolniony obszar pamięci w innym miejscu programu. 1

3. Manipulowanie obszarami pamięci Po włączeniu w programie pliku nagłówkowego string.h możemy korzystać nie tylko z opisywanych w jednej ze wcześniejszych instrukcji funkcji do manipulacji ciągami znaków, ale również do dwóch funkcji, których działanie jest bardziej ogólne. Obie działają na dowolnych obszarach pamięci o zadanej wielkości. Pierwszą z nich jest funkcja memset(). Służy ona do wypełniania obszaru pamięci zadaną wartością. Jako pierwszy argument przyjmuje wskaźnik na obszar pamięci, który ma zostać wypełniony. Może to być równie dobrze obszar przydzielony dynamicznie, jak i statycznie. Drugim argumentem jest wartość, którą ten obszar trzeba wypełnić, może to być np. znak, a trzecim jest rozmiar wypełnianego obszaru podany w bajtach. Wartością zwracaną jest wskaźnik na początek tego obszaru, ale ta wartość najczęściej jest ignorowana. Funkcja memset() jest często stosowana do zerowania obszarów pamięci, np. tablic. Drugą funkcją jest memcpy(), która służy do kopiowania zawartości obszarów pamięci. Najczęściej za jej pomocą kopiuje się tablice, za wyjątkiem tablic znakowych, w których przypadku używa się opisanych wcześniej strcpy() lub strncpy(). Funkcja memcpy() przyjmuje trzy argumenty. Pierwszym jest wskaźnik na obszar pamięci do którego mają być kopiowane dane. Drugim jest wskaźnik na obszar pamięci, z którego mają być kopiowane dane. Obszary te nie powinny na siebie nachodzić, a więc nie można za pomocą tej funkcji np. kopiować elementów tablicy do niej samej. Trzecim argumentem jest liczba bajtów, które mają być przekopiowane. Funkcja zwraca wskaźnik na obszar docelowy, ale ta wartość jest najczęściej ignorowana. W pliku nagłówkowym string.h zadeklarowano również inne funkcje o działaniu podobnym do memcpy() lub memset(). Są to np. funkcje memmove() i memccpy(). Ich działanie nie będzie jednak opisywane w tej instrukcji. 4. Definiowanie typów bazowych dynamicznych struktur danych Bazowe typy danych takich struktur jak listy, kolejki stosy i drzewa są definiowane z użyciem struktur ze wskaźnikami. Listing 1 zawiera definicję przykładowego bazowego typu danych, który może być użyty do budowy np. jednokierunkowej listy liniowej. Zawarto w nim również deklarację zmiennej wskaźnikowej tego typu. struct list_node { int a; struct list_node *next; }; struct list_node *first; Listing 1: Typ bazowy listy jednokierunkowej i zmienna wskaźnikowa Taki sposób definiowania typu bazowego struktury danych ma tę zaletę, że w każdym miejscu jego użycia jawnie jest zapisane, iż jest to typ bazujący na strukturze. Jego główną wadą jest rozciągłość zapisu. Przy deklarowaniu zmiennych globalnych i lokalnych oraz parametrów i typów wartości zwracanych przez funkcje należy zawsze powtarzać słowo kluczowe struct. Jeśli więc funkcja zwraca wskaźnik na element kolejki i przyjmuje jako argumenty dwa wskaźniki, odpowiednio na początek i koniec tej kolejki, to prototyp takiej funkcji jest bardzo długi, co zmniejsza jego czytelność. Aby poradzić sobie z tym problemem można skrócić zapis typu bazowego używając słowa kluczowego typedef. Służy ono do tworzenia nowych nazw dla wbudowanych lub zdefiniowanych przez programistę typów danych. Sposób jego użycia jest następujący: typedef typ_danych nowa_nazwa; Kod z listingu 1 może zatem zostać zapisany krócej przy użyciu słowa kluczowego typedef (listing 2). 2

typedef struct list_node { int a; struct list_node *next; } node; node *first; Listing 2: Użycie typedef dla typu bazowego dynamicznej struktury danych Dzięki temu rozwiązaniu zapis nazwy typu bazowego znacznie się skraca, jednakże z tego zapisu nie możemy wywnioskować jaki tak na prawdę jest to typ. Informacja o tym, że jego definicja oparta jest na strukturze zostaje zatracona. To rozwiązanie można więc swobodnie stosować tylko w programach o niewielkich rozmiarach. W innych przypadkach należy stosować je rozważnie, a korzystać głownie z pierwszego z przedstawionych sposobów definiowania typów bazowych dla dynamicznych struktur danych. 5. Wykrywanie wycieków pamięci Jednym z najczęściej popełnianych przez programistów błędów podczas korzystania z dynamicznego przydziału pamięci jest tracenie adresów do przydzielonych obszarów pamięci. W ten sposób powstają tzw. wycieki pamięci, czyli obszary pamięci, do których nie można się dostać i nie można ich zwolnić. W przeciwieństwie do takich błędów jak użycie niewłaściwego wskaźnika, przyczyny wycieków pamięci są trudne do wykrycia, bowiem ich skutki ujawniają się zawsze z pewnym opóźnieniem. Najczęściej wtedy, gdy nie ma już wolnej pamięci dla nowych przydziałów i nie można zwolnić przydzielonych obszarów, bo albo są w użyciu, albo uległy wyciekowi. Kompilator gcc zapewnia mechanizm, który ułatwia lokalizację przyczyn wycieków pamięci. Ten mechanizm składa się z dwóch funkcji języka c oraz jednego polecenia powłoki. Aby go użyć najpierw do kodu źródłowego musimy dołączyć plik nagłówkowy mcheck.h. Następnie w kodzie źródłowym, przed wszystkimi przydziałami pamięci umieszczamy wywołanie funkcji mtrace(). Wywołanie drugiej funkcji, jaką jest muntrace() umieszczamy za wszystkimi zwolnieniami obszarów przydzielonej pamięci. Program powinniśmy skompilować z opcją dodającą informacje dla debuggera, czyli -g. Przed uruchomieniem skompilowanego programu należy w powłoce systemowej utworzyć zmienną środowiskową o nazwie malloc_trace, której wartością będzie ścieżka dostępu do pliku tekstowego zawierającego informacje o ewentualnych wyciekach pamięci. W przypadku powłoki bash możemy taką zmienną stworzyć następująco: export malloc_trace=./plik.txt Proszę zwrócić uwagę na brak spacji po obu stronach znaku =. Kropka w ścieżce dostępu oznacza katalog bieżący. Jeśli plik memory.txt nie istnieje, to zostanie automatycznie stworzony po wydaniu tego polecenia. Po uruchomieniu programu informacje o ewentualnych wyciekach będą zapisane w tym pliku. Jego zawartość nie jest łatwa do odszyfrowania, dlatego stworzono osobny program - polecenie powłoki, który przedstawia zawarte w nim informacje w sposób czytelny. To polecenie nazywa się również mtrace i używamy go następująco: mtrace plik_wykonywalny plik.txt, gdzie plik_wykonywalny jest plikiem ze skompilowanym programem, a plik.txt zawiera dane o ewentualnych wyciekach pamięci. Jeśli do nich nie doszło, to na ekranie zobaczymy zdanie: No memory leaks. W przeciwnym przypadku zostanie wydrukowana tabela zawierająca informację o adresach obszarów pamięci, które nie zostały zwolnione, ich rozmiarze oraz miejscu w kodzie, gdzie zostały one utworzone. Liczby będą wypisane w kodzie szesnastkowym. 3

6. Przykładowy program - użycie stosu 1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<string.h> 4 #include<mcheck.h> 5 6 typedef struct stack_node 7 { 8 char arg[100]; 9 struct stack_node *next; 10 } node; 11 12 node *push(node *top, char *str) 13 { 14 node *new_node = NULL; 15 new_node = (node*)malloc(sizeof(node)); 16 if(new_node==null) { 17 fprintf(stderr,"błąd przydziału pamięci\n"); 18 return top; 19 } 20 strncpy(new_node->arg,str,100); 21 new_node->next=top; 22 return new_node; 23 } 24 25 node *pop(node **top) 26 { 27 node *next = NULL, *old_top = NULL; 28 if(*top!=null) { 29 next=(*top)->next; 30 (*top)->next = NULL; 31 old_top = *top; 32 *top = next; 33 } 34 return old_top; 35 } 36 37 int main(int argc, char **argv) 38 { 39 node *top = NULL; 40 int i; 41 mtrace(); 42 for(i=0;i<argc;i++) 43 top = push(top,argv[i]); 44 while(top) { 45 node *tmp = pop(&top); 46 printf("%s\n",tmp->arg); 47 free(tmp); 48 tmp=null; 49 } 50 muntrace(); 51 return 0; 52 } Listing 3: Program, który odkłada na stos swoje argumenty 4

Listing 3 zawiera kod programu odkładającego swoje argumenty wywołania na stos. Jeśli ten program zostanie wywołany bez argumentów, to na stos zostanie odłożona, a następnie zdjęta i wydrukowana na ekranie nazwa pod jaką został wywołany. Jeśli zostaną mu przekazane argumenty wywołania w dowolnej liczbie, to również zostaną one odłożone na stosie, a potem zdjęte i wydrukowane w osobnych wierszach na ekranie wraz z nazwą. Ze względu na specyfikę działania stosu te informacje zostaną wypisane w odwrotnej kolejności. W wierszach 6-9 zdefiniowano przy użyciu typedef typ bazowy stosu 1. Funkcja push() (wiersze 11-22) zapisuje poszczególne argumenty wywołania programu na stosie. Odpowiedzialna jest ona za tworzenie i dodawanie nowych elementów stosu. Ta funkcja przyjmuje dwa argumenty: wskaźnik na szczyt stosu (może mieć wartość null) i wskaźnik na ciąg znaków, który ma być zapisany w nowym elemencie stosu. Zwraca ona wskaźnik na nowy szczyt stosu lub stary szczyt stosu, jeśli nie udało się utworzyć nowego elementu. Do wypisywania informacji o niepowodzeniu realizacji przydziału pamięci (wiersz 16) wykorzystywana jest funkcja fprintf(). Jako pierwszy argument został jej podany wskaźnik do standardowego strumienia diagnostycznego (stderr), który związany jest z ekranem monitora. Z pewnych względów, które nie będą tu opisane, jest to rozwiązanie lepsze niż stosowanie funkcji printf() do wyświetlania opisu błędu. Do kopiowania argumentu, który zapisany jest w postaci łańcucha znaków (wiersz 19) użyta została funkcja strncpy(), gdyż pozwala ona ograniczyć liczbę kopiowanych znaków do wielkości, która mieści się w polu arg pojedynczego elementu stosu. Funkcja pop() zdejmuje pojedynczy element ze szczytu stosu. Jej implementacja jest bardziej skomplikowana niż funkcji push(). Ponieważ musi być zmodyfikowany wskaźnik na szczyt stosu, to do tej funkcji, jako argument, przekazywany jest jego adres (wiersz 44), stąd parametrem funkcji jest podwójny wskaźnik typ node (wiersz 24). Jego użycie komplikuje trochę odwołania do poszczególnych pól elementu szczytowego stosu (wiersze 28 i 29), ale jest niezbędne do prawidłowego działania programu. Funkcja pop() nie zwalnia pamięci przydzielonej na szczytowy element, a jedynie odłącza go od reszty stosu i zraca jego adres. Zwalnianie dokonywane jest po jej wywołaniu, w wierszu 46. Ponieważ argument wywołania programu zapisywany jest w tablicy znaków będącej integralną częścią pojedynczego elementu stosu, to rozwiązanie polegające na zwróceniu wskaźnika na łańcuch znaków i zwolnienie elementu w funkcji pop() jest błędne i nie zostało tu zastosowane. Przed odwołaniem do szczytowego elementu stosu funkcja sprawdza, czy on istnieje (wiersz 27). Jeśli nie, to zwracana jest przez nią wartość null. Obie opisane funkcje są wywoływane w funkcji main(). Proszę zwrócić uwagę na inicjowanie we wszystkich funkcjach zmiennych lokalnych, które są wskaźnikami, a także na nadawanie wskaźnikowi tmp wartości null, tuż po wywołaniu funkcji free(). Sposób wywołania funkcji pop() i push() pokazany jest w wierszach 44 i 42. Jeśli po skompilowaniu i wykonaniu wszystkich niezbędnych czynności opisanych w poprzednim rozdziale, uruchomimy program z np. trzema argumentami, to polecenie mtrace wypisze na ekranie informację, że nie ma żadnych wycieków pamięci. Jeśli jednak umieścimy w komentarzu wiersz 46, to po wykonaniu polecenia mtrace zobaczymy na ekranie informację podobną do tej: Memory not freed: ----------------- Address Size Caller 0x00000000018b1460 0x70 at /home/arek/programming/c/mtrace/stack.c:14 0x00000000018b14e0 0x70 at /home/arek/programming/c/mtrace/stack.c:14 0x00000000018b1560 0x70 at /home/arek/programming/c/mtrace/stack.c:14 0x00000000018b15e0 0x70 at /home/arek/programming/c/mtrace/stack.c:14 1 Numery wierszy nie stanowią części kodu, zostały dodane wyłącznie w celu ułatwienia jego opisu. 5