ניהול זכרון ב־C++‎ - האגדות והמציאות

ב־17.8.2008, מאת ארתיום; פורסם תחת: פיתוח, תכנה ומחשבים, C++‎‏; ‏14 תגובות

אחת הסיבות שמפתחים רבים כל־כך אוהבים את C#/Java, היא העובדה שהזיכרון מנוהל ע"י Garbage Collector ומסיר (כמעט) כל דאגה לניהול הזיכרון מהמפתח.

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

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

נתחיל עם הדוגמאות. אני אציג שלושה קטעי הקוד: אחד ב־Java, אחד ב־C ואחד ב־C++‎. בכל אחת מהדוגמאות, אני אצור מבנה זהה, המורכב מאובייקט שמחזיק הפניות/מצביעים לשני אובייקטים אחרים.

קטע קוד שכתוב ב־Java:

class A { public int a; }
class B { public int b; }

class X {
    public A ar;
    public B br;
    public X(int x,int y) {
        ar=new A();
        br=new B();
        ar.a=x;
        br.b=y;
    }
};
class test {
    public static void main(String[] args)
    {
        X r=new X(10,20);
        System.out.println(r.ar.a);
        System.out.println(r.br.b);
    }
};

כפי שניתן לראות, אין כל דאגה ניהול זיכרון --- GC של Java עושה את הכל בשבילנו.

הנה קטע קוד דומה ב־C.

#include <stdio.h>
#include <stdlib.h>

struct A {int a;};
struct B {int b;};

struct X {
    struct A *pa;
    struct B *pb;
};

void X_delete(struct X *p)
{
    if(!p) return;
    free(p->pa);
    free(p->pb);
    free(p);
}

struct X *X_new(int x,int y)
{
    struct X *res;
    if((res=calloc(1,sizeof(struct X)))==NULL)
        goto error_exit;
    if((res->pa=malloc(sizeof(struct A)))==NULL)
        goto error_exit;
    if((res->pb=malloc(sizeof(struct B)))==NULL)
        goto error_exit;
    res->pa->a=x;
    res->pb->b=y;
    return res;
error_exit:
    X_delete(res);
    return NULL;
}

int main()
{
    struct X *r=X_new(10,20);
    if(!r) return 1;
    printf("%d %d\n",r->pa->a,r->pb->b);
    X_delete(r);
    return 0;
}

כפי שניתן לראות ניהול הזיכרון הוא גוזל ממנו כ־20 שורות נוספות. בהחלט לא נעים.

אז בואו נראה איך אותו דבר ייראה ב־C++‎?

#include <iostream>
#include <memory>
#include <boost/shared_ptr.hpp>
using boost::shared_ptr;
using namespace std;

struct A {int a;};
struct B {int b;};

struct X {
    shared_ptr<A> pa;
    shared_ptr<B> pb;
    X(int x,int y)
    {
        pa=shared_ptr<A>(new A);
        pb=shared_ptr<B>(new B);
        pa->a=x;
        pb->b=y;
    }
};

int main()
{
    auto_ptr<X> r(new X(10,20));
    cout<< r->pa->a << " " << r->pb->b <<endl;
    return 0;
}

איזה פלא!!! הקוד של C++‎ זהה כמעט לחלוטין לקוד של Java! רק עם הבדל קטן: אני אומר במפורש בכל פעם: כיצד אני רוצה לנהל זיכרון:

במחלקה X אני מנהל זיכרון בעזרת reference counting. וב־main אני אומר שאני רוצה למחוק את האובייקט ברגע שאני יוצא מפונקציה.

למעשה, יכולתי לכתוב constructor ו־destructor עבור מחלקה X, יכולתי לכתוב new ו־delete ב־main. אבל אנחנו כבר לא ב־C, אנחנו בעידן אחר.

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

הערות:

  1. boost::shared_ptr אומנם חלק מספריית boost אבל הוא כבר חלק מ־C++0x --- הסטנדרט החדש של C++‎, וגם קיימות עשרות ספריות אחרות, מלבד boost, שנותנות פונקציונליות דומה.
  2. למרות שנראה שב־C אני עושה יותר (בודק בכל פעם הקצאות), בפעול זה נעשה גם ב־C++‎, רק שבמקום לקבל NULL כערך, אני אקבל exception:‏ std::bad_alloc.

תגובות

ארז, ב־17.8.2008, 12:56

יפה, אבל להציג את BOOST כC++, שלא לדבר על הסטנדרט שאולי במקרה הטוב ייכנס בעוד שנה שנתיים כ"הסטנדרט החדש"?

למה לא תעיף את כל ניהול הזיכרון של הקוד שכתבת בC לתוך איזו ספריה, תעשה לה INCLUDE והנה "ניהול זיכרון מודרני". יש יתרונות לניהול הזיכרון הידני בC++, מכיוון שלא תמיד יעיל לתת לCG לעשות מה שבא לו, מתי שבא לו, ותחפש את החברים שלך. לפעמים שווה לכתוב עוד כמה שורות קוד ולא לגלות בפרודקשין שהשרת עף על 100% משאבים כי הGC היה בהפסקת אוכל.

ik_5, ב־17.8.2008, 13:04

הערה כלפי ג'אווה: השתמשת ב class בשביל להגדיר שדות בלבד. למה לא להשתמש ב interface במקום (סתם הערה שולית) ?!

לגבי ניהול זיכרון. אם כבר פסקל פתרה לך חלק מהבעיות האלו עם class היות והניהול זכרון נמצא לך "מאחורי הקלעי". אתה כמובן יכול להשפיע עליו, אבל ב99% מהפעמים אין לך צורך. שים לב שעם פסקל אין לך צורך בספרייה כמו Boost או להשתמש ב Genercis בשביל להשיג את אותה "תחושה" של ג'אווה (אם כי אתה כן חייב לשחרר זכרון, אבל זה נעשה בשורת קוד אחת ולא בכמה ובתור שיגרה של המחלקה שלך).

ארתיום, ב־17.8.2008, 13:20

יפה, אבל להציג את BOOST כC++, שלא לדבר על הסטנדרט שאולי במקרה הטוב ייכנס בעוד שנה שנתיים כ"הסטנדרט החדש"?

...

אין לך צורך בספרייה כמו Boost

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

למשל, תחליף את

#include <boost/shared_ptr.hpp>
using boost::shared_ptr;

ב:

#include <tr1/memory>
using std::tr1::shared_ptr;

וזה יעבוד באותה צורה (בדוק על gcc 4.1 לא יודע לגבי ישנים יותר).

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

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

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

על זה אני מדבר. באותה מידה יכולתי לכתוב, delete בתוך destructor בלי shared_ptr.

נדב, ב־17.8.2008, 14:11

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

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

הם עובדים (במפורש) רק עם טיפוסים סטנדרטיים של ansi c++. הסיבה, היא שהם מכילים templates מוכנים מראש של כל האופציות (בפרוייקט הראשון קידד אותם ידנית, השני מייצר אותם באמצעות קובץ perl). מכאן המוגבלות שלהם.

למשל, אתה לא יכול ליצור delegate שמקבל struct point. כמו כן, כל פעם שאתה יותר delegate עם חתימה שונה (שלוש int, מול שני int ואז *char) אתה מנפח את הקוד של התוכנית שלך. ואחרון חביב, והכי חשוב, הקישור אל המצביעים האמיתיים נעשה בזמן קומפילציה, ולא בזמן ריצה כמו שמתבקש ממנגנון כזה. זה אומר שאתה חייב לדעת את הכתובת של המצביעים בזמן הקומפילציה - עוד מגבלה של שני המימושים.

השפה היא מוגבלת, אפשר לעקוף חלק מהמגבלות הללו, ולפעמים אפשר להסתיר את התחביר הממש מכוער של c++. אבל בכל מיקרה, אתה תיתקע בגבול עליון שהשפה לא מאפשרת לך לעבור אותו.

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

ואילו הדוגמאות שהבאתי לו: "http://www.codeproject.com/cpp/ImpossiblyFastCppDelegate.asp http://www.codeproject.com/cpp/FastDelegate.asp"

היה לי גם ויכוח דומה על GC

ארתיום, ב־17.8.2008, 14:54

שמע, נדב,

אני הייתי שמח לקבל קישור לדיון המקורי. בנוסף, אני מסכים שב־C++‎ המצב של signals/delegates לא מזהיר... בגלל זה יש boost::bind.

בכל אופן, אני לא בא להוכיח שום דבר. ברור לי ש־C#/Java הם יותר קלים לכתיבה. אבל גם ברור לי שהיחס כלפי C++‎ כמו ל־C הוא לא נכון.

כל מה שרציתי להראות הוא: ניהול זיכרון ב־C++‎ הוא לא מה שהיה פעם.

נדב, ב־17.8.2008, 15:07

אין לי אפשרות כי זה התנהל בהודעות פרטיות בוואטסאפ

ארתיום, ב־17.8.2008, 15:16

בכל מקרה, נדב, ייתכן שאתה מחפש את זה:

#include <iostream>
#include <tr1/functional>

using namespace std;
using tr1::function;
using tr1::bind;
using namespace std::tr1::placeholders;

class someclass {
public:
    int n;
    void somefunc(int x){
        cout<<n+x<<endl;
    };
};

int main()
{

    boost::function<void(int)> delegate;
    someclass s;
    s.n=10;
    delegate=bind(&someclass::somefunc,&s,_1));

    delegate(15);
    return 0;
}

נכון שלא ניתן לעשות את זה עבור פונקציות וירטואליות, אבל, מהרבה בחינות זה חזק מ־delegate --- למשל, אפשר לעשות bind כמעט לכל פונקציה גם עם ה"חתיכה" שלה שונה.

ארתיום, ב־18.8.2008, 10:14

נכון שלא ניתן לעשות את זה עבור פונקציות וירטואליות

פלטתי שטות

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

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

כך שלמעשה, יש לך delegate ב־C++‎.

גיא, ב־18.8.2008, 15:24

היי ארתיום,

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

:)

ארתיום, ב־18.8.2008, 16:00

תודה, תוקן :)

(על זה אומרים אוווופס)

Shlomi Fish, ב־18.8.2008, 16:16

"אני אציג שלוש קטעי הקוד" ==> צריך להיות "אני אציג שלושה קטעי קוד"

ארתיום, ב־18.8.2008, 17:59

"אני אציג שלוש קטעי הקוד" ==> צריך להיות "אני אציג שלושה קטעי קוד"

צודק, תוקן, תודה!

הבלוג של ארתיום, ב־19.8.2008, 16:08

על C++0x ועל TR1.

מה זה C++0x‏ ו־TR1‏? הם השינויים בסטנדרט החדש של C++‎, כאשר C++0x מדבר בעיקר על שינויים בשפה, ביניהם: הוספת פונקציות למדא , טיפוס auto‏ ורבים אחרים. הסטנדרט

קרויז, ב־29.8.2008, 1:27

בדוגמא של ++C יכולת להשתמש ב-auto_ptr ואז לא היה לך צורך ב-boost

הוסף תגובה:

 
 כתובת דוא"ל לא תוצג
 

ניתן לכתוב תגובות עם שימוש בתחביר Markdown.

חובה לאפשר JavaScript כדי להגיב.

דפים

נושאים