إدخال النصوص في لغة C: شرح scanf و fgets وأمثلة عملية للتعامل الآمن

目次

1. المقدمة

لغة C تُعد من أهم اللغات لتعلّم أساسيات البرمجة. ومن بين مفاهيمها الأساسية، يُعتبر “إدخال السلاسل النصية” وظيفة لا غنى عنها عند استقبال البيانات من المستخدم. في هذه المقالة، سنشرح بالتفصيل طرق إدخال النصوص في لغة C، مع تقديم تقنيات ونصائح للتعامل معها بشكل آمن.

بالنسبة للمبتدئين، قد يكون التعامل مع الأخطاء أو مخاطر الأمان عند معالجة النصوص المدخلة أمراً مربكاً. لذلك، تغطي هذه المقالة كل شيء من الدوال الأساسية إلى أمثلة عملية متقدمة، بهدف إكسابك مهارات برمجية عملية.

إذا فهمت كيفية إدخال النصوص في C بشكل صحيح وتمكنت من تنفيذها بأمان، فستكون قد خطوت أولى خطواتك نحو تطوير برامج أكثر تقدماً. لنبدأ الآن بالمحتوى التفصيلي.

2. ما هو إدخال السلاسل النصية في C؟ شرح المفهوم الأساسي

ما هي السلسلة النصية؟

في لغة C، يتم تمثيل السلاسل النصية كمصفوفات من الأحرف. وتنتهي كل سلسلة برمز إنهاء خاص '\0'، والذي يحدد نهاية السلسلة. بفضل هذه الميزة، يمكن التعامل مع النصوص دون الحاجة لتحديد طولها بشكل صريح.

العلاقة بين السلاسل النصية والمصفوفات

في C، السلسلة النصية هي فعلياً مصفوفة من النوع char. على سبيل المثال، يمكن التصريح عن سلسلة كالتالي:

char str[10];  // مصفوفة لتخزين نص حتى 10 أحرف

في هذا المثال، يتم حجز مساحة لذاكرة تكفي حتى 10 أحرف. ولكن، حرف واحد يُستخدم لرمز الإنهاء '\0'، لذا العدد الفعلي للأحرف الممكن إدخالها هو 9 فقط.

أمثلة على النصوص الحرفية (String Literals)

النص الحرفي هو نص محاط بعلامتي اقتباس مزدوجتين (” “). المثال التالي يوضح ذلك:

char greeting[] = "Hello";

في هذه الحالة، يتم التعامل مع greeting كمصفوفة ذات حجم 6 (5 أحرف + رمز الإنهاء).

لماذا نحتاج إلى إدخال النصوص؟

غالباً ما تتطلب البرامج إدخال بيانات من المستخدم، مثل تسجيل الاسم أو العنوان أو كتابة كلمات مفتاحية للبحث. لذلك، فإن إدخال النصوص والتعامل معها بشكل آمن وفعّال يُعتبر أمراً أساسياً في واجهات التطبيقات.

侍エンジニア塾

3. الدوال الأساسية لإدخال النصوص في C وأمثلة على استخدامها

3-1. دالة scanf

الاستخدام الأساسي لدالة scanf

تُستخدم الدالة scanf للحصول على البيانات من الإدخال القياسي (لوحة المفاتيح). عند إدخال نصوص، نستخدم المُحدد %s.

مثال على الكود:

#include <stdio.h>

int main() {
    char str[50];  // مصفوفة لتخزين نص حتى 50 حرفاً
    printf("أدخل النص: ");
    scanf("%s", str);  // قراءة النص من الإدخال القياسي
    printf("النص المدخل: %s\n", str);
    return 0;
}

يعرض هذا البرنامج النص الذي يقوم المستخدم بإدخاله.

ملاحظات حول scanf

  1. لا يمكنه التعامل مع الفراغات:
    الدالة scanf تعتبر المسافة، التاب، أو السطر الجديد كفواصل. لذلك يتم قطع النص عند أول فراغ.

مثال:
الإدخال:

Hello World

الإخراج:

Hello
  1. خطر تجاوز الذاكرة (Buffer Overflow):
    إذا كان النص المدخل أطول من حجم المصفوفة، فقد يؤدي ذلك إلى تخريب الذاكرة وانهيار البرنامج أو استغلال ثغرات أمنية.

الحل: يُنصح باستخدام دوال أكثر أماناً مثل fgets (سيتم شرحها لاحقاً).

3-2. دالة fgets

الاستخدام الأساسي لدالة fgets

تُعتبر الدالة fgets أكثر أماناً لأنها تسمح بقراءة النص حتى عدد محدد من الأحرف، وتشمل أيضاً محرف الانتقال للسطر الجديد. هذا يمنع تجاوز الذاكرة (Buffer Overflow).

مثال على الكود:

#include <stdio.h>

int main() {
    char str[50];  // مصفوفة بطول 50 حرف
    printf("أدخل النص: ");
    fgets(str, sizeof(str), stdin);  // إدخال النص بأمان
    printf("النص المدخل: %s", str);
    return 0;
}

يقرأ هذا البرنامج النص حتى 50 حرفاً ويعرضه بشكل آمن.

مزايا fgets

  1. منع تجاوز الذاكرة: بفضل تحديد حجم المصفوفة.
  2. يدعم النصوص التي تحتوي على فراغات: حيث يتم إدخال المسافات والتاب بشكل طبيعي.

ملاحظات على fgets

  1. التعامل مع محرف السطر الجديد: النص الناتج قد يحتوي على \n مما يؤدي إلى ظهور سطر إضافي.

مثال لإزالة السطر الجديد:

str[strcspn(str, "\n")] = '\0';
  1. بقايا البيانات في المخزن المؤقت: عند استخدام fgets ثم إدخال جديد، قد تبقى بيانات غير مستخدمة. يمكن مسحها باستخدام fflush(stdin) أو getchar().

3-3. أيهما تختار؟ scanf أم fgets؟

الدالةالاستخدامالملاحظات
scanfنصوص قصيرة وبسيطة بدون مسافاتخطر تجاوز الذاكرة وعدم دعم الفراغات.
fgetsآمن ويدعم النصوص مع فراغاتيتطلب معالجة لمحرف \n أحياناً.

للمبتدئين أو البرامج العملية، يُوصى دائماً باستخدام fgets لأنه أكثر أماناً.

4. تقنيات عملية لإدخال النصوص بأمان

4-1. منع تجاوز الذاكرة (Buffer Overflow)

ما هو تجاوز الذاكرة؟

هو إدخال بيانات أطول من حجم المصفوفة، مما يؤدي إلى الكتابة خارج حدود الذاكرة. قد يسبب انهيار البرنامج أو ثغرات أمنية.

مثال خطر:

char str[10];
scanf("%s", str);  // بدون تحديد حجم الإدخال

إذا أدخل المستخدم أكثر من 10 أحرف، سيحدث تجاوز للذاكرة.

الحل 1: استخدام fgets

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

int main() {
    char str[10];
    printf("أدخل النص: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';  // إزالة محرف السطر الجديد
    printf("النص المدخل: %s\n", str);
    return 0;
}

الحل 2: التحقق من طول الإدخال

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

int main() {
    char str[10];
    printf("أدخل نص (حتى 9 أحرف): ");
    fgets(str, sizeof(str), stdin);
    if (strlen(str) >= sizeof(str) - 1 && str[strlen(str) - 1] != '\n') {
        printf("النص طويل جداً.\n");
        return 1;
    }
    str[strcspn(str, "\n")] = '\0';
    printf("النص المدخل: %s\n", str);
    return 0;
}

4-2. تنفيذ معالجة الأخطاء

أهمية معالجة الأخطاء

معالجة الأخطاء تجعل البرنامج أكثر متانة وتمنع الانهيار عند إدخال بيانات غير متوقعة.

الحل 1: إعادة المحاولة (Retry)

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

int main() {
    char str[10];
    int valid = 0;

    while (!valid) {
        printf("أدخل نص (حتى 9 أحرف): ");
        fgets(str, sizeof(str), stdin);

        if (strlen(str) >= sizeof(str) - 1 && str[strlen(str) - 1] != '\n') {
            printf("النص طويل جداً. حاول مرة أخرى.\n");
            while (getchar() != '\n');  // مسح المدخلات الزائدة
        } else {
            str[strcspn(str, "\n")] = '\0';
            valid = 1;
        }
    }

    printf("النص المدخل: %s\n", str);
    return 0;
}

الحل 2: تصفية الإدخال (Filtering)

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

int isValidInput(const char *str) {
    for (int i = 0; str[i] != '\0'; i++) {
        if (!isalnum(str[i])) {  // السماح بالأحرف والأرقام فقط
            return 0;
        }
    }
    return 1;
}

int main() {
    char str[50];
    printf("أدخل نص يحتوي على أحرف وأرقام فقط: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';

    if (isValidInput(str)) {
        printf("النص صالح: %s\n", str);
    } else {
        printf("إدخال غير صالح.\n");
    }
    return 0;
}

5. الدوال غير الموصى بها والبدائل الآمنة

5-1. خطورة الدالة gets

ما هي الدالة gets؟

الدالة gets كانت تُستخدم لقراءة النصوص من الإدخال القياسي. مثالها الأساسي:

char str[50];
gets(str);  // قراءة النص من المستخدم

قد تبدو هذه الدالة بسيطة وسهلة الاستخدام، لكنها خطيرة جداً.

مشاكل دالة gets

  1. خطر تجاوز الذاكرة: لا تحدد طول النصوص المدخلة، مما يؤدي إلى تخريب الذاكرة.
  2. ثغرات أمنية: يمكن استغلالها في هجمات مثل Buffer Overflow Attack.
  3. إلغاؤها من المعايير: تم اعتبارها غير آمنة في معيار C99 وحُذفت نهائياً في C11.

مثال خطر:

char str[10];
gets(str);  // لا يوجد حد للإدخال

5-2. البدائل الآمنة

استخدام fgets

الدالة fgets هي البديل الآمن لأنها تحدد حجم الإدخال.

مثال آمن:

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

int main() {
    char str[50];
    printf("أدخل النص: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';  // إزالة محرف السطر الجديد
    printf("النص المدخل: %s\n", str);
    return 0;
}

مقارنة مع scanf

يمكن استخدام scanf لإدخال النصوص، لكنه لا يدعم الفراغات. لذلك، إذا كان البرنامج يحتاج إلى نصوص معقدة أو طويلة، يُفضل fgets.

استخدام getline

في بيئات POSIX، يمكن استخدام getline لقراءة النصوص مع تخصيص الذاكرة ديناميكياً، مما يجعلها مناسبة للنصوص الطويلة.

مثال:

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

int main() {
    char *line = NULL;
    size_t len = 0;
    ssize_t read;

    printf("أدخل النص: ");
    read = getline(&line, &len, stdin);

    if (read != -1) {
        printf("النص المدخل: %s", line);
    }

    free(line);
    return 0;
}

6. أمثلة عملية متقدمة: إدخال النصوص متعددة الأسطر

6-1. إدخال عدة أسطر

بعض البرامج (مثل المفكرات) تحتاج إلى إدخال نصوص متعددة الأسطر.

مثال: إدخال 3 أسطر

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

#define MAX_LINES 3
#define MAX_LENGTH 100

int main() {
    char lines[MAX_LINES][MAX_LENGTH];
    printf("أدخل %d أسطر:\n", MAX_LINES);

    for (int i = 0; i < MAX_LINES; i++) {
        printf("السطر %d: ", i + 1);
        fgets(lines[i], sizeof(lines[i]), stdin);
        lines[i][strcspn(lines[i], "\n")] = '\0';
    }

    printf("\nالنصوص المدخلة:\n");
    for (int i = 0; i < MAX_LINES; i++) {
        printf("%d: %s\n", i + 1, lines[i]);
    }
    return 0;
}

6-2. التعامل مع الفراغات

مثال:

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

int main() {
    char str[100];
    printf("أدخل جملة: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';
    printf("النص المدخل: %s\n", str);
    return 0;
}

6-3. التعامل مع الرموز الخاصة

مثال: حساب عدد الرموز الخاصة

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

int main() {
    char str[100];
    int specialCount = 0;

    printf("أدخل نص: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';

    for (int i = 0; str[i] != '\0'; i++) {
        if (!isalnum(str[i]) && !isspace(str[i])) {
            specialCount++;
        }
    }

    printf("عدد الرموز الخاصة: %d\n", specialCount);
    return 0;
}

6-4. مثال تطبيقي: برنامج مذكرات بسيط

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

#define MAX_LINES 5
#define MAX_LENGTH 100

int main() {
    char lines[MAX_LINES][MAX_LENGTH];
    printf("يمكنك كتابة حتى %d أسطر:\n", MAX_LINES);

    for (int i = 0; i < MAX_LINES; i++) {
        printf("السطر %d: ", i + 1);
        fgets(lines[i], sizeof(lines[i]), stdin);
        lines[i][strcspn(lines[i], "\n")] = '\0';
    }

    FILE *file = fopen("memo.txt", "w");
    if (file == NULL) {
        printf("تعذر فتح الملف.\n");
        return 1;
    }

    for (int i = 0; i < MAX_LINES; i++) {
        fprintf(file, "%s\n", lines[i]);
    }

    fclose(file);
    printf("تم حفظ المذكرة.\n");
    return 0;
}

7. الأسئلة الشائعة (Q&A)

س1: لماذا لا يجب استخدام الدالة gets؟

ج:
لأن gets لا تضع أي قيود على طول النصوص المدخلة، مما يؤدي إلى خطر تجاوز الذاكرة. لهذا السبب تم إلغاؤها في معيار C11. البديل الآمن هو fgets.

مثال آمن:

char str[50];
fgets(str, sizeof(str), stdin);
str[strcspn(str, "\n")] = '\0';

س2: لماذا لا تستطيع scanf التعامل مع النصوص التي تحتوي على فراغات؟

ج:
لأن scanf تعتبر الفراغات (المسافة، التاب، السطر الجديد) كفواصل.

مثال:
إدخال:

Hello World

إخراج:

Hello

الحل: استخدام fgets لقراءة النصوص مع الفراغات:

char str[100];
fgets(str, sizeof(str), stdin);
str[strcspn(str, "\n")] = '\0';
printf("النص المدخل: %s\n", str);

س3: ماذا يحدث إذا تجاوز الإدخال حجم fgets؟

ج:
fgets يقطع النص عند الحد المسموح به.

الحلول:

  1. إظهار رسالة خطأ أو طلب إعادة الإدخال.
  2. استخدام getline في بيئة POSIX لأنه يخصص الذاكرة ديناميكياً.

س4: كيف أتخلص من محرف السطر الجديد الذي تُدخله fgets؟

ج:
يمكنك مسحه يدوياً:

char str[100];
fgets(str, sizeof(str), stdin);
str[strcspn(str, "\n")] = '\0';

س5: ماذا أفعل إذا بقيت بيانات إضافية في المخزن المؤقت بعد fgets؟

ج:
يمكن مسحها باستخدام getchar():

char str[10];
fgets(str, sizeof(str), stdin);

if (strchr(str, '\n') == NULL) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

س6: كيف أسمح فقط بإدخال أحرف وأرقام (alphanumeric)؟

ج:
بتطبيق فلترة على الإدخال:

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

int isValidInput(const char *str) {
    for (int i = 0; str[i] != '\0'; i++) {
        if (!isalnum(str[i])) return 0;
    }
    return 1;
}

int main() {
    char str[50];
    printf("أدخل نص يحتوي على أحرف وأرقام فقط: ");
    fgets(str, sizeof(str), stdin);
    str[strcspn(str, "\n")] = '\0';

    if (isValidInput(str)) {
        printf("صالح: %s\n", str);
    } else {
        printf("غير صالح.\n");
    }
    return 0;
}

س7: كيف أتعامل مع النصوص الطويلة جداً التي تتجاوز حجم المصفوفة؟

ج:
يمكنك استخدام getline لأنها تخصص الذاكرة تلقائياً وتدعم النصوص الكبيرة.

8. الخلاصة

في هذه المقالة، شرحنا أساسيات إدخال النصوص في لغة C بشكل آمن وفعال، بدءاً من الدوال الأساسية مثل scanf وfgets وصولاً إلى تطبيقات عملية مثل المذكرات البسيطة.

1. فهم الأساسيات

النصوص في C هي مصفوفات من نوع char وتنتهي برمز '\0'.

2. اختيار الدالة المناسبة

  • scanf: مناسب للنصوص القصيرة بدون فراغات.
  • fgets: الأكثر أماناً ويدعم النصوص مع الفراغات.
  • getline: مثالي للنصوص الطويلة جداً (في بيئات POSIX).

3. تقنيات الأمان

  1. إدارة حجم المصفوفة.
  2. إزالة محرف السطر الجديد.
  3. التحقق من صحة الإدخال (فلترة + معالجة أخطاء).

4. التطبيقات العملية

تعلمنا كيفية التعامل مع:

  • إدخال عدة أسطر.
  • التعامل مع الفراغات والرموز الخاصة.
  • حفظ البيانات في ملف نصي.

5. الاستفادة من الأسئلة الشائعة

تناولنا مشكلات مثل:
– خطورة gets ولماذا يجب استبدالها.
– كيفية التعامل مع الفراغات والرموز الخاصة.
– طرق معالجة النصوص الطويلة.

6. خطوات مستقبلية

بإمكانك التوسع نحو:

  1. استخدام دوال مكتبة النصوص مثل strlen وstrcmp.
  2. إدارة الذاكرة باستخدام malloc وrealloc.
  3. التعامل مع الملفات لإدارة البيانات.
  4. تطوير خوارزميات بحث وفرز للنصوص.

7. مهام تدريبية

  1. مهمة 1: إنشاء برنامج لإدارة قائمة أسماء.
  2. مهمة 2: برنامج للبحث عن كلمة مفتاحية.
  3. مهمة 3: نظام تسجيل بسيط باستخدام الملفات.

الخلاصة النهائية

الأمان أولاً: استخدام fgets أو getline.
إدارة الذاكرة: تجنب تجاوز السعة.
التطبيق العملي: التدريب على إدخال النصوص وحفظها ومعالجتها.

باتباع هذه النقاط، ستتمكن من تطوير برامج C أكثر أماناً واحترافية.