שפת C/מצביעים, מערכים, ופונקציות

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


שימו לב:

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

חשבון מצביעים

עריכה

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

חיבור מספר למצביע וחיסור מספר ממצביע

עריכה

נתבונן בקטע הקוד הבא:

char c = 'a';

char *p = &c;
char *p_plus_1, *p_plus_2;

p_plus_1 = p + 1;
p_plus_2 = p + 2;

השורות

p_plus_1 = p + 1;
p_plus_2 = p + 2;

משתמשות בחשבון מצביעים. בפרט, מה משמעות p + 1 וp + 2? התרשים הבא מראה את התשובה לכך:

 
חשבון מצביעים - תווים.

p + 1 וp + 2 הם מצביעים לתו אחד קדימה, ולשני תווים קדימה. אם p הוא מצביע לתו, והוא מצביע לכתובת 2000, אז p + 1 ערכו 2001, וp + 2 ערכו 2002.

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

int m = 3;

int *p = &m;
int *p_plus_1, *p_plus_2;

p_plus_1 = p + 1;
p_plus_2 = p + 2;

מה משמעות p + 1 וp + 2 כעת? התרשים הבא מראה את התשובה לכך:

 
חשבון מצביעים - מספרים שלמים.

p + 1 וp + 2 הם מצביעים לשלם אחד קדימה, ולשני שלמים קדימה. אם p הוא מצביע לשלם, והוא מצביע לכתובת 2000, אז במחשב שבו שלם תופס 4 בתים, p + 1 ערכו 2004, וp + 2 ערכו 2008.

נוכל לסכם זאת כך. אם p הוא מצביע לטיפוס t כלשהו, אז ערך p + i הוא ערך p ועוד i * sizeof(t). במילים אחרות, p + i מצביע לp ועוד i פעמים t קדימה.

באותו אופן גם חיסור שלם ממשתנה. אם p הוא מצביע לטיפוס t כלשהו, אז ערך p - i הוא ערך p פחות i * sizeof(t).. במילים אחרות, p - i מצביע לp ועוד i פעמים t אחורה.

הגדלה עצמית והקטנה עצמית של מצביע

עריכה

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

int *p = &m;

p++;

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

int *p = &m;

p = p + 1;

המשמעות של ארבע השורות הבאות, לכן:

p++;
++p;
p--;
--p;

היא, בהתאמה:

  • קידום בדיעבד של המצביע p לאיבר הבא
  • קידום לכתחילה של המצביע p לאיבר הבא
  • הסגה בדיעבד של המצביע p לאיבר הקודם
  • הסגה לכתחילה של המצביע p לאיבר הקודם

הפרש בין מצביעים

עריכה

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

לדוגמה, נניח ש-p0 מצביע לכתובת 2008, p1 מצביע לכתובת 2000, אלה מצביעים לשלמים, ובמחשב זה תופס שלם 4 בתים. אז:

p0 - p1

הוא 2.

ההפרש בין שני מצביעים מסוגים שונים אינו ניתן לחישוב בC - המהדר יתלונן על הניסיון לעשות זאת.

פעולות חשבוניות אחרות

עריכה

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

הקשר בין מצביעים למערכים

עריכה

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


 

כדאי לדעת:

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

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

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

כעת נפרט במקצת שתי נקודות אלו.

המערך כמצביע

עריכה

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

int nums[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

שמו של המערך, nums, הוא מצביע לאיברו הראשון. הבה נבדוק זאת:

int nums[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

int *p;

p = nums;

printf("%d", *p);

אם נריץ את קטע הקוד, התכנית תדפיס 0. p מצביע לאיברו הראשון של nums, שהוא 0.


מהדוגמה הנ"ל, אגב, עולה שאפשר לגשת לכל איבר במערך גם בלי להשתמש באינדקס. לדוגמה, כדי לשנות את ערך איברו השלישי של המערך ל5000, נוכל לכתוב כך:

int nums[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

int *p;

p = nums;

*(p + 2) = 5000;

נוכל לכתוב אפילו ביתר קיצור, כך:

int nums[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

int *p = nums;

*(p + 2) = 5000;

או אפילו כך:

int nums[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

*(nums + 2) = 5000;

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

nums[2] = 5000;

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

גישה למצביע כאל מערך

עריכה

נתבונן בקטע הקוד הבא:

int m = 3;

int *p = &m;

כיצד נוכל לשנות את ערכו של m על ידי p? עד עתה, ראינו שאפשר לעשות זאת באופן הבא:

*p = 5000;

אך אפשר לעשות זאת גם בעזרת אינדקס, כך:

p[0] = 5000; /* This changes m to 5000. */


השפה, אגב, אינה מגבילה זאת רק לאינדקס 0. נוכל לכתוב גם את הקוד המסוכן הבא:

int m = 3;

int *p = &m;

/* This is a dangerous line! "*/
p[2] = 5000;

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

מצביעים קבועים

עריכה

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

שלושת סוגי המצביעים הקבועים

עריכה

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

  • מצביעים שאין אפשרות לשנות את יעד ההצבעה שלהם.
  • מצביעים שאין אפשרות לשנות את הערך המוצבע שלהם.
  • מצביעים שאין אפשרות לשנות גם את יעד ההצבעה וגם את ערך והמוצבע שלהם.


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

<t> *<name>

מגדיר משתנה שמותר לשנות את יעד ההצבעה שלו, ומותר לשנות את הערך המוצבע שלו.

<t> *const <name>

מגדיר משתנה שאסור לשנות את יעד ההצבעה שלו, ומותר לשנות את הערך המוצבע שלו.

const <t> *<name>

מגדיר משתנה שמותר לשנות את יעד ההצבעה שלו, ואסור לשנות את הערך המוצבע שלו.

const <t> *const <name>

מגדיר משתנה שאסור לשנות את יעד ההצבעה שלו, ואסור לשנות את הערך המוצבע שלו.

התוכנית הבאה, לדוגמה, חוקית:

char a;
const char b = 'b';

char *p = &a;

*p = 'c';

התוכנית הבאה אינה חוקית:

char a;
const char b = 'b';

char *const p = &a;

p = &b; /* Error: can't change the destination of p! */

גם התוכנית הבאה אינה חוקית:

char a;
const char b = 'b';

const char *p = &b;

*p = 'c'; /* Error: can't change the value pointed by p! */

הקשר להעברת משתנים לפונקציות

עריכה

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

int a[] = {0, 1, 3, 2, 2, 3, 4, 5};

ורוצים לדעת כמה פעמים מופיעה 3 ב4 האיברים הראשונים שלו, אז התשובה היא 1. להלן פיתרון בעייתי:

int how_many_times_3_appears(int *p,int num)
{
	int count=0,i;

	for(i=0;i<num;++i, ++p)
	{
		if(*p = 3)/* This is wrong! */
		{
			count++;
		}
                p++;
	}
	return count;
}
int main()
{
  int a[] = {0, 1, 3, 2, 2, 3, 4, 5};
  
  int count = how_many_times_3_appears(a, 4);
  
  return 0;
}

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

    if(*p = 3) /* This is wrong! */

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

נחשוב שוב על הפונקציה how_many_times_3_appears. האם היא אמורה לשנות את הערכים המועברים אליה? לא. הבה נודיע זאת למהדר:

/* Note that p points to something that cannot be changed. */
int how_many_times_3_appears(const int *p, int len) /* const int ! */ 
{
  int i, count;
  
  for(i = 0, count = 0; i < len; ++i, ++p)
    if(*p = 3) /* This is wrong! */
      ++count;
      
  return count;
}

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

    if(*p = 3) /* This is wrong! */

ואנו נוכל לתקן אותה:

    if(*p == 3)

מצביעים לפונקציות

עריכה
 

שקלו לדלג על נושא זה

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



מהו מצביע לפונקציה?

עריכה

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

הגדרת מצביע לפונקציה

עריכה

מגדירים מצביע לפונקציה בצורה:

<return_type> (*<ptr_name>)([args]);

כאשר:

return_type הוא סוג הערך המוחזר מהפונקציה.

ptr_name הוא שם המצביע

ו-args הם הארגומנטים שאותה מקבלת הפונקציה.

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

void (*print_fn)();

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

int (*input_fn)();

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

float (*op_fn)(float a, float b);

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

float (*op_fn)(float foo, float bar);

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

float (*op_fn)(float, float);

שימוש ב-typedef

עריכה

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

typedef <return_type> (*<type_name>)([args]);

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

אפשר, לכן, להחליף את השורות הבאות:

void (*print_fn)();
float (*op_fn)(float foo, float bar);

בשורות:

typedef void (*print_fn_ptr)();
typedef float (*op_fn_ptr)(float, float);

print_fn_ptr print_fn;
op_fn_ptr op_fn;

קריאה לפונקציה דרך מצביע

עריכה

קוראים למצביע לפונקציה בצורה הבאה:

(*<fn_ptr>)([args])

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

הערה: כאן נעשה שימוש בצורה ptr = &func (כאשר ptr הוא מצביע מסויים ו-func - כתובת הפונקציה בזיכרון). באופן שקול, ניתן לרשום ptr = func (כלומר, ללא הסימן "&") והתוצאה תהייה זהה לחלוטין.

להלן דוגמה:

#include <stdio.h>
 
 
void print_2()
{
  printf("2");
}
 
float add(float x, float y)
{
  return x + y;
}
 
int main()
{
  void (*print_fn)() = &print_2;
  float (*op_fn)(float x, float y) = &add;
 
  /* Prints 2. */
  (*print_fn)();
 
  /* Prints 5.0. */
  printf("%f", (*op_fn)(2, 3) );
 
  return 0;
}

מצביעים לפונקציות כארגומנטים

עריכה

אפשר להעביר מצביעים לפונקציות כארגומנטים לפונקציות אחרות. להלן דוגמה (עריכה: שימו לב שהסיפריה (conio.h) והפונקציה getch לא שייכות לספריה הסטנדרטית של השפה ולכן יעבדו רק בקומפיילרים מסוימים):

#include <stdio.h>
#include <conio.h>
 
void print_2()
{
  printf("2");
}

void print_3()
{
  printf("3");
}

void do_something_after_keypress(void (*fn)())
{
  getch();
  
  (*fn)();
}

int main()
{
  do_something_after_keypress(&print_2);
  do_something_after_keypress(&print_3);
  
  return 0;
}

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

void do_something_after_keypress(void (*fn)())
{
  getch();
  
  (*fn)();
}

השורות הבאות בפונקציה main יגרמו לפונקציה לחכות ללחיצת מקש ואז להדפיס 2, לחכות ללחיצת מקש נוספת, ואז להדפיס 3:

do_something_after_keypress(&print_2);
do_something_after_keypress(&print_3);


 

כדאי לדעת:

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

מצביעים ל-void

עריכה
 

שקלו לדלג על נושא זה

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



שימוש

עריכה
 

כדאי לדעת:

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

מצביע מסוג void * הוא מצביע שהטיפוס אליו הוא מצביע אינו מוגדר. נתבונן על דוגמה פשוטה להשמה של מצביע כזה:

int j = 2;
int *i = &j;
void *p = i;

המשתנה p מצביע אל אותה הכתובת אליה מצביע i. גם קטע הקוד הבא הוא חוקי:

int j = 3;
void *p = &j;

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

int j = 1;
double d = 1.5;
void *p;
p = &j;
p = &d;

המשתנה p הצביע על משתנה מטיפוס int, ואז עבר להצביע על משתנה מטיפוס double, ללא צורך בשום המרה. מצביע מטיפוס void * נותן חופש רב מאוד: לא צריך להצהיר על סוג המשתנה עליו מצביעים.

מגבלות

עריכה

נסו להריץ את קטע הקוד הבא:

int j = 1;
void *p;
p = &j;
printf("%d", *p);

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

לכן:

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

גישה

עריכה

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

char c = '!';
void *p = &c;
char *cp = (char *) p;
printf("%c", *cp);

ניתן לעשות זאת גם ללא מצביע עזר, בצורה הבאה:

char c = '?';
void *p = &c;
printf("%c", *((char *) p));

הצורך במצביע לטיפוס לא ידוע

עריכה

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

המרת מצביע לתו

עריכה

דרך לעבודה עם מצביעים ל-void * ללא המרה היא התייחסות אליהם כזיכרון גולמי. זה שימושי בדוגמה כמו זו שהבאנו קודם - פונקצייה שמחליפה בין התכנים אליהם מצביעים שני משתנים מטיפוס לא ידוע. כדי לעבוד בצורה כזאת, נתייחס למצביעים כאילו הם מטיפוס unsigned char. נעיר כאן כי גודלו של משתנה מטיפוס תו (char) הוא 1 על פי הגדרה (בניגוד לסוגים אחרים בהם הגודל עשוי להיות שונה בפלטפורמות שונות). נראה כאן דוגמה לתוכנית עם פונקציית החלפה כזאת:

#include <stdio.h>

void MySwap(void *a, void *b, size_t size)
{
    int i;
    unsigned char *tmpA = (unsigned char *) a, *tmpB = (unsigned char *) b;
    for(i=0; i<size; i++)
    {
	unsigned char tmp = *tmpA;
	*tmpA = *tmpB;
	*tmpB = tmp;
	++tmpA;
	++tmpB;
    }
}

int main()
{
    int i = 2, j = 4;
    printf("A: %d B: %d\n", i, j);
    MySwap(&i, &j, sizeof(int));
    printf("A: %d B: %d\n", i, j);
    float d1 = 1.5, d2 = 3.2;
    printf("A: %g B: %g\n", d1, d2);
    MySwap(&d1, &d2, sizeof(float));
    printf("A: %g B: %g\n", d1, d2);
    printf("Messy swap: A: %g B: %d\n", d1, i);
    MySwap(&d1, &i, sizeof(int));
    printf("Messy swap: A: %d B: %g\n", i, d1);
    return 0;
}

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

דוגמה נוספת - העתקת קטעי זיכרון

עריכה

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

#include <stdio.h>

void setElement(void *dest, void *src, size_t size)
{
    int i;
    unsigned char *tmpA = (unsigned char *) src, *tmpB = (unsigned char *) dest;
    for(i=0; i<size; i++)
    {
	*tmpB = *tmpA;
	++tmpA;
	++tmpB;
    }
}

int main()
{
    char str1[] = {'H', 'e', 'l', 'l', 'o', '\0'};
    char str2[6];
    setElement(str2, str1, 6);
    printf("%s\n", str2);
    return 0;
}

פונקציית הספריה memcpy

עריכה

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

#include <stdio.h>
#include <string.h>

int main()
{
    char str1[] = {'H', 'e', 'l', 'l', 'o', '\0'};
    char str2[6];
    memcpy(str2, str1, 6);
    printf("%s\n", str2);
    return 0;
}

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

שימוש יחד עם מצביעים לפונקציות

עריכה

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

 typedef struct {
    char name[10];
    int age;
 } Person;

לא נלמד עד הקטע הבא "מבנים":

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[10];
    int age;
} Person;

// Compare Integers
int CompareInts(const void *a, const void *b) {
    int numA = *(int *)a;
    int numB = *(int *)b;
    if(numA == numB) return 0;
    if(numA > numB) return 1;
    return -1;
}

// Compares two people by their age
int ComparePeople(const void *a, const void *b) {
    Person personA = *(Person *)a;
    Person personB = *(Person *)b;
    if(personA.age == personB.age) return 0;
    if(personA.age > personB.age) return 1;
    return -1;
}

// Compare and prints the results
void Compare(const void *a, const void *b, int (*func) (const void *,const void *)) {
    int cmp;
    cmp = func(a, b);
    if(cmp == 0) printf("Equal\n");
    else if(cmp < 0) printf("B is bigger\n");
    else printf("A is bigger\n");
}

int main()
{
	int i = 2, j = 5;
	Person p1, p2;
	strcpy(p1.name, "David");
	p1.age = 29;
	strcpy(p2.name, "Shlomo");
	p2.age = 25;
	Compare(&i, &j, CompareInts);
	Compare(&p1, &p2, ComparePeople);
	return 0;
}

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

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

// Compare Integers
int CompareInts(const void *a, const void *b) {
    int numA = *(int *)a;
    int numB = *(int *)b;
    if(numA == numB) return 0;
    if(numA > numB) return 1;
    return -1;
}

void PrintIntArray(int *arr, int size) {
    int i;
    for(i=0; i<size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main()
{
    int arr[5] = {3, 7, 9, 1, -4};
    PrintIntArray(arr, 5);
    qsort(arr, 5, sizeof(int), CompareInts);
    PrintIntArray(arr, 5);
    return 0;
}

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

בעייתיות

עריכה

יחד עם היתרונות, תכנות בעזרת מצביעי void * בעייתי מכמה סיבות:

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


הפרק הקודם:
מצביעים
מצביעים, מערכים, ופונקציות
תרגילים
הפרק הבא:
מבנים