עצות לאחותי שמתחילה ללמוד C++

08/06/2015
C++

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

1. למדי להבדיל בין טיפוסי המשתנים השונים

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

string band = "zero";
band += "7";

cout << "band = " << band << endl;

אך הקוד הבא ידפיס רק את המילה zero:

string band = "zero";
band += 7;

cout << "band = " << band << endl;

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

רוצה לנסות את הקוד? בקישור הבא תמצאי סביבת עבודה מוכנה עם הקוד בפנים. כל שצריך הוא ללחוץ ״הפעל״ ולראות את זה רץ:
http://goo.gl/WyGmTh

2. זכרי לטפל בכל האזהרות של הקומפיילר

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

int main()
{
    int x = 10;
    short y = 2000000;

    if ( x > y ) {
        cout << "oh my... ";
    } else {
        cout << "ok math works";
    }

    cout << endl;
   return 0;
}

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

רוצה לנסות את הקוד? בקישור הבא תמצאי סביבת עבודה מוכנה עם הקוד בפנים. כל שצריך הוא ללחוץ ״הפעל״ ולראות את זה רץ:
http://goo.gl/lrC8HZ

3. למדי להבדיל בין שגיאות Compiler לשגיאות Linker

הקוד שלך עובר פענוח דו-שלבי במעבר מטקסט לקובץ הפעלה: תחילה ה Compiler הופך את הקוד לשפת מכונה ולאחר מכן ה Linker מקשר את כל הקבצים בתוכנית (ומידע נוסף ממערכת ההפעלה) לכדי קובץ הרצה יחיד. לפעמים ה Compiler יתלונן על הקוד ולפעמים ה Linker, ולכל אחד יש סיבות משלו להתלונן. כך לדוגמא הקוד הבא לא יעבור קומפילציה כי הפונקציה doStuff לא הוגדרה:

#include <iostream>
using namespace std;

int main()
{
    int x = doStuff();
   return 0;
}

אבל הקוד הבא לא יעבור קישור מאחר והפונקציה doStuff לא מומשה:

#include <iostream>
using namespace std;

int doStuff();

int main()
{
    int x = doStuff();
   return 0;
}

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

/tmp/ccOmpNNW.o: In function `main':                                                                                                                             
main.cpp:(.text+0x9): undefined reference to `doStuff()'                                                                                                         
collect2: error: ld returned 1 exit status    

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

רוצה לנסות את הקוד? בקישור הבא תמצאי סביבת עבודה מוכנה עם הקוד בפנים. כל שצריך הוא ללחוץ ״הפעל״ ולראות את זה רץ:
http://goo.gl/aM7RZm

4. למדי להבדיל בין משתנים אוטומטיים, References ו Pointers

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

#include <iostream>
using namespace std;

int main()
{
    string s = "hello";
    string t = s;

    // replace letters "lo" from string t
    // with the single letter "p"
    t.replace(3,2,"p");

    cout << "s = " << s << ", t = " << t << endl;
}

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

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

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

#include <iostream>

using namespace std;

int main()
{
    string s = "hello";
    string &t = s;
    string *q = &s;

    t.replace(0, 1, "H");
    q->replace(1, 1, "3");

    cout << "s = " << s << ", t = " << t << ", q = " << *q << endl;

   return 0;
}

רוצה לנסות את הקוד? בקישור הבא תמצאי סביבת עבודה מוכנה עם הקוד בפנים. כל שצריך הוא ללחוץ ״הפעל״ ולראות את זה רץ:
http://goo.gl/X1Dj9C

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

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

Mat src(100, 100, CV_32FC1);
src.release(); // will free memory

לעומת זאת אם את מקצה את הזכרון בעצמך באמצעות הפקודה new באחריותך לנקות אותו באמצעות הפונקציה delete בנוסף:

float* data = new float[100 * 100];
Mat src(100, 100, CV_32FC1, data);
src.release(); // will not free memory
delete [] data;

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

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