חזור לתיקייה: 02340124 - מבוא לתכנות מערכות

2 - מחלקות

חיפוש בתיקיה
Ctrl K
לחץ כאן או הקש Ctrl K כדי לחפש בכל התיקיה
  • סוגי מידע
  • הכמסה - encapsulation
  • מחלקות
  • שרשור שיטות
  • שיטות קבועות
  • שיטות השמה וקבלה
  • משתנים סטטים(static)
  • פונקציות סטטיות
  • בנאים
  • בנאי ברירת מחדל
  • מערכים ובנאים
  • רשימת אתחול
  • אתחול אל מול השמה
  • בנאי העתקה
  • מפרקים (Destructors)
  • מבני נתונים 🔥
  • בונים קבוצה
  • העמסת אופרטורים
  • מה אפשר ואי אפשר לעשות
  • סוגי אופרטורים
  • אופרטורים בינאריים
  • אופרטור אונרי
  • אופרטורים בתוך המחלקה!
  • המרות בין טיפוסים
  • העמסת פונקציית המרה
  • שטויות של המהדר - explicit
  • פונקציות חברות
  • העמסת אופרטור input/output
  • דוגמה - הטיפוס הרציונלי
  • למה להגדיר העמסת אופרטורים מחוץ למחלקה
  • אופרטור ההשמה
  • שינוי התנהגות ברירת המחדל
  • יצירת יצור
  • מניעת יצירת יצור
  • אופרטור אינדוקס
  • מעבר על כל האיברים
  • מימוש של Interface
  • לולאות מבוססות טווח ב-C++

2 - מחלקות

02340124

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

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

מחלקות

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

uni-folder.vercel.app

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

  • איך מחברים שני מרוכבים.
  • איך מבצעים פעולות על מרוכבים.

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

סוגי מידע

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

Data Type=Representation+Operations\text{Data Type}=\text{Representation}+\text{Operations}Data Type=Representation+Operations

עד עכשיו, מה שיכולנו לעשות כדי לבצע את מה שרצינו זה להגדיר ב-complex.hcomplex.hcomplex.h מלא פונקציות השייכות למרוכבים ואז כל מי שרוצה יוכל לבצע includeincludeinclude ולהשתמש בהם.

ב-C++C++C++, בנוסף לקבלת מבנים כמשתנים לפונקציה נוכל לבצע:

struct Complex
{
	double re, im;
	
	void init(double x, double y);
	void multiply(double a);
};

בתוך ה-cppcppcpp נכתוב:

void Complex::init(double x, double y)
{
	re = x;
	im = y;
}

משתמשים בזה כך:

int main()
{
	Complex c1;
	c1.init(4.0, 1.0);
	c1.multiply(5.0);
	return 0;
}

השיטות הללו(שם fancyfancyfancy לפעולות ששיכות לייצור) אינן תופסות מקום במבנה.

הגדרה (סימון)

לערכים מסוג ComplexComplexComplex אנו קוראים objectobjectobject או אובייקט או instanceinstanceinstance.

ניתן לקבל ארגומנטים מאותו הסוג כך:

struct Complex
{
	...
	void add(Complex other);
};

ואז ב-cppcppcpp נכתוב:

void Complex:add(Complex other)
{
	re += other.re;
	im += other.im;
}
הערה

ניתן להגדיר בתוך המבנה עצמו את השיטות. למה? כי ככה.

struct Complex
{
	double re, im;
	
	void init(double x, double y)
	{
		re = x;
		im = y;
	}
	
	void add(Complex other);
};

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

הכמסה - encapsulationencapsulationencapsulation

אנו לא רוצים שתהיה למשתמש גישה לאיך אנו יוצרים את המבנה שלנו - איך נעשה זאת? כך:

struct Complex
{
public:
	void init(double x, double y);
	void add(Complex c);

private:
	double re, im;
};

שדות שמוגדרים כ-privateprivateprivate לא ניתנים לגישה מבחוץ.

הערה

במבנה, אם לא נרשום כלום זה יהיה ציבורי ולא פרטי.

הגדרה (טיפוס נתונים מושפט)

לטיפוס אשר אנו לא תלויים במימוש שלו אנו נקרא Abstract Data Type.

Abstract Data Type=Operations\text{Abstract Data Type}=\text{Operations}Abstract Data Type=Operations

מחלקות

המילה classclassclass כמעט זהה למילה structstructstruct במובן שההבדל היחיד בין structstructstruct לבין classclassclass הוא שב-classclassclass אם לא כותבים משהו אחר אז זה privateprivateprivate ו-structstructstruct הוא publicpublicpublic. למה? התאמה עם CCC.
לרוב כאשר אנו רוצים להגדיר סוגי מידע חדשים אנו נשתמש במחלקות - classesclassesclasses.

הערה

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

דוגמה (מספרים מרוכבים)

class Complex
{
	double re, im;

public:
	void init(double x, double y);
	void add(Complex c);
};

כאשר אנו יוצרים שיטה(מטודה) שהיא פונקציה בתוך מחלקה, המהדר שולח בנוסף לפעולה את המצביע thisthisthis שהוא מצביע לאובייקט שקרא לפונקציה. אם נכתוב:

void Complex::add(Complex c)
{
	re += c.re;
	im += c.im;
}

// ...

c1.add(c2);

המהדר יתרגם זאת ל:

void add(Complex* const this, Complex c)
{
	this->re += c.re;
	this->im += c.im;
}

// ...

add(&c1, c2);

לרוב, אנו לא נדרש להצהיר את המילה thisthisthis אבל אם יש משהו לא ברור, נניח אם אנו מקבלים משתנים עם שמות זהים נדרש להצהיר.

דוגמה

void Complex::init(double re, double im)
{
	this->re = re;
	this->im = im;
}

שרשור שיטות

אם נרצה לבצע שרשור, נניח כמו זה:

Complex c1, c2, c3;
c1.add(c2).add(c3);

אז נדרש שהפונקציה תעשה:

Complex& Complex::add(Complex c)
{
	re += c.re;
	im += c.im;
	return *this;
}
הערה (למה אנחנו מחזירים רפרנס?)

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

שיטות קבועות

חלק משיטות המחלקה לא משנות את האובייקט שקרא להן - שיטות כאלו נוכל להגדיר כקבועות (constconstconst).
כאשר נגדיר שיטה כקבועות נגיד 2 דברים:

  1. השיטה לא משנה את הערך של האובייקט.
  2. השיטה ניתנת לשימוש על ידי אובייקטים קבועים.
דוגמה

class Complex
{
	double re, im;
	
public:
	void init(double x, double y);
	Complex& add(Complex c);
	double abs() const;
};

// ...

void f(Complex& c, const Complex& c2)
{
	c.add(c2);
	c2.add(c); // ERROR - c2 is const
	cout << c2.abs() << endl; // OK
}

שיטות השמה וקבלה

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

  • פונקציית קבלה(gettergettergetter) מחזירה את הערך של השדה.
  • פונקציית השמה(settersettersetter) משנה את ערך השדה.
דוגמה

double Complex::getReal() const 
{
	return re;
}

void Complex::setReal(double x)
{
	re = x;
}

או בתצורה הזו:

double Complex::real() const
{
	return re;
}

void Complex::real(double x)
{
	re = x;
}

אז למה לא פשוט לעשות את השדות ציבוריים(publicpublicpublic)?

  • הופך את השדה לחלק מה-interfaceinterfaceinterface של המחלקה.
  • מאפשר שינוי ללא בדיקות וללא הגבלות(שהרבה פעמים נרצה לעשות)
  • שימוש בשיטות השמה וקבלה מאפשר לשלוט באיזה סוגים של גישה יש לשדות - רק קריאה, רק השמה או גם וגם

נוכל בנוסף ליצור גם שיטות השמה וקבלה לשדות שלא קיימים. לדוגמה:

class Complex 
{
	double abs, arg;
	
public:
	double getReal() const;
	void setReal(double x);
};

// in the cpp file:
double Complex::getReal() const
{
	return abs * cos(arg);
}

void Complex::setReal(double x)
{
	double y = this->getImage();
	abs = sqrt(x * x + y * y);
	arg = atan2(y, x);
}

משתנים סטטים(staticstaticstatic)

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

דוגמה

class Complex
{
	double re, im;
	static int initCounter;

public:
	void init(double re, double im);
	Complex& add(Complex c);
};

// in the cpp:
int Complex::initCounter = 0;

// ...

void Complex::init(double re, double im)
{
	this->re = re;
	this->im = im;
	initCounter++;
}
הערה

לא ניתן לאתחל משתנים סטטיים בקובץ hhh אלא רק ב-cppcppcpp.

ניתן בנוסף להגדיר משתנים סטטים קבועים.

דוגמה

class Complex
{
	// ...
	static const double PI = 3.14159;
};
הערה

אותם כן ניתן להגדיר בקובץ hhh.

פונקציות סטטיות

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

דוגמה

class Complex
{
	// ...
	static Complex I();
	static int getInitNum()
	{
		return initCounter;
	}
};

// in the cpp:
Complex Complex::I()
{
	Complex result;
	result.init(0, 1);
	return result;
}

// in main:
Complex c1 = Complex::I();
cout << Complex::getInitNum() << endl; // output: 1

בנאים

כשאנו רוצים לאתחל מבנה עד כה הגדרנו שיטה initinitinit ואז היינו חייבים לאחר אתחול לקרוא לשיטה initinitinit. זה יוצר זמן שבו האובייקט נמצא במצב לא רצוי ואנו לא אוהבים את זה. לכן, יש סוג מאוד מסויים של שיטות שיכריחו את המשתמש תמיד לעבור דרך initinitinit. (אגב, זו יכולה להיות דרך פונקציה סטטית ונראה בהמשך איך עושים זאת)
הדרך הסטנדרטית היא להשתמש בבנאי(constructorconstructorconstructor) שהיא שיטה שהשם שלה הוא כשם המחלקה ואין לה ערך חזרה. מלבד זאת היא נראית כמו פונקציה סטטית וניתן להשתמש בה ב-thisthisthis.

דוגמה

\\ in .h:
class Complex 
{
	double re, im;
	
public:
	Complex(double re, double im);
};

// in the cpp:
Complex::Complex(double re, double im)
{
	this->re = re;
	this->im = im;
}

// in main:
Complex c1(1.0, 0.0);
c1 = Complex(2.0, 1.0); // temporary object

Complex *pc1 = new Complex(1.0, 2.0);
הערה

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

דוגמה

class Complex
{
	double re, im;

public:
	Complex();
	Complex(double x, double y = 0);
};

בנאי ברירת מחדל

בנאי אשר לא מקבל ארגומנטים נקרא בנאי ברירת המחדל (default  constructordefault\; constructordefaultconstructor) והוא מה שנקרה כאשר אובייקט נוצר ללא ארגומנטים או ללא קריאה לבנאי באופו ישיר.
המהדר יגדיר את האובייקט באופן אוטומטי עם בנאי ברירת המחדל.

דוגמה

Complex c1;
print(Complex());

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

הערה

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

מערכים ובנאים

אחת המקומות בהם בנאים ברירת מדחל נקראים/דרושים הם כאשר מגדירים מערך.

Complex array1[10];
Complex* pArray = new Complex[10];
Complex* pArray2 = new Complex[10](); // same as above

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

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

רשימת אתחול

כאשר נסתכל על בנאי של מחלקה כמו הבאה:

// in the h:
class Array
{
	int* data;
	int size;
	
public:
	Array(int size);
};

// in the cpp:
Array::Array(int n)
{
	data = new int[n];
	size = n;
}

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

Array::Array(int n) : data(new int[n]), size(n)
{
	// nothing lest to do here (and that's fine)
}

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

דוגמה

ל-ArrayArrayArray אין בנאי ברירת מחדל ולכן לא ניתן לבצע את הנ"ל באובייקט אחר ונדרש להשתמש ברשימת אתחול כך:

// in the h:
class BigInteger
{
	Array digits;
	const int base;

public:
	BigInteger(int base, int len);
	// ...
};

// in cpp:
BigInteger::BigInteger(int base, int len) : base(base), digits(len)
{}

אתחול אל מול השמה

ב-C++C++C++ יש לנו מספר דרכים לעשות את אותו הדבר:

Complex c(1.0, 1.0); // c initialized
Complex c2(c); // c2 initilized
Complex c3 = c; // c3 initilaized

c = c2; // assignment

בהשמה השדות פשוט מועתקים מאובייקט אחד לשני(איך? בהמשך)

בנאי העתקה

לכל סוג משתנה TTT קיים בנאי הלוקח ארגומנט יחיד const   T&const\;\ T\&const T& והוא נקרא בנאי העתקה.
לרוב, אנו נרצה להגדיר אותו בעצמנו כדי למנוע העתקת כתובות של מצביעים.

דוגמה

// in h:
class Array
{
	int* data;
	int size;

public:
	Array(int size);
	Array(const Array&);
	~Array();
};

// in cpp:
Array::Array(const Array& arr) : data(new int[arr.size]), size(arr.size)
{
	for (int i = 0; i < size; ++i)
	{
		data[i] = arr.data[i];
	}
}

// in main:
Array a(10);
Array b(a); // copy constructor
Array c = a; // copy constructor

אם אנו לא מגדירים בנאי העתקה בעצמנו נוצר אחד ברירת מחדל שהוא פשוט מעתיק את השדות.
בנאי העתקה נקרא כאשר אובייקט מועבר לפונקציה או מוחזר מפונקציה ע"י ערך (by  valueby\; valuebyvalue) לכן, בנאי העתקה הוא מאוד חשוב

הערה

לפעמים המעבד יחליט לבצע אופטימיזציה שתחליט לא לקרוא לבנאי העתקה. (עד C++17C++17C++17 הקומפיילר היה יכול לבחור לעשות זאת או לא - מ-C++17C++17C++17 זה חובה ולכן יעבוד גם אם לא יהיה קיים בנאי העתקה - שנראה איך לעשות זאת בהמשך)

מפרקים (DestructorsDestructorsDestructors)

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

class Array
{
	int* data;
	int size;
	
public:
	Array(int size);
	~Array(); // destructor
	int& atIndex(int index);
};

מפרק נקרא באופן אוטומטי ולכן:

  • יש רק אחד כזה בכל המחלקה
  • הוא לא מקבל ארגומנטים כלל
הערה

המפרק נקרא גם כאשר אנו מוחקים את האובייקט עם deletedeletedelete אם הוא פויינטר לאובייקט.

דוגמה (עם ה-hhh ממקודם)

Array::Array(int size)
{
	data = new int[size];
	this->size = size;
}

Array::~Array()
{
	delete[] data;
}

int& Array::atIndex(int index)
{
	assert(index >= 0 && index < size);
	return data[index];
}
הערה

  • כשבונים אובייקט, קודם כל מאתחלים את כל שדות המחלקה בסדר שהם כתובים.
  • כשהורסים אובייקט, קודם קוראים למפרק ורק אז אנו הורסים את כל השדות.

מבני נתונים 🔥

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

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

הגדרה 1 (מבנה נתונים)

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

בונים קבוצה

הגדרה 2 (קבוצה)

קבוצה היא איחוד של איברים כך שאין כפילויות ואין חשיבות לסדר.

הממשק אליו נדרש לקבוצה הוא:

  1. הוספה - AddAddAdd
  2. הסרה - RemoveRemoveRemove
  3. האם מכיל - ContainsContainsContains
  4. גודל - SizeSizeSize
דוגמה (לשימוש)

#include "set.h"
#include <iostream>
using std::cout;
using std::endl;

int main()
{
	Set set1, set2;

	for (int j = 0; i < 20; j += 2)
	{
		set1.add(j);
	}
	for (int j = 0; j < 12; j += 3)
	{
		set2.add(j);
	}
	
	Set unionSet = Set::union(set1, set2);
	Set interSet = Set::intersection(set1, set2);
	
	cout << set1.toString() << endl;
	cout << set2.toString() << endl;
	cout << unionSet.toString() << endl;
	cout << interSet.toString() << endl;
	return 0;
}

עכשיו, לאחר שראינו את ה-ADTADTADT של קבוצה ואיך נשתמש במבנה הנתונים שנבנה, נרצה לממש את מבנה הנתונים של קבוצה של מספרים שלמים.

איך נממש את הסט שלנו? נוכל אולי להשתמש במערך אשר מוגדר דינאמית ולכן נוכל לשנות את גודלו או שנוכל להשתמש ברשימה מקושרת (דרכים טובות יותר ילמדו בקורס מבני נתונים בסמסטר הבא - לדוגמה hash  tableshash\; tableshashtables)
אנחנו היום הולכים להשתמש במערך לא ממויין(כי זה הכי קל) ונשנה את גודלו באופן אוטומטי כשנדרש. לשם כך נשתמש במשתנה maxSizemaxSizemaxSize בנוסף למשתנה sizesizesize כדי שנוכל לדעת מתי אנו צריכים לשנות את גודל המערך.
מבחינת סיבוכיות יהיה לנו:

UnorderedOrderedLookupΘ(n)Θ(log⁡n)InsertΘ(n)Θ(n)RemoveΘ(n)Θ(n)\begin{array}{c|c|c|} & Unordered & Ordered \\ \hline Lookup & \Theta(n) & \Theta(\log n) \\ Insert & \Theta(n) & \Theta(n) \\ Remove & \Theta(n) & \Theta(n) \\ \hline \end{array}LookupInsertRemove​UnorderedΘ(n)Θ(n)Θ(n)​OrderedΘ(logn)Θ(n)Θ(n)​​

נתחיל מהממשק בקובץ .h.h.h:

#pragma once
#include <string>

class Set
{
public:
	Set();
	Set(const Set&);
	~Set();
	
	bool add(int number); // true if added, false if already exists
	bool remove(int number);
	bool contains(int number) const;
	int getSize() const;
	
	static Set union(const Set&, const Set&);
	static Set intersection(const Set&, const Set&);
	
	std::string toString() const;
	
private:
	int maxSize; // the allocated size of the array
	int size;    // the current number of elements
	int* data;   // the elements of the set
	
	int find(int number) const;
	void expand();
	
	/* The initial size of the set array */
	static const int INITIAL_SIZE = 10;
	
	/* The factor by witch to expand the array when needed */
	static const int EXPAND_RATE = 2;
	
	/* Returned by 'find' when an element is no in the set */
	static const int NUMBER_NOT_FOUND = -1;
};

כעת, לאחר שבנינו את הממשק וחלק מהמימוש, נוכל לבנות את שאר המימוש בקובץ ה-cppcppcpp:

#include "set.h"

Set::Set() : maxSize(INITIAL_SIZE), size(0), data(new int[INITIAL_SIZE])
{ }

Set::Set(const Set& set) : 
	maxSize(max(set.getSize(), INITIAL_SIZE)), 
	size(set.getSize()),
	data(new int[maxSize])
{
	for (int i = 0; i < size; i++)
	{
		data[i] = set.data[i];
	}
}

Set::~Set()
{
	delete[] data;
}

int Set::find(int number) const
{
	for (int i = 0; i < size; i++)
		if (data[i] == number)
			return i;
	return NUMBER_NOT_FOUND;
}

bool Set::contains(int number) const
{
	return find(number) != NUMBER_NOT_FOUND;
}

int Set::getSize() const
{
	return size;
}

bool Set::add(int number)
{
	if (containes(number))
		return false;
	if (size >= maxSize)
		expand();
	
	data[size++] = number;
	return true;
}

void Set::expand()
{
	int newSize = maxSize * EXPAND_RATE;
	int* newData = new int[newSize];
	
	for (int i = 0; i < size; i++)
	{
		newData[i] = data[i];
	}
	
	delete[] data;
	data = newData;
	maxSize = newSize;
}

bool Set::remove(int number)
{
	int index = find(number);
	if (index === NUMBER_NOT_FOUND)
		return false;
	
	data[index] = data[--size];
	return true;
}

Set Set::union(const Set& set1, const Set& set2)
{
	Set unionSet = set1; // copying the object
	for (int i = 0; i < set2.getSize(); i++)
	{
		unionSet.add(set2.data[i]);
	}
	
	return unionSet;
}

Set Set::intersection(const Set& set1, const Set& set2)
{
	Set intersectionSet;
	for (int i = 0; i < set1.getSize(); i++)
	{
		if (set2.contains(set1.data[i]))
			intersectionSet.add(set1.data[i]);
	}
	
	return intersectionSet;
}

אנחנו כמובן ממשים SetSetSet כדי שאנשים אחרים יוכלו להשתמש בו. אנו יכולנו לממש עוד פונקציות כמו EqualsEqualsEquals או ContainsContainsContains. האם נרצה לממש גם אותן?

  • מצד אחד כן, כי זה מאוד נוח למשתמש
  • מצד שני לא, כי זה מקשה על תחזוקה

ישנן שתי בעיות פה שכדאי לשים עליהן זרקור:

  • הקבוצה הזו היא קבוצה של intintint-ים בלבד. אם היינו רוצים לממש קבוצה של משתנה אחר, הקוד היה נראה כמעט אותו הדבר רק שהיינו מחליפים את הסוגים של המשתנים
    את הבעיה הזו נפתור עוד שלושה פרקים עם Parametric Polymorphism שהוא בעצם TemplatesTemplatesTemplates ב-C++C++C++.

  • ראינו שיש כמה מימושים אפשריים לקבוצה. האם יש לנו דרך להבטיח שכל המימושים השונים יממשו את אותו הממשק? בנוסף, האם יש קוד שלא משנה איזה סט הוא מקבל הוא יעבוד בסיבוכיות הכי נמוכה? (יחליף מימושים לפי הצורך) כלומר, אנו רוצים שיהיה כמה מימושים שונים לאותו היצור ושנוכל להשתמש בכולם.

    לפי הכלים שראינו לא באמת שכן יכול להיות רק cppcppcpp אחד שממש קובץ hhh.
    אנחנו עוד נראה פתרון עם השם הפלצני Subtyping Polymorphism או פשוט ירושה ופולימורפיזם :)

העמסת אופרטורים

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

דוגמה

מה שיש לנו עכשיו:

void f2(Complex c1) 
{ 
	Complex c2(3.0, 2.0); 
	Complex input; 
	input.readFrom(cin); 
	c2.multiply(input); 
	c1.add(c2); 
	c2.printTo(cout); 
}

איך אנו רוצים שזה יראה:

void f2(Complex c1) 
{ 
	Complex c2(3.0, 2.0); 
	Complex input; 
	cin >> input; 
	cout << (c1 + c2 * input); 
}

ב-C++C++C++ אנו יכולים להעמיס את פונקציות האופרטורים ולהפוך את הקוד שלנו להרבה יותר נחמד.
עם קצת עבודה נוכל לכתוב קוד שנראה נהדר כמו בדוגמה.

מה אפשר ואי אפשר לעשות

נרצה להגדיר אופרטור חיבור למחלקת המרוכבים שלנו כך:

Complex operator+(const Complex& x, const Complex& y)
{
	Complex result(x.real() + y.real(), x.image() + y.image());
	return result;
}
הערה

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

הערה

הפונקציה הזו ממומשת מבחוץ אך אפשר לממש אותה גם בתוך המחלקה ונראה זאת בהמשך.

אנו יכולים להעמיס את האופרטורים הבאים:

+−∗/%^&∣∼!=<>+=−=∗=/=%=^=&=∣=<<>>>>=<<===!=<=>=&&∣∣++−−−>∗,−>[]()newnew[]deletedelete[]\begin{matrix} + & & - & & * & & / & & \% & & \hat{} & & \& \\ | & & \sim & & ! & & = & & < & & > & & += \\ -= & & *= & & /= & & \%= & & \hat{}= & & \&= & & |= \\ << & & >> & & >>= & & <<= & & == & & != & & <= \\ >= & & \&\& & & || & & ++ & & -- & & ->^* & & , \\ -> & & [] & & () & & new & & new[] & & delete & & delete[] \end{matrix}+∣−=<<>=−>​​−∼∗=>>&&[]​​∗!/=>>=∣∣()​​/=%=<<=++new​​%<^===−−new[]​​^>&=!=−>∗delete​​&+=∣=<=,delete[]​

לעומתם, את האופרטורים הבאים לא ניתן להעמיס:

::..∗?:sizeoftypeid:: \qquad \qquad . \qquad \qquad .^* \qquad \qquad ?: \qquad \qquad sizeof \qquad \qquad typeid::..∗?:sizeoftypeid

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

הערה

כשאנו מגדירים את +++ זה לא נותן לנו אוטומטית את +=+=+= ואת זה נדרש לממש בעצמנו.
אנו טכנית יכולים לתת להם מימושים שונים לחלוטין אבל זה לא טוב.

סוגי אופרטורים

אופרטורים בינאריים

אופרטור בינארי הוא אופרטור בעל שני אופרנדים.
ניתן להגדיר אותו בתור שיטה בתוך מחלקה ואז האופרטור הראשון יהיה thisthisthis וניתן להגדיר אותו מבחוץ.
בהמשך אנו נראה שעדיף להגדיר מבחוץ.

דוגמה

בתוך מחלקה

class Complex
{
	// ...

public:
	// ...
	Complex operator+(const Complex& x) const;
};

מחוץ למחלקה:

Complex operator+(const Complex& x, const Complex& y);

אופרטור אונרי

אופרטור אונרי מקבל אופרנד אחד וגם אותו ניתן להגדיר בפנים ובחוץ.
(גם את זה עדיף להגדיר מחוץ למחלקה אבל זה פחות קריטי)

דוגמה

בתוך המחלקה:

class Complex
{
	// ...

public:
	// ...
	Complex operator-() const;
};

מחוץ למחלקה:

Complex operator-(cont Complex& x);
הערה

ניתן להגדיר את האופרטורים כגם בינארים וגם אונריים כך:

Complex operator-(cont Complex& x); // -x
Complex operator-(cont Complex& x, const Complex& y); // x-y

אופרטורים בתוך המחלקה!

את האופרטורים =,[],(),−>=,[],(),->=,[],(),−> ניתן להגדיר אך ורק בתוך המחלקה ולא בחוץ.
זה מאוד הגיוני שכן אין להם מימוש באמת מחוץ למחלקות.

המרות בין טיפוסים

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

דוגמה (עם מספר רציונלי)

Rational r1(1, 2);
int n = 1;
Rational r2 = r1 + n;

אנו נרצה לעשות את זה מבלי להגדיר מלא operator+-ים(לטיפוסים השונים).
במקום זה נוכל להגדיר המרה מ-intintint למספר רציונלי.

ניתן לעשות זאת בשתי דרכים:

  1. המרה ע"י בנאי.
  2. המעסת פונקציית המרה.

העמסת פונקציית המרה

העמסה של פונקציית המרה נשעית באופן קצת שונה ממה שראינו עד כה.
נניח ונקח מספר רציונלי ונרצה להמיר אותו ל-doubledoubledouble נעשה זאת כך:

class Rational
{
	// ...
	operator double() const;
};

// in the CPP:
Rational::operator double() const
{
	// ...
}
הערה

שימוש נפוץ של זה הוא הגדרת המרות ל-boolboolbool כדי להשתמש בטיפוסים בתנאים.
אל תעשו את זה - זה מבלבל.

שטויות של המהדר - explicitexplicitexplicit

לפעמים המהדר עושה שטויות. נניח במקרה הבא: יש מספר רציונלי ו-doubledoubledouble ואנו לא יודעים אם המהדר יהפוך את המספר לרציונלי או ל-doubledoubledouble:

Rational r(1, 2);
cout << r + 1.2;

עוד משהו די רע הוא נניח אם הגדרנו מערך עם גודל ובנאי Array(int size); ואז יכול לקרות משהו מעניין שכזה:

void f(Array& a, int i)
{
	a = 5;
}

הדבר הזה יצור מערך חדש באורך 5 וזה יהיה aaa כי aaa הוא רפרנס..

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

הערה (כלל אצבע בקורס)

תמיד להגדיר כל בנאי עם ארגומנט 1 או שיטת המרה כ-expicitexpicitexpicit
על זה מורידים בבחינה :)

פונקציות חברות

בואו נגדיר אופרטור ====== עבור מספרים מרוכבים. הוא לא משנה את הארגומנטים של אף אחד מהמשתנים ולכן הוא יהיה constconstconst. אנו נרצה להגדיר זאת כפונקציה ולא שיטה (נראה למה אחר כך) אך אנו לא יכולים לגשת בתצורה זאת לשדות הפנימיים של האובייקט.
במקרה הזה אנו יכולים להצהיר בתוך המחלקה על פונקציה מבחוץ שתהיה חברה ותוכל להשתמש בשדות הפרטיים כך:

// in h:
class Complex
{
	// ...
	friend bool operator==(const Complex& c1, const Complex& c2);
};

// in c:
bool operator==(const Complex& c1, const Complex& c2)
{
	return c1.real == c2.real && c1.image == c2.image;
}

ההגדרה בתוך המחלקה גם משמשת כהצהרה על הפונקציה :)

הערה

באופן כללי אל תשמשו בזה - זה רע.
זה בעצם שובר את כל הקונספט של ADTADTADT.

העמסת אופרטור input/outputinput/outputinput/output

את האופרטורים האלו אפשר לממש אך ורק כחברים. אנו הולכים לראות את זה ממש עוד שניה במספר הרציונלי שנבנה ולכן אני לא הולך לתת פה דוגמה.
מה שחשוב לשים לב להם הוא שאנו מחזירים std::ostream&std::ostream\&std::ostream& והם מקבלים גם אחד שכזה.

דוגמה - הטיפוס הרציונלי

אנחנו הולכים להגדיר עכשיו טיפוס למספר רציונלי אשר הינו שבר של שני מספרים שלמים - ab\frac{a}{b}ba​.
לו אנו נגדיר את כל הפונקציות העמסת אופרטורים שאנו רוצים.

נתחיל מהקובץ .h.h.h:

class Rational
{
	int numerator, denominator;
	
	void simplify();
	static int gcd(int x, int y);
	friend bool operator==(const Rational& r1, const Rational& r2); 
	friend std::ostream& operator<<(std::ostream& os, const Rational& r);

public:
	Rational(int numerator = 0, int denominator = 1); // this actually defines 3 constructors.
	// this constructor also defines a conversion from int to rational.
	
	Rational operator+(const Rational& r) const;
	Rational& operator+=(const Rational& r); /* changes this so it's not const. also returns 
											    a refrence so we could concatenate += */
	Rational& operator-=(const Rational& r);
	Rational operator-(const Rational& r) const;
	Rational operator-() const;
	
	explicit operator double() const;
};

הקובץ .cpp.cpp.cpp:

void Rational::simplify()
{
	if (denominator < 0) 
	{ 
		denominator = -denominator; 
		numerator = -numerator; 
	} 
	
	int common = gcd(numerator, denominator); 
	if (common != 0) 
	{ 
		numerator /= common; 
		denominator /= common; 
	} 
	else { throw DivisionByZero(); }
}

Rational::Rational(int numerator, int denominator) : 
	numerator(numerator), denominator(denominator)
{
	if (denominator == 0)
		throw DivisionByZero(); // see later
	
	simplify();
}

// Operator + is more complicated than += case + creates a new object
// => we will implement + using +=
Rational& Rational::operator+=(const Rational& r)
{
	numerator = numerator * r.denominator + r.numerator * denominator;
	denominator = denominator * r.denominator;
	simplify();
	return *this;
}

Rational Rational::operator+(const Rational& r) const
{
	Rational result(*this);
	return (result += r); // this will call copy-constructor to go from &Rational to Rational
}

Rational Rational::operator-() const
{
	return Rational(-numerator, denominator);
}

Rational& Rational::operator-=(const Rational& r)
{
	return *this += -r;
}

Rational Rational::operator-(const Rational& r) const
{
	return Rational(*this) -= r;
}

Rational::operator double() const
{
	return double(numerator) / denominator;
}

bool operator==(const Rational& r1, const Rational& r2) 
{ 
	return r1.numerator == r2.numerator && r1.denominator == r2.denominator; 
}

std::ostream& operator<<(std::ostream& os, const Rational& r) 
{ 
	os << r.numerator; 
	if (r.denominator != 1) 
	{ 
		os << "/" << r.denominator; 
	} 
	return os; 
}

למה להגדיר העמסת אופרטורים מחוץ למחלקה

ל-+++ יש בעיה.
אם נכתוב את השורה הבאה הכל יעבוד אחלה (ויייייייי)

r2 = r + 3;

המספר 3 מומר למספר רציונלי והכל עובד.
הכיוון השני לעומת זאת:

r2 = 3 +r;

לא יעבוד. זאת מפאת שהמהדר לא ימיר מספר לאובייקט סתם ככה ואז קריאה ל-3.operator+(r) לא תעבוד - כי מה זה בכלל?

לכן, בדרך כלל נגדיר +++ בתוך פונקציה ולא שיטה כך:

Rational operator+(const Rational& r1, const Rational& r2) 
{ 
	Rational result = r1; 
	result += r2; 
	return result; 
}

אופרטור ההשמה

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

דוגמה

נניח ולא העמסנו את האופרטור השמה של המימוש שלנו של קבוצה ממקודם. אזי, יווצר אוטומטית האחד הזה:

Set& Set::operator=(const Set& s)
{
	size = s.size;
	maxSize = s.maxSize;
	data = s.data;
	return *this;
}

אוי לא, אנו מעתיקים כתובת של פוינטר(ה-datadatadata הוא מערך אם אתם לא זוכרים)
לכן, אנו צריכים לבצע שינוי לאופרטור כך:

Set& Set::operator=(const Set& s)
{
	if (this == &s)
		return *this;
	delete[] data;
	
	maxSize = max(INITIAL_SIZE, s.getSize());
	data = new int[maxSize];
	size = s.getSize();
	
	for (int i = 0; i < size; i++)
		data[i] = s.data[i];
	return *this;
}

בעצם, אופרטור ההשמה הוא מאותה קבוצה של בנאי העתקה וההורס/המפרק(DestructorDestructorDestructor) שכן מה שאנו עושים בהשמה זה בעצם הריסה של האובייקט שעושים עליו השמה ואז העתקה באמצעות בנאי ההעתקה של האובייקט שניתן.

Assignment=Destructor+Copy Constructor\text{Assignment}=\text{Destructor}+\text{Copy Constructor}Assignment=Destructor+Copy Constructor

שינוי התנהגות ברירת המחדל

לפעמים אין הגיון בביצוע העתקה או הריסה לאובייקיטים. במקרים כאלו המחלקה אינה Value  TypeValue\; TypeValueType.
לכן, נרצה למנוע מהמהדר ליצור לנו את היצורים שאיננו רוצים. איך נעשה זאת?

יצירת יצור

כדי ליצור נניח בנאי ברירת מחדל אם יצרנו בנאי אחר, יכולנו לכתוב:

class Person 
{ 
	std::string name; 
	// ...
	
public: 
	Person() = default; 
	Person(std::string name); 
};
הערה

=default=default=default זה יותר טוב מאשר פשוט לרשום בעצמנו את בנאי ברירת המחדל(אם לא שינינו שם שום דבר) שכן אם יש שדות חדשים הם מתעדכנים אוטומטית.

הערה

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

מניעת יצירת יצור

כדי למנוע מיצירת בנאי העתקה ו/או השמה נוכל לכתוב:

class WebServer
{
	// ...

public:
	WebServer(const WebServer&) = delete;
	void operator=(const WebServer&) = delete;
};

אופרטור אינדוקס

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

דוגמה

נניח האובייקט המטורף 'מערך שגם יודע את האורך שלו' כך:

class Array 
{ 
public: 
	explicit Array(int n); 
	Array(const Array&); 
	~Array(); 
	
	Array& operator=(const Array&); 
	int size() const; 
	int& operator[](int index); 
	
private: 
	int* data; 
	int size; 
};

// IN CPP:
int& Array::operator[](int index)
{
	if (index < 0 || index >= size)
		throw out_of_range("Bad index");
	return data[index];
}

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

דוגמה

const int& Array::operator[](int index) const
{
	if (index < 0 || index >= size)
		throw out_of_range("Bad index");
	return data[index];
}
הערה (למה אנו מחזירים רפרנס מלכתחילה?)

כדי שנוכל לעשות את זה:

result[i] = a[i] * val;

מעבר על כל האיברים

כשיש לנו מערך אנו יודעים איך לעבור עליו: לקחת פויינטר להתחלה שלו ולקדם את הפויינטר עד הסוף של המערך. זה עובד במערכים כי הם רציפים בזיכרון כמו נניח רשימה מקושרת או ADTADTADT שבו בכלל אין לנו דרך לדעת מה הסדר.
איך נוכל לבצע איטרציה על איזשהו אובייקט שיצרנו?

דוגמה

נניח ונרצה לעשות משהו כזה עם הקבוצות שלנו:

#include "set.h"

int main()
{
	Set set;
	for (int j = 0; j < 20; j += 2)
	{
		set.add(j);
	}
	
	int sum = 0;
	for (every value in set)
	{
		sum += value;
	}
}

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

אנחנו הולכים להגדיר 2 פוינטרים: begin()begin()begin() אשר ייצג את ההתחלה ו-end()end()end() אשר ייצג את הסוף.

דוגמה

// in .h:
class Set
{
	// ...
public:
	// ...
	const int* begin() const;
	const int* end() const;
};

// in .cpp:
const int* Set::begin() const
{
	return data;
}

const int* Set::end() const
{
	return data + size;
}

// in main:
void setSum(const Set& set)
{
	int sum = 0;
	for (const int* it = set.begin(); it != set.end(); ++it)
	{
		sum += *it;
	}
	cout << "sum of set is " << sum << endl;
}

בתכלס, המימוש שראינו עד כה עובד אך ורק למערך. אנו נרצה שזה לא יהיה תלוי מימוש ולכן נבצע משהו מעט אחר עכשיו.
אנו נצור מחלקה חדשה אשר תשמש אותנו לאיטרציה. אילו אופרטורים יהיו למחלקה הזאת? יהיה לה אופרטור !=!=!= כדי לבדוק האם הגענו לסוף, אופרטור ∗*∗ כדי להוציא ערכים ואופרטור ++++++ כדי לקדם אותנו קדימה.

דוגמה

class Set
{
public:
	// ...
	class Iterator;
	Iterator begin() const;
	Iterator end() const;
private:
	// ...
};
הערה

הגדרה של מחלקה בתוך מחלקה אחרת מאפשרת גישה לשדות הפרטיים של המחלקה החיצונית מהמחלקה הפנימית וגם גורמת לכך שהשם המלא יכלול גם את השם של המחלקה החיצונית.
למשל, בדוגמה שראינו עכשיו: Set::Iterator

מימוש של InterfaceInterfaceInterface

בקובץ set.hset.hset.h לאחר המחלקה SetSetSet נוכל לכתוב:

class Set::Iterator
{
	const Set* set;
	int index;
	Iterator(const Set* set, int index);
	friend class Set; // allow Set to call the constructor
public:
	const int& operator*() const;
	Iterator& operator++(); // prefix (++it)
	Iterator operator++(int); // postfix (it++)
	
	bool operator==(const Iterator& it) const;
	bool operator!=(const Iterator& it) const;
	
	Iterator(const Iterator&) = default;
	Iterator& operator=(const Iterator&) = default;
};

כעת, ב-set.cppset.cppset.cpp נכתוב:

Set::Iterator::Iterator(const Set* set, int index) : set(set), index(index) 
{ }

const int& Set::Iterator::operator*() const
{
	if (index < 0 && index >= set->getSize())
		throw out_of_range("Bad index");
	return set->data[index];
}

Set::Iterator& Set::Iterator::operator++()
{
	++index;
	return *this;
}

Set::Iterator Set::Iterator::operator++(int)
{
	Iterator result = *this;
	++(*this);
	return result;
}

bool Set::Iterator::operator==(const Iterator& i) const
{
	if (set != i.set)
		return false;
	return index == i.index;
}

bool Set::Iterator::operator!=(const Iterator& i) const
{
	return !(*this == i);
}

// now for things in the Set class:
Set::Iterator Set::begin() const
{
	return Iterator(this, 0);
}

Set::Iterator Set::end() const
{
	return Iterator(this, size);
}

לולאות מבוססות טווח ב-C++C++C++

עד כה, כדי לעשות את האיטרציה נדרשנו לעשות:

for (it = s.begin(); it != s.end(); ++it)
{
	int x = *it;
	// use x
}

אבל זה מסובך ולא כיף. נראה כעת דרך נחמדה ועדיפה לביצוע זאת:

for (int x : s)
{
	// use x
}
דוגמה

int sum = 0;
for (int value : set)
{
	sum += value;
}

לולאות מבוססות טווח עובדות לכל מחלקה עם begin()begin()begin() ו-end()end()end() המחזירים IteratorIteratorIterator חוקי אשר יש לו את האופרטורים ++,!=,∗++,!=,*++,!=,∗.
זה כמובן עובד לכל מבנה נתונים בסיסי הנמצא בספריה הסטנדרטית של C++C++C++.

הערה

כדאי להגדיר דברים כרפרנס על מנת למנוע העתקות מיותרות:

for (const Student& s : students)
{
	cout << s.average() << endl;
}

for (Student& s : students)
{
	s.grades[0].applyFactor(5);
}