420 likes | 768 Views
תרגול מס' 12. ספרית התבניות הסטנדרטית ( STL ) כתיבת אלגוריתמים גנריים מצביעים חכמים. ספרית התבניות הסטנדרטית ( STL ). vector list iterator. ספרית התבניות הסטנדרטית. קיימת בכל מימוש של ++ C מכילה אוספים (Containers) ואלגוריתמים משתמשת בתבניות (templates) : אוספי הנתונים גנריים
E N D
תרגול מס' 12 ספרית התבניות הסטנדרטית (STL) כתיבת אלגוריתמים גנריים מצביעים חכמים
ספרית התבניות הסטנדרטית (STL) vector list iterator מבוא לתכנות מערכות - 234122
ספרית התבניות הסטנדרטית • קיימת בכל מימוש של ++C • מכילה אוספים (Containers)ואלגוריתמים • משתמשת בתבניות (templates): • אוספי הנתונים גנריים • האלגוריתמים המסופקים גנריים • מאפשרת הרחבה ע"י המשתמש • שומרת על יעילות הקוד מבוא לתכנות מערכות - 234122
std::vector • נכיר תחילה את האוסף הפשוט ביותר בספריה – vector. • מערך סדור של איברים • מאפשר גישה לכל איבר באוסף • מאפשר הוספת והסרת איברים • הערה: הקוד בשקפים הוא חלקי וחסרות בו תכונות נוספות ומתקדמות יותר של אוספי ה-STL.דוגמאות הקוד מתרכזות בשימוש בסיסי ופשוט. מבוא לתכנות מערכות - 234122
מתודות נבחרות של vector template<typenameT> classvector { vector(); vector(constvector& c); vector(size_tnum, constT& val = T()); ~vector(); T& operator[](size_t index); constT& operator[](size_t index) const; vectoroperator=(constvector& v); T& at(size_tloc); constT& at(size_tloc) const; voidpop_back(); voidpush_back(constT& val); size_tsize() const; }; • גישה בעזרת אופרטור [] אינה בטוחה • אינה מוודאת את חוקיות האינדקס • גישה בעזרת at() זורקת חריגה מסוג out_of_range עבור אינדקס לא חוקי מבוא לתכנות מערכות - 234122
דוגמאות לשימוש ב-vector intmain() { vector<int> v1; // empty vector vector<int> 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(); } constvector<int> copy(v1); //Safely copies a vector return 0; } מבוא לתכנות מערכות - 234122
יתרונות • ניהול זיכרון ע"י המחלקה • לא צריך לזכור לשחרר ידנית את המערך • ניתן לשנות את גודל המערך בקלות • ניתן ליצור העתקים ולבצע השמות בקלות • גישה בטוחה • רק כאשר משתמשים ב-at() • כיצד נוכל ליצור וקטור בטוח עבור []? template<typenameT> classSafeVector : publicvector<T> { SafeVector() : vector<T>() {} SafeVector(int s) : vector<T>(s) {} T& operator[](int i) { returnat(i);} constT& operator[](int i) const{ returnat(i);}}; מבוא לתכנות מערכות - 234122
std::list • דומה לוקטור, אך המימוש הוא ברשימה מקושרת • שימוש ברשימה מקושרת מאפשר הוספת איברים מהירה יותר, אך אינו מאפשר גישה מהירה לאמצע האוסף • ההבדל העיקרי בין list ל-vector הוא במימוש, לכן לשני האוספים מנשק דומה • השימוש במנשק דומה מאפשר למשתמש להחליף בקלות את סוג האוסף בשימוש גם בשלבים מאוחרים • כאמור, סיבוכיות היא שיקול משני בד"כ ולכן ניתן להשתמש ב-vector, ורק בהמשך כשיש צורך ברשימה מקושרת ניתן לעבור לשימוש בה מבוא לתכנות מערכות - 234122
מתודות נבחרות של list template<typenameT> classlist { list(); list(constlist& c); list(size_tnum, constT& val = T()); ~list(); listoperator=(constlist& v); voidpop_back(); voidpush_back(constT& val); voidpop_front(); voidpush_front(constT& val); size_tsize() const; }; • נוספו מתודות להסרת והוספת איברים גם בראש הרשימה • מתודות לגישה לאיבר באמצע הרשימה נעלמו • כיצד נוכל לאפשר גישה למשתמש בצורה נוחה? מבוא לתכנות מערכות - 234122
איטרטורים - Iterators • על מנת לאפשר מעבר סדור על איברי אוסף נשתמש באיטרטור • איטרטור יצטרך לאפשר את הפעולות הבאות לפחות: • קידום: שינוי האיטרטור כך שיצביע לאיבר הבא • קריאה: החזרת האיבר המוצבע ע"י האיטרטור • השוואה: בדיקה האם שני איטרטורים מצביעים לאותו איבר • איטרטור אינו מחלקה,אלא מושג (concept) • למשל מצביע הוא איטרטורעבור מערך של C int* array = newint[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; } מבוא לתכנות מערכות - 234122
איטרטורים וה-STL • כל האוספים המאפשרים מעבר על איבריהם ב-STL מספקים iterator בעזרתו ניתן לעבור על איבריהם • המתודה begin() מחזירה iterator מתאים לתחילת האוסף • המתודה end() מחזירה iterator מתאים לאיבר "דמה" אחרי האיבר האחרון • למשתמש לא אכפת כיצד ממומש האיטרטור! מבוא לתכנות מערכות - 234122
דוגמאות לשימוש באיטרטורים • הדפסת איברי vector: • הדפסת איברי list: • ניתן לגשת בצורה דומה לכל אוסף ב-STL המאפשר גישה לאיבריו • הטיפוס עבור ה-iterator המתאים מוגדר בתוך כל אוסף בעזרת השם container<int>::iterator for(vector<int>::iterator i = v.begin();i != v.end(); ++i) { cout << *i << endl; } for(list<int>::iterator i = l.begin();i != l.end(); ++i) { cout << *i << endl; } מבוא לתכנות מערכות - 234122
const_iterator • בנוסף כל אוסף מגדיר גם טיפוס const_iterator • כך ניתן להגן על האוסףמפני שינויים כאשר הואמוגדר כ-const • טיפוס האיטרטור המוחזרנקבע בעזרת העמסה עלהמתודות המתאימות vector<int> v; vector<int>::iterator i = v.begin(); *i = 7; // O.K.! *i is int& constvector<int> v; vector<int>::const_iterator i = v.begin(); *i = 7; // error! *i is constint& מבוא לתכנות מערכות - 234122
עוד שימושים • רוב המתודות עבור האוספים משתמשות באיטרטורים כדי לציין מיקומים • הוספת איבר לתוך רשימה ממוינת: • מחיקת האיבר החמישי בוקטור: • שינויים באוסף עלולים לגרום לביטול התוקף של איטרטור (invalidation) • גישה לאיטרטור שאינו בתוקף אינה מוגדרת • למשל vector::insert עלולה לגרום לכל האיטרטורים לצאת מתוקף • מומלץ להניח את ההנחה המחמירה שכל שינוי של אוסף מוציא מתוקף את כל האיטרטורים שלו • אם הנחה זו מחמירה מדי ניתן למצוא מידע מדויק בתיעוד האוסף list<double>::iterator i = myList.begin(); while(i != myList.end() && num < *i) { ++i;} myList.insert(i, num); // Insert num before i myVector.erase(v.begin() + 4); מבוא לתכנות מערכות - 234122
סוגי איטרטורים • אוספים שונים מחזירים איטרטורים שונים • ל-listלמשל, יש bidirectional iterator המאפשר קידום האיטרטור עם אופרטור++ והחזרתו לאחור עם אופרטור-- • ל vector יש random access iteratorהמאפשר גם חיבור מספרים לאיטרטור וחישוב הפרש בין שני איטרטורים (בדומה למצביע) • ל-vector ו-list מס' רב של מתודות ואופרטורים נוספים • כמעט כל פעולה שעולה בדעתכם לבצע כבר ממומשת ב-STL • ניתן פשוט לחפש באינטרנט: • למשל http://www.cppreference.com/ • יש עוד אוספים ב-STL • למשל set, stack ומבני נתונים נוספים שלא נלמדו בקורס מבוא לתכנות מערכות - 234122
ספרית התבניות הסטנדרטית - סיכום • הספריה הסטנדרטית של C++ מגדירה מספר אוספים נוחים לשימוש • השתמשו ב-vector במקום במערך • הגישה לאוספים מתבצעת בעזרת איטרטורים כך שניתן לגשת באותה צורה לאוספים שונים מבוא לתכנות מערכות - 234122
כתיבת אלגוריתמים גנריים איטרטורים אלגוריתמים function objects מבוא לתכנות מערכות - 234122
אלגוריתם max template<typenameIterator> Iteratormax(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;} • ברצוננו לכתוב פונקציה אשרמוצאת את האיבר המקסימלי • כיצד נוכל לאפשר לפונקציהלעבוד על כל אוסף אפשרי? • דוגמאות לשימוש: • מציאת הערך הגדול ביותר בוקטור • מחיקת האיבר הגדול ביותר ברשימה intm = *max(myVector.begin(), myVector.end()); myList.erase(max(myList.begin(), myList.end())); מבוא לתכנות מערכות - 234122
יתרונות • אלגוריתם יחיד מתאים לכל המקרים: • אוספים שונים • רק חלקים מהאוסף ניתנים למעבר • קל להתאים קוד קיים שיעבוד עם האלגוריתם • למשל שימוש באלגוריתם עבור מערך רגיל של C: int* myArray = newint[size]; //... intm = *max(myArray, myArray+size); מבוא לתכנות מערכות - 234122
algorithm • מלבד מבני הנתונים ה-STL מכיל גם אוסף אלגוריתמים מועילים הניתנים להפעלה על כל מבנה נתונים מתאים • חלקם פשוטים: • וחלקם פחות: template<typenameIterator, typenameT> Iteratorfind(Iterator start, Iterator end, constT& val); template<typenameIterator> voidsort(Iterator start, Iterator end); template<typenameIterator> voidrandom_shuffle(Iterator start, Iterator end); מבוא לתכנות מערכות - 234122
Function Objects • במקום לשלוח מצביעים לפונקציות ב STL-משתמשים ב-“function objects” • Function object הוא כל עצם המעמיס את אופרטור () (אופרטור ההפעלה) • במקרה זה אנו מגדירים אתהמחלקה כפונקציה המקבלתint ומחזירה bool classLessThanZero { public: booloperator()(int m) const { return m < 0; } }; void f() { LessThanZeroisNegative; cout << isNegative(-4) << endl; // true cout << isNegative(6) << endl; // false } מבוא לתכנות מערכות - 234122
Function Objects classSumOf{public:booloperator()(int a, int b, int s) { return a + b == s;}}; • אופרטור ההפעלה ניתן להגדרה עם מספר כלשהו של פרמטרים • בניגוד למצביעים לפונקציות, ניתן לזכור מצב ב-Function Object ולכן ניתן להשתמש באותה מחלקה למס' פונקציות השונות רק בפרמטר כלשהו • שימו לב להבדל בין קריאה לבנאי להפעלת האופרטור המתבצעת על עצם מהטיפוס. voidg() {SumOfisSum;cout << isSum(2,3,5) << endl; // truecout << isSum(1,2,5) << endl; // false} classLessThan{intn;public: LessThan(int n) : n(n) {} booloperator()(int m) const{return m < n;}}; voidh() {LessThanisLess(5);LessThan isLess2(8);cout << isLess(4) << endl; // truecout << isLess(6) << endl; // falsecout << isLess2(6) << endl; // true} מבוא לתכנות מערכות - 234122
Function Objects - דוגמה • נוכל להשתמש בפונקציה שהגדרנו בקריאה לאלגוריתמים מסוימים: template<typenameIterator, typenamePredicate> Iteratorfind_if(Iterator first, Iterator last, Predicatepred) { for (; first != last; first++) if (pred(*first)) break; return first; } vector<int> v; vector<int>::iterator i = find_if(v.begin(),v.end(),LessThan(2)); vector<int>::iterator j = find_if(v.begin(),v.end(),LessThan(7)); מבוא לתכנות מערכות - 234122
דוגמה נוספת • ניצור קריטריון מיון חדש למספרים שלמים כך שנוכל למיין לפי ערך המספר מודולו m: • בכדי למיין פשוט נקרא לאלגוריתםהמיון עם פרמטר נוסף: classLessThanModulo { intm; public: LessThanModulo(int m) : m(m) {} booloperator()(int a, int b) { returna%m < b%m; } }; sort(v.begin(), v.end(), LessThanModulo(5)); מבוא לתכנות מערכות - 234122
הוספת תמיכה באיטרטורים classString { intlength; char* value; public://... typedefchar* iterator; typedefconstchar* const_iterator;iteratorbegin() {returnvalue;} const_iteratorbegin() const{returnvalue;} iteratorend() {returnvalue+length;} const_iteratorend() const{returnvalue+length;}//...}; • ניזכר במחלקת String שהגדרנו בתרגול 9, כיצד נוכל להשתמש באלגוריתמים הקיימים עבור מחלקה זו? • עלינו להוסיף תמיכה באיטרטורים במחלקה. • במקרה של String, נוכל פשוט להשתמש במצביע ל-char • שימו לב להגדרות הטיפוסים שאנו מוסיפים, כדי שהמשתמש לא יצטרךלדעת בעצמו שאנו משתמשים במצביעל-char • לא תמיד ניתן להשתמש בטיפוס קיים כאיטרטור, לפעמים נצטרך להגדיר מחלקה חדשה עבור איטרטור מבוא לתכנות מערכות - 234122
שימוש • האלגוריתם transform מפעיל פונקציה על תחום מסוים ומציב לתחום אחר את ערך ההחזרה • נשתמש בו ובפונקצית הספריה של Ctoupper() כדי להמיר מחרוזת לאותיות גדולות transform(str.begin(),str.end(),str.begin(),toupper); מבוא לתכנות מערכות - 234122
כתיבת אלגוריתמים גנריים - סיכום • הספריה הסטנדרטית בנויה מאוספים ואלגוריתמים • איטרטורים מהווים את הדבק המאפשר לשני חלקים אלו להישאר גנריים • משתמשים ב-Function Objects כדי לאפשר שליחת פונקציות לאלגוריתמים אוספים אלגוריתמים איטרטורים מבוא לתכנות מערכות - 234122
מצביעים חכמים smart_ptr shared_ptr מבוא לתכנות מערכות - 234122
מצביעים חכמים • מצביעים אחראים לרובן המוחלט של שגיאות הזיכרון • הצלחנו להיפטר מרוב השימושים הלא בטוחים במצביעים בעזרת שימוש בתכונות של ++C • ניצול בנאים, הורסים והשמות לטיפול מסודרבזיכרון • שימוש ב-STL ואוספים לטיפול מסודרבאלגוריתמים של מבני הנתונים • אם ננסה להשתמש בהורשה וב-vector מתעוררת בעיה: • עבודה עם מצביעים מוחקת את כל היתרונות שהשגנו! • צריך לנהל זיכרון בצורה מפורשת בכל מקום בתוכנית! vector<Shape> shapes; Circlecircle(3.5); shapes.push_back(circle); vector<Shape*> shapes; //... deleteshapes.back(); shapes.pop_back(); מבוא לתכנות מערכות - 234122
smart_ptr • נוכל ליצור מחלקה, המתנהגת כמצביע אך מוסיפה התנהגות נוספת • למשל, שחרור העצם המוצבעבזמן הריסת המצביע • כלומר ניצור מצביע "חכם" יותר • מה קורה במקרה וה-newהשנינכשל בכל אחת מהפונקציות? template<typenameT> classsmart_ptr { T* data; typedefTelement_type; public:explicitsmart_ptr(T* ptr = NULL) : data(ptr) {} ~smart_ptr() {deletedata; } T& operator*() const{return*data; } T* operator->() const{ returndata;}}; intbad() { Shape* ptr1 =newCircle(5.0); Shape* ptr2 =new Square(5.0); //... } intgood() { smart_ptr<Shape> ptr1(new Circle(5.0)); smart_ptr<Shape> ptr2(new Square(5.0)); //... } מבוא לתכנות מערכות - 234122
העתקה • כיצד יתנהג המצביע במקרה של העתקה? • מה צריכה להיות ההתנהגות עבור בנאי העתקה והשמה? • ניסיון ראשון: העתקת המצביע תבצע "העברת בעלות" של העצם המוצבע • התנהגות זו ממומשת ב-auto_ptr הקיים בספריה הסטנדרטית של ++C • מאפשרת העברת/החזרת עצמים מוקצים דינאמית מפונקציות מבוא לתכנות מערכות - 234122
העתקה • שימוש ב-auto_ptr כדי להחזיר עצם חדש: • בניגוד למצביע רגיל – מוגדר בקוד מי אחראי לשחרור העצם. • שימוש ב-auto_ptr כדי לחסוך העתקות: • בעיה! ההתנהגות עבור "העתקה" של auto_ptrאינה סטנדרטית - ההעתק אינו זהה למקור! • לא ניתן לאחסן auto_ptr בתוך אוספים של ה-STL מאחר והם מסתמכים על תכונה זו • כיצד ניתן ליצור מצביע חכם שגם יאפשר העתקה נכונה? auto_ptr<Shape> createMyShape() { returnauto_ptr<Shape>(new Circle(5.0)); } auto_ptr<vector<int> > getNPrimeNumbers (int n) { // ... } מבוא לתכנות מערכות - 234122
שיפור - מצביע משותף • ניצור מצביע חכם יותר מתקדם: shared_ptr • לכל עצם מוצבע נשמור מונה הצבעות, וכל המצביעים ישתמשו במונה זה על מנתלדעת מתי יש לשחרר את העצם המוצבע Object 2 Object shared_ptr<Object> shared_ptr<Object> shared_ptr<Object> 1 Object* ptr Object* ptr Object* ptr int* counter int* counter int* counter מבוא לתכנות מערכות - 234122
shared_ptr - דוגמה • בעזרת shared_ptr לא נצטרך לדאוג יותר לזכור לשחרר מצביעים, כאשר המצביע האחרון לעצם כלשהו ישתחרר הוא ישחרר את העצם • נוכל לכתוב קוד המשתמש במצביעים בקלות וללא סכנה לבלבול • לדוגמה, בקוד הבא קשה לראות אילו עצמים יש לשחרר ומתי: vector<shared_ptr<Shape> > function(constvector<Point>& points) { shared_ptr<Shape> shape(newPolygon(points)); shared_ptr<Circle> circle(new Circle(5.0)); shared_ptr<Square> square(new Square(2.0));vector<shared_ptr<Shape> > vec; vec.push_back(shape); vec.push_back(circle); vec.push_back(square); vector<shared_ptr<Shape> > copy = vec; copy.pop_back(); return copy;} מבוא לתכנות מערכות - 234122
shared_ptr - מימוש template<typenameT> classshared_ptr { private: T* data; int* counter; typedefTelement_type; voidcheckInvariant() const { assert((data && counter && *counter > 0) || (!data && !counter)); } מבוא לתכנות מערכות - 234122
shared_ptr - מימוש voidincreaseCount() { checkInvariant(); if (counter) { (*counter)++; } } voiddecreaseCount() { checkInvariant(); if (counter && --*counter == 0) { deletecounter; deletedata; counter = NULL; data = NULL; } } מבוא לתכנות מערכות - 234122
shared_ptr - מימוש public: explicitshared_ptr(T* ptr = NULL) : data(ptr), counter(data ? newint(1) : NULL) { } shared_ptr(constshared_ptr<T>& other) : data(other.data), counter(other.counter) { increaseCount(); checkInvariant(); } template<typenameS> friendclassshared_ptr; template<typenameS> shared_ptr(constshared_ptr<S>& other) : data(other.data), counter(other.counter) { increaseCount(); checkInvariant(); } מבוא לתכנות מערכות - 234122
shared_ptr - מימוש ~shared_ptr() { checkInvariant(); decreaseCount(); checkInvariant(); } template<typenameS> shared_ptr<T>& operator=(shared_ptr<S>& other) { checkInvariant(); other.increaseCount(); decreaseCount(); counter = other.counter; data = other.data; checkInvariant(); return *this; } מדוע אין צורך בבדיקת הצבה עצמית? מבוא לתכנות מערכות - 234122
shared_ptr - מימוש T& operator*() const { checkInvariant(); assert(data != NULL); return *data; } T* operator->() const { checkInvariant(); assert(data != NULL); returndata; } T* get() const { checkInvariant(); returndata; } המתודה get() מסופקת בכדי לאפשר שימוש של המחלקה גם עם קוד ישן שאינו תומך ב-shared_ptr מבוא לתכנות מערכות - 234122
shared_ptr - מימוש operator bool() { checkInvariant(); returndata; } voidreset() { decreaseCount(); data = NULL; counter = NULL; } }; המרה לערך בוליאני תאפשר לנו לבדוק האם המצביע תקין כמו מצביע רגיל. המתודה reset() מאפשרת למשתמש צורה נוחה יותר לאיפוס מצביע מבוא לתכנות מערכות - 234122
shared_ptr - מימוש template<typenameT, typenameS> booloperator==(constshared_ptr<T>& first, constshared_ptr<S>& second) { returnfirst.get() == second.get(); } template<typenameT, typenameS> booloperator!=(constshared_ptr<T>& first, constshared_ptr<S>& second) { return !(first == second); } template<typenameT, typenameS> booloperator<(constshared_ptr<T>& first, constshared_ptr<S>& second) { returnfirst.get() < second.get(); } מבוא לתכנות מערכות - 234122
shared_ptr - סיכום • יתרונות: • כל הקוד לניהול הזיכרון נכתב רק פעם אחת • קל לחלוק עצמים אשר לא ברור מי האחרון שמשתמש בהם • ניתן להשתמש בכל האוספים שראינו עם מצביעים ובפרט עם ירושות • הקוד נהייה פשוט יותר ועמיד יותר בפני שגיאות זיכרון • עדיין קיימות מספר בעיות: • shared_ptr אינו מתאים לאחסון מצביע למערך! • כי השחרור הוא ע"י delete ולא delete[] • ניתן ליצור בקלות shared_array שיתאים לבעיה זו, אבל ממילא עדיף vector! • המצביעים לא ישוחררו אם קיימים קשרים מעגליים • במקרה זה יש לנקות את המצביעים בצורה מפורשת עם reset • פתרון טוב יותר למקרה זה דורש היכרות עם מצביע חכם נוסף והזמן קצר מלהכיל אותו מבוא לתכנות מערכות - 234122