Menu

מחרוזות ב-C: מערכי char, התו המסיים ודוגמאות

ל-C אין טיפוס מחרוזת: מחרוזת היא מערך של char שמסתיים בבית '\0'. העמוד הזה בונה את המודל המנטלי הזה, ואז עוסק בליטרלים, בהדפסה, במעבר על תווים, ולמה אי אפשר להעתיק מחרוזת עם =.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

רוב השפות נותנות לכם טיפוס string שיודע מה האורך שלו. C לא. ב-C מחרוזת היא פשוט מערך של char עם כלל אחד מוצמד: אחרי הטקסט בא בית עם הערך אפס, שנכתב '\0' ונקרא התו המסיים (null terminator). כל השאר לגבי מחרוזות ב-C נובע מהמוסכמה האחת הזו.

מחרוזת היא מערך char ועוד '\0'

מצהירים על מחרוזת על ידי אתחול מערך של char מליטרל:

המערך נראה כמו ארבעה בתים, לא שלושה:

name:  +-----+-----+-----+------+
       | 'A' | 'd' | 'a' | '\0' |
       +-----+-----+-----+------+
index:    0     1     2     3

sizeof(name) הוא 4. הקומפיילר ספר את התווים, הוסיף אחד בשביל התו המסיים, וקבע את גודל המערך בשבילכם. הבית הנוסף הזה הוא הסיבה ש-char name[3] = "Ada"; הוא באג שמחכה לקרות: הטקסט נכנס, אבל התו המסיים לא, כך ששום דבר בהמשך לא יכול לדעת איפה המחרוזת נגמרת.

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

char name[32] = "Ada";   /* 3 תווים, תו מסיים, ו-28 בתים פנויים */

הבתים שלא בשימוש מתמלאים באפסים על ידי המאתחל, וזה לא מזיק.

למה התו המסיים חשוב

אף פונקציה ב-C לא מקבלת אורך. printf("%s", s) מקבלת כתובת אחת והולכת קדימה ומדפיסה בתים עד שהיא פוגשת '\0'. לכן מערך של char בלי תו מסיים אינו מחרוזת, והתייחסות אליו כמחרוזת קוראת את כל הזיכרון שבמקרה בא אחריו.

הכלל לשנן: sizeof נותן לכם את האחסון, strlen נותן לכם את הטקסט. עבור char buf[32] = "Ada", sizeof buf הוא 32 ו-strlen(buf) הוא 3. הם עונים על שאלות שונות, והבלבול ביניהם הוא אחד הבאגים הנפוצים ביותר ב-C.

ליטרלים של מחרוזות מול מערכי char

שתי השורות האלה נראות דומות ומתנהגות אחרת לגמרי:

char  a[] = "hello";   /* מערך ששייך לכם, מאותחל מהליטרל */
char *p   = "hello";   /* מצביע שמכוון לליטרל עצמו */

a הוא מערך של 6 בתים שמחזיק עותק פרטי של הטקסט. אפשר לשנות אותו. p מצביע לתוך נתוני המחרוזות לקריאה בלבד של התוכנית: הליטרל משותף, וכתיבה דרך p היא undefined behavior שבדרך כלל קורסת בזמן ריצה ולא בזמן קומפילציה.

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

עוד הבדל אחד שכדאי להכיר: sizeof a הוא 6, אבל sizeof p הוא הגודל של מצביע (8 ברוב המערכות), לא משנה כמה ארוך הטקסט.

הדפסה וקריאה של מחרוזות

%s מדפיס מחרוזת, ו-%c מדפיס תו בודד.

לקלט, הימנעו לגמרי מ-gets: היא הוסרה מהשפה כי אי אפשר להשתמש בה בבטחה. השתמשו ב-fgets, שמקבלת את גודל ה-buffer ולא תגלוש:

fgets שומרת את ירידת השורה שלחצתם, וכמעט אף פעם לא רוצים אותה במחרוזת. שורת ה-strcspn מוצאת את ה-'\n' הראשון ודורסת אותו בתו מסיים, דרך סטנדרטית וקומפקטית לחתוך אותו.

מעבר על מחרוזת תו אחר תו

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

התנאי text[i] != '\0' מקוצר לעיתים קרובות ל-text[i] בלבד, כי '\0' הוא אפס ולכן false. שתי הצורות נכונות, והצורה המפורשת קלה יותר לקריאה בזמן הלמידה.

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

ה-cast ל-unsigned char לפני הקריאה ל-toupper הוא לא קישוט: הפונקציות של <ctype.h> לא מוגדרות לערכים שליליים, ו-char רגיל יכול להיות שלילי עבור בתים מעל 127.

אי אפשר להעתיק מחרוזת עם =

זה הקיר שכל מתחיל נתקל בו:

char a[10];
a = "hello";        /* שגיאה: assignment to expression with array type */

char b[10] = "hi";
char c[10];
c = b;              /* שגיאה: מאותה סיבה */

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

כדי להעתיק בזמן ריצה, העתיקו את הבתים עם strcpy מתוך <string.h>:

אותו היגיון מסביר למה == לא משווה מחרוזות. if (a == b) משווה שתי כתובות, ולכן הוא false עבור שני מערכים שונים שמחזיקים טקסט זהה. השתמשו במקום זה ב-strcmp(a, b) == 0. שתי הפונקציות והקרובות הבטוחות יותר שלהן מוסברות בעמוד פונקציות מחרוזות.

מערכים של מחרוזות

רשימה של מחרוזות היא או מערך של מצביעים (לטקסט קבוע שרק קוראים), או מערך char דו־ממדי (כשכל רשומה צריכה להיות ניתנת לשינוי):

מערך המצביעים שומר שלוש כתובות לתוך ליטרלים לקריאה בלבד: קומפקטי, אבל לא ניתן לשינוי. הצורה char[3][16] שומרת 48 בתים מהזיכרון שלכם, כך שכל שורה אפשר לכתוב מחדש.

טעויות נפוצות

  • לשכוח את הבית של התו המסיים. ב-char s[5] = "hello" אין מקום ל-'\0'. קבעו גודל של strlen + 1.
  • שימוש ב-sizeof במקום שבו התכוונתם ל-strlen. sizeof על מצביע נותן את גודל המצביע, לא את אורך הטקסט.
  • כתיבה דרך char * לליטרל. הצהירו על מצביעים כאלה כ-const char *.
  • השוואה עם ==. זה משווה כתובות. השתמשו ב-strcmp.
  • קריאה עם scanf("%s", buf). אין לה מגבלת גודל והיא גולשת בשמחה, העדיפו את fgets.

שאלות נפוצות

איך מצהירים על מחרוזת ב-C?

כמערך של char: char name[] = "Ada";. ל-C אין טיפוס מחרוזת מובנה: המערך הזה מחזיק את 'A', 'd', 'a' ובית רביעי מוסתר, '\0', שמסמן את הסוף. אם אתם רוצים מקום לשנות את הטקסט אחר כך, תנו למערך גודל מפורש: char name[32] = "Ada";.

מה זה התו המסיים null ב-C?

הבית '\0' (הערך אפס) שמסמן איפה מחרוזת נגמרת. כל פונקציה סטנדרטית, printf("%s"), strlen, strcpy, קוראת קדימה עד שהיא מוצאת את הבית הזה. מערך של char בלי תו כזה אינו מחרוזת, והעברתו לפונקציות האלה קוראת מעבר לסוף הזיכרון שלכם.

למה אי אפשר להשים מחרוזת עם = ב-C?

כי מערך אינו ערך שאפשר להעתיק עם =. char a[10]; a = "hi"; לא יתקמפל. העתיקו במקום זה את התווים עם strcpy(a, "hi") מתוך <string.h>, או אתחלו בזמן ההצהרה, char a[10] = "hi";, שזה המקום היחיד שבו הקיצור עובד.

מה ההבדל בין char * לבין char [] ב-C?

char s[] = "hi" יוצר מערך שאפשר לשנות, עם עותק פרטי של הטקסט. char *p = "hi" יוצר מצביע לליטרל של מחרוזת, שנמצא בזיכרון לקריאה בלבד: קריאה ממנו בסדר, וכתיבה דרכו קורסת. השתמשו במערך כשאתם מתכוונים לשנות, ובמצביע כשאתם רק קוראים.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל