מצביעים
ראינו כי עבודה עם מצביעים רגילים היא כאב ראש והם הגורם העיקרי לשגיאות זכרון.
במת"מ, ראינו עד כה הרבה דרכים להיפטר מכל מיני שימושים לא בטוחים במצביעים בכך שניצלנו בנאים, הורסים והשמות וכו'.
עם זאת, יש מקרים בהם עדיין נשתמש בפוינטרים. לדוגמה, במקרים המערבים ירושה(שעוד נראה בהרצאות)
Stack<Circle*> circles;
circles.push(new Circle(3.5));
// ...
delete circles.top();
circles.pop();
מצביע מיוחד - unique_ptr
על מנת לפתור חלק מהבעיות הללו, נוכל ליצור מחלקה המתנהגת כמצביע אך מוסיפה התנהגות נוספת, למשל, שחרור העצם המוצבע בזמן הריסת האובייקט.
כלומר, ניצור מצביע חכם שיבצע שחרור אוטומטי.
template <typename T>
class unique_ptr
{
private:
T* data;
public:
explicit unique_ptr(T* ptr = nullptr)
: data(ptr) {}
~unique_ptr() { delete data; }
T& operator*() { return *data; }
T* operator->() { return data; }
};
int good()
{
unique_ptr<Circle> ptr1(new Circle(5.0));
unique_ptr<Circle> ptr2(new Circle(7.0));
// ...
}
מה יקרה אם נבצע השמה או העתקה? אנחנו ניצור שני מבנים אשר מצביעים לאותו המקום בזכרון, והריסה של אחד מהם תהרוס גם את השני. זה בעייתי שכן אנו רוצים הורס אחד לבנאי אחד ושלא יהיו שני יצורים שמנהלים את אותו הזכרון. לשם כך, נמנע את האפשרות לבצע השמה או העתקה כך:
template <typename T>
class unique_ptr
{
public:
// ...
unique_ptr(const unique_ptr&) = delete;
void operator=(const unique_ptr&) = delete;
};
לא נוכל לפתור בעיה זאת עם שכפול האובייקט המוצבע, שכן ככה לא עובדים פוינטרים וגם זה יצור לנו בעיות.
ניתן להעביר בעלות על זכרון בין מצביעים כחמים באמצעות המוגדרת ב- כך:
unique_ptr<Circle> ptr1(new Circle(5.0));
unique_ptr<Circle> ptr2 = std::move(ptr1);
לאחר העברת הבעלות, אינו הבעלים של הזכרון המוצבע ולא ניתן להשתמש בו. ליתר דיוק כרגע.
ניתן להעביר בעלות גם לפונקציה כך:
void foo(unique_ptr<Circle> arg)
{
// ...
}
int main()
{
unique_ptr<Circle> ptr1(new Circle(5.0));
foo(std::move(ptr1));
}
לא ניתן להשתמש במצביע מחוץ לפונקציה לאחר העברת הבעלות.
אין צורך להשתמש ב- ב-. וזהו המקום היחיד שאפשר לא להשתמש.
מצביע משותף - shared_ptr
מה אם נרצה מספר מצביעים אשר הינם הבעלים של אותו זכרון? עכשיו נראה דרך לעשות זאת.
מי אחראי לשחרור הזכרון במצב זה? האחרון שמשתמש בזכרון. כלומר, המצביע האחרון שמשוחרר בעצמו.
את זה נעשה באמצעות מונה אשר רק כאשר הוא ונקרא ההורס, הוא גם הורס את המצביע עצמו.
template <class T>
class shared_ptr
{
private:
T* data;
int* counter;
public:
// ...
shared_ptr(const shared_ptr& other) : data(nullptr), counter(nullptr)
{
if (other.data)
{
this->data = other.data;
this->counter = other.counter;
(*counter)++;
}
}
~shared_ptr()
{
(*counter)--;
if (*(this->counter) == 0)
{
delete this->data;
delete this->counter;
}
}
// ...
};
פה רק ראינו את רק את החלק המעניין של המחלקה וכל החלקים האחרים סטנדרטיים.
האם תמיד ניתן להשתמש ב-shared_ptr?
נניח ויש לנו רשימה מקושרת דו כיוונית המורכבת מצמתים המחזיקים אחד לשני:
מה קורה אם אנו הורסים את המצביע ? הזכרון לא ישוחרר שכן יש לנו הצבעה מעגלית.
לכן, הצבעה מעגלית ⟸ לא משתמשים ב-.
יצירת מצביעים חכמים ומשותפים
מה שעשינו עד כה זה:
int good()
{
unique_ptr<Circle> ptr1 = unique_ptr<Circle>(new Circle(5.0));
shared_ptr<Circle> ptr1 = shared_ptr<Circle>(new Circle(5.0));
// ...
}
כעת, נוכל לבצע זאת בדרך טובה יותר כך:
int better()
{
unique_ptr<Circle> ptr1 = make_unique<Circle>(5.0);
shared_ptr<Circle> ptr1 = make_shared<Circle>(5.0);
// ...
}
ספרתיית התבניות הסטנדרטית ()
הספרייה מכילה אוספים ואלגוריתמים אשר משתמשים בתכנות גנרי על מנת שנוכל להשתמש בהם כיצד שאנו רוצים.
נסתכל על חלק מהדברים שיש בספרייה הסטנדרטית ונתחיל בכמה מבני נתונים:
- מבני נתונים ששומרים נתונים לפי סדר הכנסה:
- מבני נתונים ששומרים נתונים בסדר ממוין כלשהו:
- מבני נתונים שלא שומרים על סדר:
- ועוד רבים אחרים.
וקטור
האוסף הכי נפוץ הוא הדורש #include <vector>.
וקטור הוא מערך דינאמי אשר איבריו הם רציפים בזיכרון.
הוא מאפשר גישה מהירה באמצעות אינדקסים והכנסה והסרה מהירה של איברים מסוף הוקטור.
הערה: הכנסה והסרה מכל מקום אחר בוקטור עלולה להיות איטית מאוד.
void readAndPrint()
{
vector<int> numbers;
int input;
while (cin >> input)
{
numbers.push_back(input);
}
for (int i = 0; i < numbers.size(); ++i)
cout << numbers[i] << " ";
}
נסתכל על חלק מהמחלקה עצמה:
template <typename T>
class vector
{
vector();
vector(const vector& c);
vector(size_t num, const T& val = T());
vector& operator=(const vector& v);
~vector();
T& operator[](size_t index);
const T& operator[](size_t index) const;
void push_back(const T& val);
void pop_back();
void resize(size_t size, val = T());
size_t size() const;
};
ניתן לגשת לערך בוקטור באמצעות ואם האינדקס אינו תקין התוצאה אינה מוגדרת ונותנת ערכי זבל או שגיאת זכרון.
קיימת שיטה() בשם המעניקה גישה בטוחה יותר אך יותר איטית. (אם האינדקס אינו תקין נזרקת חריגה)
הוקטור גם תומך באינטרציה:
- שיטות כמו מחזירות איטרטור לתחילת הוקטור ואיטרטור במקום אחד אחרי הסוף.
- ו- מספקות ממשק לאיטרציה בסדר הפוך.
vector<int> v = { 2, 3, 4, 1, 2, 4 };
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it)
cout << *it << " ";
או:
vector<int> v = { 2, 3, 4, 1, 2, 4 };
for (vector<int>::reverse_iterator it = v.rbegin(); it != v.rend(); ++it)
cout << *it << " ";
ניתן להוסיף ולהסיר איברים ביעילות מסוף הוקטור:
vector<string> words = { "Hello", "World" };
words.push_back("!!!");
words.pop_back();
words.resize(5);
ניתן להוסיף ולהסיר איברים מכל מקום בוקטור אך לא ביעילות כך:
vector<string> words = { "Hello", "World" };
vector<string>::iterator it = words.begin();
++it;
words.insert(it, "Lovley");
words.erase(words.begin());
האובייקטים המאוחסנים בתוך הוקטור חייבים בנאי העתקה ואופרטור השמה ואין צורך בבנאי חסר ארגומנטים.