חזור לתיקייה: תרגול

7 - מצביעים חכמים

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה
  • מצביעים
  • מצביע מיוחד - unique_ptr
  • מצביע משותף - shared_ptr
  • האם תמיד ניתן להשתמש ב-shared_ptr?
  • יצירת מצביעים חכמים ומשותפים
  • ספרתיית התבניות הסטנדרטית (STL)
  • וקטור

7 - מצביעים חכמים

02340124

מבוא לתכנות מערכות

רשימות תרגול לקריאה בשבת

מצביעים חכמים

הופק מתוך תקיית התואר של שקד

uni-folder.vercel.app

מצביעים

ראינו כי עבודה עם מצביעים רגילים היא כאב ראש והם הגורם העיקרי לשגיאות זכרון.
במת"מ, ראינו עד כה הרבה דרכים להיפטר מכל מיני שימושים לא בטוחים במצביעים בכך שניצלנו בנאים, הורסים והשמות וכו'.

עם זאת, יש מקרים בהם עדיין נשתמש בפוינטרים. לדוגמה, במקרים המערבים ירושה(שעוד נראה בהרצאות)

דוגמה (למה שאנחנו לא רוצים)

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;
};
הערה

לא נוכל לפתור בעיה זאת עם שכפול האובייקט המוצבע, שכן ככה לא עובדים פוינטרים וגם זה יצור לנו בעיות.

ניתן להעביר בעלות על זכרון בין מצביעים כחמים באמצעות movemovemove המוגדרת ב-stdstdstd כך:

unique_ptr<Circle> ptr1(new Circle(5.0));
unique_ptr<Circle> ptr2 = std::move(ptr1);

לאחר העברת הבעלות, ptr1ptr1ptr1 אינו הבעלים של הזכרון המוצבע ולא ניתן להשתמש בו. ליתר דיוק ptr1→nullptrptr1\to nullptrptr1→nullptr כרגע.

הערה

ניתן להעביר בעלות גם לפונקציה כך:

void foo(unique_ptr<Circle> arg)
{
	// ...
}

int main()
{
	unique_ptr<Circle> ptr1(new Circle(5.0));
	foo(std::move(ptr1));
}

לא ניתן להשתמש במצביע מחוץ לפונקציה לאחר העברת הבעלות.

הערה

אין צורך להשתמש ב-movemovemove ב-returnreturnreturn. וזהו המקום היחיד שאפשר לא להשתמש.

מצביע משותף - shared_ptr

מה אם נרצה מספר מצביעים אשר הינם הבעלים של אותו זכרון? עכשיו נראה דרך לעשות זאת.
מי אחראי לשחרור הזכרון במצב זה? האחרון שמשתמש בזכרון. כלומר, המצביע האחרון שמשוחרר בעצמו.
את זה נעשה באמצעות מונה אשר רק כאשר הוא 111 ונקרא ההורס, הוא גם הורס את המצביע עצמו.

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?

נניח ויש לנו רשימה מקושרת דו כיוונית המורכבת מצמתים המחזיקים shared_ptrshared\_ptrshared_ptr אחד לשני:
​7 - מצביעים חכמים 2026-06-22 17.07.45.excalidraw.svg
מה קורה אם אנו הורסים את המצביע headheadhead? הזכרון לא ישוחרר שכן יש לנו הצבעה מעגלית.
לכן, הצבעה מעגלית ⟸ לא משתמשים ב-shared_ptrshared\_ptrshared_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);
	// ...
}

ספרתיית התבניות הסטנדרטית (STLSTLSTL)

הספרייה מכילה אוספים ואלגוריתמים אשר משתמשים בתכנות גנרי על מנת שנוכל להשתמש בהם כיצד שאנו רוצים.

נסתכל על חלק מהדברים שיש בספרייה הסטנדרטית ונתחיל בכמה מבני נתונים:

  • מבני נתונים ששומרים נתונים לפי סדר הכנסה: vector,deque,array,listvector,deque,array,listvector,deque,array,list
  • מבני נתונים ששומרים נתונים בסדר ממוין כלשהו: set,map,multisetset,map,multisetset,map,multiset
  • מבני נתונים שלא שומרים על סדר: unordered_set,unordered_mapunordered\_set,unordered\_mapunordered_set,unordered_map
  • ועוד רבים אחרים.

וקטור

האוסף הכי נפוץ הוא std::vectorstd::vectorstd::vector הדורש #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;
};

ניתן לגשת לערך בוקטור באמצעות operator[  ]operator[\;]operator[] ואם האינדקס אינו תקין התוצאה אינה מוגדרת ונותנת ערכי זבל או שגיאת זכרון.
קיימת שיטה(methodmethodmethod) בשם at()at()at() המעניקה גישה בטוחה יותר אך יותר איטית. (אם האינדקס אינו תקין נזרקת חריגה)

הוקטור גם תומך באינטרציה:

  • שיטות כמו begin(),end()begin(),end()begin(),end() מחזירות איטרטור לתחילת הוקטור ואיטרטור במקום אחד אחרי הסוף.
  • rebign()rebign()rebign() ו-rend()rend()rend() מספקות ממשק לאיטרציה בסדר הפוך.
דוגמה

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());
הערה

האובייקטים המאוחסנים בתוך הוקטור חייבים בנאי העתקה ואופרטור השמה ואין צורך בבנאי חסר ארגומנטים.