C++ מעמיק

מדריך לשפת C++

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

עיצוב מונחה עצמים

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

למידה לפי דוגמה 3

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

תרגיל מס' 1: תרגול נוסף עם מצביעים

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

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

void Unknown(int *p, int num);
void HardToFollow(int *p, int q, int *num);

void Unknown(int *p, int num) {
  int *q;

  q = #
  *p = *q + 2;
  num = 7;
}

void HardToFollow(int *p, int q, int *num) {
  *p = q + *num;
  *num = q;
  num = p;
  p = &q;
  Unknown(num, *p);
}

main() {
  int *q;
  int trouble[3];

  trouble[0] = 1;
  q = &trouble[1];
  *q = 2;
  trouble[2] = 3;

  HardToFollow(q, trouble[0], &trouble[2]);
  Unknown(&trouble[0], *q);

  cout << *q << " " << trouble[0] << " " << trouble[2];
}

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

תרגיל מס' 2: תרגול נוסף עם שיעורים וחפצים

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

תרגיל מס' 3: מערכים רב-ממדיים

כדאי לשקול את התוכנית הבאה: 

const int kStudents = 25;
const int kProblemSets = 10;

// This function returns the highest grade in the Problem Set array.
int get_high_grade(int *a, int cols, int row, int col) {
  int i, j;
  int highgrade = *a;

  for (i = 0; i < row; i++)
    for (j = 0; j < col; j++)
      if (*(a + i * cols + j) > highgrade)  // How does this line work?
        highgrade = *(a + i*cols + j);
  return highgrade;
}

int main() {
 int grades[kStudents][kProblemSets] = {
   {75, 70, 85, 72, 84},
   {85, 92, 93, 96, 86},
   {95, 90, 83, 76, 97},
   {65, 62, 73, 84, 73}
 };
 int std_num = 4;
 int ps_num = 5;
 int highest;

 highest = get_high_grade((int *)grades, kProblemSets, std_num, ps_num);
 cout << "The highest problem set score in the class is " << highest << endl;

 return 0;
}

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

כותבים תוכנית שמפעילה מערך 3dim וממלאת את הערך של המאפיין השלישי בסכום של כל שלושת האינדקסים. כאן זה הפתרון שלנו.

תרגיל מס' 4: דוגמה לשימוש נרחב בעיצוב OO

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

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

בדיקת יחידה

מבוא

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

ל'בדיקות יחידה' יש את המאפיינים הבאים. הם...

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

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

הבדיקות המתבצעות כחלק מבדיקת היחידה מתוארות בהמשך.

בעולם אידיאלי, אנחנו בודקים את הדברים הבאים:

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

רמת הכיסוי של הקוד

בפועל, אנחנו לא יכולים להשיג "כיסוי קוד" מלא במסגרת הבדיקות שלנו. כיסוי קוד הוא שיטת ניתוח שקובעת אילו חלקים של מערכת תוכנה בוצעו (מכוסה) על ידי החבילה של בקשות הבדיקה ואילו חלקים לא בוצעו. אם ננסה להשיג כיסוי של 100%, נקדיש יותר זמן לכתיבת בדיקות יחידה מאשר בכתיבת הקוד עצמו! כדאי ליצור בדיקות יחידה (unit testing) של כל הנתיבים הבלתי תלויים הבאים. זו יכולה להיות במהירות בעיה מעריכית.

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

במקום לנסות כיסוי של 100%, אנחנו מתמקדים בבדיקות שמעלות את הביטחון שהמודול פועל כראוי. אנחנו בודקים דברים כמו:

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

מסגרות לבדיקת יחידה

רוב המסגרות לבדיקת יחידות משתמשות בטענות נכוֹנוּת (assertions) כדי לבדוק ערכים במהלך ביצוע של נתיב. טענות נכוֹנוּת (assertions) הן הצהרות שבודקות אם תנאי מסוים מתקיים. התוצאה של טענת נכונות יכולה להיות הצלחה, כישלון לא קטלני או כישלון קטלני. לאחר ביצוע טענת נכוֹנוּת (assertion), התוכנית ממשיכה כרגיל אם התוצאה היא הצלחה או כישלון לא קריטי. במקרה של כשל חמור, הפונקציה הנוכחית תבוטל.

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

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

התקנת CPPUnit

יש להוריד את הקוד של CPPUnit מ-SourceForge. מוצאים ספרייה מתאימה ומציבים בה את הקובץ tar.gz. לאחר מכן, מזינים את הפקודות הבאות (ב-Linux או Unix), ומחליפים את שם קובץ ה-cppunit המתאים:

gunzip filename.tar.gz
tar -xvf filename.tar

אם אתם עובדים ב-Windows, ייתכן שתצטרכו למצוא כלי עזר כדי לחלץ קובצי tar.gz. השלב הבא הוא הידור הספריות. צריך לשנות זאת לספרייה של cppunit. יש שם קובץ INSTALL שמספק הוראות ספציפיות. בדרך כלל צריך להריץ:

./configure
make install

אם תיתקלו בבעיות, תוכלו לעיין בקובץ INSTALL. הספריות בדרך כלל נמצאות בספרייה cppunit/src/cppunit. כדי לבדוק אם ההידור פועל, צריך להיכנס לספרייה cppunit/examples/simple ולהקליד "make". אם הכול מתבצע בסדר, הכול מוכן.

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

למה עליי לעשות את זה??

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

מניסיון רב, מהנדסים יודעים שהסיכויים שתוכנית מסוימת תפעל בניסיון הראשון הם קטנים מאוד. בדיקות היחידות מתבססות על הרעיון הזה על ידי יצירת תוכניות בדיקה שמאפשרות בדיקה עצמית וחזרה עליהן. טענות הנכוֹנוּת (assertions) מחליפות את מקום הבדיקה הידנית של הפלט. ומכיוון שקל לפרש את התוצאות (הבדיקה עוברת בהצלחה או נכשלת), אפשר להריץ את הבדיקות שוב ושוב, וכך לספק רשת ביטחון שבזכותה הקוד עמיד יותר בפני שינויים.

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

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

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

הקדש זמן כדי לכתוב בדיקות יחידה באמצעות CPPUnit לאפליקציית מסד הנתונים Composer. לקבלת עזרה אפשר לעיין בספרייה / cppunit/examples/.

הסבר על Google

מבוא

אפשר לדמיין נזיר בימי הביניים מעיין באלפי כתבי יד בארכיוני המנזר.“Where is that one by Aristotle...”

ספריית Monastary

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

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

תחילת העבודה עם אחזור מידע

חתול בכובע

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

(COW ו-BEE) או CROWS

נחיל כמה מודלים קלאסיים לאחזור מידע כדי לעזור לנו לפתור את הבעיה הזו.

גישה ברורה היא כוח אכזרי: השיגו את כל 46 הסיפורים של ד"ר סוס, והתחילו לקרוא. צריך לשים לב איזה ספר מכיל את המילים COW ו-BEE. עם זאת, צריך לחפש ספרים שכוללים את המילה CROWS. מחשבים הרבה יותר מהירים מזה אנחנו. אם יש לנו את כל הטקסט מהספרים של ד"ר סוס בצורה דיגיטלית, למשל כקובצי טקסט, נוכל פשוט לחפש בקבצים האלה. השיטה הזו עובדת היטב עבור אוסף קטן כמו הספרים של ד"ר סוס.

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

גישה אחרת מלבד gRep, היא ליצור אינדקס של המסמכים באוסף מראש, לפני ביצוע החיפוש. אינדקס ב-IR דומה לאינדקס בחלק האחורי של ספר לימוד. אנחנו מכינים רשימה של כל המילים (או המונחים) בכל סיפור של ד"ר סוס, תוך השמטת מילים כמו "the", "and", וקשרים אחרים, מילות יחס וכו' (המילים האלה נקראות מילות מעצור). לאחר מכן אנחנו מייצגים את המידע הזה בדרך שמאפשרת למצוא את המונחים ולזהות את הסיפורים שהם מקורם.

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

טבלה של ספרים ומילים

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

נחזור לבעיה המקורית:

(COW ו-BEE) או CROWS

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

(100001 ו-010011) או 000010 = 000011

התשובה: "מר בראון קאן מו! Can You? ו-The Lorax. זה איור של מודל אחזור בוליאני, שהוא מודל בהתאמה מדויקת.

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

שיפורים מסוימים

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

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

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

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

  1. שמור את הסמנים בשתי הרשימות ועברו על שתי רשימות הפרסומים בו-זמנית.
  2. בכל שלב, משווים את ה-DocsID שאליו מצביעים שני המצביעים.
  3. אם הם זהים, יש להכניס את ה-DocsID הזה לרשימת התוצאות, אחרת, להתקדם עם הסמן שמפנה ל-docID הקטן יותר.

כך ניתן ליצור אינדקס הפוך:

  1. הקצה מזהה מסמך לכל מסמך רלוונטי.
  2. מזהים את המונחים הרלוונטיים לכל מסמך (יצירת אסימון).
  3. לכל מונח צריך ליצור רשומה שכוללת את המונח, את ה-DocsID שבו הוא נמצא ואת התדירות במסמך הזה. הערה: אם מונח מסוים מופיע ביותר ממסמך אחד, עשויות להיות רשומות מרובות שלו.
  4. ממיינים את הרשומות לפי מונח.
  5. כדי ליצור את המילון ואת רשימת הפרסומים, צריך לעבד רשומות בודדות של מונח, וגם לשלב את הרשומות המרובות של מונחים שמופיעים ביותר ממסמך אחד. יוצרים רשימה מקושרת של מזהי Docs (לפי סדר ממוין). לכל מונח יש גם תדירות, שהיא סכום התדרים בכל הרשומות של המונח.

הפרויקט

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

זהו התהליך האפשרי להשלמת הפרויקט:

  1. הדבר הראשון שצריך לעשות הוא להגדיר אסטרטגיה לזיהוי מונחים במסמכים. מכינים רשימה של כל מילות מעצור שאפשר לחשוב עליהן, וכותבים פונקציה שקוראת את כל המילים שבקבצים, שומרת את המונחים ומתעלמת מהן. יכול להיות שיהיה צורך להוסיף עוד מילות מעצור לרשימה בזמן שבודקים את רשימת המונחים באיטרציה.
  2. כתיבת מקרי בדיקה של CPPUnit כדי לבדוק את הפונקציה, וקובץ Makefile שיאחד את כל המידע עבור ה-build. כדאי לבדוק בקובץ CVS את הקבצים, במיוחד אם אתם עובדים עם שותפים. כדאי לבדוק איך אפשר לפתוח את מכונת ה-CVS למהנדסים מרוחקים.
  3. הוספת עיבוד כדי לכלול נתוני מיקום. כלומר, איזה קובץ ואיפה נמצא המונח בקובץ? כדאי לחשוב על חישוב כדי להגדיר את מספר הדף או את מספר הפסקה.
  4. כתיבת מקרי בדיקה של CPPUnit כדי לבדוק את הפונקציונליות הנוספת הזו.
  5. ליצור אינדקס הפוך ולאחסן את נתוני המיקום ברשומה של כל מונח.
  6. כתיבה של מקרי בדיקה נוספים.
  7. תכנון ממשק שיאפשר למשתמש להזין שאילתה.
  8. בעזרת אלגוריתם החיפוש שתואר למעלה, מעבדים את האינדקס ההופכי ומחזירים את נתוני המיקום למשתמש.
  9. אל תשכחו לכלול גם מקרי בדיקה עבור החלק האחרון הזה.

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

תכונה נוספת

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

אפליקציה: להגיע לכל מקום!

כאן תוכלו לראות איך מיישמים את העקרונות האלה בכתובת Panoramas.dk.