שפת התכנות טכנית מבוססת על (גל אומר לחשוב על זה ש- זה אנגלית ולטינית בהתאמה - האותיות זהות אך זה לא אותה שפה כלל וכלל)
הרחבות ל- עבור
נתחיל בללמוד את החלקים החסרים לנו מ- כדי שנוכל לעבור בקלות ל-.
מבנים חדשים
זהו טיפוס השייך לקבוצה סופית של ערכים.
נניח נרצה להגדיר את ימות השבוע. כמובן שאנו יכולים לעשות כך:
#define SUNDAY 1
#define MONDAY 2
וכך הלאה, אך ניתן לעשות גם:
enum Month { JAN, FEB, MAR, /* ... */, DEC, MONTH_NUM };
enum Season { SUMMER=1, FALL, WINTER=8, SPRING };
לרוב, כאשר מגדירים ללא מספרים(כמו ב-) זה מתחיל ב-. לכן: .
שימוש
Month today = APR;
Season seasons[MONTH_NUM];
seasons[JAN] = WINTER;
דבר נחמד שניתן לעשות הוא להגדיר משתנה נוסף בקצה ה- ואז הוא יהיה הכמות של כל האיברים ב-.
int count = 0;
for (int month = 0; month < MONTH_NUM; month++)
{
count += (seasons[month] == WINTER);
}
print("%d months are in winter\n", count);
ניתן גם להגדיר פונקציה עם :
Month next_month(Month current)
{
if (current == DEC)
return JAN;
return (Month)(current + 1); // רוב הי
}
נעדיף להשתמש ב- כדי שלא יהיו לנו מספרי קסם בקוד וכדי שנוכל להבין את הקוד שלנו.
בתכלס, זה פשוט מערך של אינטים עם שמות.
בעיה
נשים לב שכאשר יש לנו שני אינמים נניח:
enum PowerState
{
OFF, HIBERNATE, SLEEP, ON
};
enum FlashMode
{
OFF, ON, AUTO, REDEYE
};
תהיה לנו בעיה שבה אנו לא יודעים אם ו- הוא שמגיע מ-PowerState או מ-FlashMode.
על מנת לתקן את הבעיה, נשתמש ב-.
מאוד דומה ל- אך השימוש בו דורש שם מלא כך:
enum class PowerState
{
Off, Hibernate, Sleep, On
};
enum class FlashMode
{
Off, On, Auto, RedEye
};
ואז השימוש בהם הינו:
PowerState state = PowerState::Off;
FlashMode mode = FlashMode::Off;
וכך ניתן למנוע בלגנים בקוד גדול שרושמים אותו אנשים שונים שלאו דווקא יכלו לבדוק במאות קבצים אם קיים במקום אחר.
לאופרטור קוראים scope resultion operator אבל אנחנו נקרא לו נקודותיים-נקודותיים.
מה לא עושים
ב- לא ניתן לבצע
PowerState state = PowerState::Sleep;
int x = state; // NOOOOOOOOOOOOOO
כדי לבצע זאת נעשה:
int x = (int) state;
כאשר אנו מתכנתים דברים יותר מורכבים לעבוד עם משתנים בסיסיים זה מסובן וריגרוזי. לכן, נדרש ליצור חדש שלו קוראים .
מבנה() הוא אוסף של טיפוסים.
struct name
{
<fields-list>
};
המבנה עצמו ממומש על ידי משתנים בסיסיים, אך כשאנחנו משתמשים במבנה, אנו לא חושבים על המימוש שלו, אלא על המבנה כיחידה אחת.
נסתכל על דוגמה:
struct Complex
{
double real;
double imagenary;
};
Complex c1, c2;
Complex *p2 = &c2;
c1.real = 1.0;
c1.imagenary = 3.0;
p2->real = 2.0;
p2->imagenary = 5.0;
printf("c1 = %f + i*%f", c1.real, c2.imagenary);
נשים לב כי למבנים יש תצורה מיוחדת בהתאמה לפוינטרים. במקום לכתוב:
(*p1).real = 2.0;
אנו יכולים לכתוב:
p1->real = 2.0;
enum Gender
{
MAN, WOMAN, OTHER
};
struct Address
{
char* streetAddress;
char* city;
unsigned int zip;
};
struct Person
{
unsigned long id;
char* name;
Gender gender;
Address address;
};
unsigned long id;
char *name1;
Person person, *person_ptr;
...
person.id = id;
person_ptr->id = id + 1;
person_ptr->name = name1;
person.address.zip = person_ptr->address.zip;
פוינטרים לפונקציות
פונקציה היא גם יצור שיש לו מקום בזיכרון לכן, ניתן להצביע אליה. השם של הפונקציה לא כלול במצביע.
נניח עבור הפונקציות:
double sin(double);
double cos(double);
double fabs(double);
נוכל לבצע:
double (*fptr)(double); // fptr can point to any function that takes one double and returns a double.
fptr = cos;
fptr = printf; // Will NOT COMPILE (printf has the wrong type)
double x = fptr(PI/2); // we dont even need to derefrence it to use it.
נוכל להשתמש בפוינטרים לפונקציות כדי לקבל פונקציה בתור פרמטר של פונקציה אחרת כך:
void apply_to_array(double a[], int n, double (*operation)(double))
{
for (int i = 0; i < n; i++)
{
a[i] = operation(a[i]);
}
}
אזי, נוכל לקרוא לפונקציה הזו כך:
double a[100];
...
apply_to_array(a, 100, sin);
#include <math.h>
double derivative(double (*func)(double), double x)
{
const double h = 1e-6;
double diff = func(x + h) - func(x - h);
return diff / (2 * h);
}
double square(double x)
{
return x * x;
}
void compute()
{
printf("%lf", derivative(square(5)));
printf("%lf", derivative(sin(45)));
}
הגדרת
המילה מאפשרת לתת שמות שונים לסוגי משתנים קיימים. לדוגמה:
typedef int Length; // Length is a nickname for int
Length len; // = int len;
Length lengths[SIZE]; // = int lengths[SIZE];
ניתן עדיין להשתמש ב- ובנוסף, ניתן לתת כמה שמות ל- ע"י כמה -ים.
המהדר פשוט מחליף את השם לסוג המקורי.
למה אנו עושים את זה? הקוד קריא יותר ככה. אפשר להבין יותר מה קורה ומה זה כל דבר.
ניתן להגדיר הכל ב- כך:
typedef int (*Callback)(int, char*); // use Callback
ניתן בנוסף להשתמש בזה כדי להסתיר את הסוג האמיתי של משתנים. נניח פה:
#include <stdio.h>
FILE* file = fopen("text.txt");
ב- יש לנו גם דרך אחרת לכתוב כך:
using Point = Real[3];
Point p;
הגדרת
המילה השמורה מאפשרת להגדיר משתנה לא בר שינוי. כך:
const type identifier = experssion;
const double PI = 3.141592654;
const double EPSILON = 1e-6;
double rad;
if (PI * rad * rad < EPSILON)
{
printf("circle is too small!\n");
}
ההבדל בין ל- הוא:
- משתנה מקבל את ערכו בזמן ריצה
- משתנה הוא תלוי בניגוד ל- שאינו תלוי.
ההבדל בין ל- הוא שניתן לשים ב- דברים שהם אינם .
מצביע סטטי
מצביע סטטי הוא סוג של מצביע שלא יכול לשנות את המקום שאליו הוא מצביע. הערך במקום שאליו המצביע מצביע ניתן לשינוי.
int x = 5;
int* const ptr = &x;
*ptr = 7;
// The next line will NOT COMPILE (ptr is const!)
ptr++;
האם זה שימושי? לא באמת.
שימו שאם נכתוב:
const int* ptr = &x;
זה מצביע ל- ולא מצביע סטטי.
בנוסף, ניתן לחבר בין מצביע סטטי למצביע למשתנה סטטי כך:
const int* const ptr2 = &x;
נראה את כולם ביחד:
const int n = 5; // a const int
const int* ptr = &n; // a pointer to a const int
int* const cptr = &n; // a const pointer to an int (in this case it will lead to an error cause n is const)
const int* const cptr2 = &n; // a const pointer to a const int
ב-, אם ננסה לשים מצביע לערך סטטי בתוך מצביע רגיל תהיה לנו שגיאת קומפילציה אלא אם נבצע המרה:
const int n = 10;
const int* p = &n;
int* q = p; // ERROR
int* q2 = (int*)p; // compiles
השורה האחרונה מציגה משהו שלא באמת נרצה לעשות - אך אנו צריכים לדעת כי זוהי אפשרות.
משתנה מסוג
משתנה מסוג קיים רק ב- והוא גרסה פשוטה יותר של מצביע.
מסמנים משתנה מסוג זה באמצעות והוא מתנהג כמעט בדיוק כמו מצביע רק בלי לכתוב כוכביות.
int i = 1
int& r = i;
int x = r;
r = 2;
בסוף קטע התוכנית הזו נקבל כי:
בעצם, הרפרנסים הם מצביעים קבועים - לא ניתן לשנות את המיקום אליו הם מצביעים. זה שימושי אם אנחנו צריכים משהו חלש יותר ממצביע שהוא יותר קל לשימוש.
לא ניתן ליצור רפרנס ל-.
int i = 0, j = 5;
int& r = i;
r = j; // same as i = j; or i = 5;
int* p = &r; // p points to i
void swap(int& a, int& b)
{
int temp = a;
a = b;
b = temp;
}
לא נוכל להשתמש בערך במקום במשתנה. לדוגמה השורה הבאה תתן לנו שגיאה:
swap(x, 3); // ERROR
-ים קבועים
נוכל להגדיר גם רפרנסט קבוע() שבדומה לפוינטר של קבוע, לא ניתן לשנות את הערך שאליו הרפרנס מצביע.
void print(const int& r)
{
cout << r << endl;
}
void print(const Complex& c)
{
cout << c.re << " " << c.im << endl;
}
ניתן לשלוח לרפרנס קבוע ערך זמני זאת בניגוד לרפרנס לא קבוע.
int a = 5;
print(a);
print(3);
print(3+a);
לא כדאי להשתמש ברפרנסים סתם ככה. אבל במקרים של מבנים גדולים או מסובכים, במידה ואנו רוצים להעביר אליה אז אנו נדרש להעתיק את כל המחרוזת. עם רפרנס סטטי נוכל גם לוודא שהמשתנה לא ישתנה וגם נוכל להעביר אותו לפונקציה מבלי לבזבז זמן ומשאבים על העתקת המשתנה הגדול והכבד.
החזרת של פונקציה
אנו יכולים להשתמש ברפרנס גם כערך המוחזר מפונקציה כך:
int& max(int array[], int size)
{
int maxIndex = 0;
for (int i = 1; i < size; i++)
{
if (array[i] > array[maxIndex])
maxIndex = i;
}
return array[maxIndex];
}
כעת, נוכל להשתמש ברפרנס כדי להשים דברים באיבר הזה במערך כך:
int array[] = { 1,3,4,2,5 };
max(array, 5) = 7;
הקוד הנ"ל מבצע בעצם את השורה array[4]=7;.
לעולם אל תחזירו רפרנס לוקלי שכן המשתנה אליו הרפרנס מצביע יעלם עם סוף הפונקציה.
העמסה -
אנו יכולים ב- להגדיר מספר פונקציות עם אותו השם כאשר ההבדל ביניהן הוא הארגומנטים אותן הן מקבלים(סוגים ומספר)
המהדר בוחר את הפונקציה הכי מתאימה (איזו פונקציה הכי מתאימה? "זו שאלה נהדרת לבחינה" - גל)
void swap(int&, int&);
void swap(double&, double&);
int a, b;
double c, d;
swap(a,b); // קריאה לפונקציה הראשונה
swap(c,d); // קריאה לפונקציה השנייה
swap(a,c); // SYNTAX ERROR
ארגומנטים רגילים
מקרה פרטי של העמסה הוא שאנו יכולים להגדיר ערכים בסיסיים או רגילים לארגומנטים של פונקציה ואם נקרא לפונקציה ללא הארגומנט הזה הוא יושם לערך הבסיסי.
void printNumbers(int n = 10);
את הפונקציה נוכל להגדיר כך:
void printNubmers(int n)
{
for (int i = 0; i < n; i++)
cout << i << " ";
cout << endl;
}
ולבסוף נוכל לקרוא לה:
printNumbers(5);
printNubmers(); // printNumbers(10);
את הערכים הבסיסיים נגדיר רק ב- ולא בהגדרה.
זיכרון דינאמי
ב- כדי להגדיר ולמחוק זכרון השתמשנו ב- ו- אבל ב- נשתמש בפונקציות חדשות וטובות יותר והן ו-.
int* ptr = new int(5);
int* array = new int[10]();
// ...
delete ptr;
delete[] array;
ניתן להגדיר פוינטר למערך גם בלי הסוגריים כך int* array = new int[10];
ההבדל הוא שאם שמים סוגריים אז הערכים במערך יאופסו ואם לא שמים סוגריים הערכים במערך יהיו ערכי זבל.
קלט ופלט
נסתכל על קוד ב-:
#include <stdio.h>
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
int main()
{
printf("Hello World!\n");
printf("5! = %d\n", factorial(5));
return 0;
}
כעת נרצה לכתוב אותו ב- ולשם כך נשתמש ב-:
#include <iostream>
using namespace std;
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
int main()
{
cout << "Hello world!" << endl;
cout << "5! = " << factorial(5) << endl;
return 0;
}
ב- אנו משתמשים בקונספט של כדי לקלוט מהמשתמש וכדי לשלוח פלט למשתמש. ה- שיש לנו הינם:
- - הקלט הסטנדרטי
- - הפלט הסטנדרטי
- - הפלט שגיאות הסטנדרטי
ניתן לשרשר גם פלט וגם קלט עם .
כעת נרצה לראות גם קצת פלט.
#include <iostream>
using namespace std;
int main()
{
int n;
const char* str = "Hello";
int* ptr = &n;
cin >> n;
cout << str << endl;
cerr << n << " at " << ptr << endl;
return 0;
}
קבצים ב-
ניתן לקלוט ולפלוט לקבצים ב- עם ה- הבאים:
- - לקריאה מקבצים
- - לכתיבה לקבצים
וכך אנו משתמשים בהם:
ifstream codeFile("code.txt"); // attempts to open file
if (!codeFile)
{
// handle error
}
int code;
codeFile >> code;
ofstream primeFile("primes.txt");
primeFile << 2 << 3 << 5 << 7 << 11 << endl;
נוכל בנוסף לקרוא תווים בודדים כך:
void copy(ifstream& source, ofstream& dest)
{
char c;
while (!source.eof())
{
source.get(c); // like source >> c
dest.put(c); // like dest << c
}
}
ולקרוא שורות שלמות כך:
ifstream source;
char line[256];
while (source.getline(line, sizeof(line)))
{
int val = std::stoi(line);
cout << val * val << endl;
}
נוכל למצוא שגיאות במהירות עם המקרו שמוגדר ב-<cassert>.
אנו משתמשים ב- כדי לבצע דיבאג ולרוב הוא לא רץ אלא אם נבקש מהמהדר לעשות זאת.
המאקרו הזה לוקח תנאי, ואם הוא לא מתקיים אז הוא מקריס את התוכנה ונותן לנו הערה נחמדה לפלט.
#include <cassert>
...
int sum(int* array, int size)
{
assert(array != NULL && size >= 0);
...
}
מומלץ להשתמש ב- רק בפונקציות פנימיות שידוע שכל מי שקורא להם בודק את התנאים מראש
" הוא משהו שנועד לבדוק דברים שהם בטוח נכון"
עבודה עם מספר קבצים
ב- יש לנו שני סוגי קבצים:
- קבצי המסומנים שבו נמצאות כמעט ורק הצהרות.
- קבצי המסומנים שבהם נמצאות ההגדרות והפעולות השונות.
את הקבצים מחבר ביחד ה- שאז לוקח אותם למהדר שיוצר איזשהו קובץ ביניים שבו יש סוג מסויים של אסמבלי שקרוב לקוד מכונה של המכונה ולאחר מכן, בחלק השני של המהדר לוקחים את הקוד ביניים הזה והופכים אותו לקוד מכונה המתאים למערכת שלכם.
אם פונקציה משתמשת בפונקציה באותו הקובץ, אזי צריכה להיות מוגדרת או מוצהרת לפני .
כאשר אנו עובדים עם מספר קבצים, אם בקובץ אחד פונקציה משתמשת בפונקציה מקובץ אחר, הקובץ אחר צריך להיות בלתי תלוי בקובץ בו הפונקציה משתמשת.
ישנו כלל אצבע: טיפוס אחד פר קובץ (באופן רעיוני)
שילוב בין קבצים
ניתן לקמפל כמה קבצים ביחד בפשטות:
> gcc main.cpp utility.cpp testing.cpp
וזה עובד (ככל הנראה יש חשיבות לסדר) והפונקציות שמשתמשות אחת בשניה חייבות להיות מוצהרות בקובץ בו הם משתמשים. עוד שניה נראה כי לא עושים ככה.
בואו נניח נגדיר לנו טיפוס - את כל מה שקשור ל- נשים בשני קבצים: .
// complex.h:
struct Complex
{
double re, im;
}
Complex complexAdd(Complex x, Complex y);
// complex.cpp:
#include "complex.h"
Complex complexAdd(Complex x, Complex y)
{
Complex z;
z.re = x.re + y.re;
z.im = x.im + y.im;
return z;
}
נשים לב כי עשינו בקובץ לקובץ . מה זה?
הוא דבר מאוד מטומטם, הוא פשוט לוקח את הקובץ שעשית לו ומעתיק אותו לקובץ.
כל קובץ יבצע לקובץ שלו (במידה וקיים לו אחד שכזה)
הקדם מעבד
זהו יצור אשר פעול לפני המהדר ומבצע כמה דברים כגון:
- העתקת קבצים:
#include <file-name> #include "file-name" - הגדרות מאקרו:
#define identifier value #define identifier(variables) value #undef identifier - הידור על תנאי: ניתן להגיד למהדר להדר משהו רק אם איזשהו תנאי מתקיים:
#if condition #ifdef identifier #ifndef identifier #else #endif
כאשר אנו מקשרים קבצים אנו נבצע רק לקובץ וזה מספיק בשביל שה-(שנלמד עליו באת"ם) ידע לצרף גם את הקובץ .
בעיות ב-
מדי פעם יהיו לנו מספר קבצים שבהם יופיע אותו ה-. אנו לא רוצים לבצע העתקה פעמיים ולכן אנו נרשום בראש כל את השורה:
#pragma once
#pragma once
struct Complex
{
double re, im;
}
Complex conjugate(Complex);
const Complex I = {0.0, 1.0};
#include "utility.h"
...
#pragma once
#include "utility.h"
void testComplexConjugate();
#include "utility.h"
#include "testing.h"
...
#include "utility.h"
#include "testing.h"
...
מרחבים עם שמות
אנו לא רוצים שיווצר לנו מצב ששני מתכנתים הגדירו פונקציה בעלת אותו השם בלי לדעת, מה שיכול ליצור שגיאות מוזרות ומשונות.
בשביל לפתור את זה אנו נשתמש במרחבים עם שמות - אשר יאפשרו לנו לשים איזה מזהה לפני שמות של פונקציות, משתנים, מבנים ועוד.
namespace fast_math
{
double sqrt(double);
double cos(double);
double sin(double);
}
כעת נדרש לקרוא לפונקציות המוגדרות בתוך המרחב ע"י שימוש בשם המלא של הפונצקיות:
<namespace>::<item>
fast_math::sqrt(4);
אם אנו משתמשים בקובץ מסויים הרבה פעמים מפונקציות ממרחב מסויים ואין לנו סיכוי ליצור לאף פונקציה שאנו משתמשים בה קונפליט אז אנו יכולים לעשות:
using namespace <name>;
זה יכול ליצור מלאאאא קונפליקטים ולכן מומלץ לא לעשות וגם אם כן עושים לעשות זאת לדבר בודד ולא ל- כך:
using std::string;
לא מבצעים בקובץ שכן אז אצל כולם יהיה וזה יכול ליצור קונפליקטים עם פונקציות לוקליות.