C: الثوابت وتحويل الأنواع
الثوابت أشبه بقيمة pi في الرياضيات - تُحدد مرة ولا تتغير أبدا. تحويل الأنواع أشبه بصرف العملات: للعملات المختلفة أسعار ثابتة، لكن انتبه لفقدان الدقة عند التحويل.
1. القيم الحرفية
القيمة الحرفية هي قيمة ثابتة مكتوبة مباشرة في شفرتك. لا تحتاج لحسابها - يمكنك رؤية قيمتها بنظرة.
(1) القيم الحرفية الصحيحة
int a = 42;
int b = 0xFF;
int c = 052;
int d = 0b101010;
| الأساس | البادئة | مثال | القيمة العشرية |
|---|---|---|---|
| عشري | لا شيء | 42 |
42 |
| سداسي عشري | 0x أو 0X |
0xFF |
255 |
| ثماني | 0 |
052 |
42 |
| ثنائي (امتداد GNU لـ C99 / معيار C23) | 0b أو 0B |
0b101010 |
42 |
int x = 010; تساوي 8، وليس 10 - خطأ كلاسيكي للمبتدئين.
القيم الحرفية الصحيحة افتراضيا من نوع int. إذا تجاوزت القيمة نطاق int، تصبح تلقائيا long أو long long. يمكنك أيضا تحديد النوع يدويا:
| اللاحقة | المعنى | مثال |
|---|---|---|
u أو U |
غير موقّع | 42U |
l أو L |
long | 42L |
ll أو LL |
long long | 42LL |
UL |
unsigned long | 42UL |
L الكبيرة بدلا من l الصغيرة، لأن l يُسهل الخلط بينها وبين الرقم 1. 42l تبدو مثل 421.
(2) القيم الحرفية للفاصلة العائمة
double a = 3.14;
double b = 2.0e8;
double c = 1.5E-3;
float d = 3.14f;
القيم الحرفية للفاصلة العائمة افتراضيا من نوع double. أضف اللاحقة f أو F لنوع float، أو l/L لنوع long double.
float f = 3.14;
3.14 قيمة حرفية من نوع double. إسنادها إلى متغير float يسبب اقتطاعا ضمنيا، وقد يحذّر المترجم. الطريقة الصحيحة:
float f = 3.14f;
(3) القيم الحرفية للمحارف
محرف واحد محاط بعلامات اقتباس مفردة:
char ch = 'A';
char newline = '\n';
char zero = '\0';
نوع القيمة الحرفية للمحرف في C هو int (وليس char) - تفصيلة دقيقة يسهل تفويتها:
printf("%zu\n", sizeof('A'));
4
في C، sizeof('A') تساوي 4 (حجم int)، بينما في C++ تساوي 1 (حجم char). هذا اختلاف دقيق بين C و C++.
(4) القيم الحرفية للسلاسل النصية
تسلسل من المحارف محاط بعلامات اقتباس مزدوجة:
printf("مرحبا بالعالم!\n");
القيم الحرفية للسلاسل النصية نوعها char[]. يُضيف المترجم تلقائيا \0 كمنهي، لذا "abc" تشغل فعليا 4 بايت (a، b، c، \0).
char *s = "hello";
s[0] = 'H';
2. المُعدِّل const
const هي كلمة مفتاحية في C تجعل المتغير "للقراءة فقط" - بمجرد تهيئته، لا يمكن تعديله:
const int MAX_SIZE = 100;
const float PI = 3.14159f;
const char NEWLINE = '\n';
محاولة تعديل متغير const تسبب خطأ ترجمة:
const int MAX_SIZE = 100;
MAX_SIZE = 200;
error: assignment of read-only variable 'MAX_SIZE'
(1) const والمصفوفات
هل يمكن استخدام متغير const لتحديد حجم مصفوفة؟
const int SIZE = 10;
int arr[SIZE];
const int في C ليست ثابت وقت ترجمة (بل مجرد متغير للقراءة فقط وقت التشغيل). أدخل C99 المصفوفات متغيرة الطول (VLAs)، لذا الشفرة أعلاه تُترجم تحت C99، لكن VLAs مميزة اختيارية لا تدعمها جميع المترجمات.
إذا كنت تحتاج ثابت وقت ترجمة حقيقي لحجم مصفوفة، استخدم #define أو enum.
(2) const والمؤشرات
الجمع بين const والمؤشرات تحد كلاسيكي في C. هناك ثلاث حالات:
int value = 42;
const int *p1 = &value;
int * const p2 = &value;
const int * const p3 = &value;
| التعريف | المعنى |
|---|---|
const int *p |
البيانات المُشار إليها لا يمكن تعديلها؛ المؤشر نفسه يمكنه الإشارة لمكان آخر |
int * const p |
المؤشر نفسه لا يمكن تعديله (عنوان ثابت)؛ البيانات المُشار إليها يمكن تغييرها |
const int * const p |
لا المؤشر ولا البيانات المُشار إليها يمكن تعديلها |
* تقع const. إذا كانت على اليسار، فهي تعدّل البيانات. إذا كانت على اليمين، فهي تعدّل المؤشر.
3. ثوابت وحدات الماكرو #define
#define توجيه معالج أولي يُستخدم لتعريف ثوابت وحدات الماكرو:
#define PI 3.14159
#define MAX_SIZE 100
#define NEWLINE '\n'
يستبدل المعالج الأولي جميع تكرارات PI بـ 3.14159 قبل الترجمة. ليست متغيرا - لا تشغل ذاكرة ولا لديها فحص أنواع.
(1) #define مقابل const
| الميزة | #define |
const |
|---|---|---|
| متى تُعالج | مرحلة المعالجة الأولية (استبدال نصي) | مرحلة الترجمة |
| فحص الأنواع | لا | نعم |
| النطاق | من التعريف حتى نهاية الملف | تتبع قواعد نطاق المتغيرات |
| التنقيح | المنقح لا يمكنه رؤية اسم الماكرو | المنقح يمكنه رؤية متغير const |
| يمكن أخذ عنوانها | لا | نعم |
const - لديها فحص أنواع وقيود نطاق، ما يجعلها أكثر أمانا. استخدم #define لعلامات الترجمة الشرطية أو حين تحتاج ثوابت وقت الترجمة.
(2) مزالق وحدات الماكرو الشائعة
#define DOUBLE(x) x + x
int result = DOUBLE(3) * 2;
قد تتوقع 3 + 3 * 2 = 12، لكن الماكرو يتوسع إلى 3 + 3 * 2 = 9. وحدات الماكرو هي استبدال نصي بحت - لا تحترم أسبقية المعاملات.
الحل: أضف أقواسا للماكرو:
#define DOUBLE(x) ((x) + (x))
4. ثوابت التعدادات
enum طريقة أخرى لتعريف ثوابت أعداد صحيحة:
enum Color {
RED,
GREEN,
BLUE
};
افتراضيا، تبدأ القيم من 0 وتتزايد: RED = 0، GREEN = 1، BLUE = 2. يمكنك أيضا تعيين القيم يدويا:
enum Weekday {
MON = 1,
TUE,
WED,
THU,
FRI,
SAT = 100,
SUN
};
TUE = 2، WED = 3، ...، SUN = 101 (كل قيمة لاحقة أكبر بواحد من السابقة).
قيم التعدادات هي ثوابت وقت ترجمة حقيقية، صالحة لأحجام المصفوفات وسيناريوهات مشابهة. مقارنة بـ #define، تحمل التعدادات معلومات النوع ومرئية بالاسم في المنقح - فضّلها حينما أمكن.
5. التحويل الضمني للأنواع
عند خلط أنواع مختلفة في تعبير، تقوم C تلقائيا بتحويل الأنواع. فهم قواعد التحويل الضمني مفتاح لتجنب الأخطاء.
(1) الترويج الصحيح
في التعابير، يُروّج char و short تلقائيا إلى int قبل أي عملية:
char a = 10;
char b = 20;
printf("%zu\n", sizeof(a + b));
4
نتيجة a + b هي int (4 بايت)، وليس char (1 بايت).
(2) التحويل الحسابي
عند جمع قيم من أنواع مختلفة في عملية، يُحوّل النوع الأدنى تلقائيا إلى النوع الأعلى. التسلسل الهرمي للتحويل من الأدنى إلى الأعلى:
int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double
القاعدة: قبل عملية، يُحوّل المعامل ذو الرتبة الأدنى تلقائيا إلى الرتبة الأعلى.
int a = 5;
double b = 2.0;
printf("%.1f\n", a / b);
2.5
يُحوّل a تلقائيا إلى double، ثم تُنفذ قسمة الفاصلة العائمة، والنتيجة 2.5.
(3) تحويل الإسناد
أثناء الإسناد، تُحوّل القيمة في الطرف الأيمن تلقائيا إلى نوع الطرف الأيسر:
int x = 3.14;
double y = 42;
3.14 المُسندة إلى int تُقتطع إلى 3؛ 42 المُسندة إلى double تصبح 42.0.
int n = 2.9;
n تساوي 2، وليس 3.
(4) فخ القسمة الصحيحة
هذا أكثر المزالق شيوعا للمبتدئين:
int a = 5;
int b = 2;
double result = a / b;
printf("%.1f\n", result);
2.0
تتوقع 2.5، لكنك تحصل على 2.0. السبب: a / b قسمة صحيحة، نتيجتها 2 (عدد صحيح)، الذي يُحوّل بدوره إلى القيمة double وهي 2.0.
الحل: اجعل أحد المعاملات على الأقل عددا عشريا:
double result = (double)a / b;
أو:
double result = a * 1.0 / b;
أو:
double result = 5.0 / 2;
6. التحويل الصريح للأنواع
حين لا يقوم التحويل الضمني بما تحتاجه، يمكنك استخدام التحويل الصريح لتخبر المترجم بالضبط بالنوع الذي تريده:
(1) الصياغة
(type_name)expression
▶ مثال
#include <stdio.h>
int main(void) {
int a = 7;
int b = 2;
printf("قسمة صحيحة: %d\n", a / b);
printf("مع التحويل: %.2f\n", (double)a / b);
return 0;
}
قسمة صحيحة: 3
مع التحويل: 3.50
(double)a تحول a أولا إلى 7.0. ثم، عند جمعها مع b، يُروّج b أيضا ضمنيا إلى double، وتُنفذ قسمة الفاصلة العائمة.
▶ مثال
#include <stdio.h>
#include <math.h>
int main(void) {
double x = 3.7;
printf("اقتطاع مباشر: %d\n", (int)x);
printf("تقريب: %d\n", (int)(x + 0.5));
printf("دالة round(): %.0f\n", round(x));
return 0;
}
اقتطاع مباشر: 3
تقريب: 4
دالة round(): 4
(int)(-3.7) نتيجتها -3، وليس -4. هذا يختلف عن "التقريب نحو الأسفل" (نحو السالب اللانهائي)، لذا كن حذرا.
7. مزالق تحويل الأنواع الشائعة
(1) خلط الموقّعة وغير الموقّعة
int a = -5;
unsigned int b = 10;
if (a + b > 0) {
printf("أكبر من 0\n");
} else {
printf("أصغر من أو يساوي 0\n");
}
أكبر من 0
قد تظن أن -5 + 10 = 5 > 0 صحيح، لكن سلوك هذه الشفرة يعتمد على قواعد التحويل الضمني. عند خلط int و unsigned int في عملية، يُحوّل int إلى unsigned int. -5 تصبح عددا موجبا كبيرا جدا عند تفسيرها كغير موقّعة، فالنتيجة فعلا أكبر من 0 - لكن هذا ربما ليس المنطق الذي قصدته.
(2) فقدان الدقة
int big = 123456789;
float f = big;
printf("%d\n", big);
printf("%.0f\n", f);
123456789
123456792
float تمتلك فقط 6-7 أرقام معنوية ولا يمكنها تمثيل عدد صحيح من 9 أرقام بدقة - الثلاثة أرقام الأخيرة تُفقد. كن حذرا جدا عند إسناد أعداد صحيحة كبيرة إلى float.
(3) تحويل التجاوز
char ch = 200;
printf("%d\n", ch);
على المنصات التي يكون فيها char موقّعا، 200 تتجاوز نطاق char (-128 إلى 127)، ما يؤدي إلى سلوك غير معرف - ربما تُطبع -56. إذا كنت تحتاج تخزين قيم من 0 إلى 255، استخدم unsigned char.
(4) تحويل المؤشرات
التحويل بين مؤشرات من أنواع مختلفة خطير:
int value = 0x41424344;
char *p = (char *)&value;
printf("%c\n", *p);
المخرجات تعتمد على ترتيب بايتات النظام (كبير النهاية أو صغير النهاية) وغير قابلة للنقل. هذا النوع من التحويل يُستخدم فقط في البرمجة منخفضة المستوى ويجب أن يتجنبه المبتدئون.
❓ أسئلة شائعة
📖 ملخص
- القيم الحرفية أربعة أنواع: صحيحة، فاصلة عائمة، محارف، وسلاسل نصية. انتبه للواحق (f/L/UL) وبادئات الأساس (0x/0/0b)
- const تجعل المتغير للقراءة فقط؛ فضّل const على #define. #define استبدال نصي للمعالجة الأولية بلا فحص أنواع
- التحويل الضمني يتبع قواعد الترويج الصحيح والتحويل الحسابي - الأنواع الأدنى تتحول تلقائيا إلى الأعلى
- القسمة الصحيحة أكثر فخاخ C شيوعا: 5/2 = 2؛ للحصول على 2.5، يجب أن يكون أحد المعاملات عشريا على الأقل
- خلط الأعداد الموقّعة وغير الموقّعة يُنتج نتائج غير متوقعة - تجنبه أو استخدم التحويل الصريح
📝 تمارين
- اكتب برنامجا يُخرج العدد 255 بالصيغ العشرية والسداسية عشرية والثمانية، ولاحظ صيغ المخرجات
- اكتب برنامجا لحساب مساحة دائرة (نصف القطر كـ int بقيمة 10، و pi معرّفة كـ const double)، مع الحذر من فخ القسمة الصحيحة
- اكتب برنامجا لاختبار التالي وسجل النتائج: قيمة (int)3.9، وقيمة (int)(-3.9)، ومخرجات %d بعد
char ch = 128. اشرح كل نتيجة