Download presentation
Presentation is loading. Please wait.
1
תרגול 12 Standard Template Library כתיבת אלגוריתמים גנריים מצביעים חכמים
2
ספרית התבניות הסטנדרטית קיימת בכל מימוש של ++C מכילה אוספים (Containers) ואלגוריתמים. משתמשת בתבניות (templates): –אוספי הנתונים גנריים –האלגוריתמים המסופקים גנריים –מאפשרת הרחבה ע " י המשתמש –שומרת על ביצועים גבוהים 2
3
Vector נכיר תחילה את האוסף הפשוט ביותר בספריה – vector. –מערך סדור של איברים –מאפשר גישה לכל איבר באוסף –מאפשר הוספת והסרת איברים הערה : הקוד בשקפים הוא חלקי וחסרות בו תכונות נוספות ומתקדמות יותר של אוספי ה -STL. דוגמאות הקוד מתרכזות בשימוש בסיסי ופשוט. 3
4
מתודות נבחרות של vector template class vector { vector(); vector(const vector& c); vector(size_t num, const T& val = T()); ~vector(); T& operator[](size_t index); const T& operator[](size_t index) const; vector operator=(const vector& v); T& at(size_t loc); const T& at(size_t loc) const; void pop_back(); void push_back(const T& val); size_t size() const; }; הערה : גישה בעזרת אופרטור [] אינה בטוחה אינה מוודאת את חוקיות האינדקס לעומת זאת, גישה בעזרת at() זורקת חריגה במקרה והאינדקס אינו חוקי מסוג out_of_range 4
5
דוגמאות לשימוש ב -vector int main() { vector v1; // empty vector vector v2(20, 8);// 20 integers, all of value 8 for(size_t i = 0; i < v2.size(); ++i) { cout << v2[i]; // Unsafe access by index } for(int i = 0; i < 10; ++i) { v1.push_back(i); // Inserts items at the end of the vector } while(v1.size() > 0) { v1.pop_back(); } const vector copy(v1);//Safely copies a vector return 0; } 5
6
יתרונות ניהול זיכרון ע " י המחלקה –לא צריך לזכור לשחרר ידנית את המערך –ניתן לשנות את גודל המערך בקלות –ניתן ליצור העתקים ולבצע השמות בקלות גישה בטוחה –רק כאשר משתמשים ב -at() 6
7
שאלה : כיצד ניתן ליצור וקטור בטוח ? בד " כ מחיר בדיקת גישה לוקטור אינו משפיע על ביצועי התכנית. ניצור גרסה של וקטור המוודאת לכל גישה האם היא חוקית : template class SafeVector : public vector { SafeVector() : vector () {} SafeVector(int s) : vector (s) {} T& operator[](int i) { return at(i); } const T& operator[](int i) const { return at(i); } }; 7
8
List דומה לוקטור, אך המימוש הוא ברשימה מקושרת. –שימוש ברשימה מקושרת מאפשר הוספת איברים מהירה יותר, אך אינו מאפשר גישה מהירה לאמצע האוסף. –ההבדל העיקרי בין list ל -vector הוא במימוש, לכן לשני האוספים מנשק דומה. –השימוש במנשק דומה מאפשר למשתמש להחליף בקלות את סוג האוסף בשימוש גם בשלבים מאוחרים. כאמור, סיבוכיות היא שיקול משני בד " כ ולכן ניתן להשתמש ב -vector, ורק בהמשך כשיש צורך ברשימה מקושרת ניתן לעבור לשימוש בה. 8
9
מתודות נבחרות מ -list template class list { list(); list(const list& c); list(size_t num, const T& val = T()); ~list(); list operator=(const list& v); void pop_back(); void push_back(const T& val); void pop_front(); void push_front(const T& val); size_t size() const; }; נוספו מתודות להסרת והוספת איברים גם בראש הרשימה נעלמו המתודות לגישה לאיבר באמצע הרשימה כיצד נוכל לאפשר גישה למשתמש בצורה נוחה ? 9
10
Iterators ע " מ לאפשר מעבר סדור על איברי אוסף נשתמש בעצם מסוג Iterator. נדרוש את הפעולות הבאות מ -Iterator ( בינתיים ): –קידום – שינוי האיטרטור כך שיצביע לאיבר הבא –קריאה – החזרת האיבר המוצבע ע " י האיטרטור –השוואה – בדיקה האם שני איטרטורים מצביעים לאותו איבר איטרטור אינו מחלקה, אלא מושג (concept). למשל מצביע הוא איטרטור עבור מערך של C: int* array = new int[n];... int* i = array; cout << *i << endl; // Reading the iterator i++; // Advancing the iterator by 1 if (i == array+n) { // Comparing to iterators cout << "End of array!" << endl; } 10
11
Iterators & STL כל האוספים המאפשרים מעבר על איבריהם ב -STL מספקים iterator בעזרתו ניתן לעבור על איבריהם. המתודה begin() מחזירה iterator מתאים לתחילת האוסף. המתודה end() מחזירה iterator מתאים לאיבר " דמה " אחרי האיבר האחרון. למשתמש לא אכפת כיצד ממומש האיטרטור ! 11
12
דוגמה לשימוש בוקטור הדפסת איברי vector: for(vector ::iterator i = v.begin();i != v.end(); ++i) { cout << *i << endl; } הדפסת איברי list: for(list ::iterator i = l.begin(); i != l.end(); ++i) { cout << *i << endl; } בצורה דומה ניתן לגשת לכל אוסף ב -STL הטיפוס עבור ה -iterator מוגדר בתוך כל אוסף בצורה הבאה : container ::iterator 12
13
const_iterator בנוסף כל אוסף מגדיר גם טיפוס const_iterator. –כך ניתן להגן על האוסף מפני שינויים כאשר הוא מוגדר כ -const –טיפוס האיטרטור המוחזר נקבע בעזרת העמסה על המתודות המתאימות vector v; vector ::iterator i = v.begin(); *i = 7; // O.K.!! // *i is int& const vector v; vector ::const_iterator i = v.begin(); *i = 7; // syntax error! // *i is const int& 13
14
עוד שימושים רוב המתודות עבור האוספים משתמשות באיטרטורים כדי לציין מיקומים. –הוספת איבר לתוך רשימה ממוינת : list ::iterator i = myList.begin(); while (i != myList.end() && num < *i){ ++i; } myList.insert(i, num); // Insert num before I –מחיקת האיבר החמישי בוקטור : myVector.erase(v.begin()+4); 14
15
סוגי איטרטורים אוספים שונים מחזירים איטרטורים שונים. ל -list, למשל, יש bidirectional iterator המאפשר קידום האיטרטור עם אופרטור ++ והחזרתו לאחור עם אופרטור --. ל -vector יש random access iterator המאפשר גם חיבור מספרים לאיטרטור וחישוב הפרש בין שני איטרטורים ( בדומה למצביע ). 15
16
עוד STL? ל -vector ו -list מס ' רב של מתודות ואופרטורים נוספים. –כמעט כל פעולה שעולה בדעתכם לבצע כבר ממומשת ב -STL. –ניתן פשוט לחפש באינטרנט : למשל http://www.cppreference.com/.http://www.cppreference.com/ יש עוד אוספים ב -STL. –למשל set, stack ומבני נתונים נוספים שלא נלמדו בקורס. 16
17
כתיבת אלגוריתמים גנריים ברצוננו לכתוב פונקציה אשר מוצאת את האיבר המקסימלי. כיצד נוכל לאפשר לפונקציה לעבוד על כל אוסף אפשרי ? 17
18
אלגוריתם max template Iterator max(Iterator start, Iterator end) { if (start == end) { return end; } Iterator maximum = start; for(Iterator i = ++start; i != end; ++i) { if (*i > *maximum) { maximum = i; } return maximum; } 18
19
אלגוריתם max - שימושים דוגמאות לשימוש : –מציאת הערך הגדול ביותר בוקטור. int m = *max(myVector.begin(), myVector.end()); – מחיקת האיבר הגדול ביותר ברשימה. myList.erase(max(myList.begin(), myList.end())); 19
20
יתרונות אלגוריתם יחיד מתאים לכל המקרים : –אוספים שונים –רק חלקים מהאוסף ניתנים למעבר –קל להתאים קוד קיים שיעבוד עם האלגוריתם למשל שימוש באלגוריתם עבור מערך רגיל של C: int* myArray = new int[size];... int m = *max(myArray, myArray+size); 20
21
algorithm מלבד מבני הנתונים ה -STL מכיל גם אוסף אלגוריתמים מועילים הניתנים להפעלה על כל מבנה נתונים מתאים : חלקם פשוטים : Iterator find(Iterator start, Iterator end, const T& val); וחלקם פחות : void sort(Iterator start, Iterator end); void random_shuffle(Iterator start, Iterator end); 21
22
Function Objects במקום לשלוח מצביעים של פונקציות ב -STL משתמשים ב -“Function objects”. Function object הוא כל עצם המעמיס את אופרטור () ( אופרטור ההפעלה ) 22
23
דוגמה ל -Function Object class LessThanZero { public: bool operator()(int m) const { return m < 0; } }; int main() { LessThanZero isNegative; cout << isNegative(-4) << endl; // true cout << isNegative(6) << endl; // false return 0; } את המחלקה מגדירים עם אופרטור ההפעלה. במקרה זה אנו מגדירים את המחלקה כפונקציה המקבלת int ומחזירה bool. 23
24
דוגמה ל -Function Object class SumOf { public: bool operator()(int a, int b, int s) { return a+b == s; } }; int main() { SumOf isSum; cout << isSum(2,3,5) << endl; // true cout << isSum(1,2,5) << endl; // false return 0; } אופרטור ההפעלה ניתן להגדרה עם כל מספר של פרמטרים. 24
25
דוגמה ל -Function Object class LessThan { public: LessThan(int n) : n(n) {} bool operator()(int m) const{ return m < n; } private: int n; }; int main() { LessThan isLess(5); LessThan isLess2(8); cout << isLess(4) << endl; // true cout << isLess(6) << endl; // false cout << isLess2(6) << endl; // true return 0; } עד עכשיו לא הצגנו את היתרון על שימוש במצביע לפונקציה : בניגוד למצביעים לפונקציות, ניתן לזכור מצב ב -Function Object ולכן ניתן להשתמש באותה מחלקה למס ' פונקציות השונות רק בפרמטר כלשהו. ( היזכרו כיצד פתרנו את אותה בעיה ב -C) שימו לב להבדל בין קריאה לבנאי להפעלת האופרטור המתבצעת על עצם מהטיפוס. 25
26
Function Objects - דוגמה נוכל להשתמש בפונקציה שהגדרנו בקריאה לאלגוריתמים מסוימים : template Iterator find_if(Iterator first, Iterator last, Predicate pred) { for (; first != last; first++) if (pred(*first)) break; return first; } vector v; vector ::iterator i = find_if(v.begin(),v.end(),LessThan(2)); vector ::iterator j = find_if(v.begin(),v.end(),LessThan(7)); 26
27
דוגמה נוספת ל -Function Objects ניצור קריטריון מיון חדש למספרים שלמים, נמיין לפי ערך המספר מודולו m: class LessThanModulo { public: LessThanModulo(int m) : m(m) {} bool operator()(int a, int b) {return a%m < b%m;} private: int m; }; בכדי למיין פשוט נקרא לאלגוריתם המיון עם פרמטר נוסף : sort(v.begin(), v.end(), LessThanModulo(5)); 27
28
הוספת איטרטורים ניזכר במחלקת String שהגדרנו בתרגול 9, כיצד נוכל להשתמש באלגוריתמים הקיימים עבור מחלקה זו ? עלינו להוסיף תמיכה באיטרטורים במחלקה. 28
29
הוספת תמיכה באיטרטורים ב -String class String { private: int length; char* value; public:... typedef char* iterator; typedef const char* const_iterator; iterator begin() { return value; } const_iterator begin() const { return value; } iterator end() { return value+length; } const_iterator end() const { return value+length; }... }; במקרה של String, נוכל פשוט להשתמש במצביע ל -char. שימו לב להגדרות הטיפוסים שאנו מוסיפים, כדי שהמשתמש לא יצטרך לדעת בעצמו שאנו משתמשים במצביע ל -char 29
30
שימוש האלגוריתם transform מפעיל פונקציה על תחום מסוים ומציב לתחום אחר את ערך ההחזרה. נשתמש בו ובפונקצית הספריה של C toupper() כדי להמיר מחרוזת לאותיות גדולות. transform(str.begin(),str.end(),str.begin(),toupper); 30
31
סיכום הספריה הסטנדרטית בנויה מאוספים ואלגוריתמים איטרטורים מהווים את הדבק המאפשר לשני חלקים אלו להישאר גנריים אוספיםאוספיםאלגוריתמיםאלגוריתמים איטרטוריםאיטרטורים 31
32
מצביעים חכמים מצביעים אחראים לרובן המוחלט של שגיאות הזיכרון הצלחנו להיפטר מרוב השימושים הלא בטוחים במצביעים בעזרת שימוש בתכונות של ++C –ניצול בנאים, הורסים והשמות לטיפול מסודר בזיכרון. –שימוש ב -STL ואוספים לטיפול מסודר באלגוריתמים של מבני הנתונים. 32
33
בעיה vector shapes; Circle circle(3.5); shapes.push_back(circle); // Only the "Shape part" // is copied!! vector shapes;... delete shapes.back(); shapes.pop_back(); // We must delete ourselves // when removing items from // the vector ירושות דורשות שימוש במצביעים עבודה עם מצביעים מוחקת את כל היתרונות שהשגנו ! –צריך לנהל זיכרון בצורה מפורשת בכל מקום בתוכנית ! 33
34
smart_ptr נוכל ליצור מחלקה, המתנהגת כמצביע אך מוסיפה התנהגות נוספת. –למשל, שחרור העצם המוצבע בהריסת המצביע. כלומר ניצור מצביע " חכם " יותר. 34
35
smart_ptr template class smart_ptr { private: T* pointedData; typedef T element_type; public: explicit smart_ptr(T* ptr = NULL) : pointedData(ptr) {} ~smart_ptr() { delete pointedData; } T& operator*() const { return *pointedData; } T* operator->() const { return pointedData; } }; 35
36
יתרונות שימוש ב -smart_ptr מה קורה במקרה וה -new השני נכשל בכל אחת מהפונקציות ? int bad() { Shape* ptr1 = new Circle(5.0); Shape* ptr2 = new Square(5.0);... } int good() { smart_ptr ptr1(new Circle(5.0)); smart_ptr ptr2(new Square(5.0));... } 36
37
העתקה כיצד יתנהג המצביע במקרה של העתקה ? –מה צריכה להיות ההתנהגות עבור בנאי העתקה והשמה ? ניסיון ראשון : העתקת המצביע תבצע " העברת בעלות " של העצם המוצבע –התנהגות זו ממומשת ב -auto_ptr הקיים בספריה הסטנדרטית של ++C. –מאפשרת העברת / החזרת עצמים מוקצים דינאמית מפונקציות. 37
38
שימוש ב -auto_ptr שימוש ב -auto_ptr כדי להחזיר עצם חדש : –בניגוד למצביע רגיל – מוגדר בקוד מי אחראי לשחרור העצם. auto_ptr createMyShape() { return auto_ptr (new Circle(5.0)); } שימוש ב -auto_ptr כדי לחסוך העתקות : auto_ptr > getNPrimeNumbers (int n) {... } 38
39
העתקה בעיה ! ההתנהגות עבור " העתקה " של auto_ptr אינה סטנדרטית –ההעתק אינו זהה למקור ! לא ניתן לאחסן auto_ptr בתוך אוספים של ה -STL מאחר והם מסתמכים על תכונה זו. כיצד ניתן ליצור מצביע חכם שגם יאפשר העתקה נכונה ? 39
40
שיפור – מצביע משותף ניצור מצביע חכם יותר מתקדם : shared_ptr לכל עצם מוצבע נשמור מונה הצבעות, וכל המצביעים ישתמשו במונה זה על מנת לדעת מתי יש לשחרר את העצם המוצבע. Object 1 1 2 2 Object* ptr int* counter shared_ptr Object* ptr int* counter shared_ptr Object* ptr int* counter shared_ptr 40
41
משתנים ואינווריאנטה template class shared_ptr { private: T* pointedData; int* referenceCounter; typedef T element_type; void checkInvariant() const { assert((pointedData && referenceCounter && (*referenceCounter)>0) || (!pointedData && !referenceCounter)); } 41
42
עדכון המונה void increaseCount() { checkInvariant(); if (referenceCounter) { (*referenceCounter)++; } void decreaseCount() { checkInvariant(); if(referenceCounter && --*referenceCounter == 0){ delete referenceCounter; delete pointedData; referenceCounter = NULL; pointedData = NULL; } 42
43
אתחול ושחרור public: explicit shared_ptr(T* ptr = NULL) : pointedData(ptr), referenceCounter(pointedData ? new int(1) : NULL){} shared_ptr(const shared_ptr & other) : pointedData(other.pointedData), referenceCounter(pointedData ? other.referenceCounter : NULL) { increaseCount(); checkInvariant(); } 43
44
אתחול ושחרור template friend class shared_ptr; template shared_ptr(const shared_ptr & other) : pointedData(other.pointedData), referenceCounter(pointedData ? other.referenceCounter : NULL) { increaseCount(); checkInvariant(); } ~shared_ptr() { checkInvariant(); decreaseCount(); checkInvariant(); } מטרת הבנאי היא לאפשר השמה בין shared_ptr של טיפוסים שונים, למשל בגלל הורשה. 44
45
גישה למצביע T& operator*() const { checkInvariant(); assert(pointedData != NULL); return *pointedData; } T* operator->() const { checkInvariant(); assert(pointedData != NULL); return pointedData; } T* get() const { checkInvariant(); return pointedData; } המתודה get() מסופקת בכדי לאפשר שימוש של המחלקה גם עם קוד ישן שאינו תומך ב -shared_ptr 45
46
אופרטור השמה template shared_ptr & operator=( shared_ptr & other) { checkInvariant(); other.increaseCount(); decreaseCount(); referenceCounter = other.referenceCounter; pointedData = other.pointedData; checkInvariant(); return *this; } מה קורה במקרה של הצבה עצמית ? מדוע אין צורך בבדיקה ? 46
47
operator bool() { checkInvariant(); return pointedData; } void reset() { decreaseCount(); pointedData = NULL; referenceCounter = NULL; } }; המרה לערך בוליאני תאפשר לנו לבדוק האם המצביע תקין כמו מצביע רגיל. המתודה reset() מאפשרת למשתמש צורה נוחה יותר לאיפוס מצביע עוד מתודות שימושיות 47
48
פונקציות השוואה template bool operator==(const shared_ptr & first, const shared_ptr & second) { return first.get() == second.get(); } template bool operator!=(const shared_ptr & first, const shared_ptr & second) { return !(first==second); } template bool operator & first, const shared_ptr & second) { return first.get() < second.get(); } 48
49
דוגמאות vector > function() { shared_ptr shape(new Polygon(points)); shared_ptr circle(new Circle(5.0)); shared_ptr square(new Square(2.0)); vector > vec; vec.push_back(shape); vec.push_back(circle); vec.push_back(square); vector > copy = vec; copy.pop_back(); return copy; } כל ההעתקות בטוחות ! האחרון שיוצא סוגר את האור ! תרגיל לבית ( למשועממים ): מיצאו את הצורה שיש למחוק בסוף הפונקציה... איך היה נראה הקוד ללא שימוש במצביעים חכמים ? 49
50
יתרונות כל הקוד לניהול הזיכרון נכתב רק פעם אחת. קל לחלוק עצמים אשר לא ברור מי האחרון שמשתמש בהם. ניתן להשתמש בכל האוספים שראינו עם מצביעים ובפרט עם ירושות. הקוד נהייה פשוט יותר ועמיד יותר בפני שגיאות זיכרון. –מה קורה בדוגמה הקודמת אם אחת מהקצאות הזיכרון נכשלת ? מה היה קורה ללא המצביעים החכמים ? 50
51
לא הכל מושלם ! shared_ptr אינו מתאים לאחסון מצביע למערך ! –כי השחרור הוא ע " י delete ולא delete[]. –ניתן ליצור בקלות shared_array שיתאים לבעיה זו. אבל ממילא עדיף להשתמש ב -vector! המצביעים לא ישוחררו אם קיימים קשרים מעגליים –במקרה זה יש לנקות את המצביעים בצורה מפורשת עם reset. –פתרון טוב יותר למקרה זה דורש היכרות עם מצביע חכם נוסף והזמן קצר מלהכיל אותו. 51
52
לסיכום כתיבת ספריות ב -++C היא מסובכת. השימוש בהן לעומת זאת הוא פשוט. –מאפשר כתיבת קוד טוב יותר בפחות זמן ופחות שורות. ניתן למצוא מימוש פשוט של shared_ptr באתר הקורס. 52
Similar presentations
© 2024 SlidePlayer.com Inc.
All rights reserved.