מחרוזות – איך הן עובדות?

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

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

קצת על הזכרון של המחשב

למחשבים (ולשרתים) יש זכרון RAM (ר"ת Random Access Memory, זכרון גישה אקראי), הזכרון הזה מכיל מידע שמערכת ההפעלה והתוכנות משתמשות בו באופן תכוף. כלומר כאשר מגדירים משתנה בתוכנה – הוא יישמר בזכרון.

הזכרון בנוי מתאים בגודל byte, לכל תא יש כתובת שהוא מספר סידורי רץ. נניח התא הראשון הוא בכתובת 0, ותלוי בגודל הזכרון של המחשב שלכם, הכתובת יכולה להגיע גם מעל 4 מיליארד אם יש לכם מעל 4GB של זכרון RAM.

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

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

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

איך נראה תו בזכרון? (על קידודים שונים)

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

במחשבים הראשונים, מן הסתם – היה רק אנגלית. הגדירו 127 תווים ראשונים שיהיו ידועים לכולם, ולמעשה ניתן היה לשמור בכל תא בגודל byte (כלומר 8 ביטים) תו אחד, כי הרי 127 תווים זה רק 7 ביטים ולכן זה הספיק. כעת, כאשר ניגשו לתא מסוים והתייחסו אליו כעל תו בודד והדפיסו אותו – אז הדפיסו לפי טבלת ההמרה של 127 התווים האלה. טבלת ההמרה הזאת נקראת ASCII Table, ואפשר לחפש באינטרנט את הטבלה הזאת כיוון שהיא בשימוש עד היום.

אם תא בודד הכיל תו אחד, איך הגדרנו מחרוזת? פשוט, לוקחים רצף של מספר תאים, ממלאים בתווים, ואז בתא האחרון של הרצף (למעשה בסוף המחרוזת) שמו תא שכולו מלא ב-0ים. יש לזה סימון מיוחד – "\0" (להזכירכם – ברוב שפות התכנות, סלש אחורי (backslash) בתוך מחרוזת מציין תו מיוחד ולאחריו בא ההגדרה של התו המיוחד, במקרה הזה – 0). כעת כשרצו להדפיס מחרוזת – היו שולחים את כתובת התא של תחילת המחרוזת והמחשב היה מדפיס עד תו ה-0.

ואז הגיע הצורך לתמיכה בשפות נוספות – ולכן אמרו משהו פשוט – הרי יש לנו אפשרות בתא לעוד 127 אפשרויות – אז נגדיר קידודים שונים, למשל קידוד עברית, צרפתית או ערבית, ולכל קידוד יהיה טבלת המרה משלו. כאשר נדפיס את המחרוזת נציין על איזה קידוד מדובר והמחשב ידע באיזה טבלת המרה להשתמש. זה היה טוב כי זה היה חסכוני אבל זה גם יצר בעיות – למשל אם לא היה ידוע מה הקידוד אז היו רואים ג'יבריש, אותיות לא מובנות ולא קשורות בעליל. צורה זו של שמירת התווים נקראה ANSI Encoding.

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

  1. אם הסיבית הראשונה היא 0, אז מדובר על 127 האפשרויות של ASCII, והתו מורכב מתא אחד.
  2. אם 3 הסיביות הראשונות הן 110, אז נשתמש ב-2 תאים בשביל התו.
  3. אם 4 הסיביות הראשונות הן 1110, אז נשתמש ב-3 תאים בשביל התו.
  4. אם 5 הסיביות הראשונות הן 11110, אז נשתמש ב-4 תאים בשביל התו.

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

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

אז איך זה קורה הלכה למעשה?

אני לא אדבר על שפות נמוכות כמו c כי שם זה דיי ישיר. אני אדבר רגע על שפות עילית כמו סי שארפ כי שם זה מעניין (הערה – אני לא יודע אם זה ככה באמת גם בג'אווה ופייתון, מוזמנים להאיר את עיני, אבל העקרון אמור להיות זהה).

לתוכנה יש טבלה של כל המחרוזות שבשימוש התוכנה. כל פעם שהתוכנה מגיעה לקטע קוד שיש בו מחרוזת, למשל הצבה בתוך ערך או אפילו הדפסה, לדוגמה "Hello World", היא בודקת האם המחרוזת הספציפית הזאת כבר נמצאת בטבלת המחרוזות. אם כן – היא מחזירה מצביע למחרוזת הזאת. אחרת – היא מוסיפה את המחרוזת לטבלה ומחזירה מצביע. מכאן שהשוואה של 2 מחרוזות היא כמו השוואה של 2 מצביעים שמצביעים לאותו התא, ולכן זה עובד נכון למרות ש-string הוא class.

בטבלה הזאת לא ניתן לשנות את המחרוזות, ולכן אם אנחנו עושים את הפעולה של "Hello" + "World" אנחנו למעשה יוצרים 3 מחרוזות בטבלה – יוצרים את (1) "Hello", את (2) "World", ואת (3) "HelloWorld". עכשיו תארו לכם מה קורה כשאתם מוסיפים הרבה מחרוזות? ועוד משתנים? נהיה מורכב…

בשביל להמנע ממצבים לא נעימים כאלה, שגם גוזלים הרבה כוח עיבוד – יש מספר פתרונות, להלן 2 עיקריים. הפתרון הראשון – להשתמש ב String Interpolation, וזה האפשרות להשתמש במחרוזת (שלפניה יש סימן דולר $) שבה אנחנו כבר מכניסים את המשתנים, למשל $"Hello {myName}", וזה יוצר מחרוזת של Hello עם הערך של המשתנה myName. הפתרון השני – להשתמש ב-StringBuilder. בעזרת StringBuilder נוכל לבנות מחרוזות ארוכות ללא החשש לעבודה הבעייתית של טבלת המחרוזות. דוגמה חיה:

מה עם מחרוזות סודיות?

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

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

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

סיכום

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

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

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

כתיבת תגובה

האימייל לא יוצג באתר. שדות החובה מסומנים *