تخطَّ إلى المحتوى

النصوص في C: دوال string.h بالتفصيل

الدرس 8 من 10 · C

في درس المصفوفات عرفت أن النص في C ليس نوعاً خاصاً، بل مصفوفة حروف تنتهي بحرف النهاية '\0'. وبعد أن فهمت المؤشرات، حان وقت التعامل مع النصوص باحتراف: نسخها ومقارنتها والبحث فيها وقراءتها من المستخدم بأمان. كل الدوال هنا من المكتبة string.h ما لم نذكر غير ذلك.

هناك طريقتان لتعريف نص، وبينهما فرق مهم:

char name[] = "Omar";
const char *title = "Student";
name[0] = 'A';
printf("%s\n", name);
printf("%s\n", title);
printf("%zu %zu\n", sizeof(name), strlen(name));
الناتج
Amar
Student
5 4
  • char name[] = "Omar";: مصفوفة من 5 حروف (أربعة مع حرف النهاية) تملكها أنت، وتستطيع تعديل حروفها.
  • const char *title = "Student";: مؤشر إلى نص ثابت مخزن في منطقة للقراءة فقط. محاولة تعديله تنهي البرنامج، ولهذا نكتب const.
  • sizeof تعطي حجم المصفوفة كله، و strlen تعدّ الحروف حتى حرف النهاية.

تخيّل النص في C مثل قطار: العربات حروف، وآخر عربة علامة حمراء '\0' تقول «انتهى القطار». كل دوال النصوص تمشي عربة عربة حتى تصل إلى العلامة.

لا يمكن نسخ نص بعلامة = بعد تعريفه، فاسم المصفوفة ليس متغيراً عادياً. نستخدم strcpy:

char source[] = "Hello";
char copy[20];
strcpy(copy, source);
copy[0] = 'J';
printf("%s %s\n", source, copy);
الناتج
Hello Jello

strcpy(إلى, من): الوجهة أولاً ثم المصدر، وتنسخ الحروف كلها مع حرف النهاية.

العلامة == بين نصين تقارن العنوانين لا المحتوى. المقارنة الصحيحة بـ strcmp:

printf("%d\n", strcmp("apple", "apple"));
printf("%d\n", strcmp("apple", "banana") < 0);
printf("%d\n", strcmp("cat", "car") > 0);
char answer[] = "yes";
if (strcmp(answer, "yes") == 0) {
printf("موافق\n");
}
الناتج
0
1
1
موافق

strcmp تُرجع:

  • صفراً إذا تطابق النصان.
  • عدداً سالباً إذا كان الأول يأتي قبل الثاني أبجدياً.
  • عدداً موجباً إذا كان يأتي بعده.

انتبه: الصفر هنا يعني «متساويان»، مع أن الصفر في الشروط يعني «خطأ». لذلك اكتب المقارنة كاملة == 0 ولا تختصرها.

char email[] = "omar@example.com";
char *at = strchr(email, '@');
if (at != NULL) {
printf("%s\n", at + 1);
printf("%d\n", (int)(at - email));
}
if (strstr(email, "example") != NULL) {
printf("يحتوي على example\n");
}
if (strstr(email, "gmail") == NULL) {
printf("لا يحتوي على gmail\n");
}
الناتج
example.com
4
يحتوي على example
لا يحتوي على gmail
  • strchr(نص, حرف): تُرجع مؤشراً إلى أول ظهور للحرف، أو NULL إذا لم يوجد.
  • at + 1: مؤشر إلى ما بعد العلامة، وطباعته بـ %s تعطي بقية النص.
  • at - email: طرح مؤشرين يعطي المسافة بينهما، أي موضع الحرف.
  • strstr(نص, جزء): مثلها لكن تبحث عن نص كامل.

لأن النص ينتهي بـ '\0'، تستطيع المرور عليه دون معرفة طوله. والمكتبة ctype.h فيها دوال لفحص الحروف وتحويلها:

#include <ctype.h>
char text[] = "Room 204b";
int digits = 0;
for (int i = 0; text[i] != '\0'; i++) {
if (isdigit(text[i])) {
digits++;
}
text[i] = toupper(text[i]);
}
printf("%s\n", text);
printf("%d\n", digits);
الناتج
ROOM 204B
3

isdigit تفحص هل الحرف رقم، و isalpha هل هو حرف أبجدي، و isspace هل هو مسافة. و toupper و tolower تحوّلان حالة الحرف.

snprintf تعمل مثل printf تماماً، لكنها تكتب في مصفوفة بدل الشاشة، ولا تتجاوز الحجم الذي تحدده أبداً:

char line[50];
int age = 15;
snprintf(line, sizeof(line), "%s is %d years old", "Omar", age);
printf("%s\n", line);
printf("%zu\n", strlen(line));
char tiny[6];
snprintf(tiny, sizeof(tiny), "%s", "Programming");
printf("%s\n", tiny);
الناتج
Omar is 15 years old
20
Progr

في المثال الثاني النص أطول من المصفوفة، فقصّته snprintf عند خمسة حروف وتركت مكاناً لحرف النهاية، بدل الكتابة خارج الحدود. وقد ينبهك مترجم حديث مثل GCC بتحذير -Wformat-truncation إلى أن النص سيُقص. اجعلها خيارك الأول للنسخ والدمج والتحويل من أرقام إلى نص.

قراءة سطر من المستخدم: fgets

Section titled “قراءة سطر من المستخدم: fgets”

scanf("%s", name) تتوقف عند أول مسافة، ولا تعرف حجم مصفوفتك. الأفضل fgets التي تقرأ سطراً كاملاً ولا تتجاوز الحجم:

#include <stdio.h>
#include <string.h>
int main(void) {
char name[40];
printf("اكتب اسمك الكامل: ");
fgets(name, sizeof(name), stdin);
name[strcspn(name, "\n")] = '\0';
printf("مرحباً %s\n", name);
printf("عدد الحروف: %zu\n", strlen(name));
return 0;
}

إذا كتب المستخدم Omar Ali سيظهر:

اكتب اسمك الكامل: Omar Ali
مرحباً Omar Ali
عدد الحروف: 8

fgets تحتفظ برمز السطر الجديد \n الناتج عن ضغط Enter في آخر النص. السطر name[strcspn(name, "\n")] = '\0'; يحذفه: strcspn تُرجع موضع أول \n، فنضع مكانه حرف النهاية.

الدالتان atoi و atof من stdlib.h تحوّلان النص إلى عدد:

#include <stdlib.h>
int total = atoi("250") + 50;
printf("%d\n", total);
printf("%.1f\n", atof("3.5") * 2);
printf("%d\n", atoi("12abc"));
printf("%d\n", atoi("abc"));
الناتج
300
7.0
12
0

atoi تقرأ الأرقام من أول النص وتتوقف عند أول حرف غريب. وإذا لم تجد رقماً أصلاً تُرجع صفراً، فلا تستطيع التمييز بين النص "0" والنص "abc". للتحقق الدقيق من إدخال المستخدم توجد strtol، وهي تخبرك أين توقفت القراءة.

  1. اقرأ جملة من المستخدم بـ fgets، واطبع عدد حروفها وعدد كلماتها (عدّ المسافات وأضف واحداً).
  2. اكتب دالة void reverse(char text[]) تعكس النص في مكانه بتبديل الحروف من الطرفين.
  3. اكتب دالة int countChar(const char *text, char c) تعدّ مرات ظهور حرف في نص.
  4. تحدٍّ: اكتب دالة int isPalindrome(const char *word) تُرجع 1 إذا كانت الكلمة تُقرأ من الجهتين بنفس الشكل مثل level.
  • المقارنة بـ ==: if (answer == "yes") تقارن العناوين وتفشل دائماً تقريباً. استخدم strcmp(...) == 0.
  • وجهة أصغر من المصدر: char small[4]; strcpy(small, "Hello"); تجاوز للمخزن. احسب الحجم مع حرف النهاية، أو استخدم snprintf.
  • نسيان حرف النهاية: إذا ملأت مصفوفة حروف يدوياً ولم تضع '\0' في آخرها، ستكمل printf القراءة في ذاكرة عشوائية.
  • تعديل نص ثابت: char *s = "Hi"; s[0] = 'h'; ينهي البرنامج. عرّفه مصفوفة char s[] = "Hi"; إذا أردت تعديله.

سؤال

ماذا تُرجع strcmp("cat", "cat")؟

سؤال

لماذا نفضّل fgets على scanf مع %s لقراءة النصوص؟

  • char s[] مصفوفة قابلة للتعديل، و const char * مؤشر إلى نص ثابت.
  • strcpy للنسخ و strcmp للمقارنة (الصفر يعني التطابق).
  • strchr و strstr تبحثان وتُرجعان مؤشراً أو NULL.
  • snprintf تبني النصوص دون تجاوز الحجم، و fgets تقرأ سطراً كاملاً بأمان.
  • atoi و atof تحوّلان النص إلى رقم، وتأكد دائماً أن الوجهة تتسع لما تكتبه.