دليل شامل للغة البرمجة C من المبتدئين وحتى المستوى المتقدم مع شرح تفصيلي للأوامر والمفاهيم
لغة C هي لغة برمجة عامة الغرض طوّرها دينيس ريتشي في مختبرات بيل بين عامي 1969-1973 لاستخدامها في نظام التشغيل يونكس. تعتبر من أكثر لغات البرمجة استخداماً وتأثيراً في تاريخ الحوسبة، حيث تشكل الأساس للغات الأخرى مثل C++ وC# وJava وPython.
تستخدم لغة C على نطاق واسع في تطوير أنظمة التشغيل، المترجمات، قواعد البيانات، برامج التحكم بالأجهزة، وتطبيقات الأداء العالي. تعلم لغة C يوفر أساساً متيناً لفهم كيفية عمل الحواسيب والبرمجيات بشكل عام.
تطورت لغة C عبر الزمن من خلال عدة معايير قياسية:
قبل البدء بكتابة برامج بلغة C، تحتاج إلى إعداد بيئة العمل المناسبة. يتطلب ذلك تثبيت مترجم لغة C (compiler) ومحرر نصوص أو بيئة تطوير متكاملة (IDE).
#include <stdio.h>
int main() {
// هذا تعليق - أول برنامج في لغة C
printf("مرحبا بالعالم!\n"); // طباعة على الشاشة
return 0; // إرجاع قيمة 0 للدلالة على نجاح البرنامج
}
شرح كل سطر:
#include <stdio.h>
- استدعاء مكتبة الإدخال والإخراج القياسية للتعامل مع العمليات الأساسية.int main()
- تعريف الدالة الرئيسية في البرنامج التي يبدأ منها التنفيذ. int
تشير إلى أن الدالة ترجع قيمة صحيحة.// هذا تعليق
- تعليق لا يؤثر على تنفيذ البرنامج.printf("مرحبا بالعالم!\n");
- طباعة النص على الشاشة. الرمز \n
يمثل سطر جديد.return 0;
- إرجاع قيمة 0 لنظام التشغيل للإشارة إلى نجاح البرنامج.$ gcc hello_world.c -o hello_world
$ ./hello_world
مرحبا بالعالم!
لغة C توفر عدة أنواع أساسية من البيانات التي يمكن استخدامها لتخزين القيم المختلفة. فهم هذه الأنواع مهم لكتابة برامج فعالة واستخدام الذاكرة بشكل أمثل.
نوع البيانات | الحجم | النطاق | الوصف |
---|---|---|---|
char | 1 بايت | -128 إلى 127 | يستخدم لتخزين الأحرف أو الأعداد الصغيرة |
unsigned char | 1 بايت | 0 إلى 255 | نفس النوع السابق لكن بدون إشارة |
int | 4 بايت (عادة) | -2,147,483,648 إلى 2,147,483,647 | للأعداد الصحيحة |
unsigned int | 4 بايت (عادة) | 0 إلى 4,294,967,295 | أعداد صحيحة بدون إشارة |
short | 2 بايت | -32,768 إلى 32,767 | أعداد صحيحة أصغر |
long | 4 أو 8 بايت | -2,147,483,648 إلى 2,147,483,647 أو أكثر | أعداد صحيحة كبيرة |
float | 4 بايت | ±3.4e±38 (تقريباً) | أعداد عشرية بدقة 6 أرقام |
double | 8 بايت | ±1.7e±308 (تقريباً) | أعداد عشرية بدقة 15 رقم |
void | - | - | يستخدم لتمثيل عدم وجود قيمة |
sizeof()
لمعرفة حجم نوع البيانات بالبايت.unsigned
تزداد قيمة الحد الأعلى لكن لا يمكن تخزين قيم سالبة.#include <stdio.h>
int main() {
// تعريف متغيرات من أنواع مختلفة
char character = 'A';
int integer = 123;
float decimal = 3.14;
double precise = 3.141592653589793;
// طباعة قيم المتغيرات
printf("الحرف: %c\n", character);
printf("العدد الصحيح: %d\n", integer);
printf("العدد العشري: %f\n", decimal);
printf("العدد العشري الدقيق: %lf\n", precise);
// طباعة حجم كل نوع
printf("حجم char: %lu بايت\n", sizeof(character));
printf("حجم int: %lu بايت\n", sizeof(integer));
printf("حجم float: %lu بايت\n", sizeof(decimal));
printf("حجم double: %lu بايت\n", sizeof(precise));
return 0;
}
المتغيرات هي أسماء معطاة لمواقع في ذاكرة الحاسوب لتخزين القيم التي يمكن تغييرها أثناء تنفيذ البرنامج.
name
يختلف عن Name
.int
, float
, return
).// تعريف متغير بدون تهيئة
int number;
// تعريف متغير مع تهيئة
int count = 10;
// تعريف عدة متغيرات من نفس النوع
float x, y, z;
// تعريف وتهيئة عدة متغيرات
char a = 'A', b = 'B', c = 'C';
الثوابت هي قيم لا يمكن تغييرها بعد تعريفها. هناك عدة طرق لتعريف الثوابت في C:
// باستخدام #define (بدون إشارة مساواة أو فاصلة منقوطة)
#define PI 3.14159
// باستخدام const (مع إشارة مساواة وفاصلة منقوطة)
const float GRAVITY = 9.8;
int main() {
printf("قيمة باي: %f\n", PI);
printf("الجاذبية: %f\n", GRAVITY);
// الكود التالي سيسبب خطأ في الترجمة
// GRAVITY = 10.5; // لا يمكن تغيير قيمة الثابت
return 0;
}
يحدد مدى حياة المتغير أين يمكن استخدامه في البرنامج:
تستخدم العوامل (Operators) لإجراء عمليات على المتغيرات والقيم. تقدم لغة C مجموعة واسعة من العوامل.
العامل | الوصف | مثال |
---|---|---|
+ | الجمع | a + b |
- | الطرح | a - b |
* | الضرب | a * b |
/ | القسمة | a / b |
% | باقي القسمة | a % b |
++ | زيادة بمقدار 1 | a++ أو ++a |
-- | نقصان بمقدار 1 | a-- أو --a |
العامل | الوصف | مثال |
---|---|---|
== | يساوي | a == b |
!= | لا يساوي | a != b |
> | أكبر من | a > b |
< | أصغر من | a < b |
>= | أكبر من أو يساوي | a >= b |
<= | أصغر من أو يساوي | a <= b |
العامل | الوصف | مثال |
---|---|---|
&& | و المنطقية (AND) | a > 0 && b > 0 |
|| | أو المنطقية (OR) | a > 0 || b > 0 |
! | النفي (NOT) | !a |
العامل | الوصف | مثال | ما يعادله |
---|---|---|---|
= | تعيين قيمة | a = b | a = b |
+= | تعيين بعد الجمع | a += b | a = a + b |
-= | تعيين بعد الطرح | a -= b | a = a - b |
*= | تعيين بعد الضرب | a *= b | a = a * b |
/= | تعيين بعد القسمة | a /= b | a = a / b |
%= | تعيين بعد باقي القسمة | a %= b | a = a % b |
#include <stdio.h>
int main() {
int a = 10, b = 3;
// العوامل الحسابية
printf("a + b = %d\n", a + b);
printf("a - b = %d\n", a - b);
printf("a * b = %d\n", a * b);
printf("a / b = %d\n", a / b); // النتيجة: 3 (قسمة صحيحة)
printf("a %% b = %d\n", a % b); // النتيجة: 1 (باقي القسمة)
// عوامل المقارنة
printf("a == b: %d\n", a == b); // النتيجة: 0 (false)
printf("a != b: %d\n", a != b); // النتيجة: 1 (true)
printf("a > b: %d\n", a > b); // النتيجة: 1 (true)
// العوامل المنطقية
int x = 5, y = 0;
printf("x > 0 && y > 0: %d\n", x > 0 && y > 0); // النتيجة: 0 (false)
printf("x > 0 || y > 0: %d\n", x > 0 || y > 0); // النتيجة: 1 (true)
printf("!y: %d\n", !y); // النتيجة: 1 (true لأن y = 0)
// عوامل التعيين
int c = 20;
c += 5; // c = c + 5
printf("c بعد c += 5: %d\n", c); // النتيجة: 25
return 0;
}
عمليات الإدخال والإخراج أساسية في أي لغة برمجة. في C، تتم هذه العمليات من خلال دوال مكتبة stdio.h
.
هي الدالة الأساسية للطباعة على الشاشة وتسمح بتنسيق البيانات المختلفة.
printf("محدد التنسيق", البيانات);
محددات التنسيق الشائعة:
المحدد | النوع | مثال |
---|---|---|
%d, %i | عدد صحيح (int) | printf("العدد: %d", 10); |
%f | عدد عشري (float) | printf("العدد: %f", 3.14); |
%c | حرف (char) | printf("الحرف: %c", 'A'); |
%s | سلسلة نصية (string) | printf("النص: %s", "مرحبا"); |
%% | علامة النسبة المئوية (%) | printf("النسبة: 10%%"); |
تستخدم لطباعة سلسلة نصية متبوعة بسطر جديد.
puts("مرحبا بالعالم"); // تطبع "مرحبا بالعالم" مع إضافة سطر جديد
تستخدم لطباعة حرف واحد.
putchar('A'); // تطبع الحرف 'A'
هي الدالة الأساسية لقراءة مدخلات المستخدم وتخزينها في متغيرات.
scanf("محدد التنسيق", &المتغير);
⚠️ يجب استخدام علامة & قبل اسم المتغير في scanf() (باستثناء المصفوفات) للإشارة إلى عنوان المتغير في الذاكرة.
#include <stdio.h>
int main() {
int age;
float height;
char name[50]; // مصفوفة حروف لتخزين الاسم
// قراءة عدد صحيح
printf("أدخل عمرك: ");
scanf("%d", &age);
// قراءة عدد عشري
printf("أدخل طولك بالمتر: ");
scanf("%f", &height);
// مسح المدخل السابق (حل مشكلة المؤقت)
getchar();
// قراءة سلسلة نصية
printf("أدخل اسمك: ");
scanf("%s", name); // لاحظ أننا لا نستخدم & مع المصفوفات
// عرض البيانات المدخلة
printf("\nمرحبا %s، عمرك %d سنة وطولك %.2f متر.\n", name, age, height);
return 0;
}
تستخدم لقراءة حرف واحد من المدخلات.
char ch;
ch = getchar(); // قراءة حرف واحد
تستخدم لقراءة سطر كامل من النص.
⚠️ دالة gets() تعتبر غير آمنة لأنها لا تتحقق من حجم المصفوفة، مما قد يؤدي إلى تجاوز الحدود. يفضل استخدام fgets() بدلاً منها.
char str[100];
printf("أدخل نصاً: ");
fgets(str, 100, stdin); // قراءة سطر بأمان (باستخدام fgets بدلا من gets)
printf("النص المدخل: %s", str);
بنى التحكم تسمح للبرنامج باتخاذ قرارات وتنفيذ عمليات متكررة بناءً على شروط معينة. تنقسم بنى التحكم إلى ثلاث فئات رئيسية: بنى الشرط، بنى التكرار (الحلقات)، وبنى التحويل.
تنفذ كتلة من الكود فقط إذا كان الشرط صحيحاً.
if (شرط) {
// الكود الذي سيتم تنفيذه إذا كان الشرط صحيحاً
}
#include <stdio.h>
int main() {
int num = 10;
if (num > 0) {
printf("العدد موجب.\n");
}
return 0;
}
تنفذ كتلة من الكود إذا كان الشرط صحيحاً، وكتلة أخرى إذا كان خاطئاً.
if (شرط) {
// الكود الذي سيتم تنفيذه إذا كان الشرط صحيحاً
} else {
// الكود الذي سيتم تنفيذه إذا كان الشرط خاطئاً
}
#include <stdio.h>
int main() {
int num = -5;
if (num >= 0) {
printf("العدد موجب أو صفر.\n");
} else {
printf("العدد سالب.\n");
}
return 0;
}
تسمح باختبار شروط متعددة بالتتابع.
if (شرط1) {
// الكود الذي سيتم تنفيذه إذا كان الشرط1 صحيحاً
} else if (شرط2) {
// الكود الذي سيتم تنفيذه إذا كان الشرط1 خاطئاً والشرط2 صحيحاً
} else {
// الكود الذي سيتم تنفيذه إذا كانت جميع الشروط السابقة خاطئة
}
#include <stdio.h>
int main() {
int score = 85;
if (score >= 90) {
printf("تقدير: ممتاز\n");
} else if (score >= 80) {
printf("تقدير: جيد جداً\n");
} else if (score >= 70) {
printf("تقدير: جيد\n");
} else if (score >= 60) {
printf("تقدير: مقبول\n");
} else {
printf("تقدير: راسب\n");
}
return 0;
}
اختصار لجملة if-else البسيطة.
النتيجة = (شرط) ? قيمة_إذا_صحيح : قيمة_إذا_خاطئ;
#include <stdio.h>
int main() {
int a = 10, b = 20;
int max;
// استخدام العبارة الشرطية
max = (a > b) ? a : b;
printf("الرقم الأكبر هو: %d\n", max);
return 0;
}
تستخدم عندما نعرف مسبقاً عدد مرات التكرار.
for (التهيئة; الشرط; التحديث) {
// الكود الذي سيتم تكراره
}
#include <stdio.h>
int main() {
int i;
// طباعة الأرقام من 1 إلى 5
for (i = 1; i <= 5; i++) {
printf("%d ", i);
}
printf("\n");
return 0;
}
تستخدم عندما لا نعرف مسبقاً عدد مرات التكرار، وتستمر طالما الشرط صحيح.
while (شرط) {
// الكود الذي سيتم تكراره
}
#include <stdio.h>
int main() {
int num = 1;
// طباعة الأرقام من 1 إلى 5
while (num <= 5) {
printf("%d ", num);
num++;
}
printf("\n");
return 0;
}
مشابهة لحلقة while، لكنها تضمن تنفيذ جسم الحلقة مرة واحدة على الأقل قبل اختبار الشرط.
do {
// الكود الذي سيتم تكراره
} while (شرط);
#include <stdio.h>
int main() {
int num = 1;
// طباعة الأرقام من 1 إلى 5
do {
printf("%d ", num);
num++;
} while (num <= 5);
printf("\n");
return 0;
}
تُستخدم للخروج من الحلقة.
#include <stdio.h>
int main() {
int i;
for (i = 1; i <= 10; i++) {
if (i == 6) {
break; // الخروج من الحلقة عند i = 6
}
printf("%d ", i);
}
printf("\n"); // النتيجة: 1 2 3 4 5
return 0;
}
تُستخدم لتخطي التكرار الحالي والانتقال إلى التكرار التالي.
#include <stdio.h>
int main() {
int i;
for (i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue; // تخطي الأرقام الزوجية
}
printf("%d ", i);
}
printf("\n"); // النتيجة: 1 3 5 7 9
return 0;
}
تُستخدم جملة switch كبديل لسلسلة طويلة من جمل if-else if عندما نريد مقارنة قيمة متغير مع قيم ثابتة متعددة.
switch (التعبير) {
case قيمة1:
// الكود الذي سيتم تنفيذه إذا كان التعبير = قيمة1
break;
case قيمة2:
// الكود الذي سيتم تنفيذه إذا كان التعبير = قيمة2
break;
// يمكن إضافة المزيد من الحالات
default:
// الكود الذي سيتم تنفيذه إذا لم تتطابق أي من الحالات السابقة
}
#include <stdio.h>
int main() {
char grade = 'B';
switch (grade) {
case 'A':
printf("ممتاز!\n");
break;
case 'B':
printf("جيد جداً!\n");
break;
case 'C':
printf("جيد!\n");
break;
case 'D':
printf("مقبول!\n");
break;
case 'F':
printf("راسب!\n");
break;
default:
printf("تقدير غير صالح!\n");
}
return 0;
}
⚠️
لاحظ أهمية استخدام عبارة break
في نهاية كل حالة. بدونها، سيستمر التنفيذ في الحالات التالية حتى يصل إلى عبارة break
أو نهاية جملة switch
.
الدوال هي مجموعة من التعليمات البرمجية التي تؤدي مهمة محددة. تساعد الدوال على تقسيم البرنامج إلى أجزاء منطقية وإعادة استخدام الكود.
الشكل العام لتعريف الدالة في C:
نوع_الإرجاع اسم_الدالة(قائمة_المعاملات) {
// جسم الدالة
return قيمة_الإرجاع; // إذا كان نوع الإرجاع ليس void
}
يمكن الإعلان عن دالة قبل تعريفها باستخدام نموذج الدالة، مما يسمح باستدعاء الدالة قبل تعريفها في الكود.
// نموذج الدالة
نوع_الإرجاع اسم_الدالة(قائمة_أنواع_المعاملات);
#include <stdio.h>
// نماذج الدوال
int add(int, int);
void printResult(int);
int main() {
int result = add(5, 3);
printResult(result);
return 0;
}
// تعريف الدوال
int add(int a, int b) {
return a + b;
}
void printResult(int result) {
printf("النتيجة: %d\n", result);
}
في لغة C، يتم تمرير المعاملات بالقيمة افتراضياً، مما يعني أن الدالة تعمل على نسخة من المتغير وليس المتغير الأصلي.
#include <stdio.h>
void modify(int num) {
num = num * 2; // تعديل النسخة المحلية من num
printf("داخل الدالة: %d\n", num);
}
int main() {
int x = 10;
printf("قبل استدعاء الدالة: %d\n", x);
modify(x);
printf("بعد استدعاء الدالة: %d\n", x); // قيمة x لم تتغير
return 0;
}
يمكن تمرير عنوان المتغير (باستخدام المؤشرات) للسماح للدالة بتعديل المتغير الأصلي.
#include <stdio.h>
void modifyRef(int *num) {
*num = *num * 2; // تعديل القيمة الأصلية عبر المؤشر
printf("داخل الدالة: %d\n", *num);
}
int main() {
int x = 10;
printf("قبل استدعاء الدالة: %d\n", x);
modifyRef(&x); // تمرير عنوان x
printf("بعد استدعاء الدالة: %d\n", x); // الآن x = 20
return 0;
}
لغة C لا تدعم المعاملات الافتراضية مباشرة كما في اللغات الأخرى، لكن يمكن محاكاة هذا السلوك باستخدام تقنيات مختلفة.
#include <stdio.h>
// تعريف قيمة افتراضية باستخدام ماكرو
#define DEFAULT_MULTIPLIER 2
int multiply(int num, int multiplier) {
return num * multiplier;
}
// دالة أخرى تستدعي الأولى مع قيمة افتراضية
int multiplyDefault(int num) {
return multiply(num, DEFAULT_MULTIPLIER);
}
int main() {
printf("5 * 3 = %d\n", multiply(5, 3));
printf("5 * DEFAULT = %d\n", multiplyDefault(5));
return 0;
}
عند تمرير مصفوفة إلى دالة، يتم تمرير المؤشر إلى العنصر الأول في المصفوفة.
#include <stdio.h>
// طريقة 1: مصفوفة بدون تحديد الحجم
void printArray(int arr[], int size) {
int i;
for (i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
// طريقة 2: باستخدام المؤشر
void printArrayPtr(int *arr, int size) {
int i;
for (i = 0; i < size; i++) {
printf("%d ", *(arr + i)); // يمكن أيضاً استخدام arr[i]
}
printf("\n");
}
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int size = sizeof(numbers) / sizeof(numbers[0]);
printArray(numbers, size);
printArrayPtr(numbers, size);
return 0;
}
الدالة التكرارية (Recursive Function) هي دالة تستدعي نفسها. هذا النمط مفيد لحل المشكلات التي يمكن تقسيمها إلى مشكلات فرعية مشابهة.
#include <stdio.h>
// دالة تكرارية لحساب العدد التراكمي (Factorial)
int factorial(int n) {
// الحالة القاعدية (Base Case)
if (n == 0 || n == 1) {
return 1;
}
// الحالة التكرارية (Recursive Case)
else {
return n * factorial(n - 1);
}
}
int main() {
int num = 5;
printf("%d! = %d\n", num, factorial(num)); // 5! = 120
return 0;
}
مثال آخر: دالة تكرارية لحساب سلسلة فيبوناتشي.
#include <stdio.h>
// دالة تكرارية لحساب فيبوناتشي
int fibonacci(int n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
int main() {
int i;
printf("سلسلة فيبوناتشي للعشر أرقام الأولى: ");
for (i = 0; i < 10; i++) {
printf("%d ", fibonacci(i));
}
printf("\n"); // النتيجة: 0 1 1 2 3 5 8 13 21 34
return 0;
}
⚠️ يجب الحذر عند استخدام الدوال التكرارية، لأنها قد تؤدي إلى استنزاف ذاكرة المكدس (Stack Overflow) إذا كان عمق التكرار كبيراً جداً. تأكد دائماً من وجود حالة قاعدية تنهي التكرار.
المصفوفة هي مجموعة من العناصر من نفس النوع مخزنة في مواقع متتالية في الذاكرة. تسمح المصفوفات بتخزين ومعالجة مجموعات من البيانات تحت اسم متغير واحد.
// تعريف مصفوفة بدون تهيئة
int numbers[5]; // مصفوفة من 5 أعداد صحيحة
// تعريف وتهيئة مصفوفة
int scores[5] = {85, 92, 78, 90, 88};
// تعريف وتهيئة مصفوفة بدون تحديد الحجم (يحدد الحجم تلقائياً)
char vowels[] = {'a', 'e', 'i', 'o', 'u'};
// تهيئة جزئية (العناصر المتبقية تُهيأ بالقيمة 0)
float values[5] = {1.5, 2.5};
#include <stdio.h>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
// الوصول إلى عنصر واحد
printf("العنصر الثالث: %d\n", numbers[2]); // الفهارس تبدأ من 0
// تعديل قيمة عنصر
numbers[3] = 45;
printf("العنصر الرابع بعد التعديل: %d\n", numbers[3]);
// طباعة جميع العناصر باستخدام حلقة
int i;
printf("جميع العناصر: ");
for (i = 0; i < 5; i++) {
printf("%d ", numbers[i]);
}
printf("\n");
return 0;
}
#include <stdio.h>
int main() {
int numbers[] = {10, 20, 30, 40, 50, 60};
// حساب عدد العناصر في المصفوفة
int size = sizeof(numbers) / sizeof(numbers[0]);
printf("عدد العناصر في المصفوفة: %d\n", size);
return 0;
}
المصفوفات متعددة الأبعاد هي مصفوفات تحتوي على مصفوفات أخرى. الأكثر شيوعاً هي المصفوفات ثنائية الأبعاد التي يمكن تصورها كجداول.
// تعريف مصفوفة ثنائية الأبعاد
int matrix[3][4]; // مصفوفة 3×4
// تعريف وتهيئة مصفوفة ثنائية الأبعاد
int grid[2][3] = {
{1, 2, 3}, // الصف الأول
{4, 5, 6} // الصف الثاني
};
// طريقة أخرى للتهيئة
int another[2][3] = {1, 2, 3, 4, 5, 6};
#include <stdio.h>
int main() {
int matrix[3][4] = {
{10, 20, 30, 40},
{50, 60, 70, 80},
{90, 100, 110, 120}
};
// الوصول إلى عنصر واحد
printf("العنصر في الصف 1، العمود 2: %d\n", matrix[1][2]); // 70
// تعديل قيمة عنصر
matrix[2][3] = 125;
printf("العنصر بعد التعديل: %d\n", matrix[2][3]);
// طباعة جميع عناصر المصفوفة باستخدام حلقات متداخلة
int i, j;
printf("المصفوفة كاملة:\n");
for (i = 0; i < 3; i++) {
for (j = 0; j < 4; j++) {
printf("%4d", matrix[i][j]);
}
printf("\n");
}
return 0;
}
// مصفوفة ثلاثية الأبعاد (2×3×4)
int cube[2][3][4];
// الوصول إلى عنصر
cube[1][2][3] = 100;
// تهيئة مصفوفة ثلاثية الأبعاد
int cube[2][2][2] = {
{{1, 2}, {3, 4}}, // الطبقة الأولى
{{5, 6}, {7, 8}} // الطبقة الثانية
};
في لغة C، السلاسل النصية هي مصفوفات من نوع char
تنتهي بمحرف نهاية النص '\0' (NULL terminator).
// طريقة 1: باستخدام مصفوفة حروف
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
// طريقة 2: باستخدام النص المحاط بعلامات اقتباس
char str2[] = "Hello"; // المترجم يضيف '\0' تلقائياً
// طريقة 3: تحديد الحجم (يجب أن يكون كافياً للنص + محرف النهاية)
char str3[10] = "Hello"; // المساحة المتبقية تُملأ بـ '\0'
// مؤشر إلى سلسلة نصية ثابتة
char *str4 = "Hello";
⚠️
عند استخدام char *str = "Hello"
، فإن السلسلة النصية تكون ثابتة (read-only)، ولا ينبغي محاولة تعديلها. لسلاسل نصية قابلة للتعديل، استخدم مصفوفات الحروف.
توفر مكتبة string.h
العديد من الدوال للتعامل مع السلاسل النصية:
#include <stdio.h>
#include <string.h>
int main() {
char str1[20] = "Hello";
char str2[] = "World";
char str3[20];
int len;
// حساب طول السلسلة
len = strlen(str1);
printf("طول السلسلة str1: %d\n", len);
// نسخ سلسلة
strcpy(str3, str1);
printf("str3 بعد النسخ: %s\n", str3);
// دمج سلسلتين
strcat(str1, str2);
printf("str1 بعد الدمج: %s\n", str1);
// مقارنة سلسلتين
if (strcmp(str1, str3) == 0) {
printf("السلسلتان متطابقتان\n");
} else {
printf("السلسلتان غير متطابقتين\n");
}
// البحث عن حرف في سلسلة
char *ptr = strchr(str1, 'o');
if (ptr) {
printf("تم العثور على 'o' في الموضع: %ld\n", ptr - str1);
}
return 0;
}
بعض الدوال الشائعة الأخرى للتعامل مع السلاسل النصية:
strncpy()
: نسخ عدد محدد من الأحرف.strncat()
: دمج عدد محدد من الأحرف.strstr()
: البحث عن سلسلة فرعية.strtok()
: تقسيم السلسلة إلى أجزاء.// تعريف مؤشر (بدون تهيئة)
int *ptr;
// تعريف متغير ومؤشر يشير إليه
int num = 10;
int *ptr = # // & هو عامل "عنوان" يرجع عنوان المتغير
// يمكن أيضاً تعريف المؤشر أولاً ثم إسناد العنوان لاحقاً
int *ptr;
ptr = #
#include <stdio.h>
int main() {
int num = 10;
int *ptr = #
printf("قيمة num: %d\n", num);
printf("عنوان num: %p\n", &num);
printf("قيمة ptr (أي عنوان num): %p\n", ptr);
printf("القيمة التي يشير إليها ptr: %d\n", *ptr); // * هو عامل "محتوى العنوان"
// تغيير قيمة المتغير من خلال المؤشر
*ptr = 20;
printf("قيمة num بعد التعديل: %d\n", num);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = NULL; // تهيئة مؤشر بالقيمة NULL
printf("قيمة المؤشر الفارغ: %p\n", ptr);
// تحقق دائماً من أن المؤشر ليس فارغاً قبل استخدامه
if (ptr != NULL) {
printf("القيمة التي يشير إليها ptr: %d\n", *ptr);
} else {
printf("المؤشر فارغ ولا يشير إلى أي عنوان صالح.\n");
}
return 0;
}
⚠️ محاولة الوصول إلى محتوى مؤشر فارغ (NULL) ستؤدي إلى خطأ في التنفيذ (segmentation fault). لذا من الممارسات الجيدة دائماً التحقق من أن المؤشر ليس NULL قبل محاولة الوصول إلى محتواه.
هناك علاقة وثيقة بين المؤشرات والمصفوفات في لغة C. اسم المصفوفة هو في الواقع مؤشر إلى أول عنصر فيها.
#include <stdio.h>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int *ptr = numbers; // لا تحتاج لعامل &
printf("عنوان المصفوفة (numbers): %p\n", numbers);
printf("عنوان أول عنصر (&numbers[0]): %p\n", &numbers[0]);
printf("قيمة المؤشر (ptr): %p\n\n", ptr);
// الوصول إلى عناصر المصفوفة باستخدام المؤشر
printf("العناصر باستخدام الفهارس المصفوفة: %d %d %d\n", numbers[0], numbers[1], numbers[2]);
printf("العناصر باستخدام الفهارس المؤشر: %d %d %d\n", ptr[0], ptr[1], ptr[2]);
printf("العناصر باستخدام عمليات المؤشر: %d %d %d\n\n", *ptr, *(ptr + 1), *(ptr + 2));
// تغيير قيم المصفوفة باستخدام المؤشر
*ptr = 15; // تغيير العنصر الأول
*(ptr + 2) = 35; // تغيير العنصر الثالث
printf("المصفوفة بعد التعديل: ");
int i;
for (i = 0; i < 5; i++) {
printf("%d ", numbers[i]);
}
printf("\n");
return 0;
}
#include <stdio.h>
int main() {
int numbers[5] = {10, 20, 30, 40, 50};
int *ptr = numbers;
printf("ptr يشير إلى العنصر: %d\n", *ptr);
printf("ptr + 1 يشير إلى العنصر: %d\n", *(ptr + 1));
printf("ptr + 2 يشير إلى العنصر: %d\n", *(ptr + 2));
// تحريك المؤشر للأمام
ptr++;
printf("بعد ptr++، ptr يشير إلى العنصر: %d\n", *ptr);
ptr += 2;
printf("بعد ptr += 2، ptr يشير إلى العنصر: %d\n", *ptr);
// تحريك المؤشر للخلف
ptr--;
printf("بعد ptr--، ptr يشير إلى العنصر: %d\n", *ptr);
return 0;
}
ℹ️
عند إجراء عمليات حسابية على المؤشرات، يتم تعديل العنوان بناءً على حجم نوع البيانات. فمثلاً، إذا كان المؤشر من نوع int
وافترضنا أن حجم int
هو 4 بايت، فإن ptr + 1
سيزيد العنوان بمقدار 4 بايت، وليس بايت واحد.
تتيح لغة C للمبرمج إدارة الذاكرة بشكل ديناميكي من خلال مجموعة من الدوال الموجودة في مكتبة stdlib.h
.
malloc()
: تخصيص كتلة ذاكرة بحجم محدد.calloc()
: تخصيص مجموعة من العناصر وتهيئتها بالقيمة صفر.realloc()
: تغيير حجم كتلة ذاكرة تم تخصيصها سابقاً.free()
: تحرير كتلة ذاكرة تم تخصيصها ديناميكياً.#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
int n, i;
printf("أدخل عدد العناصر: ");
scanf("%d", &n);
// تخصيص ذاكرة ديناميكياً
ptr = (int*) malloc(n * sizeof(int));
// التحقق من نجاح تخصيص الذاكرة
if (ptr == NULL) {
printf("خطأ: فشل في تخصيص الذاكرة.\n");
return 1;
}
// إدخال العناصر
printf("أدخل %d عدد:\n", n);
for (i = 0; i < n; i++) {
scanf("%d", &ptr[i]);
}
// عرض العناصر
printf("العناصر المدخلة: ");
for (i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
// تحرير الذاكرة عند الانتهاء
free(ptr);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
int n, i, new_size;
printf("أدخل عدد العناصر: ");
scanf("%d", &n);
// تخصيص ذاكرة وتهيئتها بالقيمة صفر
ptr = (int*) calloc(n, sizeof(int));
if (ptr == NULL) {
printf("خطأ: فشل في تخصيص الذاكرة.\n");
return 1;
}
// عرض العناصر (جميعها صفر بسبب calloc)
printf("العناصر بعد calloc: ");
for (i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
// إدخال عناصر جديدة
printf("أدخل %d عدد:\n", n);
for (i = 0; i < n; i++) {
scanf("%d", &ptr[i]);
}
// تغيير حجم المصفوفة
printf("أدخل الحجم الجديد: ");
scanf("%d", &new_size);
ptr = (int*) realloc(ptr, new_size * sizeof(int));
if (ptr == NULL) {
printf("خطأ: فشل في إعادة تخصيص الذاكرة.\n");
return 1;
}
// إذا كان الحجم الجديد أكبر، أدخل العناصر الإضافية
if (new_size > n) {
printf("أدخل %d عدد إضافي:\n", new_size - n);
for (i = n; i < new_size; i++) {
scanf("%d", &ptr[i]);
}
}
// عرض العناصر بعد التعديل
printf("العناصر بعد realloc: ");
for (i = 0; i < new_size; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
// تحرير الذاكرة
free(ptr);
return 0;
}
⚠️ الأخطاء الشائعة في إدارة الذاكرة:
free()
بعد الانتهاء منها.الهياكل (Structures) والاتحادات (Unions) هي أنواع بيانات مركبة في لغة C تسمح بتجميع عناصر من أنواع مختلفة تحت اسم واحد.
الهيكل هو مجموعة من عناصر من أنواع مختلفة يمكن الوصول إليها باستخدام اسم واحد. يمكن تعريف هيكل كنوع بيانات جديد في البرنامج.
// تعريف هيكل
struct اسم_الهيكل {
نوع_البيانات1 اسم_العنصر1;
نوع_البيانات2 اسم_العنصر2;
// ...
نوع_البياناتN اسم_العنصرN;
};
#include <stdio.h>
#include <string.h>
// تعريف هيكل للطالب
struct Student {
char name[50];
int roll_number;
float marks;
};
int main() {
// إنشاء متغير من نوع Student
struct Student s1;
// تعيين قيم للعناصر
strcpy(s1.name, "أحمد محمد");
s1.roll_number = 101;
s1.marks = 85.5;
// عرض بيانات الطالب
printf("معلومات الطالب:\n");
printf("الاسم: %s\n", s1.name);
printf("الرقم: %d\n", s1.roll_number);
printf("الدرجات: %.1f\n", s1.marks);
return 0;
}
// طريقة 1: تهيئة عند التعريف
struct Student s1 = {"أحمد محمد", 101, 85.5};
// طريقة 2: تهيئة عنصر بعنصر
struct Student s2;
strcpy(s2.name, "محمد علي");
s2.roll_number = 102;
s2.marks = 90.0;
// طريقة 3: تهيئة باستخدام designated initializers (C99)
struct Student s3 = {
.marks = 75.0,
.name = "سارة أحمد",
.roll_number = 103
};
#include <stdio.h>
#include <string.h>
struct Student {
char name[50];
int roll_number;
float marks;
};
int main() {
struct Student s1;
struct Student *ptr = &s1;
// تعيين قيم باستخدام المؤشر
strcpy(ptr->name, "أحمد محمد"); // استخدام عامل -> للوصول للعناصر
ptr->roll_number = 101;
ptr->marks = 85.5;
// عرض بيانات الطالب باستخدام المؤشر
printf("معلومات الطالب:\n");
printf("الاسم: %s\n", ptr->name);
printf("الرقم: %d\n", ptr->roll_number);
printf("الدرجات: %.1f\n", ptr->marks);
return 0;
}
#include <stdio.h>
#include <string.h>
struct Student {
char name[50];
int roll_number;
float marks;
};
int main() {
// مصفوفة من 3 طلاب
struct Student students[3];
int i;
// إدخال بيانات الطلاب
for (i = 0; i < 3; i++) {
printf("أدخل بيانات الطالب %d:\n", i+1);
printf("الاسم: ");
scanf("%s", students[i].name);
printf("الرقم: ");
scanf("%d", &students[i].roll_number);
printf("الدرجات: ");
scanf("%f", &students[i].marks);
}
// عرض بيانات جميع الطلاب
printf("\nقائمة الطلاب:\n");
for (i = 0; i < 3; i++) {
printf("الطالب %d: %s، الرقم: %d، الدرجات: %.1f\n",
i+1, students[i].name, students[i].roll_number, students[i].marks);
}
return 0;
}
الاتحاد يشبه الهيكل، لكن جميع أعضائه يشتركون في نفس مساحة الذاكرة. حجم الاتحاد يساوي حجم أكبر عضو فيه.
#include <stdio.h>
// تعريف اتحاد
union Data {
int i;
float f;
char str[20];
};
int main() {
union Data data;
// عند تعيين قيمة لعضو في الاتحاد، تتغير قيمة الأعضاء الأخرى
data.i = 10;
printf("data.i: %d\n", data.i);
printf("data.f: %f\n", data.f); // قيمة غير متوقعة
data.f = 220.5;
printf("data.i: %d\n", data.i); // قيمة غير متوقعة
printf("data.f: %f\n", data.f);
// حجم الاتحاد = حجم أكبر عضو
printf("حجم الاتحاد: %zu بايت\n", sizeof(data));
return 0;
}
ℹ️ تُستخدم الاتحادات لتوفير الذاكرة عندما نحتاج إلى تخزين قيم مختلفة الأنواع، لكن ليس في نفس الوقت. مثلاً، في تطبيق رسائل النظام، قد تحتوي الرسالة على أنواع مختلفة من البيانات حسب نوع الرسالة.
الحقول البتية هي ميزة خاصة في هياكل لغة C تسمح بتحديد عدد البتات المطلوب لكل عضو، مما يوفر الذاكرة.
#include <stdio.h>
struct PackedData {
unsigned int flag1 : 1; // حقل من بت واحد
unsigned int flag2 : 1;
unsigned int flag3 : 1;
unsigned int type : 4; // حقل من 4 بت
unsigned int value : 8; // حقل من 8 بت
};
int main() {
struct PackedData data;
data.flag1 = 1;
data.flag2 = 0;
data.flag3 = 1;
data.type = 7; // قيمة بين 0 و 15 (2^4 - 1)
data.value = 127; // قيمة بين 0 و 255 (2^8 - 1)
printf("الأعلام: %u %u %u\n", data.flag1, data.flag2, data.flag3);
printf("النوع: %u\n", data.type);
printf("القيمة: %u\n", data.value);
printf("حجم الهيكل: %zu بايت\n", sizeof(data));
return 0;
}
توفر لغة C مجموعة من الدوال للتعامل مع الملفات من خلال مكتبة stdio.h
. تسمح هذه الدوال بإنشاء وقراءة وكتابة وتعديل الملفات.
FILE *fopen(const char *filename, const char *mode);
أوضاع فتح الملف:
الوضع | الوصف |
---|---|
"r" | فتح للقراءة فقط. يجب أن يكون الملف موجوداً. |
"w" | فتح للكتابة فقط. إذا كان الملف موجوداً، يتم محو محتواه. إذا لم يكن موجوداً، يتم إنشاؤه. |
"a" | فتح للإضافة. إذا كان الملف موجوداً، تتم الكتابة في نهايته. إذا لم يكن موجوداً، يتم إنشاؤه. |
"r+" | فتح للقراءة والكتابة. يجب أن يكون الملف موجوداً. |
"w+" | فتح للقراءة والكتابة. إذا كان الملف موجوداً، يتم محو محتواه. إذا لم يكن موجوداً، يتم إنشاؤه. |
"a+" | فتح للقراءة والإضافة. إذا كان الملف موجوداً، تتم القراءة من البداية والكتابة في النهاية. إذا لم يكن موجوداً، يتم إنشاؤه. |
int fclose(FILE *stream);
#include <stdio.h>
int main() {
FILE *file;
// فتح ملف للكتابة
file = fopen("test.txt", "w");
// التحقق من نجاح فتح الملف
if (file == NULL) {
printf("خطأ في فتح الملف!\n");
return 1;
}
printf("تم فتح الملف بنجاح.\n");
// إغلاق الملف
fclose(file);
printf("تم إغلاق الملف.\n");
return 0;
}
توجد عدة دوال للكتابة إلى الملفات:
تعمل مثل printf() لكنها تكتب إلى ملف بدل الشاشة.
#include <stdio.h>
int main() {
FILE *file;
int num = 123;
float pi = 3.14;
char name[] = "أحمد";
file = fopen("data.txt", "w");
if (file == NULL) {
printf("خطأ في فتح الملف!\n");
return 1;
}
// كتابة نص منسق إلى الملف
fprintf(file, "الرقم: %d\n", num);
fprintf(file, "باي: %.2f\n", pi);
fprintf(file, "الاسم: %s\n", name);
fclose(file);
printf("تمت الكتابة إلى الملف بنجاح.\n");
return 0;
}
لكتابة سلسلة نصية إلى ملف.
int fputs(const char *str, FILE *stream);
لكتابة حرف واحد إلى ملف.
int fputc(int char, FILE *stream);
لكتابة كتل من البيانات إلى ملف.
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
#include <stdio.h>
struct Student {
char name[50];
int roll;
float marks;
};
int main() {
FILE *file;
struct Student student = {"أحمد محمد", 101, 85.5};
file = fopen("student.dat", "wb"); // فتح ملف ثنائي
if (file == NULL) {
printf("خطأ في فتح الملف!\n");
return 1;
}
// كتابة هيكل الطالب إلى الملف
fwrite(&student, sizeof(struct Student), 1, file);
fclose(file);
printf("تمت كتابة بيانات الطالب إلى الملف الثنائي.\n");
return 0;
}
توجد عدة دوال للقراءة من الملفات:
تعمل مثل scanf() لكنها تقرأ من ملف بدل لوحة المفاتيح.
#include <stdio.h>
int main() {
FILE *file;
int num;
float pi;
char name[50];
file = fopen("data.txt", "r");
if (file == NULL) {
printf("خطأ في فتح الملف!\n");
return 1;
}
// قراءة البيانات من الملف
fscanf(file, "الرقم: %d\n", &num);
fscanf(file, "باي: %f\n", &pi);
fscanf(file, "الاسم: %s\n", name);
fclose(file);
// عرض البيانات المقروءة
printf("الرقم: %d\n", num);
printf("باي: %.2f\n", pi);
printf("الاسم: %s\n", name);
return 0;
}
لقراءة سطر من الملف.
char *fgets(char *str, int n, FILE *stream);
لقراءة حرف واحد من الملف.
int fgetc(FILE *stream);
لقراءة كتل من البيانات من ملف.
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
#include <stdio.h>
struct Student {
char name[50];
int roll;
float marks;
};
int main() {
FILE *file;
struct Student student;
file = fopen("student.dat", "rb"); // فتح الملف الثنائي للقراءة
if (file == NULL) {
printf("خطأ في فتح الملف!\n");
return 1;
}
// قراءة بيانات الطالب من الملف
fread(&student, sizeof(struct Student), 1, file);
fclose(file);
// عرض بيانات الطالب
printf("الاسم: %s\n", student.name);
printf("الرقم: %d\n", student.roll);
printf("الدرجات: %.1f\n", student.marks);
return 0;
}
يمكن التنقل إلى موضع معين في الملف باستخدام دوال مثل fseek()
و rewind()
و ftell()
.
#include <stdio.h>
int main() {
FILE *file;
char buffer[100];
long position;
file = fopen("example.txt", "w+"); // فتح للقراءة والكتابة
if (file == NULL) {
printf("خطأ في فتح الملف!\n");
return 1;
}
// كتابة بعض البيانات
fputs("السطر الأول\n", file);
fputs("السطر الثاني\n", file);
fputs("السطر الثالث\n", file);
// الحصول على الموضع الحالي
position = ftell(file);
printf("الموضع الحالي: %ld بايت\n", position);
// العودة إلى بداية الملف
rewind(file);
printf("العودة إلى بداية الملف.\n");
// قراءة السطر الأول
fgets(buffer, 100, file);
printf("قراءة: %s", buffer);
// الانتقال إلى موضع معين (بداية السطر الثالث)
fseek(file, 24, SEEK_SET); // القيمة 24 قد تختلف حسب طول السطور
printf("الانتقال إلى موضع معين.\n");
// قراءة من الموضع الجديد
fgets(buffer, 100, file);
printf("قراءة: %s", buffer);
fclose(file);
return 0;
}
المعالج القبلي (Preprocessor) هو برنامج يعالج الكود المصدري قبل ترجمته. يتم تنفيذ موجهات المعالج القبلي (التي تبدأ بعلامة #) في مرحلة ما قبل الترجمة.
يستخدم للتضمين محتويات ملف في الكود المصدري:
// تضمين ملف من المكتبة القياسية
#include <stdio.h>
// تضمين ملف محلي
#include "myheader.h"
يستخدم لتعريف الثوابت الرمزية والماكروهات:
// تعريف ثابت رمزي
#define PI 3.14159
#define MAX_SIZE 100
// تعريف ماكرو بدون معاملات
#define NEWLINE printf("\n");
// تعريف ماكرو بمعاملات
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
printf("قيمة PI: %f\n", PI);
printf("مربع 5: %d\n", SQUARE(5));
printf("القيمة العظمى من 10 و 20: %d\n", MAX(10, 20));
NEWLINE;
return 0;
}
⚠️ يجب الحذر عند تعريف الماكروهات وإحاطة المعاملات بأقواس لتجنب أخطاء الأولوية في العمليات الحسابية.
يسمح بتضمين أو استبعاد أجزاء من الكود بناءً على شروط معينة:
#include <stdio.h>
// تعريف ثابت للتحكم في الإصدار
#define DEBUG 1
int main() {
int x = 10;
// تضمين كود للتصحيح فقط
#if DEBUG
printf("وضع التصحيح: x = %d\n", x);
#endif
// مثال آخر
#ifdef DEBUG
printf("DEBUG معرّف.\n");
#else
printf("DEBUG غير معرّف.\n");
#endif
// التحقق من عدم تعريف ثابت
#ifndef MAX_SIZE
#define MAX_SIZE 1000
#endif
printf("MAX_SIZE = %d\n", MAX_SIZE);
return 0;
}
توفر لغة C بعض الماكروهات المدمجة مثل:
#include <stdio.h>
int main() {
printf("اسم الملف الحالي: %s\n", __FILE__);
printf("رقم السطر الحالي: %d\n", __LINE__);
printf("وقت الترجمة: %s\n", __TIME__);
printf("تاريخ الترجمة: %s\n", __DATE__);
printf("إصدار المعيار: %ld\n", __STDC_VERSION__);
return 0;
}
نستعرض في هذا القسم بعض المفاهيم المتقدمة في لغة C والتي تساعد المبرمجين على كتابة برامج أكثر فعالية وقوة.
التعدادات هي نوع بيانات مخصص يتكون من مجموعة من الثوابت المسماة (المعدودات). تساعد على جعل الكود أكثر وضوحاً وقابلية للقراءة.
#include <stdio.h>
// تعريف تعداد للأيام
enum Day {
SUNDAY, // قيمة 0 (افتراضياً)
MONDAY, // قيمة 1
TUESDAY, // قيمة 2
WEDNESDAY, // قيمة 3
THURSDAY, // قيمة 4
FRIDAY, // قيمة 5
SATURDAY // قيمة 6
};
// تعداد مع قيم محددة
enum Month {
JAN = 1, FEB, MAR, APR, MAY, JUN,
JUL, AUG, SEP, OCT, NOV, DEC
};
int main() {
enum Day today = WEDNESDAY;
enum Month month = JUL;
printf("اليوم: %d\n", today);
printf("الشهر: %d\n", month);
// استخدام التعدادات في العبارات الشرطية
if (today == FRIDAY || today == SATURDAY) {
printf("عطلة نهاية الأسبوع!\n");
} else {
printf("يوم عمل.\n");
}
return 0;
}
تُستخدم typedef
لإنشاء أسماء مستعارة لأنواع البيانات، مما يجعل الكود أكثر وضوحاً وأسهل في الصيانة.
#include <stdio.h>
#include <string.h>
// استخدام typedef مع الأنواع الأساسية
typedef unsigned int uint;
typedef char * string;
// استخدام typedef مع الهياكل
typedef struct {
char name[50];
int age;
float salary;
} Employee;
// استخدام typedef مع المؤشرات للدوال
typedef int (*MathFunc)(int, int);
// دوال للاختبار
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// استخدام الأنواع المعرفة بـ typedef
uint num = 10;
string name = "أحمد";
printf("العدد: %u\n", num);
printf("الاسم: %s\n", name);
// استخدام typedef مع الهياكل
Employee emp1;
strcpy(emp1.name, "محمد");
emp1.age = 30;
emp1.salary = 5000.0;
printf("الموظف: %s، العمر: %d، الراتب: %.2f\n", emp1.name, emp1.age, emp1.salary);
// استخدام typedef مع مؤشرات الدوال
MathFunc operation = add;
printf("الجمع: %d\n", operation(5, 3));
operation = subtract;
printf("الطرح: %d\n", operation(5, 3));
return 0;
}
توفر لغة C مجموعة من العوامل للتعامل المباشر مع البتات. هذه العمليات مفيدة في برمجة الأنظمة المضمنة وعندما نحتاج إلى كفاءة في استخدام الذاكرة.
العامل | الاسم | الوصف |
---|---|---|
& | AND البتية | تعطي 1 إذا كان البت في كلا العددين 1 |
| | OR البتية | تعطي 1 إذا كان البت في أحد العددين أو كليهما 1 |
^ | XOR البتية | تعطي 1 إذا كان البت في أحد العددين 1 وليس في كليهما |
~ | NOT البتية | تعكس قيمة كل بت (1 يصبح 0 و 0 يصبح 1) |
<< | الإزاحة لليسار | تزيح البتات لليسار، وتملأ البتات الجديدة بأصفار |
>> | الإزاحة لليمين | تزيح البتات لليمين، وتملأ البتات الجديدة بأصفار أو بقيمة بت الإشارة |
#include <stdio.h>
int main() {
unsigned int a = 60; // 00111100 في الثنائي
unsigned int b = 13; // 00001101 في الثنائي
int result;
// عمليات AND، OR، XOR البتية
result = a & b; // 00001100 = 12
printf("a & b = %d\n", result);
result = a | b; // 00111101 = 61
printf("a | b = %d\n", result);
result = a ^ b; // 00110001 = 49
printf("a ^ b = %d\n", result);
result = ~a; // 11000011 للبايت الأقل أهمية (قيمة سالبة للـ int)
printf("~a = %d\n", result);
// عمليات الإزاحة
result = a << 2; // 11110000 = 240
printf("a << 2 = %d\n", result);
result = a >> 2; // 00001111 = 15
printf("a >> 2 = %d\n", result);
return 0;
}
#include <stdio.h>
// تعريف الأعلام البتية
#define FLAG_A (1 << 0) // 00000001
#define FLAG_B (1 << 1) // 00000010
#define FLAG_C (1 << 2) // 00000100
#define FLAG_D (1 << 3) // 00001000
int main() {
unsigned char flags = 0; // 00000000
// ضبط العلم (تعيين بت إلى 1)
flags |= FLAG_A; // 00000001
flags |= FLAG_C; // 00000101
printf("الأعلام بعد الضبط: %d\n", flags);
// فحص العلم
if (flags & FLAG_A) {
printf("العلم A مضبوط.\n");
}
if (flags & FLAG_B) {
printf("العلم B مضبوط.\n");
} else {
printf("العلم B غير مضبوط.\n");
}
// مسح العلم (تعيين بت إلى 0)
flags &= ~FLAG_A; // 00000100
printf("الأعلام بعد المسح: %d\n", flags);
// تبديل حالة العلم
flags ^= FLAG_C; // 00000000
flags ^= FLAG_D; // 00001000
printf("الأعلام بعد التبديل: %d\n", flags);
return 0;
}
اتباع أفضل الممارسات في البرمجة يساعد على كتابة كود أكثر أماناً وقابلية للصيانة والتطوير. فيما يلي بعض الممارسات الموصى بها عند البرمجة بلغة C:
free()
بعد الانتهاء منها.
fgets()
بدلاً من gets()
للقراءة من المدخلات.
strncpy()
بدلاً من strcpy()
.
#define
و const
للقيم الثابتة بدلاً من القيم الحرفية في الكود.
التعرف على الأخطاء الشائعة في لغة C يساعد المبرمجين على تجنبها وتسريع عملية تصحيح الأخطاء.
يحدث عندما لا يتم تحرير الذاكرة المخصصة ديناميكياً.
// مثال على تسرب الذاكرة
void memoryLeak() {
int *ptr = (int*) malloc(sizeof(int));
*ptr = 10;
// نسيان استدعاء free(ptr)
return; // تسرب ذاكرة - لا يمكن الوصول إلى ptr بعد الخروج من الدالة
}
محاولة الوصول إلى ذاكرة تم تحريرها بالفعل.
int *ptr = (int*) malloc(sizeof(int));
*ptr = 10;
free(ptr); // تحرير الذاكرة
*ptr = 20; // خطأ: استخدام الذاكرة بعد تحريرها
الكتابة خارج حدود المصفوفة المخصصة.
int arr[5];
for (int i = 0; i <= 5; i++) { // خطأ: التكرار يشمل i=5 (خارج حدود المصفوفة)
arr[i] = i; // تجاوز حدود المصفوفة عند i=5
}
من أكثر الأخطاء شيوعاً هو نسيان وضع الفاصلة المنقوطة في نهاية العبارات.
int x = 10 // خطأ: نسيان الفاصلة المنقوطة
printf("قيمة x: %d\n", x);
استخدام عامل التعيين (=) بدلاً من عامل المقارنة (==) في العبارات الشرطية.
int x = 5;
if (x = 10) { // خطأ: هذا يقوم بتعيين x = 10 ثم تقييم الشرط كـ true
printf("x تساوي 10\n");
}
// الصحيح:
if (x == 10) {
printf("x تساوي 10\n");
}
استخدام متغير قبل تعريفه.
sum = a + b; // خطأ: استخدام المتغير sum قبل تعريفه
int sum;
// الصحيح:
int sum;
sum = a + b;
استخدام مؤشر غير مهيأ يمكن أن يؤدي إلى سلوك غير متوقع أو انهيار البرنامج.
int *ptr; // مؤشر غير مهيأ
*ptr = 10; // خطأ: محاولة الكتابة إلى عنوان غير معروف
// الصحيح:
int *ptr = (int*) malloc(sizeof(int));
if (ptr != NULL) {
*ptr = 10;
}
إرجاع مؤشر إلى متغير محلي سيؤدي إلى مشاكل لأن المتغير المحلي يتم تدميره عند الخروج من الدالة.
int* getLocalPtr() {
int localVar = 10;
return &localVar; // خطأ: إرجاع مؤشر إلى متغير محلي سيختفي
}
// الصحيح: استخدام الذاكرة الديناميكية
int* getSafePtr() {
int *ptr = (int*) malloc(sizeof(int));
*ptr = 10;
return ptr; // يجب على المستدعي تحرير هذه الذاكرة
}
عدم فهم الفرق بين اسم المصفوفة والمؤشر.
char str[] = "Hello";
char *ptr = "World";
str = ptr; // خطأ: لا يمكن تعيين قيمة لاسم المصفوفة
// الصحيح:
strcpy(str, ptr); // نسخ محتوى المؤشر إلى المصفوفة
شروط الحلقات غير الصحيحة قد تؤدي إلى حلقات لا نهائية أو عدم تنفيذ الحلقة.
// حلقة لا نهائية
int i = 0;
while (i < 10) {
printf("%d ", i);
// نسيان زيادة قيمة i
}
// عدم تنفيذ الحلقة
for (i = 10; i < 0; i++) { // خطأ: شرط غير صحيح (i < 0 دائماً خاطئ عندما i = 10)
printf("%d ", i);
}
محاولة القسمة على صفر تسبب خطأ في وقت التشغيل.
int a = 10, b = 0;
int result = a / b; // خطأ: قسمة على صفر
// الصحيح: التحقق قبل القسمة
if (b != 0) {
result = a / b;
} else {
printf("خطأ: محاولة القسمة على صفر\n");
}
استخدام العامل المنطقي الخاطئ في العبارات الشرطية.
int age = 25;
// قد تكون مقصودة كـ "إذا كان العمر أقل من 18 أو أكبر من 60"
if (age < 18 && age > 60) { // خطأ منطقي: لا يمكن للعمر أن يكون أقل من 18 وأكبر من 60 في نفس الوقت
printf("سعر تذكرة مخفض\n");
}
// الصحيح:
if (age < 18 || age > 60) {
printf("سعر تذكرة مخفض\n");
}
استخدام مؤشر الملف دون التحقق من نجاح عملية الفتح.
FILE *file = fopen("data.txt", "r");
fprintf(file, "بيانات للكتابة"); // خطأ: لم يتم التحقق مما إذا كان file == NULL
// الصحيح:
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
perror("خطأ في فتح الملف");
} else {
fprintf(file, "بيانات للكتابة");
fclose(file);
}
استخدام محددات تنسيق غير متوافقة مع نوع البيانات.
int num = 10;
float pi = 3.14;
printf("العدد: %f\n", num); // خطأ: استخدام %f مع متغير من نوع int
printf("باي: %d\n", pi); // خطأ: استخدام %d مع متغير من نوع float
// الصحيح:
printf("العدد: %d\n", num);
printf("باي: %f\n", pi);
نسيان إغلاق الملفات بعد الانتهاء من استخدامها.
void writeToFile() {
FILE *file = fopen("data.txt", "w");
if (file != NULL) {
fprintf(file, "بيانات للكتابة");
// نسيان استدعاء fclose(file)
}
return; // الملف سيظل مفتوحاً مما قد يؤدي إلى تسرب الموارد
}
-Wall -Wextra
مع GCC).assert()
للتحقق من الشروط الأساسية أثناء التطوير.لغة C هي لغة برمجة قوية وفعالة تشكل أساساً للعديد من اللغات والأنظمة الحديثة. على الرغم من أنها ليست اللغة الأسهل للتعلم، إلا أن فهم مفاهيمها الأساسية يوفر فهماً عميقاً لكيفية عمل أنظمة الحوسبة.
في هذه الوثيقة، تناولنا الجوانب الأساسية والمتقدمة للغة C، بدءاً من أنواع البيانات والمتغيرات، مروراً ببنى التحكم والدوال، وصولاً إلى المفاهيم المتقدمة مثل المؤشرات وإدارة الذاكرة والتعامل مع الملفات. كما تطرقنا إلى أفضل الممارسات والأخطاء الشائعة وكيفية تجنبها.
استمر في الممارسة وتطبيق ما تعلمته من خلال مشاريع عملية. التعلم المستمر والتجربة هما مفتاح إتقان البرمجة بلغة C.