C/C++ מדריכי
C הקדמה לתכנות ב
C++ הקדמה להיררכית מחלקות ב הצגה מזורזת :C++ הבנת דף הבית

שיעור 13: מחרוזות ב C

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

מחרוזת ב C היא פשוט מערך של תווים. השורה הבאה מצהירה על מערך שיכול להכיל מחרוזת של עד 99 תווים:

char str[100];

היא מכילה תווים כפי שניתן לצפות: str[0] הוא התו הראשון במחרוזת, str[1] הוא התו השני, וכן הלאה. אבל מדוע המערך של 100 אלמנטים לא יכול להכיל עד 100 תווים? כיוון ש C משתמשת במחרוזות עם סיומת null, שמשמעותו שסוף כל מחרוזת מסומן בערך ה 0 ASCII (תו ה null), שמיוצג ב C גם כ '0\'.

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

כיוון ש C לא מספקת תמיכה ברורה במחרוזות בשפה עצמה, כל פונקציות הטיפול במחרוזות מיושמות בספריות. פעולות הקלט/פלט של מחרוזות (gets, puts, וכן הלאה) מיושמות ב <stdio.h>, וסדרה של פונקציות טיפול במחרוזות פשוטות למדי מיושמות ב <string.h> (במערכות מסוימות <strings.h>). העובדה שמחרוזות אינן טבעיות ל C מכריחה אותך ליצור קוד עקיף למדי. לדוגמא, נניח שאתה רוצה לבצע השמה של מחרוזת אחת למחרוזת אחרת, כלומר, אתה רוצה להעתיק את התוכן של מחרוזת אחת לשניה. בפסקל, משימה זו היא קלה:

program samp;  
var    
    s1,s2:string;  
begin    
    s1:='hello';   
    s2:=s1;  
end. 

ב C, ראינו בשיעור 12, שאתה לא יכול פשוט לבצע השמה של מערך אחד לשני. עליך להעתיק אותו אלמנט אלמנט. ספרית המחרוזות ( <<string.h או <strings.h>) מכילה פונקציה בשם strcpy בשביל משימה זו. הקוד הבא מדגים איך להשתמש ב strcpy כדי להשיג את אותן תוצאות ב C כמו בקוד הפסקל לעיל:

#include <string.h> 
void main()  
{    
    char s1[100],s2[100];     
     strcpy(s1,"hello"); /* copy "hello" into s1 */    
    strcpy(s2,s1);      /* copy s1 into s2 */ 
} 

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

program samp;  
var    
    s1,s2:string;  
begin    
    readln(s1);    
    readln(s2);    
    if s1=s2 then      
        writeln('equal')    
    else if (s1<s2) then      
        writeln('s1 less than s2')    
    else      
        writeln('s1 greater than s2');  
end.

להלן אותו הקוד ב C:

#include <stdio.h>  
#include <string.h> 
void main()  
{    
    char s1[100],s2[100]; 
     gets(s1);    
    gets(s2);    
    if (strcmp(s1,s2)==0)       
        printf("equal\n");    
    else if (strcmp(s1,s2)<0)      
        printf("s1 less than s2\n");    
    else      
        printf("s1 greater than s2\n");  
} 

פונקציות נפוצות אחרות בספריית המחרוזות כוללות את strlen, שמחזירה את האורך של מחרוזת, ו strcat, שמשרשרת שתי מחרוזות. ספריית המחרוזות מכילה מספר פונקציות נוספות, שניתן לעיין בהן על ידי קריאת קובץ ההדרכה. שים לב שהרבה מהיכולות הסטנדרטיות של פסקל, כגון copy, delete, pos, וכן הלאה, חסרות.

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

int strlen(char s[])  
{    
    int x; 
    x=0;    
    while (s[x] != '\0') 
         x=x+1;    
    return(x);  
}

רוב מתכנתי C נמנעים מגישה זו כיוון שהיא נראית לא יעילה. במקום זאת, הם משתמשים לעיתים קרובות בגישה מבוססת-מצביעים:

int strlen(char *s)  
{    
    int x=0; 
    while (*s != '\0')    
    {      
        x++;      
        s++;    
    }    
    return(x);  
} 

ניתן לקצר קוד זה ל:

int strlen(char *s)  
{    
    int x=0; 
    while (*s++)      
        x++;    
    return(x);  
} 

אני מתאר לעצמי שמומחה C אמיתי יוכל לקצר קוד זה אפילו יותר.

כאשר אני מהדר את שלושת קטעי הקוד האלו ב MicroVAX באמצעות gcc, ללא שימוש באופטימיזציה, ומריץ כל אחד 20,000 פעמים על מחרוזת של 120 תווים, קטע הקוד הראשון מניב זמן של 12.3 שניות, השני 12.3 שניות, והשלישי 12.9 שניות. מה זה אומר? לי, זה אומר שעליך לכתוב את הקוד בדרך שהכי קלה לך להבנה. מצביעים באופן כללי מניבים קוד מהיר יותר, אבל קוד ה strlen לעיל מראה שלא תמיד זהו המצב.

ניתן לבצע את אותו תהליך עם strcpy:

strcpy(char s1[],char s2[])  
{    
    int x; 
    for (x=0; x<=strlen(s2); x++)      
        s1[x]=s2[x];  
} 

שים לב כאן שה => חשוב ללולאת ה for כיוון שהקוד מעתיק אז את ה '0\'. וודא שהעתקת את ה '0\'. באגים גדולים נוצרים מאוחר יותר אם שוכחים את זה, כיוון שלמחרוזת אין סוף ולכן אורכה לא ידוע. שים לב גם שקוד זה הוא מאוד לא יעיל כיוון ש strlen נקראת כל פעם במהלך לולאת ה for. כדי לפתור בעיה זו, ניתן להשתמש בקוד הבא:

strcpy(char s1[],char s2[])  
{    
    int x,len; 
     len=strlen(s2);    
    for (x=0; x<=len; x++)      
        s1[x]=s2[x];  
}

גרסת המצביעים היא דומה:

strcpy(char *s1,char *s2)  
{    
    while (*s2 != '\0')    
    {      
        *s1 = *s2;      
        s1++;      
        s2++;    
    }  
}

ניתן לתמצת את הקוד הזה יותר:

strcpy(char *s1,char *s2)  
{    
    while (*s2)      
        *s1++ = *s2++;  
} 

אם אתה רוצה, אתה אפילו יכול לומר while(*s1++=*s2++);. לגרסה הראשונה של strcpy לוקח 415 שניות להעתיק מחרוזת של 120 תווים 10,000 פעמים, לשניה לוקח 14.5 שניות, לשלישית 9.8 שניות, ולרביעית 10.3 שניות. כפי שאתה יכול לראות, מצביעים מספקים האצת ביצועים משמעותית כאן.

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

char *strcpy(char *s1, char *s2)

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

program samp;  
var    
    s:string;  
begin    
    readln(s);    
    while (s[1] <> ' ') and (length(s)>0) do      
        delete(s,1,1);    
    writeln(s);  
end; 

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

program samp;  
var    
    s:string;    
    x:integer;  
begin    
    readln(s);    
    x:=0;    
    while (s[x+1] <> ' ') and (x<length(s)) do      
        x:=x+1;    
    delete(s,1,x);    
    writeln(s);  
end; 

בטכניקה זו, כל אות מוזזת פעם אחת בלבד. ב C ניתן להימנע מההזזה כולה:

#include <stdio.h>  
#include <string.h>   
 
void main()  
{    
    char s[100],*p; 
     gets(s);    
    p=s;    
    while (*p==' ')      
        p++;    
    printf("%s\n",p);  

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


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

נניח שאתה יוצר את שני קטעי הקוד הבאים ומריץ אותם:

Fragment 1 
 
{    
    char *s;  
    
    s="hello";    
    printf("%s\n",s);  
} 
 
Fragment 2 
 
{    
    char s[100]; 
   
    strcpy(s,"hello");    
    printf("%s\n",s);  
} 

שני קטעים אלו מייצרים את אותו פלט, אבל ההתנהגות הפנימית שלהם שונה למדי. בקטע 2, אינך יכול לכתוב s="hello";. כדי להבין את ההבדלים, עליך להבין איך טבלת קבועי המחרוזות עובדת ב C.

במהלך הידור התוכנית שלך, המהדר יוצר קובץ שפת מכונה, שמכיל את קוד המכונה שלך וטבלה של כל קבועי המחרוזות שהוגדרו בתוכנית. בקטע 1, הפקודה s="hello"; גורמת ל s להצביע לכתובת של המחרוזת hello בטבלת קבועי המחרוזות. כיוון שמחרוזת זו נמצאת בטבלת קבועי המחרוזות, ולכן מבחינה טכנית היא חלק מהקוד בר הביצוע, אינך יכול לשנות אותה. אתה יכול רק להצביע אליה ולהשתמש בה בצורה של קריאה בלבד.

בקטע 2, המחרוזת hello גם כן קיימת בטבלת הקבועים, כך שניתן להעתיק אותה לתוך מערך התווים המכונה s. כיוון ש s אינו מצביע, הפקודה s="hello"; לא תעבוד בקטע 2, היא אפילו לא תתקמפל.

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

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

void main()  
{    
    char *s; 
   
    s=(char *) malloc (100);    
    s="hello";    
    free(s);  
} 

היא מתקמפלת היטב, אבל נותנת שגיאת סגמנטציה בשורת ה free כאשר מריצים אותה. שורת ה malloc מקצה גוש בגודל 100 בתים וקובעת את s להצביע עליו, אבל עכשיו השורה של s="hello"; היא בעייתית. היא נכונה מבחינת המינוח כיוון ש s הוא מצביע, אולם, כאשר s="hello"; מתבצע, s מצביע למחרוזת בטבלת קבועי המחרוזות והגוש המוקצה מתייתם. כיוון ש s מצביע לטבלת קבוע המחרוזות, המחרוזת לא ניתנת לשינוי, free נכשלת כיוון שהיא לא יכולה לשחרר גוש בתחום ביצוע. הקוד הנכון הוא:

void main()  
{    
    char *s; 
     s=(char *) malloc (100);    
    strcpy(s,"hello");    
    free(s);  
}

שגיאות ב C מהן יש להימנע:

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

תרגילים

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

2. כתוב פונקציה שהופכת מחרוזת לאותיות גדולות.

3. כתוב פונקציה שמוציאה את המילה הראשונה ממחרוזת ומחזירה את יתר המחרוזת.




לדף הראשון

<< לדף הקודם

לדף הבא >>