מבוא למצביעים-למשתנים ולעצמים  

 

 


תוכן הפרק

הגדרת מצביע לקיחת כתובת משתנה (unary operator &) וגודלמשתנה בבתים (operator sizeof)

גישה למשתנה  דרך מצביע (unary operators * )

מצביע לעצם  operator ->

מערך של מצביעים

המצביע  this  בתוך  פונקצית של מחלקה.

האופרטורים  new delete

מצביעים ומערכים

מערכים בגודל דינאמי - באמצעות new delete

 

 

הגדרת מצביע לקיחת כתובת משתנה (unary operator &) וגודלמשתנה בבתים (operator sizeof)

·        בזמן ריצת התוכנית כל משתנה או עצם שמור בזכרון המחשב.

·        לכל בית בזכרון המחשב יש כתובת.

·        אם גודל המשתנה/עצם a  בבתים  (sizeof a) גדול מבית אחד הרי המשתנה מאוחסון בגוש רציף של בתים בזכרון.

·        הבית בעל הכתובת הנמוכה ביותר, שממנו מתחיל שטח אחסון העצם, מכונה כתובת העצם  (object address) ..

·        משתנה המיועד להכיל כתובת של עצם/משתנה אחר מכונה מצביע (pointer).

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

 

 

#include <iostream.h>

 

void main()

{

    int n1 = 2, n2 = 3;

    int *p;    // p is a pointer to int

    p = &n1;   // p recives the adress of varaible n1

    cout << "Value of n1 is: " << n1 << endl;

    cout << "Adress of n1 is: " << p << endl;

 

    p = &n2;

    cout << "Value of n2 is: " << n2 << endl;

    cout << "Adress of n2 is: " << p << endl;

 

    cout << "Address of p itself: " << &p << endl;

 

    cout << "Size of varaible n1 in bytes is: " << sizeof(n1) << endl;

    cout << "Size of type int in bytes is: " << sizeof(int) << endl;

}

 

//---- -Output ---------

Value of n1 is: 2

Adress of n1 is: 0x0064FDEC

Value of n2 is: 3

Adress of n2 is: 0x0064FDF0

Line Callout 2 (Accent Bar): 0x0064FDEC

Address of p itself: 0x0064FDF4

Size of varaible n1 in bytes is: 4

Size of type int in bytes is: 4

 

n1:

 

 

n2:

 

 

p:

 

 
 

 

 

 

 

 

 

 

 

 

 


·                    int  *p  פירושו שהמשתנה בשם  p  מיועד להכיל כתובת של משתנה מטיפוס  int.

·                    האופרטור האונרי  &  -  מחזיר כתובת של משתנה או עצם.

·                    p בתחילה מכיל כתובת של משתנה  n1.

·                    לאחר מכן  p  מכיל כתובת של משתנה  n2.

·                    האופרטור  sizeof מחזור גודל בבתים של עצם/משתנה (לדוגמא n1) או טיפוס.(לדוגמא  int).

 

חזרה להתחלה

גישה למשתנה  דרך מצביע (unary operators * )

·                    לסימן * מספר משמעויות:

1.                     הגדרת משתנה מסוג מצביע לדוגמא:       int *p;

2.                     אופרטור אונרי -  גישה למשתנה שהמצביע  p  מצביע עליו:       *p = 8

3.                     אופרטור בינארי - כפל:   a = b * c.

                         ·        לאחר ההשמה   p = &n   המשתנה  n     והבטוי   *p   שקולים.

                         ·        מצביע מוגדר כמצביע רק לטיפוס (int double ...)  מסויים. אין אפשרות להשים למצביע כתובת של משתנה מטיפוס שונה מזה שהוא מוגדר להצביע עליו.

 

#include <iostream.h>

 

void main()

{

    int n = 7;

    int *p;   // p is a pointer to int

    p = &n;   // p recives the adress of varaible n1

   

    double d = 3.1415;

    // p = & d; - error: p can point only to varaible of type int!

    double *q;  // q is a pointer to double

    q = &d; // OK

 

    cout << "Value of *p is: " << *p << endl;

    cout << "Value of  n is: " << n << endl;

    cout << "Adress of n is: " << p << endl;

    *p = 9;  //

    cout << "Value of *p is: " << *p << endl;

    cout << "Value of  n is: " << n << endl;

    ++*p;

    cout << n << endl;

    *p = *p * *p;

    cout << n << endl;

    cin >> *p; // same as cin >> n;

    cout << n << endl;

 

    cout << "Value of  d is: " << d << endl;

    cout << "Value of *q is: " << *q << endl;

    cout << "Adress of d is: " << q << endl;

}

//---- Output --------

Value of *p is: 7

Value of  n is: 7

Adress of n is: 0x0065FDE4

Value of *p is: 9

Value of  n is: 9

10

100

5

Value of  d is: 3.1415

Value of *q is: 3.1415

Adress of d is: 0x0065FDE8

 

חזרה להתחלה

 

מצביע לעצם  operator ->

·                    מצביע יכול להצביע גם על עצמים.

·                    אם  p  מצביע לעצם בכדי לגשת לאיבר x  בעצם  חייבים לכתוב   (*p).x  אם נכתוב   *p.x   הכוונה היא   *(p.x).

·                    בגלל זאת הכינו קיצור בשפה  במקום לכתוב  (*p).x   כותבים       p->x.

 

 

#include <iostream.h>

#include <math.h>

 

class p2d {

public:

   int x, y;

   double length() const { return sqrt(x * x + y * y); }

};

 

void main()

{

    p2d p1, p2;

 

    p2d *q; // q is a pointer to object of class p2d

    q = &p1;

    (*q).x = 3;

    (*q).y = 3;

    cout << "length of p1 is: " << (*q).length() << endl;

 

    // using the more elegant ->

    q->x = 4;

    q->y = 6;

    cout << "length of p1 is: " << q->length() << endl;

 

    p2 = *q;   // p2 = p1

    cout << "length of p2 is: " << p2.length() << endl;

 

    q->x = 1;  // changing only p1

    q->y = 2;

    cout << "length of p1 is: " << q->length() << endl;

    cout << "length of p2 is: " << p2.length() << endl;

}

// --- Output ---

length of p1 is: 4.24264

length of p1 is: 7.2111

length of p2 is: 7.2111

length of p1 is: 2.23607

length of p2 is: 7.2111

חזרה להתחלה

 


מערך של מצביעים 

#include <iostream.h>

 

int sum_ptr_arr(int* par[], int n)

{

    int sum = 0;

    for (int i = 0; i < n; i++)

        sum += *par[i];

    return sum;

}

 

void main()

{

    int* a[3]; // a is array of 3 pointers to int;

    int x = 1, y = 2, z = 3;

    int i; // used by for 

 

    a[0] = & x;

    a[1] = & y;

    a[2] = & z;

 

    for (i = 0; i < 3; i++)

        cout << a[i] << ": " << *a[i] << endl;

    cout << endl;

 

    for (i = 0; i < 3; i++)  // increment each varaible

        ++*a[i];

    for (i = 0; i < 3; i++)

        cout << *a[i] << ", ";

    cout << endl;

    cout << x << "--" << y << "--" << z << endl;

    cout << "sum is: " << sum_ptr_arr(a, 3) << endl;

}

//---Output---

x0064FDE8: 10

0x0064FDE0: 2

0x0064FDDC: 3

 

2, 3, 4,

2--3--4

sum is: 9

 

·                    a  הוא מערך שכל איבר בו הוא מצביע ל  int, כלומר  a[i]  הוא מטיפוס מצביע  ו  *a[i]  הוא מטיפוס  int.

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

 

 

 


 

      מוגדר   default constructor  שיופעל. בדוגמא שלנו  pa[1]  מאותחל על ידי p2d(0, 0) - שהוא בנאי ברירת המחדל.

 

 

#include <iostream.h>

#include <math.h>

 

class p2d {

    int x, y;

public:

   p2d(int xx=0, int yy=0) : x(xx), y(yy) {}

   double length() const { return sqrt(x * x + y * y); }

   void show() const { cout << '(' << x << ", " << y << ')'; }

   void trans(int dx, int dy) { x += dx; y += dy; }

};

 

// translate points in array of pointers to p2d's

void translate_parr(p2d* par[], int n, int dx, int dy)

{

    for(int i = 0; i < n; i++)

        par[i]->trans(dx, dy);

}

 

// show points in array of pointers to p2d's

void show_parr(p2d* par[], int n)

{

    for(int i = 0; i < n; i++)

    {

        par[i]->show();

        cout << endl;

    }

}

 

void main()

{

    p2d p1(1, 2), p2(3, 4);

    p2d a[2] = {p2d(5,6)};

 

    p2d* pa[4];

    pa[0] = &a[0];

    pa[1] = &p1;

    pa[2] = &p2;

    pa[3] = &a[1];

 

    show_parr(pa, 4);

    translate_parr(pa, 4, -2, 4);

    show_parr(pa, 4);

}

//----Output--------

(6 ,5)

(1, 2)

(3, 4)

(0, 0)

(3, 10)

(-1, 6)

(1, 8)

(-2, 4)

 

חזרה להתחלה

המצביע  this  בתוך  פונקצית של מחלקה.

·      כל פונקציה של מחלקה חייבת להיות מופעלת על עצם, לדוגמא:      object.f()

·      המילה השמורה  this  משמעותה (רק בתוך פונקציה של מחלקה) מצביע לעצם שעליו מופעלת הפונקציה.

 

#include <iostream.h>

class B {

            int n;

public:

          B() n(0) {}

            void f() {  cout << this << endl; }

          void g() {  cout << this->n << endl; } // this->n is  n

      B h() { return *this; }  // returns a copy of this object

};

 

void main()

{

            B b;

            b.f();  // prints this which is address of b

            cout << &b << endl;   // prints address of b

          b.g(); // print 0

          B b1 = b.h();  // asign a copy of b into b1

    

}

output:

0x0064FDF4

0x0064FDF4

חזרה להתחלה

האופרטורים  new delete  

 

עד היום הכרנו מספר אופני הקצאת זכרון:

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

             ·        לעצם סטטי - קיים כל זמן קיום התוכנית.

 

בעיה:

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

             ·        לדוגמא: גודל של מערך חייב להיות מספר קבוע הידוע בזמן ההידור.

 

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

 

את הבעיות הללו פותרים האופרטורים  new   ו    delete

 

void main()

{

    int *p;   // p contains garbage address!!!.

    // *p = 10;  // Error: never! use uninitialized varaiable.

   

    int n;

    p = &n;

    *p = 10; // fine.

   

    p = new int;  // creating a new int in heap memory.

    *p = 10;      // storing 10

    delete p;     // free memory that p points to

}

             ·        new T - מקצה בזכרון הערימה עצם מטיפוס T.

             ·        P  מקבל את כתובת העצם ולכן אפשר לעבוד עמו. - לעצם אין שם, כמו למשתנה.

             ·        delete p  - משחרר את שטח הזכרון ש  new  יצר.   p  חייב להיות אותה הכתובת בדיוק ש  new  יצר!

             ·        אם לא מבצעים  delete  על הכתובת שב-  p   שטח הזכרון ישאר מוקצה עד סיום התוכנית. בלי קשר לאיזו פונקציה עשתה  new.

 

 

class X {

    int a, b;

public:

    X(int aa, int bb) : a(aa), b(bb) {}

    ~X() { cout << "destructing" << endl; }

    vois show() const { cout <<a << ", " << b << endl;}

};

 

void main()

{

    X *q;

    // q = new X; // Error: must provide arguments to constructor

    q = new X(4, 5);  // fine

    q->show();   // prints: 4, 5

    delete q;    // prints: destructing - destructor operated.

}

·                    new יכול ליצור גם עצמים -ב  heap memory.

·                    אם יש למחלקה בנאי חייבים לספק לו ארגומנטים - אלא אם יש גם default constructor.

·                    delete מפעיל את ה  destructor  "רגע"  לפני שמשחררים את זכרון העצם.

 

 


מצביעים ומערכים

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

 

int a[5];

מגדיר בלוק זכרון רציף המכיל חמישה intים עוקבים.

a:

 

a[0] a[1]         a[4]

 
 

 

 

 

 


int *pa = &a[0];

a[0] a[1]         a[4]

 
 

 

 

 

 

 


הבטוי pa מצביע לאיבר הראשון במערך;  הוא מכיל את הכתובת של a[0].

  1. ההשמה   x = *pa;     תעתיק את תוכן a[0] ל   x.

 

אם pa מצביע לאיבר מסויים במערך הרי הבטוי:

  1. pa + 1      מצביע לאיבר הבא.
  2. pa + i      מצביע לאיבר שנמצא i איברים מהמקום ש pa  מצביע אליו.
  3. pa - i      מצביע לאיבר שנמצא i איברים לפני המקום ש pa  מצביע אליו.

 

לכן אם pa מצביע ל a[0]:

  1. *(pa + 1)     מתיחס ל a[1]
  2. pa+4

     

    pa+1

     
    *(pa + i)     מתיחס ל a[i]

 

 

 

 

 

 

 

 


  1. התכונות הללו נכונות לגבי מצביע מכל טיפוס (פרט ל void) והחישוב מתבצע באופן הבא:

            בכדי לחשב את ערך הבטוי pa + i   מוסיפים לכתובת     ב pa      i*sizeof(*pa)בתים    (sizeof ערכו מספר הבתים הדרוש לאחסן את    

            ערך הביטוי שהוא מופעל עליו -  יוסבר בהמשך).

 

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

  1. identifier שהוא שם של מערך, מייצג למעשה את כתובת איבר ה 0 של המערך. הטיפוס שלו הוא מצביע לטיפוס של איבר במערך.
  2. לאחר ההשמה  pa = &a[0];    ל pa  ו  a  אותם ערכים. מאחר שגם a הוא מזהה המייצג את כתובת האיבר הראשון.  הטיפוס של a הוא מצביע ל int  בדיוק כמו pa.
  3. מכיוון ש pa  ו  a בעלי אותו ערך ואותו טיפוס אפשר לכתוב:

a[i]   º  *(a + i)

&a[i]  º  &(*(a + i)) º  a + i

המהדר של C++ הופך כל מופע של a[i]   ל *(a+i) .

 

  1. אפשר להשתמש במצביע בכתיב מערך:            pa[i]   º  *(pa + i)              

 

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

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

  1. התוכנית יכולה לעוף מייד עם בצוע הפניה האסורה.

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


  1. ההבדל ביו שם של מערך ומצביע:
  2. מצביע הוא משתנה ולכן מותר לשנות את ערכו. בניגוד לכך, שם של מערך הוא בעל ערך קבוע (הכתובת של תחילת המערך) ולכן אי אפשר לשנות ערך של שם של מערך.

pa = a; // O.K.

pa++;   // O.K.: pa points to next element

a = pa; // Error: a is constant (array name is a constant pointer)

a++;    // Error: for the same reason

 

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

  1. שם המערך הוא מצביע לאיבר הראשון במערך.
  2. יוצרים עותק של המצביע לאיבר הראשון במערך ואותו מעבירים על ידי  call by value.
  3. בתוך הפונקציה מותר לשנות את ערך העותק.

לכן נוצר המצב שהמערכים עוברים על ידי call by reference, בגלל שמועבר על ידי   call by value שם המערך, שהוא מצביע לאיבר הראשון במערך.

 

מערכים בגודל דינאמי - באמצעות new delete

 

p = new T[n];

·                    new T[n] - מקצה גוש זכרון רציף של n איברים, כל אחד מטיפוס T.

·                    n  יכול להיות ערך של מתשנה שיקבע רק בזמן ריצה - מערך דינאמי.

·                    p  מצביע לאיבר הראשון במערך של עצמים מטיפוס  T.

·                    delete[] p  מוחק את המערך ש  new  הקצה - הכתובת ב  p  חייבת להיות אותה הכתובת ש  new  יצר.

#include <iostream.h>

void main()

{

    char *s;

    cin >> s; // Error: MUST initialize pointer

 

    int n;

    cout << "enter max length of word: ";

    cin >> n;

   

    s = new char[n];

    cin >> s;   // read onr word from input.

    cout << s << endl;

   

    // delete s; // Error: must use delete[] - array

    delete[] s;

}

·                    אסור לבלבל בין הקצאה- שיחרור של עצם בודד לבין אלה של מערך:

#include <iostream.h>

void main()

{

    int *a;

    cin >> n; // read number of numbers.

    a = new char[n];

   

    for (int i = 0; i < n; i++)

        cin >> a[i];

    for (i = n-1; i >= 0 ; i--)

        cout << a[i] << endl;

   

    delete[] a;

}

 


 

class X {

    int a, b;

public:

    X(int aa, int bb) : a(aa), b(bb) {}

    ~X() { cout << "destructing" << endl; }

    vois show() const { cout <<a << ", " << b << endl;}

};

 

void main()

{

    X *a;

    a = new X[5]; // Error: must provide arguments to constructor

}

             ·        כאשר מקצים מערך של עצמים וקיים בנאי, חייב להיות גם  default constructor!!

             ·        ה  default constructor  מופעל על כל עצם במערך!

             ·        כאשר הורסים את המערך, אם קיים  destructor  הוא מופעל על כל עצם במערך.

class X {

    int a, b;

public:

    X() : a(0), b(0) {}

    X(int aa, int bb) : a(aa), b(bb) {}

    ~X() { cout << "destructing" << endl; }

    vois show() const { cout <<a << ", " << b << endl;}

};

 

void main()

{

    X *a;

    a = new X[5]; // fine: call default constructor 5 times!

    for(int i = 0; i < 5; i++)

        a[i].show();

    delete[] a;  // call destructor 5 times!

}

 

 

             ·        נגדיר מערך של מצביעים לעצמים:

void main()

{

    X* *ap;  // ap pointer to (pointer to X)

    cout << "Enter number of objects: ";

    int n;

    cin >> n;

    ap = new X*[n]; // creating new array of pointers

    for (int i = 0; i < n; i++)

    {

        cout << "Enter object: a b: ";

        int x, y;

        cin >> x >> y;

        ap[i] = new X(x, y);

    }

    for (i = 0; i < n; i++)

        ap[i]->show();

    // more complex deletion!!

    for (i = 0; i < n; i++) // delete each object

        delete ap[i];

    delete[] ap;            // delete array of pointers ap

}

             ·        רוצים להגדיר מערך עם עצמים, שכל עצם יאותחל בערכים שונים מ  default.

             ·        מקצים מערך של מצביעים, ולאחר מכן מקצים + אתחול כל עצם בנפרד.

             ·        גם המחיקה היא מורכבת יחסית: מוחקים כל עצם ולאחר מכן את מערך המצביעים.

             ·        הכלל - לכל  new  חייב להתבצע ה  delete ההופכי לו.

 

 

 

חזרה להתחלה